1.样本概况
1.1 应用程序信息
MD5值:814DE98DC72E4AB0001BA7F287239D2D
简单功能介绍:连连看单机游戏
1.2 分析环境及工具
系统环境:win7 32位
分析工具:OD,VS2013,CE
1.3 分析目标
获取游戏关键调用并直接调用,实现外挂功能
2.具体分析过程
2.1 寻找游戏本体
一般登录器都会在适当的时机创建游戏本体的进程,而且通过观察该登录器的界面行为
共弹出两个窗口,此时可以对常见的窗口API下断,比如:
CreateWindowA
CreateWindowW
DialogBoxParamA
DialogBoxParamW
CreateProcessA
CreateProcessW
通过查看任务管理器等进程查看工具发现该进程创建了另外一个进程,新创建的进程创建了游戏本体进程
弹窗为一个新的进程,使用OD附加该进程,点击继续之后,游戏开始
到目录下直接运行进程游戏本体不能运行,所以弹窗创建进程时做了一些操作,附加该弹窗进程qqlck.ocx。我们已经知道是该弹窗启动了游戏本体,所以对关键API下断 CreateProcessA/W
可以看出创建游戏本体进程的方式为挂起创建,则当前进程会操作远程进程,所以对关键API下断 WriteProcessMemory。
以上可以看出其修改了游戏本体中VA为0x43817a地址处的一个字节为0,随后唤醒该进程的主线程,游戏本体开始运行。
使用LordPE查看游戏本体数据
使用010Editor对exe指定地址处的数据修改(如果文件为只读,则拷贝一份重新打开)
至此,游戏本体已经可以单独运行。
2.2 分析游戏本体的消除call(如有代码验证贴出关键代码)
游戏本体为VC6的MFC程序(注意ecx传递this指针)
分析切入点
通过CE修改器查找游戏中变动的数据,这里以剩余方块数量为例进行查找。
CE附加游戏本体
运行游戏,以剩余方块数量作为切入点,查找修改方块数量的地址
CE右键查找访问该地址的代码
游戏没有随机基值,打开OD,转到该地址进行分析
进入函数内部分析
进入判断方块类型的call,得到地图地址
得出函数地图的地址为 ecx+4,为12b850,且方块开头地址为12b858。
既然得到了地图,下面所有的操作都是围绕地图进行,我们可以在地图上下内存断点进型栈回溯的一系列分析
消除call的分析思路
既然是消除,那么相同点在消除后会往地图中写入0,表示此点已经被消除,既然会写内存,那么就可以下内存写断点(这样整个内存页就包含了整张地图,比较方便),通过栈回溯的方式查找消除call的调用点。
运行游戏,断下,分析断点周围数据
对该函数进行栈回溯,寻找再上层的调用,找到消除函数
到这里我们可以直接使用该call消除一个方块,再往上层回溯,可以一次消除配合指 南针的提示一次消除两个方块两个方块。
分析该层函数的参数
发现EBX来自于本层函数的参数2,我们来到上层函数,查看参数2和参数3的来源
通过反复尝试,发现两个参数改变发生在此处
进入函数0x429025分析得出修改为
往上追踪esi的来源,发现已经来到了mfc的库中,一直作为this指针逐层传递,这里有一个技巧,基于对象编程中,如果两个线程之间要分享数据,一般会有全局指针等进行中转,即一般的类对象会有全局指针进行数据共享,可以在CE中直接搜索esi的值,即对象的地址,我们查找有没有全局指针保存。
以上ECX即this指针的值是在栈区或者堆区,这里我们猜测他可能存在全局指针保存
需要交换数据的对象的地址
通过测试,可以选择0x47FDE0这个地址,其他地址没有测试
总结:这里已经完成了消除call的分析,大致原型
Void fun(arg1 = 0,arg2 = pMap,arg3=pPoint1,arg4=pPoint2,
arg5=*(*(0x47FDE0)+0x1e84)+0x30,arg6=*(*(*(0x47FDE0)+0x1e84)+0x50))
funAddress:0x41C68E
2.3分析道具call
思路:对于指南针等道具会做到自动寻找地图相同点进行连接或者消除,则必定访问地图元素,在使用道具前设置地图内存访问断点,栈回溯找相应的call
栈回溯
再回溯一层,可以看出所有道具的调用call
分析调用过程得出道具结构的地址,通过修改内存值可以获取道具并调用。这里不能直接构造参数调用,因为函数内部可能会使用到内存数据,数据不正确就会造成调用失败。
至此,道具call的分析也基本已经完成
基本原型
Void fun(arg1 = default,arg2=default,arg3=type)
funAddr = *(*(*(0x47FDE0)+0x494)+0x28)
调用call的实现
指南针call的实现:
// 这里实现对指南针call的调用
/*
0041DE4D | . 8B86 94040000 MOV EAX,DWORD PTR DS:[ESI+0x494]
0041DE53 | . 8D8E 94040000 LEA ECX, DWORD PTR DS : [ESI + 0x494]; 当追寻不到ecx的来源时,到CE中去搜索,寻找基址
0041DE59 | . 52 PUSH EDX; EDX是通过坐标遍历得到
0041DE5A | . 53 PUSH EBX
0041DE5B | . 53 PUSH EBX
0041DE5C | . FF50 28 CALL DWORD PTR DS : [EAX + 0x28]; 三个参数,使用道具,第三个参数用于确定道具
0041E691 . B8 B0764400 MOV EAX,kyodai.004476B0
*/
_asm{
// 初始化道具内存值
mov ecx, 0x47FDE0
mov ecx, [ecx]
add ecx, 0xa64
mov eax, toolType
sub eax, 0xf0
shl eax, 0x4
lea eax, [ecx + eax + 0x4]
// 修改类型
inc eax
mov edx,toolType
mov dword ptr[eax],edx
// 修改数量
inc eax
mov dword ptr[eax],0x3
// 调用函数
mov ecx, 0x47FDE0
mov ecx, [ecx]
add ecx, 0x494
push toolType
push 0
push 0
/*
call 0041E691 这里不能直接使用call rel的方式 因为这里
会被编译成 E8 的call 后面的立即数会被当做偏移
要使用 FF call 的方式 ,即后面的寄存器中或者内存中给出的
直接就是地址,直接对绝对地址进行调用
*/
//mov eax,0x41E691
//CALL DWORD PTR DS : [EAX + 0x28];
mov eax, [ecx]
add eax, 0x28
mov eax, [eax]
call eax
}
单个消除的实现:
// 首先调用获取两点坐标的call
/*
0041E75E > \8B8E F0190000 MOV ECX,DWORD PTR DS:[ESI+0x19F0] 传递对象地址 12fa14+0x19f0+0x494
0041E764 . 8D45 D8 LEA EAX,DWORD PTR SS:[EBP-0x28] 传入参数,点2
0041E767 . 50 PUSH EAX
0041E768 . 8D45 E0 LEA EAX,DWORD PTR SS:[EBP-0x20] 传出参数,点1
0041E76B . 50 PUSH EAX
0041E76C . E8 CEAA0000 CALL kyodai.0042923F 获取坐标call
*/
POINT p1, p2;
// 指南针获取两个可以连接的点
_asm{
// 传递ecx
mov ecx, 0x47FDE0
mov ecx, [ecx]
mov ecx, [ecx + 0x19f0 + 0x494]
lea eax, p1.x
push eax
lea eax, p2.x
push eax
mov eax, 0x42923F
call eax
}
// 调用消除call
/*
0041AB10 |. 8B7D 10 MOV EDI,[ARG.3]
0041AB13 |> 57 PUSH EDI
0041AB14 |. 8D45 F4 LEA EAX,[LOCAL.3]
0041AB17 |. 53 PUSH EBX 12a1f4+1E48+0x30
0041AB18 |. 50 PUSH EAX
0041AB19 |. 8D45 EC LEA EAX,[LOCAL.5]
0041AB1C |. 8BCE MOV ECX,ESI
0041AB1E |. 50 PUSH EAX
0041AB1F |. 0FB645 08 MOVZX EAX,BYTE PTR SS:[EBP+0x8]
0041AB23 |. 69C0 DC000000 IMUL EAX,EAX,0xDC
0041AB29 |. 8D8430 5C1900>LEA EAX,DWORD PTR DS:[EAX+ESI+0x195C]
0041AB30 |. 50 PUSH EAX 数组地址
0041AB31 |. FF75 08 PUSH [ARG.1] 0
0041AB34 |. E8 551B0000 CALL kyodai.0041C68E ; 消除call 6参数
*/
_asm{
push 2;
mov ecx, 0x47FDE0;
mov ecx, [ecx];
mov eax, [ecx + 0x1e84]
add eax, 0x30
push eax
lea eax, p1.x
push eax
lea eax, p2.x
push eax
mov eax, ecx;
add eax, 0x195c;
push eax;
push 0
mov eax, 0x41c68e
call eax
}
3.总结
游戏的逆向一般就是找函数调用,也就是找call,对于这种逆向一般是通过内存作为中转,下断点然后使用栈回溯的方式找到访问或者修改内存的call。对于这个连连看游戏,首先是通过变化的数据,通过CE作为切入点,找到访问代码,逐步回溯,找到内存,下内存断点找到消除call(内存写),和道具call(内存读或写),所以实现方式都是通过内存断点进行回溯分析。同时,对于MFC的程序,对象编程,VC/VS使用ECX传递参数,有时一些地址被封装较深,无法找到,一般为了对象间交换数据方便,会将一些对象的地址保存起来用对象间或者线程之间的资源访问,所以对于经常被访问到的地址,考虑在CE中寻找是否有全局变量保存,因为对象一般都保存在栈或者中,即使回溯回去也不是一个基址,所以还是要通过CE查找的方式寻找保存对象的指针的基址。
4.外挂的实现
使用MFCdll编写弹出窗口,按键实现相应的逻辑。采用远程线程注入的方式注入连连看。部分代码如下
注入代码
DWORD pid;
printf("输入PID:");
scanf_s("%u", &pid);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hProcess == INVALID_HANDLE_VALUE)
{
printf("获取句柄失败\n");
return 0;
}
TCHAR szBuffer[MAX_PATH] = { 0 };
OPENFILENAME file = { 0 };
file.hwndOwner = NULL;
file.lStructSize = sizeof(file);
file.lpstrFilter = L"Dll文件(*.dll)\0*.dll\0所有文件(*.*)\0*.*\0";//要选择的文件后缀
file.lpstrInitialDir = L"C:\\";//默认的文件路径
file.lpstrFile = szBuffer;//存放文件的缓冲区
file.nMaxFile = sizeof(szBuffer) / sizeof(*szBuffer);
file.nFilterIndex = 0;
file.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER;//标志如果是多选要加上OFN_ALLOWMULTISELECT
BOOL bSel = GetOpenFileName(&file);
// 分配空间
LPVOID addr = VirtualAllocEx(hProcess, NULL, wcslen(file.lpstrFile)*2 + 2, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!addr)
{
printf("分配空间失败\n");
return 0;
}
// 内存拷贝
DWORD bytesWirte = 0;
BOOL bret = WriteProcessMemory(hProcess, addr, file.lpstrFile, wcslen(file.lpstrFile)*2 + 2, &bytesWirte);
if (!bret)
{
printf("写入失败\n");
return 0;
}
// 创建远程线程
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibraryW, addr, 0, NULL);
if (hThread == INVALID_HANDLE_VALUE)
{
printf("创建线程失败\n");
return 0;
}
WaitForSingleObject(hThread, 3000);
DWORD exitCode = 0;
GetExitCodeThread(hThread, &exitCode);
VirtualFreeEx(hProcess, addr, wcslen(file.lpstrFile)*2 + 2, MEM_RELEASE);
return 0;
道具调用的实现。
void callTool(unsigned int toolType){
// 这里实现对道具call的调用
// 参数为不同的道具
_asm{
// 初始化道具内存值
mov ecx, 0x47FDE0
mov ecx, [ecx]
add ecx, 0xa64
mov eax, toolType
sub eax, 0xf0
shl eax, 0x4
lea eax, [ecx + eax + 0x4]
// 修改类型
inc eax
mov edx,toolType
mov dword ptr[eax],edx
// 修改数量
inc eax
mov dword ptr[eax],0x3
// 调用函数
mov ecx, 0x47FDE0
mov ecx, [ecx]
add ecx, 0x494
push toolType
push 0
push 0
//mov eax,0x41E691
//CALL DWORD PTR DS : [EAX + 0x28];
mov eax, [ecx]
add eax, 0x28
mov eax, [eax]
call eax
}
}
消除调用的实现
void reduce(){
// 首先调用获取两点坐标的call
/*
0041E75E > \8B8E F0190000 MOV ECX,DWORD PTR DS:[ESI+0x19F0] 传递对象地址 12fa14+0x19f0+0x494
0041E764 . 8D45 D8 LEA EAX,DWORD PTR SS:[EBP-0x28] 传入参数,点2
0041E767 . 50 PUSH EAX
0041E768 . 8D45 E0 LEA EAX,DWORD PTR SS:[EBP-0x20] 传出参数,点1
0041E76B . 50 PUSH EAX
0041E76C . E8 CEAA0000 CALL kyodai.0042923F 获取坐标call
*/
POINT p1, p2;
// 指南针获取两个可以连接的点
_asm{
// 传递ecx
mov ecx, 0x47FDE0
mov ecx, [ecx]
mov ecx, [ecx + 0x19f0 + 0x494]
lea eax, p1.x
push eax
lea eax, p2.x
push eax
mov eax, 0x42923F
call eax
}
// 调用消除call
/*
0041AB10 |. 8B7D 10 MOV EDI,[ARG.3]
0041AB13 |> 57 PUSH EDI
0041AB14 |. 8D45 F4 LEA EAX,[LOCAL.3]
0041AB17 |. 53 PUSH EBX 12a1f4+1E48+0x30
0041AB18 |. 50 PUSH EAX
0041AB19 |. 8D45 EC LEA EAX,[LOCAL.5]
0041AB1C |. 8BCE MOV ECX,ESI
0041AB1E |. 50 PUSH EAX
0041AB1F |. 0FB645 08 MOVZX EAX,BYTE PTR SS:[EBP+0x8]
0041AB23 |. 69C0 DC000000 IMUL EAX,EAX,0xDC
0041AB29 |. 8D8430 5C1900>LEA EAX,DWORD PTR DS:[EAX+ESI+0x195C]
0041AB30 |. 50 PUSH EAX 数组地址
0041AB31 |. FF75 08 PUSH [ARG.1] 0
0041AB34 |. E8 551B0000 CALL kyodai.0041C68E ; 消除call 6参数
*/
_asm{
push 2;
mov ecx, 0x47FDE0;
mov ecx, [ecx];
mov eax, [ecx + 0x1e84]
add eax, 0x30
push eax
lea eax, p1.x
push eax
lea eax, p2.x
push eax
mov eax, ecx;
add eax, 0x195c;
push eax;
push 0
mov eax, 0x41c68e
call eax
}
}
效果如下图