腾讯游戏安全大赛2022初赛题解
复盘一下2022的腾讯游戏安全比赛。
初赛
题目说明
这里有一个画了flag的小程序,可好像出了点问题,flag丢失了,需要把它找回来。
题目
找回flag样例:
要求:
- 不得直接patch系统组件实现绘制(如:直接编写D3D代码绘制flag),只能对题目自身代码进行修改或调用。
- 找回的flag需要和预期图案(包括颜色)一致,如果绘制结果存在偏差会扣除一定分数。
- 赛后需要提交找回flag的截图和解题代码或文档进行评分。
评分标准:
根据提交截图和代码文档的时间作为评分依据。
解题过程
先打开所给的程序,发现输出 ACE 的 LOGO 过一会后会消失。
IDA,打开找到 WinMain 函数,找到消息循环函数,分析主体逻辑
看到主体的函数
有一个初始化的操作,会分配一片可读可写可执行(#define PAGE_EXECUTE_READWRITE 0x40
)内存并将代码拷贝过去,主要有 140005040
和 140006350
两个地址的代码。
第一段拷贝完成之后可以发现它 PATCH 了函数开头的几个字节,应该是防静态分析的,而且看字节大概是 PUSH,POP
指令。
下面可以看到记录了当前的时刻,如果发现起始时刻与当前时刻超过了 4000(4000MS)那么执行下面的指令,这里根据开始的运行大概能猜测出来应该就是停止绘制的代码了。
最后调用 shellcode+0x650
的代码作为入口,下面可以尝试跟一下这个函数,这里可以采用静态修改代码为真实代码,也可以动调执行到这里的时候反编译。
直接跟到入口,可以很明显地发现函数入口往后有一段对自身的调用
看地址是 shellcode+0x420
,跟过去,重建函数,是一个很标准的虚拟机流程
虚拟机的代码存在于 shellcode+0x1301
的地址,也就是第二次拷贝得到的代码。
根据自己的理解还原了一下虚拟机的流程:
- op0:Stack[0]+=Stack[1]
- op1:Stack[0]-=Stack[1]
- op2 num1,num2:Stack[num2]=Stack[num1]
- op3 num1,num2:Stack[num2]=num1
- op4 num:这里的操作很神奇,会把栈中第一个值赋值为
num^'ACE'
,第二个值赋值为一个很复杂的运算。 - op5:调用 shellcode 头部的函数。
- op6:调用 shellcode 头部的函数,与上一个唯一的区别是第五个参数。
- op7:退出
而这里的 v16-v18 大概率也是 op2 和 op3 会操作到的,也算栈中的值。
这里很容易猜测 op5 和 op6 应该是绘制函数的代码。
FFFF00 刚好是黄色的代码,将取色器放置在程序上也能发现蓝色的代码是 2DDBE7,和这里的颜色代码差了一点,但是可以尝试修改一下。
CE 找到这个位置,将代码修改一下
这里我直接把它改成 000000 也就是黑色,直接跳出来。
然后执行完下面的代码,发现输出变成黑色了
那么主要肯定是要分析 shellcode+0
处的函数代码了(看不懂直接放弃)。
虽然看不懂,但是已经知道里面传的一个值是颜色了,去分析分析其他参数的含义就可以了。这里最好的一个办法应该是 hook,去打印它的参数,为了方便可以把它限时输出这点 PATCH 了。
这个直接去找到它的跳转让它永远跳转或者永远不跳转就行了,这里是改成永远跳转,90 加前面,偏移可以不用动。
写一个 DLL 去做 HOOK,主要去 HOOK shellcode,这个地址通过全局变量可以获得(2022游戏安全技术竞赛初赛.exe+0x8308)。
64 位的程序hook一般直接用 inline
或者 hotfix
或者无痕,个人感觉 hotfix
实现起来简单,但是个人更喜欢 inline hook
,因为它 windows
消息的机制,会不停地打印数据,因此加全局变量限制输出前 100 次调用的结果。
注入器(基本通用的):
1 |
|
用于 HOOK 的 DLL:
1 | // dllmain.cpp : 定义 DLL 应用程序的入口点。 |
首先是为了输出方便创建一个终端定向标准输出,然后 IO 函数就能往终端打印了。
hook 其实就是覆盖函数头,劫持到自己的函数里面,打印出参数之后把钩子去掉,恢复回原来的样子,再去正常调用,调用结束之后重新挂回钩子,要实现输出前100条的话最后不要重新挂钩子就行。
结果(PS:输出是正常的,但是我强制都改成黄色绘制了):
因为前几个参数是 __int32
类型的,所以直接换 %d
打印一下,这里放一下部分的数据:
1 | shellcode addr=000001C5654B0000 |
可以发现,前两个参数应该是坐标(我猜的),但是出现了负值,也就是打印到了屏幕外面,而且都是黄色的点会出现这种情况,导致了 FLAG
打印不出来,因此尝试在 hook 层面修复这个 bug,把所有的负值翻转,但是发现并没什么用,说明bug应该不止那么简单,还得再分析分析。
首先就是想看看它一轮有多少个点,直接建个 set
去输出就行,把所有点保存下来。
DLL代码:
1 | // dllmain.cpp : 定义 DLL 应用程序的入口点。 |
输出:
1 | (-950,-210) |
一共是 42 个点,而数了一下它题目给的正确的点数刚好也是42个,黄点是有 11 个, 蓝点有 31 个。
但是稍微改了一下第一个第二个参数发现点会直接消失,看样子是跟第三个第四个参数会有关系。
通过左右参数的比对观察一下,正确调用时的这些局部变量分别是多少,看看能不能找到点关系(放弃)。
还是选择自己写一个虚拟机去跑。
代码太长了不放了,可以自己去 dump,我写一下我自己的虚拟机调试流程:
1 |
|
运行之后发现了一点:
第三个和第四个参数分别为用 opcode==4
时候的 x 和 y 的坐标运算得到的值。
试试看利用 opcode=4 的流程能否让程序任意位置输出色块。
1 | v13=x; |
因为每次的这个操作数都不同,这里选取第一个错误坐标 -950 50
来绘制,利用这里的逻辑去做。
DLL代码:
1 | // dllmain.cpp : 定义 DLL 应用程序的入口点。 |
注入之后,在指定的位置输出了黄色方块
说明修复思路是没有问题的,接下来可以用虚拟机流程把错误的坐标和对应的该操作数 dump 出来,hook 的时候进行替换。
后面用截图工具比了一下,发现它们水平距离都一样的,所以可以用已有的正确坐标参考,从上到下坐标分别为 50,110,170,230...
就是每隔一个查了 60
的距离,水平距离也同样是 60,那么最靠左的正确的方块是 (410,290)
,肉眼分析下来,最左边的坐标是 50,y 坐标因为对对齐的也是 50,所以第一个色块是完美还原的。
那么最左边 6 个就是
1 | 50,50 |
对角线延伸出去三个就是
1 | 110,110 |
最后两个补齐 FLAG是
1 | 110,230 |
最后的DLL:
1 | // dllmain.cpp : 定义 DLL 应用程序的入口点。 |
最终结果也是完美实现了
To be continue For Final in 2022.