目标:
先打开游戏看一下,开始游戏前有两个弹窗,关闭后会打开一个网页。
已知:
涉及技术:
找到原程序exe:
去广告:
辅助:
比如,一键调用指南针, 一键消除,秒杀(循环单消),炸弹(前提是有这个道具),
OD,CE,PEid/exeinfo
VS开发。
需要一个注入程序,一个静态MFC dll。
unsigned __stdcall threadProc()
{
CLLKAssistDlg* pAssistDlg = new CLLKAssistDlg;
pAssistDlg->DoModal();
return 0;
}
LRESULT CALLBACK wndProc()
{
switch (msg){}
return CallWindowProc(g_oldWndProc);
}
BOOL CInjectLLKApp::InitInstance()
{
SetWindowLong(wndProc);
_beginthreadex(threadProc);
}
为了防止MFC的消息机制造成干扰,我们单独创建一个线程发送自定义消息。
打开游戏,关闭开始界面,通过pchunter可以看出真正的游戏是kyodai.exe,但经过打包后它是不能直接打开的。
在弹出最后一个广告的时候,OD附加广告。这个广告进程的后缀是ocx,其实只要在遍历的进程中出现,本质还是exe。
由启动过程来看,这个程序是多进程的,对CreateProcessA/W
下断,开始游戏,断下后分析API参数,看到CreationFlags==CREATE_SUSPEND
,猜测挂起后会动态修改,然后唤醒。也就是创建挂起进程-修改-唤醒
.
这里的思路是,双击不能运行,那么肯定有动态修改。
所以,我们要对WriteProcessMemory(), ResumeThread()
下断。
断在WriteProcessMemory后,观察地址参数为0x43817A
,这就是kyodai进程被修改的地址,并且写入了一个字节\0
。
往后分析,这应该是一个Themida虚拟机保护(
push xxx; jmp xxx
),vmp一般是push xxx; call xxx
可以再开一个OD附加kyodai进程,用dump的方式得到原exe。
也可以用十六进制编辑器修改RVA为43817A的字节为00.
用lordPE得到文件偏移。
可以看到这里本来是0x01,修改为0x00即可。注意这个文件的tab处有个小锁,说明是只读的,所以要先copy一下再修改保存。
至此已经将广告去除得到真正的游戏exe,下面要实现辅助工具。
道具是有数量的,所以可以用CE。但失败了。。。
rand()下断,重新开始,肯定停在初始化的地方,可以在这里尝试找基址。
这里调用了rand,堆栈查看上一层:
0041CB15 8B8E 841E0000 MOV ECX,DWORD PTR DS:[ESI+0x1E84] ; this
0041CB1B 83C4 0C ADD ESP,0xC
0041CB1E 6A 01 PUSH 0x1
0041CB20 E8 E9C30000 CALL kyodai_c.00428F0E ; 初始化数组???
这里有ecx,猜测是对象调用成员方法,可以查看ecx地址0x003CD418
。
003CD418 0044D07C kyodai_c.0044D07C
003CD41C 0012BB50
003CD420 00000002
003CD424 00000002
003CD428 00000000
第一个成员应该是虚函数表,第二个地址指向栈,猜测是数组成员。
0012BB50 DC 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00
0012BB60 00 00 00 00 00 01 00 00 00 00 00 00 00 00 01 01
0012BB70 01 00 00 00 00 00 00 01 01 01 00 00 00 00 00 00
...
上面是初始化之前,下面看看初始化之后是否发生变化。
0012BB50 DC 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 ?.............
0012BB60 00 00 00 00 00 0C 00 00 00 00 00 00 00 00 F6 09 ..............?
0012BB70 04 00 00 00 00 00 00 0E F7 05 00 00 00 00 00 00 ......?......
0012BB80 0A 0A F0 02 04 00 00 00 00 08 09 0C F6 05 00 00 ..?......?..
0012BB90 00 00 0A 02 09 0E 0F 0D 0E 00 00 F7 F6 07 F6 0D .......黯?
0012BBA0 08 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ..............
0012BBB0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000DC
可能是大小。后面4字节不确定。后面两个0D应该就是黄球。
那么,0x12BB58
应该就是数组基址。
跳进call 0x00428F0E
,一进去就把mov dword ptr [ebp-0xc], ecx
,所以把[ebp-0xc]
就是this。
下面有段关键代码:
00428FEA 33C9 XOR ECX,ECX ; do{}
00428FEC 8B45 F4 MOV EAX,DWORD PTR SS:[EBP-0xC] ; do{}
;...
00429007 ^ 7C E3 JL SHORT kyodai_c.00428FEC ; while(ecx < 0x13)
00429009 83C7 13 ADD EDI,0x13
0042900C 81FF D1000000 CMP EDI,0xD1
00429012 ^ 7C D6 JL SHORT kyodai_c.00428FEA ; while(edi < 0xD1)
00429014 56 PUSH ESI
00429015 E8 36E80000 CALL
从游戏界面上数一下,0x13是每行的个数,0xD1是总数,所以这段代码应该是按行初始化。
既然确定了数组,那么就设置内存断点,使用道具,断下后查看堆栈,里面肯定有一个调用是使用道具。最终确定了两个call(其实有3层是内部调用关系,我们只关心最下面两层):
0041DE4D 8B86 94040000 MOV EAX,DWORD PTR DS:[ESI+0x494] ;esi == 0x12A1F4
0041DE53 8D8E 94040000 LEA ECX,DWORD PTR DS:[ESI+0x494]
0041DE59 52 PUSH EDX ;f0 ;这其实是道具ID
0041DE5A 53 PUSH EBX ;00
0041DE5B 53 PUSH EBX ;00
0041DE5C FF50 28 CALL DWORD PTR DS:[EAX+0x28] ; kyodai_c.0041E691
这段代码可以用来辅助调用工具。
第二个call(包含在上一个call里):
0041E6AC 8BF1 MOV ESI,ECX ; esi changed
;...
0041E75E 8B8E F0190000 MOV ECX,DWORD PTR DS:[ESI+0x19F0] ;this==[0x12A1F4+0x494+0x19F0]==[0x12A1F4+0x1E84]
0041E764 8D45 D8 LEA EAX,DWORD PTR SS:[EBP-0x28] ;0x00129D8C
0041E767 50 PUSH EAX
0041E768 8D45 E0 LEA EAX,DWORD PTR SS:[EBP-0x20] ;0x00129D94
0041E76B 50 PUSH EAX
0041E76C E8 CEAA0000 CALL kyodai_c.0042923F
这里传入两个地址,应该是传出参数,传出可以连接的两个点,所以在内存里监视一下。
00129D8C 00129DA4
00129D90 0012A254
00129D94 0012A254
00129D98 0040CA97 kyodai_c.0040CA97
调用之后:
00129D8C 0000000B
00129D90 00000001
00129D94 00000006
00129D98 00000001
再看下游戏:
(1,11), (1,6)
处的两个人可以相连。
如果想要一键调用这个工具,就需要this指针(ecx),根据上面的代码,由esi+0x494
加上偏移0x19f0
得到,观察一下esi的值是0x12A1F4
,用CE找一下。
OD里查找这3个常量,0x45DEBC
多次使用,估计是全局变量。
mov ecx, 0x45DEBC;
mov ecx, [ecx];
mov eax, DWORD PTR DS : [ecx + 0x494];
LEA ECX, DWORD PTR DS : [ecx + 0x494];
PUSH 0xF0;
PUSH 00;
PUSH 00;
CALL DWORD PTR DS : [EAX + 0x28];
这段汇编可以用来一键调用指南针。
同样地思路分析一下炸弹道具会发现,第一个call的F0参数是指南针ID,换成F4就变成了调用炸弹。
刚刚已经分析了获取两个点的call,还需要找到消除的call。
消除的时候会将数组对应位置清零,所以可以下内存写入断点。断下后堆栈窗口给几个call下断,继续游戏,再次消除后分析这些call,根据参数个数找到消除call,参数里应该有之前获得的两个点,最终锁定了这个call:
0041AA90 MOV EBX,DWORD PTR SS:[EBP+0xC] ; ebx=坐标数组
;...
0041AB10 MOV EDI,DWORD PTR SS:[EBP+0x10]
0041AB13 PUSH EDI ; num???
0041AB14 LEA EAX,DWORD PTR SS:[EBP-0xC]
0041AB17 PUSH EBX ; addr 0x56D448
0041AB18 PUSH EAX ; point 1
0041AB19 LEA EAX,DWORD PTR SS:[EBP-0x14]
0041AB1C MOV ECX,ESI
0041AB1E PUSH EAX ; point 2
0041AB1F MOVZX EAX,BYTE PTR SS:[EBP+0x8]
0041AB23 IMUL EAX,EAX,0xDC ; eax=0
0041AB29 LEA EAX,DWORD PTR DS:[EAX+ESI+0x195C] ; esi==0x12A1F4
0041AB30 PUSH EAX ; array
0041AB31 PUSH DWORD PTR SS:[EBP+0x8] ; 0
0041AB34 CALL kyodai_c.0041C68E ; 6个参数
先看简单的,最后一个参数是个变量,我们偷点懒,先不管他,push一个固定值4; 第0个参数是0,第1个参数是数组,这里没有用[0x12A1F4+0x1E84]
获取指针,而是另一种偏移。
两个点也很好解决。问题是那个push ebx.
参数ebx(0x0064D448
)是一个地址,观察一下是两个点的坐标:
0064D448 00000009
0064D44C 00000003
0064D450 0000000A
0064D454 00000003
这个地址该怎么获取呢。堆栈回溯分析一下,上一层是一个有7个参数的调用:
0041B440 MOV ECX,DWORD PTR DS:[ESI+0x1E84] ; 0x494+0x19F0 == 0x1E84, esi == 0x12A1F4
0041B446 LEA EAX,DWORD PTR SS:[EBP-0x18]
0041B449 PUSH EAX ; 传出参数 ebp-0x18
0041B44A MOV DWORD PTR SS:[EBP-0x18],EBX
0041B44D CALL kyodai_c.00429025 ; [ebp-0x18] == 下标数组
;....
0041B4AF PUSH EAX
0041B4B0 PUSH 0x1
0041B4B2 PUSH EDI
0041B4B3 PUSH DWORD PTR SS:[EBP-0x18] ; 坐标数组
0041B4B6 PUSH EBX
0041B4B7 CALL kyodai_c.0041AA1D ; 7个参数
也就是说这个地址是上一层函数的一个局部指针变量[ebp-0x18]
,ebp-0x18
作为传出参数在CALL kyodai_c.00429025
处获得下标数组,而且这个函数的this是我们熟悉的[ESI+0x1E84]
。进去看看。
00429025 8B5424 04 MOV EDX,DWORD PTR SS:[ESP+0x4]
00429029 8D41 30 LEA EAX,DWORD PTR DS:[ECX+0x30]
0042902C 8902 MOV DWORD PTR DS:[EDX],EAX
0042902E 8B41 50 MOV EAX,DWORD PTR DS:[ECX+0x50]
00429031 C2 0400 RETN 0x4
很短(极度舒适),这个地址是this+0x30
。
那么,下面就是我们的一键消除消息处理代码。
case WM_KILL:
{
OutputDebugStringW(L"recv WM_KILL");
POINT point1 = { 0 };
POINT point2 = { 0 };
/******************
* Get 2 points
******************/
__asm {
/*
* Help locate
*/
mov eax, eax;
mov eax, eax;
mov eax, eax;
}
__asm {
mov ecx, 0x45DEBC;
mov ecx, [ecx]; //ecx = 0x12A1F4;
LEA ECX, DWORD PTR DS : [ecx + 0x494];
mov ecx, dword ptr[ecx + 0x19f0]; //ecx = this
LEA EAX, point1;
PUSH EAX;
LEA EAX, point2;
PUSH EAX;
mov eax, 0x0042923F;
CALL eax;
}
CString str;
str.Format(L"point1(%d, %d), point2(%d, %d)", point1.x, point1.y, point2.x, point2.y);
OutputDebugStringW(str.GetBuffer());
/******************
* 消除
********************/
__asm {
push 0x4; //arg5
mov ecx, 0x45DEBC;
mov ecx, [ecx]; //ecx = 0x12A1F4;
lea eax, DWORD PTR DS : [ecx + 0x494];
mov eax, dword ptr[eax + 0x19f0]; //ecx = this
add eax, 0x30;
push eax; //arg4
lea eax, point1;
push eax; //arg3
lea eax, point2;
push eax; //arg2
mov ecx, 0x45DEBC;
mov ecx, [ecx]; //ecx = 0x12A1F4;
lea eax, DWORD PTR DS : [ecx + 0x195c];//获取数组的另一种方法
push eax; //arg1, array
push 0; //arg0
mov eax, 0x41C68E;
call eax;
}
break;
//return DefWindowProc(hWnd, msg, wParam, lParam);
}
用一键消除全部消除后继续消除,发现坐标(0,0)
出还有消除特效,所以这时寻找两点会返回(0,0)
。
加个循环发送消息,条件判断全部消除时返回-1就可以了。
LRESULT CALLBACK wndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_POINT:
{
OutputDebugStringW(L"recv WM_POINT");
__asm {
/*
* Help locate
*/
mov eax, eax;
mov eax, eax;
mov eax, eax;
}
__asm {
mov ecx, 0x45DEBC;
mov ecx, [ecx];
mov eax, DWORD PTR DS : [ecx + 0x494];
LEA ECX, DWORD PTR DS : [ecx + 0x494];
PUSH 0xF0;
PUSH 00;
PUSH 00;
CALL DWORD PTR DS : [EAX + 0x28];
}
break;
//return DefWindowProc(hWnd, msg, wParam, lParam);
}
case WM_KILL:
{
OutputDebugStringW(L"recv WM_KILL");
POINT point1 = { 0 };
POINT point2 = { 0 };
/******************
* Get 2 points
******************/
__asm {
/*
* Help locate
*/
mov eax, eax;
mov eax, eax;
mov eax, eax;
}
__asm {
mov ecx, 0x45DEBC;
mov ecx, [ecx]; //ecx = 0x12A1F4;
LEA ECX, DWORD PTR DS : [ecx + 0x494];
mov ecx, dword ptr[ecx + 0x19f0]; //ecx = this
LEA EAX, point1;
PUSH EAX;
LEA EAX, point2;
PUSH EAX;
mov eax, 0x0042923F;
CALL eax;
}
CString str;
str.Format(L"point1(%d, %d), point2(%d, %d)", point1.x, point1.y, point2.x, point2.y);
OutputDebugStringW(str.GetBuffer());
if (point1.x == 0 && point1.y == 0 && point2.x == 0 && point2.y == 0)
{
return -1;
}
/******************
* 消除
********************/
__asm {
push 0x4; //arg5
mov ecx, 0x45DEBC;
mov ecx, [ecx]; //ecx = 0x12A1F4;
lea eax, DWORD PTR DS : [ecx + 0x494];
mov eax, dword ptr[eax + 0x19f0]; //ecx = this
add eax, 0x30;
push eax; //arg4
lea eax, point1;
push eax; //arg3
lea eax, point2;
push eax; //arg2
mov ecx, 0x45DEBC;
mov ecx, [ecx]; //ecx = 0x12A1F4;
lea eax, DWORD PTR DS : [ecx + 0x195c];//获取数组的另一种方法
push eax; //arg1, array
push 0; //arg0
mov eax, 0x41C68E;
call eax;
}
break;
//return DefWindowProc(hWnd, msg, wParam, lParam);
}
case WM_BOMB:
{
OutputDebugStringW(L"recv WM_BOMB");
__asm {
/*
* Help locate
*/
mov eax, eax;
mov eax, eax;
mov eax, eax;
}
__asm {
mov ecx, 0x45DEBC;
mov ecx, [ecx];
mov eax, DWORD PTR DS : [ecx + 0x494];
LEA ECX, DWORD PTR DS : [ecx + 0x494];
PUSH 0xF4;
PUSH 00;
PUSH 00;
CALL DWORD PTR DS : [EAX + 0x28];
}
}
}
//return DefWindowProc(hWnd, msg, wParam, lParam);
return CallWindowProc(g_oldWndProc, hWnd, msg, wParam, lParam);
}
避免消息冲突,自定义消息最好大一些,我是从WM_USER+100
开始的。