10个Bug环环相扣,你能解开几个?

3,159 阅读7分钟

​**简介:**由阿里云云效主办的2021年第3届83行代码挑战赛已经收官。超2万人围观,近4000人参赛,85个团队组团来战。大赛采用游戏闯关玩儿法,融合元宇宙科幻和剧本杀元素,让一众开发者玩得不亦乐乎。

今天请来决赛赛题设计者杜万,给大家分享一下设计与解题思路。

搭配《用代码玩剧本杀?第3届83行代码大赛剧情官方解析》使用效果更佳。

第四题整体是一个C/S架构,客户客户端是一个编译好的命令行程序,不可被修改,服务端是一个 Spring Boot 的 Web 应用;赛题要求,找出服务端程序的 BUG 并修复;客户端有两个职责,一个是说去向服务端发送正常 HTTP 请求,让参赛者发现BUG。

另一个是验证 bug 修复情况,然后发送给远端的评分程序,获得评分。整个赛题是跑在我们阿里云 DevStudio 上面,在 DevStudio 里我们启动一个Intellij IDEA 的社区版,内置了应用观测器(AppObserver) 插件。

​Bug 1 :修复 Regex

我们来看第一个bug 如何修复吧。运行 ‘mvn test’,10 个测试有 9 个错误。

​这里有好几个BUG,我们先看正则表达式相关的,我们先修复ExtractHtmlTest,翻阅源码,很快能定位到 Utils.stripHtmlTag 方法,方法名字面意思是去除 HTML Tag 标签,然后仔细查看日志会发现。

​删除的Tag内容包括了 > 和 ,那说明正则有问题,下图是对正则的剖析。

​所以该 BUG上述两种修复方法都是 OK 的。

解法:将 Utils.java 里的正则表达式`<(?.*)>`改为`<(?[^>]*)>`。

Bug 2:修复尾串缺失

再次执行 mvn test,发现还有单测没有通过,我们会发现字符串少了一截。

​再次查看 Utils.stripHtmlTag 方法,发现 matcher.appendReplacement 方法,如果不熟悉该方法,查看JDK的注释后,会发现 matcher.appendReplacement 和 matcher.appendTail 是成对出现的。所以在循环外补上 matcher.appendTail(builder)。

​看图是 matcher.appendReplacement 和 matcher.appendTail 的工作机制,巧用该方法,替换字符串更得心应手。

​Bug 3:修复 EOFException

再次执行 mvn test,仅剩下 EOFException 错误了,很快能定位到报错的方法是 Utils.decodeMessage。

​通过分析 ReactiveWebSocketHandler 的头部注释和 Utils.encodeMessage 的方法,我们了解到二进制的包结构:

/**
 * 二进制包格式
 * byte 字符集长度; n1
 * byte[n1] 字符集数据;n1 = 字符集长度
 * byte[n2] 有效数据;n2 = 包总长度 - n1 - 1
 */
@Component("ReactiveWebSocketHandler")
public class ReactiveWebSocketHandler implements WebSocketHandler {

public static byte[] encodeMessage(String message, Charset charset) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(out);
byte[] charsetNameBytes = charset.toString().getBytes(ISO_8859_1);

try {
            dos.write((byte) charsetNameBytes.length);  
            dos.write(charsetNameBytes);
            dos.write(message.getBytes(charset));
            dos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }

return out.toByteArray();
    }

然后在对比 Utils.decodeMessage 可以发现是一个调用时序问题,改正方法如下:

return new String(dis.readAllBytes(), charsetNameDecoder.apply(dis));

=>

String charsetName = charsetNameDecoder.apply(dis);
return new String(dis.readAllBytes(), charsetName);

此单测 Bug 已经修完了,接下来我们来修运行态的BUG。

配置应用观测器

首先我们先配置一下应用观测器(AppObserver),在赛题的DevStudio中,已经预安装了 AppObserver ,这里配置一下IDEA的启动器,加上应用观测器的 Agent 就好了。

​配置好应用观测器后,通过 Spring Boot 的 main 函数启动 Server 端进程。

Bug 4:修复CSRF

执行项目根目录的客户端程序 round4

