2022腾讯游戏安全竞赛初赛
0x00 题
这里有一个画了flag的小程序,可好像出了点问题,flag丢失了,需要把它找回来。
要求:
1、不得直接patch系统组件实现绘制(如:直接编写D3D代码绘制flag),只能对题目自身代码进行修改或调用。
2、找回的flag需要和预期图案(包括颜色)一致,如果绘制结果存在偏差会扣除一定分数。
3、赛后需要提交找回flag的截图和解题代码或文档进行评分。
0x01 解
运行程序会发现ACE的蓝色logo,过几秒后会自动消失
上IDA,直接看看WinMain(https://learn.microsoft.com/zh-cn/windows/win32/learnwin32/winmain--the-application-entry-point)
可以看到创建了一个互斥体(CreateMutexA),保证程序单例运行
之后是创建窗口
先D3D入个门吧:https://www.cnblogs.com/skiwnchiwns/p/10343190.html
之后这里能找到D3D的初始化过程的一步
所以大概可以判定这是D3D的初始化函数
看里面包着的函数sub_140001090
可以注意到这里有两个xmmword,里面的内容可以看一下
第一个无法解析成字符串,但是第二个可以,同时可以注意到下面还有一个xmmword可以解析成字符串
由于IDA的特性,这里有点问题,可以u转unk然后d转byte,比较好读(
可以看到这两个分别是ZwAllocateirtua
和ZwFreeVirtualMem
回到函数sub_140001090
可以看到后面又strcpy了ntdll和lMemory
由此判断这里是动态获取ZwAllocateVirtualMemory和ZwFreeVirtualMem的函数地址
参考:https://learn.microsoft.com/zh-cn/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress
之后是用memcpy写了点东西
这里有两段memcpy,浅看了一下,第一个开头很多90,即nop,想到了nop雪橇
关于Nop雪橇:https://www.cnblogs.com/nice-to-meet-you/p/11240228.html
所以这可能是一段shellcode,而第二个memcpy则没看出什么,根据大哥的WP这里应该也是一段shellcode
两段shellcode之间的(即0x53C48B48这些)是填充指令
之后有个GetTickCount
这里是检测运行时间是否超过4s,如果超过则调用上面提到的ZwFreeVirtualMem清空shellcode
(所以为什么会导致打开之后几秒图案消失)
接下来dump这个shellcode下来看看干了什么
dump下来之后浏览一下,可以发现这里有一段相对比较长的地址
结合一下源程序的这里
动调找到这个v6+11249(0x2BF1)的地址
可以发现一模一样
所以这个v37就是这个D3DCompile函数了,可以把这个v37当函数看
之后还可以在sub_650中找到一大堆看不懂的东西
根据WP,这个sub_650是shellcode的入口,是D3D的一些shader初始化
不过这里挺乱的,可以把它变得好读一点
1 | cbuffer ConstantBuffer : register(b0){matrix World;matrix View;matrix Projection;} |
后面也可以看到v54被v37调用
所以可以得知这个地方拿v54去编译了
往下翻翻,还能看到这段
1 | struct VSOut |
后面和v54差不多,也是调用了D3DCompile
之后调了个函数
感觉很是重要
根据WP说,整个sub_650是动态编译shader,完毕后执行虚拟机
而这个sub_420就是虚拟机了
关于VM可以看看这个:https://bbs.kanxue.com/thread-267670.htm
很多case啊(
可以注意到这个unk_1301,里面很多这个
一眼操作码
接下来就是搞清楚各个case的作用了
先从case5和case6开始,因为这两个都调用了sub_0
根据sub_0的参数,很明显case5和case6的第五个传入的参数有问题
这里把float改成int
然后就能识别了
这个数很奇怪,莫名其妙就来这么一个数
结合一下flag,或许和绘图有关,那么猜测可能是颜色
试试
bingo,看来这两个就是绘图了
case6就是绘制ACE的logo,那么case5就是黄色的flag了
然后再分析别的
根据一般vm的操作,一个虚拟机应该有寄存器,寄存器分High和Low两个部分处理,在这个vm中可以看出v15经常被使用,所以可以作为寄存器来看
这里有两个函数
LODWORD是取DWORD的低16位
HIDWORD是取DWORD的高16位
考虑到v15经常取高位和低位进行操作,因此合理认为v15实际上是一个数组,其[0]和[1]分别是两个寄存器
因此后面优化观看体验时有必要将其设置为数组(
然后这个unk_1301里面有很多操作数,所以可以看成是代码,即vmcode(_DWORD[])
这个v7则用于指定使用vmcode的那一个,类似于指针,所以可以看做是相对地址IP
而这个result,则可以看作是从
重命名,以便更好地查看
这下好看多了
一个个分析吧
先是case 0
1 | case 0: |
一眼add
然后是case 1
1 | case 1: |
一眼sub
然后是case 2
1 | case 2: |
这个case2的vmcode应该有双操作数
v11存了vmcode的其中一个值
然后vmcode[ip+2]相当于取了vmcode的另一个值
这里应该是将reg[v11]的值给了reg[vmcode[ip+2]]中
应该是mov
然后是case 3
1 | case 3: |
这里v12存了vmcode的一个值,然后将它赋值给了reg[vmcode[ip+2]]
这里有必要提一下lea和mov的区别了
lea是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数,例如:
lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。
而mov指令则恰恰相反,例如:
mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。
所以这个应该是lea
最后看看case 4
1 | case 4: |
看不懂,应该是什么计算
这个时候发现case5和case6有点问题,应该是reg的大小不够,试着将reg大小增加一下
这里增加到32bit(即4Byte)
至此,VM的指令大体是解决了
接下来就要看看这个绘制了
这里贴一个D3D入门教程:https://blog.csdn.net/u014038143/article/details/82730776
在青色位置下断点,然后观察各参数的变化
这里贴一个绘制函数的部分参数解析(根据上面的教程和上下文可以推测出来)
1 | __int64 __fastcall sub_0( |
在绘制的函数中有几个比较重要的值
1 | v15 = a5 + (a3 ^ (a1 + a2)) % 256 - a4 % 256; |
看来长宽就是在这一段里面了,而黄色旗子无法绘制就是与a1和a2有关
先写个解释器,看看VM干了什么
1 | import struct |
最后输出结果如下:
以上图为例,在绘制黄色方块的时,在cal coordinate操作前后相比起绘制青色方块时会多出一些操作
由于绘制第0个蓝色方块时与后面绘制有所偏差,所以以后面绘制为准,将绘制黄色方块的操作与后面绘制蓝色方块的操作进行比较
上面取了两个典型的差异,可见
1.黄色方块绘制时会将regs[0]和regs[1]的位置进行互换
2.绘制某些黄色方块时会让regs[0]的值减去0x1f4然后再放入regs[5]中
3.绘制某些黄色方块时会让regs[0]的值减去0x3e8然后再放入regs[4]中
所以写补丁要去除上述差异,补丁应该执行的操作有:
1.禁止互换
2.替换0x3e8和0x1f4,使其成为0x00
上面的解释器中有写出哪些地方属于swap,哪些地方属于存在0x1f4和0x3e8的情况
据此可以写出补丁(IDA)
1 | vmcode = get_name_ea_simple("vmcode") |
先patch一下GetTickCount
然后执行一下脚本
最后结果如下: