教程上:小白向—2022腾讯游戏安全初赛分析(上)
在上部分的最后,我们获取到了shellcode0每次调取时使用的参数,并且发现程序在运行4秒钟后将清除整个画板。
移除清空逻辑
首先我们先将运行4秒后的清除画板的逻辑移除,以方便之后结果的查看。
当时我们是在程序基地址+0x1090的函数的第68行打下了断点,而断点之后的那一行,调用了一个名为GetTickCount的Windows api函数
据ai介绍,此函数用于获取自系统启动以来经过的毫秒数
而在第74行的if,是一个实时的GetTickCount减去在第69行运行时获得的毫秒数,而后与0xFA0比较,0xFA0转为10进制即为4000,4000毫秒即为4秒,由此估计if内的逻辑即为清除画板,要避免执行这段逻辑,我们只需要调整跳转的条件

鼠标点击第74行的if,按下tab键,将进入到hex代码页(如果为graphs界面,则单击空格)

从上图可以看到,比较eax与0FA0h后,如果eax小于等于(jbe意味小于等于跳转)0FA0h,则跳转到loc_7FF60D00130C。而我们希望程序能一直以前四秒的逻辑运行,因此需要将jbe改为jmp,即无条件跳转。
修改方式为右键改行,在菜单中选择Patching→Change byte

然后在弹出的窗口中,将前两个值从0F 86修改为90 E9。0F 86意味小于等于条件,A1 00 00 00意为跳转,而90 E9则是占位符nop。这样修改后就将条件移除了。

点击ok,可以看到原来的jbe变成了nop和jmp

但就此还没有结束,还需要应用此次patch,如下图右键点击该行,然后在菜单中选择patching→Apply patches to… ,然后在弹出的窗口中点击Apply patches。那我们的修改就被应用到赛题的exe程序里了,再次打开就不会有4秒后清除面板的问题了。

图案参数分析
分析时请保持IDA Pro为调试中的状态,不然所有动态的数据都无法查看
在(上)篇的最后,我们获取到了shellcode0在每次调用时使用的参数,并且分析出前两个参数是x,y坐标,以及黄点的坐标存在负数。

进入shellcode420,分析shellcode0调用时参数的来源

函数的前四个参数取自v16,与v17;而对v16与v17进行的直接的使用或修改只有在第15-20行里进行了一次初始化,将v16与v17的值设置为0。但是根据调用shellcode0时的参数看,v16与v17的值是有改变的,这意味着并不是通过直接以v16=.. v17=…,的方式来进行值的修改。
v16与v17是与其他变量在同一块区域声明的,这些变量在内存中的位置也是相邻的,因此v16与v17可以通过其他变量访问,如v16可以通过*(&v15 + sizeof(v15)),v17可以通过*(&v15 + sizeof(v15) + sizeof(v16))来访问,v11、12、13、14同理可访问v16与v17

而对于取址号&,只有case2与case3中有使用,用来获取v15的地址,因此推测 v16与v17的值是在case2与case3中设定的。

但由于case2与case3中并不是通过调用函数来设置v16、v17,因此无法通过hook来查看设置过程
这里采用的方法是自己使用c++重写这段逻辑,然后载入这个while与switch结构所依赖的数据,在自己的代码中通过print函数监视这段逻辑的运行过程。
我们首先需要进一步分析这段代码,理清楚其依赖的数据。
依赖的数据一般有两种,一种是函数被调用时的参数,但是在这段结构里,从始至终没有用到任何一个参数,因此不考虑这个;另一种就是全局变量,在IDA Pro里的颜色是深蓝色
而这段结构里的深蓝色的有dword_1CE88031301,鼠标悬浮时会复现一个int[1855]意为int数组;HIDWORD,LODWORD 是c语言标准函数;还有case5,6中用于绘制图像的函数shellcode0。显然这里面所依赖的数据就是dword_1CE88031301

所以我们需要获取这个数据,获取这个数据所用到的技术被称作dump。
dump
dump所做的就是从内存中拷贝数据,所以dump前需要知道自己所需数据的在内存中的起始地址以及结束地址
起始地址很明显,该变量去除前缀的1CE88031301就是,该数组的大小是1855,则结束地址为起始地址+1855
知道了起始地址与结束地址,就可以写dump脚本了
如图所示点击菜单

在弹出的窗口中填入代码,修改start地址以及fopen中的路径(如果路径在c盘,则需要使用管理员权限打开ida pro),点击Run按钮。

1 2 3 4 5 6 7 8 9 10 11 12
| static main(void) { auto fp, start, end, size, i; start = 0x1CE88031301; size = 1855; end = start + size; fp = fopen("C:\\output.txt", "w"); for ( i = 0; i < size; i++ ){ auto value = get_wide_dword(start + i * 4); fprintf(fp, "%d,", value); } }
|
脚本运行并不会有成功反馈,点击Run按钮, 需自行在fopen填入的路径中查找是否有对应的文件,如我所填的就可以在c盘找到一个output.txt文件