$./round4

   ___          _       ___ _____
  / __\___   __| | ___ ( _ )___ /
 / /  / _ \ / _` |/ _ \/ _ \ |_ \
/ /___ (_) | (_| |  __/ (_) |__) |
\____/\___/ \__,_|\___|\___/____/

「第四关」    致命真相
当你直面致命的真相,你是否能面对这残酷的现实?

::  通关要求                ::  达到 60 分以上
::  获胜要求                ::  分数最高且用时最短


启动客户端程序....


=== Step 1 ====
成功获得数据通道: [
    "/ws/Codeup",
    "/ws/AppObserver",
    "/ws/DevStudio",
]

=== Step 2 ====
添加用户 reporter 失败!响应状态码: 403 Forbidden, 响应消息: "An expected CSRF token cannot be found", 请求头:"Authorization: Basic YWRtaW46YWRtaW4xMjM="

=== Step 3 ====
使用 reporter 用户无法连接到:ws://localhost:8080/ws/DevStudio, 响应状态码: 401 Unauthorized, 请求头: {"authorization": "Basic cmVwb3J0ZXI6cmVwb3J0ZXI="}

Step2 有一个 CSRF 的报错,由于无法修改客户端程序,需要在 Server 端解决这个问题,关闭掉 CSRF 校验。

使用上面的报错关键字Google一下,很快能找到Spring Security的修改方法。

​然后照下午修改,再验证一下,发现响应码从 403 变成了 401,所以修改生效了。

​Bug 5:修复 Admin 用户密码错误

上一步再次执行 ./round4 ,Step2 返回了 401,并提示了请求头:"Authorization: Basic YWRtaW46YWRtaW4xMjM=",这里可以看出,使用了HTTP Basic的验证方式,然后401提示,可能是用户名和密码不对,所以这里可以用 base64 解开认证头,修改一下服务端的用户名密码。

​Bug 6:Admin 角色不对

再次执行 ./round4 后我们发现,又变回了 403,但是返回错误变成了 Access Denied。看来密码对了,但是没有权限访问,打开 WebSecurityConfig 文件,我们会发现admin角色有两种写法“ADMIN”和“admin”,问题就出在这里,我们统一改成大写试试。

​Step2,算过了,接下来出来Step3 的问题了。

Bug 7:缺失 REPORTER 角色

Step 3 报错,使用 reporter 用户无法连接到:ws://localhost:8080/ws/AppObserver, 响应状态码: 403 Forbidden, 请求头: {"authorization": "Basic cmVwb3J0ZXI6cmVwb3J0ZXI="}。

又是一个权限问题,先解开 base64 编码的 Authorization,发现用户密码都是 reporter。接下来需要借助于应用观测器,使用应用观测器在 Round4Controller.addUser 加上虚拟断点,虚拟断点和普通断点一样可以获得执行上下文的线程堆栈和变量信息,但是虚拟断点不会阻塞执行,这个特性对于生产系统非常有用。

具体操作如下图所示

​通过虚拟断点,我们发现 reporter 用户的角色名为 REPORTER,而 endpoint "/ws/**", 当前只允许ADMIN角色访问,所以在Security配置里,给该路径添加 REPORTER 角色即可。

解决了角色问题,4 个 Spring Security 相关的 BUG 都已经已经修复掉了。重启服务并执行 ./round4 我们会先发有乱码,那看看乱码怎么修

Bug 8:共享 Buffer

通过对 ReactiveWebSocketHandler 里一连串mapper的分析,我们会发现 getBufferConverter 方法返回了定长的buffer,而这个buffer后面会有一连串的0值,这个很可疑。仔细看代码发现,多次调用之间共享了同一个buffer,而没有清空。解法也很简单,把共享buffer改成每次新建即可。如下图所示:

​修复以后,再次执行 ./round4 乱码没有,但是返回内容有点少了,说明还有其他问题。

Bug 9:修复 NPE

修掉上面乱码问题以后,从客户端 round4 的运行输出里已经看不到明显的错误了,这是发现内容有点短,看Server这边的日志,会看到一个NPE的报错:

​NPE比较好修,很快能排查到一个 return null。

​改成 return ""; 即可。

Bug 10:去除 ThreadLocal

重启服务端,并再次执行 ./round4,内容多了,不过再次乱码。

​最后一个Bug,不太好调试,需要靠认证的阅读代码,理解一下上下文,能看到有一个奇怪的ThreadLocal 变量用于缓存 charsetName。

​在一个Thread里charset是不变的?去掉估计也不会影响效果,最多性能差一点,尝试去掉。

​重启服务端,并再次执行 ./round4。

​这下一切正常了。

提取线索

上面三个频道的返回包含了大赛的线索,所以我们可以使用 grep 工具赛选出来。

​剧情题我们这里就不讨论了,可以看另外一篇解密文章。

小结

共计修了 10 个 Bug

  • Regex 2个
  • Spring Security 4个
  • NPE 1个
  • EOF 1个
  • 共享状态 2个

赛题涉及到的技术

  • Spring Boot
  • Spring Security
  • Spring WebFlux
  • Java IO
  • JUnit 5
  • Regex
  • Websocket
  • CSRF
  • HTTP Basic Auth

工具

  • DevStudio(Web 版 Intellij IDEA)
  • AppObserver (CloudToolkit 插件)

原文链接

本文为阿里云原创内容,未经允许不得转载。