记录一下复现2022年腾讯游戏安全初赛的踩坑经历
这一年的初赛赛题感觉是历年来比较不猜迷且简单的 附件没套壳没混淆没反调试 先尝试运行一下 开了一个窗口渲染出了ACE
字样:
但是过了几秒就空白了 而解出这题要求让程序自己画出如下例图:
IDA静态分析
接下来先用IDA静态分析一下逻辑
WinMain
中的主要逻辑在消息循环处:
当消息队列里没有要处理的消息时会调用func1
尝试NOP掉发现就不打印图像了 那么渲染出图像的逻辑应该就在其中
一开始就用ZwAllocateVirtualMemory
分配了一块RWX的内存Target_Mem
关于该内核函数:
1 | NTSYSAPI NTSTATUS ZwAllocateVirtualMemory( |
从这里大概就能猜到下面要加载shellcode来运行了:
果然接下来就解码了一段shellcode 然后以其中的dec_func
为入口执行 还能看到会根据程序运行的时间销毁这个段 这应该就是图像几秒钟就消失的原因 patch掉这个分支以便后续调试和hook
既然程序没有任何反调试 直接调试看看里面干了什么
初步动态分析
一进来就看到了很扎眼的一串HLSL
它是用于被D3DCompile
编译为着色器的高级语言 那么下面以它为参数执行的应该就是D3DCompile
同样下面还有一段 问AI 说一个是定点着色器一个是像素着色器 不过这些都不重要 最重要的是要知道光是编译着色器是无法完成图像的渲染的 具体的坐标和颜色参数肯定会在某处传入
后续程序执行的基本上都是库函数 基本上不太可能在里面生成位置和颜色参数 直到又进入了一个唯一被调用的用户函数:
里面是一个看起来很简单的虚拟机过程:
其中reg
和draw_cube
是根据逻辑自己定义的 IDA自己识别会识别出几个变量
当opcode为5或6时还会执行一个用户函数draw_cube
它的第5个参数分别是蓝色和黄色的argb值 猜测这里应该就是绘制图像的函数 里面进行了很多浮点数运算 看不懂什么意思 但是根据在绘制蓝色方块的调用处下断进行调试大概可以看出来第1, 2个参数分别是x, y坐标 因为值都不是很大 编写IDApython脚本初步hook这两个调用看看前4个参数在每次调用时是什么:
1 | from ida_dbg import get_reg_val as regs |
得到以下结果:
1 | args : [0xfffffc4a, 0x32, 0xff1fa3ac, 0x130bd0, 0xffffff00, ...] |
黄色的调用了11次 蓝色的调用了31次 可以和显示出来的图像对上 并且观察绘制蓝色方块时的第2个参数 这个参数为0x32的有8次调用 对应了y坐标最小时的蓝色方块一共有8个 也就是说前两个参数确实对应x, y坐标
绘制黄色方块的函数也是有调用的 应该也会绘制出黄色方块 但是当绘制黄色方块时传入的位置参数有很多是负数的 即使是两个看起来都很正常的坐标 也没用任何黄色方块被绘制 那么就要分析它的虚拟机过程了 还好这一步也不算复杂 毕竟只有那么几个opcode
VM分析
dump出opcode后用python脚本模拟:
1 | opcode = [ |
得到以下结果:
1 | reg[0] = reg[8] -> 0x000032 |
可以看到要绘制黄色方块计算坐标时有好几个方块的坐标都被减去了一个很大的数导致其变为了负数 并且第3, 4个参数是在每一次计算出当前要绘制的x, y坐标后通过opcode为4的一连串运算得到的 这一连串运算的参数就是x和y坐标 也就是说第3, 4个参数大概率是两个坐标的校验值
除此之外还能够观察出来在计算要绘制的黄色方块坐标时本来出现过一个比较正常像是坐标的值 但是这个值被减去了一个较大的数值导致其变为负数 而两个坐标都正常的情况下和蓝色方块校验值的计算过程相比较 这些黄色方块的校验值被交换了位置:
这应该就是黄色方块没有被正确绘制的两个原因
接下来先验证一下想法的正确性 在python里看一下将负数修正后是否能正确绘制flag:
1 | import matplotlib.pyplot as plt |
运行结果:
可以看到已经正确绘制了 那么接下来只需要记录一下正确的位置和校验值参数并调整被交换的校验值就能绘制flag了 记录得到:
1 | x:50, 50, 50, 50, 50, 50, 110, 170, 230, 110, 170 |
下面介绍通过Detours
API进行DLL编写和函数hook的过程
编写DLL进行hook
安装Detours库
可以直接去官方仓库下载发行版本 也可以通过vcpkg拉取 这里讲一下vcpkg安装时踩的坑
vcpkg第一次启动时要安装一些对版本有要求的组件 包括:
而它自带的下载功能会列出下载文件的URL并检查所下载的文件的SHA256 其中我的vcpkg在下载powershell的时候就报了校验失败的错 这种情况直接去它列出的URL那里手动下载并放在vcpkg\downloads
目录下即可
初始化完成后执行vcpkg install detours
即可拉取到detours库
配置项目
IDE这里选择使用Visual Studio 2022 Community
新建项目时选择C++编写的动态链接库:
正式开始编写dll前需要先配置好项目的包含目录和库目录 项目->xxx和属性->VC++目录->包含目录, 库目录
因为hook基本上必须要用到WindowsAPI 所以要把Windows Kits\10\Include\%VERSION%\um
也加入到包含目录中(版本替换成自己的) :
同理在库目录中添加detours.lib的路径:
这里再讲一下踩到的第二个坑
如果包含目录中添加了Windows Kits在编写代码时仍然不能包含windows.h
的话可以检查一下是否除了通过Visual Studio Installer
安装Windows Kits还手动安装了(用Everything
搜索windows.h
看看是否除了VS2022目录下还存在别的Windows Kits) 如果是的话通过设置->应用
卸载所有Windows Kits
接下来我选择的是手动下载一个Kits 这时候应该也能再通过VSInstaller安装
完成后再编辑包含目录将新安装的Kits下的um给添加进去应该就能包含windows.h
了
编写DLL
使用detours进行目标进程函数的hook基本上是这个模板:
1 | void Start_Hook(){ |
其中pto_hook
是目标进程中要hook的函数的虚拟地址 my_hook
为要将pto_hook
替换掉的函数指针
在本题中 要hook的函数是绘制方块的函数 目标是当检测到它绘制的是黄色方块时将前4个参数替换成正确的参数
问题是绘制函数是通过shellcode解码并映射到一块随机的内存中的 我们还需要获取绘制函数的地址 现在我们知道作为shellcode入口点的函数会储存到data段中:
那么只需要获取进程中的这块内存并加上一个偏移就能获取绘制函数的虚拟地址了 经过调试可以获得这个偏移为-0x650
接下来就像pwn中ret2libc的方法来计算绘制函数的虚拟地址:
1 | __int64 dec_func = 0x8318, pto_hook; |
使用GetModuleHandle(NULL)
可以获取程序映射到虚拟内存中的基址 使用ReadProcessMemory()
可以获取一个地址中的数据
这里再讲一个踩到的坑 在跟着网上的wp复现时看到读取的内存地址直接就是0x140008318 应该是因为机器没开ASLR 正常情况下需要先获取真正的虚拟地址才能读到数据
PS | 2025/3/30
实际上并不是机器的问题 Windows对于程序本身映射到虚拟内存中的基址和Linux中的PIE一样只需要修改二进制文件本身就行了 对于PE文件 使用010的EXE.bt
模板修改以下程序头的字段即可使其以固定的基址被载入虚拟内存:
NtHeader->OptionalHeader->DllCharacteristics->IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE : 1
将字段设置为0即可关闭随机映射
至此我们获取到了要hook的函数地址:
接下来就是编写自己的函数来hook绘制函数 我们需要先定义一个目标函数的函数指针类型来方便后续调用原本的绘制函数:
1 | typedef __int64 (* pfunc)(__int64, __int64, __int64, __int64, int, __int64, __int64, __int64, __int64, __int64); |
然后调用detours中的hook函数来将绘制函数替换为自己的函数:
1 | DetourAttach((PVOID*)&pto_hook, my_hook); |
简单通过研究这个hook的原理来解释一下如何调试编译出的dll
调试DLL
先在dll中下好断点:
接下来启动宿主程序并附加调试到宿主程序:
用注入器(这里选择Xenos)将dll注入到宿主程序中即可开始调试
执行完DetourTransactionCommit()
提交了修改后查看绘制函数:
可以看到函数头被修改为了跳转到我们编写的用户函数的字节码
执行完后会跳转到一块注入器创建的内存执行被覆盖的函数头然后跳转回绘制函数:
注入DLL
用上面提到的Xenos
进行dll注入 在Advanced
中设置(其实是默认的)注入的方法为Native inject
方可进行上述的调试 使用Manual map
的方法无法进行上述方法的调试
最后附上完整dll代码以及成果:
1 |
|