这个文件中记录的,就是数组的值了
获得了虚拟机运行所依赖的数据(该数据一般被称为操作符),接下来我们需要使用c++自己编写一段模拟该虚拟机运行的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| #include<stdio.h> #include<stdlib.h> #include<string.h> int code[1855] = {}; int main() { int RIP = 0; int reg; int Stack[10]; int v13, v14; printf("start\n"); memset(Stack, 0, sizeof(Stack)); Stack[8] = Stack[9] = 50; while (RIP <= 0x1301) { int opcode = code[RIP]; RIP++; switch (opcode) {
case 0: Stack[0] += Stack[1]; printf("case 0 stack[0]=%d+%d\n", Stack[0], Stack[1]); break; case 1: Stack[0] -= Stack[1]; printf("case 1 stack[0]=%d-%d\n", Stack[0], Stack[1]); break; case 2: Stack[code[RIP + 1]] = Stack[code[RIP]]; printf("case 2 Stack[%d]=Stack[%d]=%d\n", code[RIP + 1], code[RIP], Stack[code[RIP]]); RIP += 2; break; case 3: Stack[code[RIP + 1]] = code[RIP]; printf("case 3 Stack[%d]=%d\n", code[RIP + 1], code[RIP]); RIP += 2; break; case 4: v13 = Stack[0]; v14 = Stack[0] * (Stack[1] + 1); printf("case 4 Origin: Stack[0]=%d Stack[1]=%d\n", Stack[0], Stack[1]); Stack[0] = code[RIP] ^ 0x414345; Stack[1] = ((Stack[0] ^ (Stack[1] + v13)) % 256 + (((Stack[0] ^ (v13 * Stack[1])) % 256 + (((Stack[0] ^ (Stack[1] + v14)) % 256) << 8)) << 8)); printf(" Target: Stack[0]=%d Stack[1]=%d\n", Stack[0], Stack[1]); RIP += 1; break; case 5: printf("case 5 paint(%d,%d,%d,%d,0xFFFFFF00);\n\n", Stack[4], Stack[5], Stack[6], Stack[7]); break; case 6: printf("case 6 paint(%d,%d,%d,%d,0xFF2DDBE7);\n\n", Stack[4], Stack[5], Stack[6], Stack[7]); break; case 7: printf("exit\n"); exit(0); default: printf("exit\n"); exit(0); break;
} } }
|
将dump出的数据赋值粘贴到code[1855]的声明里,就可以运行这段c++程序,查看这些操作符会使程序以什么样的顺序运行起来

于是我们找一找成功的情况(蓝色点)是怎么运行的,而不成功的情况(黄色点)又是怎么运行的
先看控制台最先输出的三个黄色点,会注意到三者经历的流程并不相同,有的第五步是case0,有的是case1;而结束前,有的只有两步case2,而有的是4步case2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| case 2 Stack[0]=Stack[8]=50 case 2 Stack[4]=Stack[0]=50 case 2 Stack[0]=Stack[4]=50 case 3 Stack[1]=1000 case 1 stack[0]=-950-1000 case 2 Stack[4]=Stack[0]=-950 case 2 Stack[0]=Stack[9]=50 case 2 Stack[5]=Stack[0]=50 case 2 Stack[0]=Stack[4]=-950 case 2 Stack[1]=Stack[5]=50 case 4 Origin: Stack[0]=-950 Stack[1]=50 Target: Stack[0]=1248208 Stack[1]=-14703700 case 2 Stack[3]=Stack[0]=1248208 case 2 Stack[0]=Stack[1]=-14703700 case 2 Stack[1]=Stack[3]=1248208 case 2 Stack[6]=Stack[0]=-14703700 case 2 Stack[7]=Stack[1]=1248208 case 5 paint(-950,50,-14703700,1248208,0xFFFFFF00);
case 2 Stack[0]=Stack[8]=50 case 2 Stack[4]=Stack[0]=50 case 2 Stack[0]=Stack[9]=50 case 3 Stack[1]=60 case 0 stack[0]=110+60 case 2 Stack[5]=Stack[0]=110 case 2 Stack[0]=Stack[5]=110 case 3 Stack[1]=500 case 1 stack[0]=-390-500 case 2 Stack[5]=Stack[0]=-390 case 2 Stack[0]=Stack[4]=50 case 2 Stack[1]=Stack[5]=-390 case 4 Origin: Stack[0]=50 Stack[1]=-390 Target: Stack[0]=1822057 Stack[1]=-1524539 case 2 Stack[6]=Stack[0]=1822057 case 2 Stack[7]=Stack[1]=-1524539 case 5 paint(50,-390,1822057,-1524539,0xFFFFFF00);
case 2 Stack[0]=Stack[8]=50 case 2 Stack[4]=Stack[0]=50 case 2 Stack[0]=Stack[4]=50 case 3 Stack[1]=1000 case 1 stack[0]=-950-1000 case 2 Stack[4]=Stack[0]=-950 case 2 Stack[0]=Stack[9]=50 case 3 Stack[1]=120 case 0 stack[0]=170+120 case 2 Stack[5]=Stack[0]=170 case 2 Stack[0]=Stack[4]=-950 case 2 Stack[1]=Stack[5]=170 case 4 Origin: Stack[0]=-950 Stack[1]=170 Target: Stack[0]=14227863 Stack[1]=-7425437 case 2 Stack[3]=Stack[0]=14227863 case 2 Stack[0]=Stack[1]=-7425437 case 2 Stack[1]=Stack[3]=14227863 case 2 Stack[6]=Stack[0]=-7425437 case 2 Stack[7]=Stack[1]=14227863 case 5 paint(-950,170,-7425437,14227863,0xFFFFFF00);
|
然后看蓝色点的执行顺序,却发现是清一色的23022302224226,因此预计这可能是正确步骤。
但是我们不能将黄色点的执行顺序直接修改为23022302224226,因为执行的步骤数不同,会影响下一个点对数据,即code[1855]的读取。比如原本执行了16步,改为14步后,那下一个点读取的就从第17步的数据,变成了第15步的数据,这可能会引起很多无法预期的错误。
我们还是先思考黄色点负数坐标的产生,观察黄色点的执行顺序,注意到是case1出现了减法,然后紧接case1的case2就出现了负数。而在蓝色点的执行顺序中,没有使用过case1,因此预期正确的步骤不需要case1,由此修改case1的代码,去掉减法操作
1 2 3 4 5 6
| case 1: printf("case 1"); break;
|
然后重新运行虚拟机程序
发现黄色点的坐标好像变正常了

