虛函數存在是為了克服類型域解決方案的缺陷,以使程序員可以在基類里聲明一些能夠在各個派生類里重新定義的函數。
1 識別簡單的虛函數
代碼示例:
#include "stdafx.h"
#include <Windows.h>class CObj
{
public:CObj():m_Obj_1(0xAAAAAAAA),m_Obj_2(0xBBBB){printf("CObj() Constructor...\r\n");}~CObj(){printf("CObj() Destructor...\r\n");}virtual void Show(int nID) // 注意這里{m_Obj_1 = 1;printf("ID:%d Who is your God? I am!\r\n",nID);}
private:int m_Obj_1;WORD m_Obj_2;
};class CPeople : public CObj
{
public:CPeople():m_People_1(0xCCCCCCCC),m_People_2(0xDDDD){printf("CPeople() Constructor...\r\n");}~CPeople(){printf("CPeople() Destructor...\r\n");}void Show(int nID){printf("ID:%d People!\r\n",nID);}
private:int m_People_1;WORD m_People_2;
};int _tmain(int argc, _TCHAR* argv[])
{CObj obj;CPeople people;CObj *pobj;pobj = &obj;pobj->Show(0);pobj = &people;pobj->Show(1);return 0;
}
// ---------- 輸出結果 ----------
// CObj() Constructor...
// CObj() Constructor...
// CPeople() Constructor...
// ID:0 Who is your God? I am!
// ID:1 People!
// CPeople() Destructor...
// CObj() Destructor...
// CObj() Destructor...
// ----------------------------
反匯編代碼:
int _tmain(int argc, _TCHAR* argv[])
{
001273B0 push ebp
001273B1 mov ebp,esp
001273B3 push 0FFFFFFFFh
001273B5 push 1B3730h
001273BA mov eax,dword ptr fs:[00000000h]
001273C0 push eax
001273C1 sub esp,108h
001273C7 push ebx
001273C8 push esi
001273C9 push edi
001273CA lea edi,[ebp+FFFFFEECh]
001273D0 mov ecx,42h
001273D5 mov eax,0CCCCCCCCh
001273DA rep stos dword ptr es:[edi]
001273DC mov eax,dword ptr ds:[001D9004h]
001273E1 xor eax,ebp
001273E3 push eax
001273E4 lea eax,[ebp-0Ch]
001273E7 mov dword ptr fs:[00000000h],eax ; 棧保護基址相關代碼CObj obj;
001273ED lea ecx,[ebp-1Ch] ; this 指針
001273F0 call 00123D87 ; CObj::CObj (0123D87h)
001273F5 mov dword ptr [ebp-4],0 ; 異常處理的輔助標志,以-1為結尾CPeople people;
001273FC lea ecx,[ebp-38h] ; this指針
001273FF call 001211DB ; CPeople::CPeople (01211DBh)
00127404 mov byte ptr [ebp-4],1 CObj *pobj;pobj = &obj;
00127408 lea eax,[ebp-1Ch] ; 將obj的this指針給eax
0012740B mov dword ptr [ebp-44h],eax ; 將this指針給pobj的指針pobj->Show(0);
0012740E mov esi,esp
00127410 push 0 ; 參數壓棧
00127412 mov eax,dword ptr [ebp-44h]
00127415 mov edx,dword ptr [eax]
00127417 mov ecx,dword ptr [ebp-44h] ; 將Obj的指針(指向的是 Obj的this指針)給ecx
0012741A mov eax,dword ptr [edx] ; 將Obj的this指針所指向的第一項的內容(即Vtbl的第一個元素)給eax
0012741C call eax
0012741C ; 在調用完CPeople的構造后,程序采用如下步驟實現
0012741C ; pobj = &obj;
0012741C ; pobj ->Show(0);
0012741C ;
0012741C ; 1、將創建完的Obj對象的this指針傳遞給pobj
0012741C ; 2、將指this指針給eax
0012741C ; 3、將this指針第一項(即虛函數表指針)傳遞給edx
0012741C ; 4、將pobj的值傳遞給ecx(注意此步)
0012741C ; 5、將既虛函數表數組的地址傳遞給eax
0012741C ; 6、調用eax
0012741E cmp esi,esp
00127420 call 00122329 pobj = &people;
00127425 lea eax,[ebp-38h] ; 將People的this指針傳給eax
00127428 mov dword ptr [ebp-44h],eax ; 將People的this指針給Objpobj->Show(1);
0012742B mov esi,esp
0012742D push 1 ; 參數壓棧
0012742F mov eax,dword ptr [ebp-44h] ; 將People的this指針給eax
00127432 mov edx,dword ptr [eax] ; 將this指針中的第一項,即Vptr給edx
00127434 mov ecx,dword ptr [ebp-44h] ; 將People的this指針給ecx
00127437 mov eax,dword ptr [edx] ; 將Vptr指向的Vtbl給eax
00127439 call eax ; 調用eax
0012743B cmp esi,esp
0012743D call 00122329 ; __RTC_CheckEspreturn 0;
00127442 mov dword ptr [ebp+FFFFFEF0h],0
0012744C mov byte ptr [ebp-4],0
00127450 lea ecx,[ebp-38h]
00127453 call 00121E10
00127458 mov dword ptr [ebp-4],0FFFFFFFFh
0012745F lea ecx,[ebp-1Ch]
00127462 call 00123BC0
00127467 mov eax,dword ptr [ebp+FFFFFEF0h]
}
如果沒有Debug的符號文件,或者逆向過程中代碼不是我們自己寫的,那就要先判斷它是否是一個類的應用。
跟進函數內部情況:
class CObj
{
00126FD0 push ebp
00126FD1 mov ebp,esp
00126FD3 sub esp,0CCh
00126FD9 push ebx
00126FDA push esi
00126FDB push edi
00126FDC push ecx
00126FDD lea edi,[ebp-0CCh]
00126FE3 mov ecx,33h
00126FE8 mov eax,0CCCCCCCCh
00126FED rep stos dword ptr es:[edi]
00126FEF pop ecx
00126FF0 mov dword ptr [this],ecx ; 取this指針 this == [ebp-8]
00126FF3 mov eax,dword ptr [this] ; 取this指針
00126FF6 mov dword ptr [eax],offset CObj::`vftable' (01B5E54h)
public:CObj():m_Obj_1(0xAAAAAAAA),m_Obj_2(0xBBBB)
00126FFC mov eax,dword ptr [this]
00126FFF mov dword ptr [eax+4],0AAAAAAAAh ; 初始化m_Obj_1為0xAAAAAAAA
00127006 mov eax,0BBBBh ; 初始化m_Obj_2為0xBBBB
0012700B mov ecx,dword ptr [this] ; this指針 this == ecx-8
0012700E mov word ptr [ecx+8],ax printf("CObj() Constructor...\r\n");
00127012 push offset string "CObj() Constructor...\r\n" (01B5E5Ch) printf("CObj() Constructor...\r\n");
00127017 call _printf (0123D00h)
0012701C add esp,4 }
0012701F mov eax,dword ptr [this] ; 將this指針作為返回值 this == ebp-8
00127022 pop edi
00127023 pop esi
00127024 pop ebx
00127025 add esp,0CCh
0012702B cmp ebp,esp
0012702D call __RTC_CheckEsp (0122329h)
00127032 mov esp,ebp
00127034 pop ebp
00127035 ret
通過閱讀以上代碼可以得出以下過程:
1)找出虛表位置,以及操作的流程
- 代碼里的例子操作了虛表 00126FF6 mov dword ptr [eax],offset CObj::`vftable' (01B5E54h)
這是一個保存函數地址的指針,再通過匯編上下文的猜測,則可大致確定這就是一個虛表,且將值傳到了寄存器參數ecx記錄地址的第一項。
以寄存器參數ecx為首地址,分別給其4偏移與8偏移處賦值
寄存器參數ecx又作為返回值傳了回去。
通過調用函數的分析,ecx里保存的是this指針,并且根據類的內存結構可知,this里的第一項是Vptr。
2)識別構造函數
- 由于此成員函數是第一個被調用的,通過代碼看出匯編函數中的第二件事是初始化數據成員。最后一件事是將this指針當做返回值返回,所以推測該函數為構造函數。
3)逐步分析函數
- 構造函數與析構函數會對Vptr操作。
- 在VS默認設置下,構造與析構前都會有相應的異常處理標記置位操作。
- 虛函數的調用一般采用eax。
2 識別較復雜的虛函數
經驗小結:
- new出來的對象會以其在堆中申請空間的指針作為this指針傳入參與構造。
- new出來的對象其虛函數調用的尋址方式與普通構造出來的不同。
- delete對象時會先析構自己,再析構父類,最后再執行delete。
- new出來的對象如果其成員函數派生于純虛函數,在delete時只調用父類的析構。
- 如果此類為抽象類(包含純虛函數),那么其虛表的對應項會填充指向庫函數__purecall的函數指針。
虛函數調用的固定模式,緊盯對各個虛表的操作。從而根據上下文即可大致確定虛函數的調用與類的析構與構造。
3 識別類的繼承關系
- 根據構造函數內的構造順序分辨此函數所屬類的繼承情況
- 總結并記錄分析結果
- VS的release版中存在同時使用ecx、esi寄存器傳遞this指針的情況。
4 逆向MFC程序
MFC程序關鍵特征點
版本 | 對應動態庫 | 靜態庫中使用MFC時的特征 | 動態庫中使用MFC的特征 |
---|---|---|---|
4.0 | mfc40.dll | call [ebp+0x14] | call [ebp+0x14] |
6.0 | mfc42.dll | call [ebp+0x14] | call [ebp+0x14] |
7.1 | mfc71.dll | call [ebp+0x14] | call [ebp+0x14] |
10.0 | mfc100.dll | call [ebp+0x14] | mov edx,[ebp+0x14] |
分析核心重點
1)判斷目標程序是不是MFC程序,如果是,判斷其MFC版本
OD快捷鍵:Ctrl+E 打開模塊窗口,并在模塊窗口尋找類似于mfc*.dll這樣的模塊。
如果找到了就可以根據DLL的名稱判定程序所用的MFC版本,如果找不到則證明這是一個在靜態庫中使用MFC的程序。
2)根據目標程序調用MFC方式的不同而采取不同的方式搜索特征
OD快捷鍵:Ctrl+F 搜索特征 call [ebp+0x14]
由于搜索的特征位于消息分發函數里,因此特征指令所在的位置應該是一個非常大的switch-case。
3)在合適的地方下斷點,并跟進到相應消息的函數中。
設置按鈕點擊事件下斷點,即可跟進到達相應消息的函數中。
這里可以參考:
看雪《MFC程序逆向》
https://bbs.pediy.com/thread-54150.htm