文章目錄
0. 準備工作
目標:
- 找到程序本身的exe
- 去掉程序中的廣告
- 實現輔助工具
先打開遊戲看一下,開始遊戲前有兩個彈窗,關閉後會打開一個網頁。
0.0 涉及知識
已知:
- 這是一個vc6 MFC程序
- 多進程(彈窗廣告)
- 有動態修改代碼,直接複製exe代碼是不能執行的
涉及技術:
- MFC dll
- SetWindowLong修改回調函數
- CallWindowProc調用指定回調函數
- 多線程
0.1 分析思路
找到原程序exe:
- 加殼
- 多進程:需要進程遍歷,對CreateProcessA/W下斷調試(棧回溯)
去廣告:
- 對話框,網頁
- 涉及API:
-
- DialogBoxA/W
-
- CreateWindowExA/W
-
- WinExec
-
- CreateProcessA/W
-
- ShellExecuteA/W
輔助:
- 分析+算法+模擬點擊:找到數組,一鍵自動連接
- 利用遊戲本身功能:分析指南針道具call
比如,一鍵調用指南針, 一鍵消除,秒殺(循環單消),炸彈(前提是有這個道具),
0.2 需要的數據
- 連連看數組
- 指南針call
- 消除call
0.3 分析工具
OD,CE,PEid/exeinfo
VS開發。
0.4 測試框架
需要一個注入程序,一個靜態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的消息機制造成干擾,我們單獨創建一個線程發送自定義消息。
1. 開幹
1.0 分析exe
打開遊戲,關閉開始界面,通過pchunter可以看出真正的遊戲是kyodai.exe,但經過打包後它是不能直接打開的。
在彈出最後一個廣告的時候,OD附加廣告。這個廣告進程的後綴是ocx,其實只要在遍歷的進程中出現,本質還是exe。
由啓動過程來看,這個程序是多進程的,對CreateProcessA/W
下斷,開始遊戲,斷下後分析API參數,看到CreationFlags==CREATE_SUSPEND
,猜測掛起後會動態修改,然後喚醒。也就是創建掛起進程-修改-喚醒
.
這裏的思路是,雙擊不能運行,那麼肯定有動態修改。
所以,我們要對WriteProcessMemory(), ResumeThread()
下斷。
1.1 動態修改
斷在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,下面要實現輔助工具。
1.2 分析指南針道具
道具是有數量的,所以可以用CE。但失敗了。。。
1.3 分析數組
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 <JMP.&MFC42.#825>
從遊戲界面上數一下,0x13是每行的個數,0xD1是總數,所以這段代碼應該是按行初始化。
1.4 再次分析指南針道具
既然確定了數組,那麼就設置內存斷點,使用道具,斷下後查看堆棧,裏面肯定有一個調用是使用道具。最終確定了兩個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就變成了調用炸彈。
1.5 分析消除調用
剛剛已經分析了獲取兩個點的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);
}
2. BUG
避免消息衝突,自定義消息最好大一些,我是從WM_USER+100
開始的。