在描点绘图的页面上尝试使用新的坐标进行绘图

发现是赛题目标图形的上下翻转,说明坐标改对了
接下来就是把结果应用到原程序了,这里使用的还是hook
注入器代码不变,dllmain函数做出一些变化,监测绘制的是否是黄色的点,如果是,那么将坐标修改为新坐标,同时由于每个点绘制时,第三四个参数都是不同的,因此猜测第三、四个参数可能会起到验证作用,所以还需要把第三四个参数也修改为新的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| #include "pch.h" #include <Windows.h> #include <stdio.h> #include <math.h> typedef __int64 (*Func)(int a1, int a2, int a3, int a4, int a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9, __int64 a10); __int64 GetBaseAddr() { HMODULE hMode = GetModuleHandle(nullptr); return (__int64)hMode; } void* shellcode = 0; BYTE HookCode[] = { 0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xFF,0xE0 }; BYTE OriginCode[0x50]; size_t HookLen = 12; __int64 times = 0; int r_x[11] = { 50,50,50,50,50,50,110,170,230,110,170 }; int r_y[11] = { 50,110,170,230,290,350,230,230,230,110,170}; int r_x1[11] = { 16258228,1822057,8889163,1897472,3743658,14122466,463184,12856971,3460907,14280177,12856715 }; int r_y1[11] = { 1248208,7673289,14227863,15215384,5377790,966258,7898116,13055771,6000615,13743405,11759583 };
__int64 HackShellcode(int a1, int a2, int a3, int a4, int a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9, __int64 a10) { memcpy(shellcode, OriginCode, HookLen); int x = a1, y = a2; if (a5 == 0xffffff00) { x = r_x[times]; y = r_y[times]; a3 = r_x1[times]; a4 = r_y1[times]; times++; if (times == 11) { times = 0; } } __int64 ret = (*(Func)shellcode)(x, y, a3, a4, a5, a6, a7, a8, a9, a10); memcpy(shellcode, HookCode, HookLen); return ret; }
void HookShellcode() { __int64 base = GetBaseAddr(); __int64 Ptr = base + 0x8308;
shellcode = (void*)(*(__int64*)Ptr); while (!shellcode) { shellcode = (void*)(*(__int64*)Ptr); printf("Find shellcode Fail\n"); Sleep(200); } printf("shellcode addr=%p\n", shellcode); memcpy(OriginCode, shellcode, HookLen); Func FuncPtr = HackShellcode; *(__int64*)(HookCode + 2) = (__int64)FuncPtr; memcpy(shellcode, HookCode, HookLen);
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: AllocConsole(); freopen("CONOUT$", "w", stdout); HookShellcode(); case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
|
重新生成dll文件,然后运行赛题程序(好像patch后不能ida pro调试,会注入不了,需要通过双击exe文件运行赛题程序),运行注入器程序注入,观察结果

发现有几个点可以显示了,但有些点还是不行。将位置与绘图网站上绘制出的图案进行比对,判断出可以显示的点为(50,110)(50,290,)(110,230)(230,230)。重新运行虚拟机,观察这几个点的运行逻辑与其他点有什么区别

能注意到显示失败的点,相比显示成功的点,在 case5前有多进行3次case2,对stack[0]与stack[1]的值进行了交换。于是我们尝试将显示失败的点的第三个参数与第四个参数再次进行交换,使得dllmain文件中的r_x1与r_y1变为
1 2
| int r_x1[11] = { 16258228,1822057,8889163,1897472,3743658,966258,463184,13055771,3460907,13743405,11759583 }; int r_y1[11] = { 1248208,7673289,14227863,15215384,5377790,14122466,7898116,12856971,6000615,14280177,12856715 };
|
重新生成dll文件,运行赛题程序,注入。

修改成功
追加——绑定exe与dll文件
到上面其实就可以结束了,但我想加一些东西。
目前要显示黄旗,每一次运行赛题程序,都要额外运行一次注入器代码,这样还是太麻烦了,最好还是将dll文件直接与exe程序绑定,使exe程序运行时自动引入dll文件
所需工具: “CFF Explorer”
使用CFF Explorer可以给exe程序注入dll文件引用,但在注入前,我们需要修改一下dll文件,修改为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| #include "pch.h" #include <Windows.h> #include <stdio.h> #include <math.h> #include <thread> typedef __int64 (*Func)(int a1, int a2, int a3, int a4, int a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9, __int64 a10); extern "C" __declspec(dllexport) void InitializeHook(); __int64 GetBaseAddr() { HMODULE hMode = GetModuleHandle(nullptr); return (__int64)hMode; } void* shellcode = 0; BYTE HookCode[] = { 0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xFF,0xE0 }; BYTE OriginCode[0x50]; size_t HookLen = 12; __int64 times = 0; int r_x[11] = { 50,50,50,50,50,50,110,170,230,110,170 }; int r_y[11] = { 50,110,170,230,290,350,230,230,230,110,170 }; int r_x1[11] = { 1248208,1822057,14227863,15215384,3743658,14122466,463184,12856971,3460907,14280177,12856715 }; int r_y1[11] = { 16258228,7673289,8889163,1897472,5377790,966258,7898116,13055771,6000615,13743405,11759583 };
__int64 HackShellcode(int a1, int a2, int a3, int a4, int a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9, __int64 a10) { memcpy(shellcode, OriginCode, HookLen); int x = a1, y = a2; if (a5 == 0xffffff00) { x = r_x[times]; y = r_y[times]; a3 = r_x1[times]; a4 = r_y1[times]; times++; if (times == 11) { times = 0; } } __int64 ret = (*(Func)shellcode)(x, y, a3, a4, a5, a6, a7, a8, a9, a10); memcpy(shellcode, HookCode, HookLen); return ret; }
void HookShellcode() { __int64 base = GetBaseAddr(); __int64 Ptr = base + 0x8308;
shellcode = (void*)(*(__int64*)Ptr); while (!shellcode) { shellcode = (void*)(*(__int64*)Ptr); printf("Find shellcode Fail\n"); Sleep(200); } printf("shellcode addr=%p\n", shellcode); memcpy(OriginCode, shellcode, HookLen); Func FuncPtr = HackShellcode; *(__int64*)(HookCode + 2) = (__int64)FuncPtr; memcpy(shellcode, HookCode, HookLen); }
extern "C" __declspec(dllexport) void InitializeHook() { std::thread(HookShellcode).detach(); }
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: AllocConsole(); freopen("CONOUT$", "w", stdout); InitializeHook(); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
|
修改了两个地方,一个是增加了导出函数 extern “C” __declspec(dllexport) void InitializeHook,因为有导出函数的 dll文件才能被CFF Explorer注入
另一处是将HookShellcode由case DLL_PROCESS_ATTACH里直接调用,变为InitializeHook里新开一个线程来调用,因为不另开一个线程的话,Sleep(200)将阻塞主进程,使得shellcode没办法载入,!shellcode始终为真,不断调用Sleep(200)
修改代码后重新生成dll文件,接下来就可以注入了
使用CFF Explorer打开赛题程序

进入“引入添加器”项,点击添加,选中之前生成的dll文件

点击引出的函数→点击按序号引入(按名称引入也可以)→点击重建引入表→点击左上角的保存图形

为了备份等各种目的,不覆盖原文件,弹窗选择否(选择了是也没关系,备份也不一定用到)

然后可以在保存的文件夹里看到新文件(我保存的文件名为test.exe)

双击运行,显示缺少dll文件

我们需要把dll文件复制到exe同文件夹里

再次双击运行exe程序,显示预期结果
