[系统安全18]面向对象逆向-类、构造函数、析构函数
1 结构体与类
在C++中,结构体与类是非常相似的,都有默认的构造函数与析构函数,也都可以提供自己的接口函数,但是除此之外它们还有有很多不同的。
1.1 变量作用域的识别
具体来说,变量总共分为以下几种类型:
全局变量:全局变量的数据总是存储在PE文件中的数据段或代码段中,在游戏修改社群内也被称为“基址”。
局部变量:局部变量的数据保存在栈中,其生命周期与其所在的函数作用域一致,访问方式是使用ebp或esp间接访问。
静态局部变量:静态局部变量的数据保存方式及访问方式与全局变量基本一致,因此很容易被误认为是全局变量。但是在保存此变量的临近位置往往会有一个标志位控制其具体的作用域,当此标志位为1时证明此变量已经被初始化。
堆变量:堆变量的数据保存在new出来的堆空间中,其作用域由与new对应的delete控制。
代码示例
#include "stdafx.h"
int g_nNum = 1; // 全局变量
int _tmain(int argc, _TCHAR* argv[])
{
int nNum = 2; // 局部变量
static int sNum = 3; // 静态局部变量
int* pNum = new int; // 堆变量
*pNum = 4;
printf("%X %X %X %X ",g_nNum,nNum,sNum,*pNum);
delete pNum;
return 0;
}
Debug汇编代码:
全局变量与静态变量是没有初始化代码的,因为已经初始化了。
add esp, 4
mov [ebp+var_8], 2
push 4
call sub_4031B6
add esp, 4
mov [ebp+var_E0], eax
mov eax, [ebp+var_E0]
mov [ebp+var_14], eax
mov eax, [ebp+var_14]
mov dword ptr [eax], 4
mov eax, [ebp+var_14]
mov ecx, [eax] ; 堆变量赋值压栈
push ecx
mov edx, dword_4B9004 ; 静态变量已经初始化了
push edx
mov eax, [ebp+var_8] ; 局部变量
push eax
mov ecx, dword_4B9000
push ecx ; 全局变量已经初始化了
push offset aXXXX ; "%X %X %X %X "
call sub_403D2D
2 识别构造与析构函数
C++ 与 C 最大的不同就在于 C++ 多了一个类。通过由汇编反推出 C++ 的类相关代码,通过代码证明面向对象编程并不是某些语言所特有的,连汇编都可以。
2.1 快速识别出类
在VS下,一般情况会使用ecx寄存器传递this指针。因为每个成员函数在被调用时都要用this指针作为第一个参数。
C++代码
#include "stdafx.h"
class CObj
{
public:
void ShowMsg(int nID)
{
printf("ID:%d Who is your God? I am!\r\n",nID);
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CObj obj;
obj.ShowMsg(9);
return 0;
}
Debug反汇编代码:
- 1)push参数后,会紧跟着把对象的地址传进ecx
- 2)根据stdcall调用规范,由于ecx是最紧邻call指令的,那么就证明ecx是被当成第一个参数传递进去的。
0104713E push 9
01047140 lea ecx,[obj]
01047143 call CObj::ShowMsg (01043E0Eh)
跟踪进ShowMsg()函数。
01046F90 push ebp
01046F91 mov ebp,esp
01046F93 sub esp,0CCh
01046F99 push ebx
01046F9A push esi
01046F9B push edi
01046F9C push ecx
01046F9D lea edi,[ebp-0CCh]
01046FA3 mov ecx,33h
01046FA8 mov eax,0CCCCCCCCh
01046FAD rep stos dword ptr es:[edi]
01046FAF pop ecx
01046FB0 mov dword ptr [this],ecx ; 保存this指针
18: printf("ID:%d Who is your God? I am!\r\n",nID);
01046FB3 mov eax,dword ptr [nID] ; 获取参数
01046FB6 push eax
01046FB7 push offset string "ID:%d Who is your God? I am!\r\n" (010D5E50h)
01046FBC call _printf (01043CE7h)
01046FC1 add esp,8
19: }
01046FC4 pop edi
01046FC5 pop esi
01046FC6 pop ebx
01046FC7 add esp,0CCh
01046FCD cmp ebp,esp
19: }
识别一个类成员函数的调用,对于简单的代码只能臆测。以下代码为类加上了构造函数,并初始化了成员函数。
#include <Windows.h>
class CObj
{
public:
CObj():m_Obj_1(0xAAAAAAAA),m_Obj_2(0xBBBB)
{
printf("CObj() Constructor...\r\n");
}
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;
};
int _tmain(int argc, _TCHAR* argv[])
{
CObj obj;
obj.Show(0);
return 0;
}
两次函数调用的参数都用到了ecx,通过反汇编代码可知ecx没有变化。通过以上论据,判断这是两个成员函数。
002471C0 push ebp
002471C1 mov ebp,esp
002471C3 sub esp,0xD0
002471C9 push ebx
002471CA push esi ; 9-51.<ModuleEntryPoint>
002471CB push edi
002471CC lea edi,[local.52]
002471D2 mov ecx,0x34
002471D7 mov eax,0xCCCCCCCC
002471DC rep stos dword ptr es:[edi]
002471DE lea ecx,[local.3] ; 获取变量地址到ecx中,保存this指针
002471E1 call 9-51.00243D73 ; 调用构造函数
002471E6 push 0x0
002471E8 lea ecx,[local.3] ; 获取变量地址到ecx中,保存this指针
002471EB call 9-51.00243B43 ; 调用类成员函数
002471F0 xor eax,eax
跟进构造函数中的反汇编代码如下:
00246FA0 push ebp
00246FA1 mov ebp,esp
00246FA3 sub esp,0CCh
00246FA9 push ebx
00246FAA push esi
00246FAB push edi
00246FAC push ecx
00246FAD lea edi,[ebp+FFFFFF34h]
00246FB3 mov ecx,33h
00246FB8 mov eax,0CCCCCCCCh
00246FBD rep stos dword ptr es:[edi]
00246FBF pop ecx
00246FC0 mov dword ptr [ebp-8],ecx ;调用ecx,保存this指针
13: class CObj
14: {
15: public:
16: CObj():m_Obj_1(0xAAAAAAAA),m_Obj_2(0xBBBB)
00246FC3 mov eax,dword ptr [ebp-8]
00246FC6 mov dword ptr [eax],0AAAAAAAAh ;m_Obj_1
00246FCC mov eax,0BBBBh ;m_Obj_2
00246FD1 mov ecx,dword ptr [ebp-8]
00246FD4 mov word ptr [ecx+4],ax
18: printf("CObj() Constructor...\r\n");
00246FD8 push 2D5E50h
00246FDD call 00243CEC
00246FE2 add esp,4
19: }
这里使用vs直接调试(去掉显示符号即可以对比,不用在OD和IDA里反复切换)。
通过回忆 C++ 的知识,C++的类中this指针始终指向本类结构的起始地址。
以下汇编中,ecx作为一个指针被当做参数传递了进来,并且还将ecx指向的地方当做了一个结构体,分别为其赋了两个值。
其次,保存有ecx值的[ebp-8]又被当做返回值传了回去。
16: CObj():m_Obj_1(0xAAAAAAAA),m_Obj_2(0xBBBB)
00246FC3 mov eax,dword ptr [ebp-8]
00246FC6 mov dword ptr [eax],0AAAAAAAAh
00246FCC mov eax,0BBBBh
00246FD1 mov ecx,dword ptr [ebp-8]
00246FD4 mov word ptr [ecx+4],ax;将ecx指向的地方当做了一个结构体,分别为其赋了两个值
...
00246FE5 mov eax,dword ptr [ebp-8] ;ecx的值又被当做返回值传了回去
因此,对this指针指向的内容进行操作,就是对类内部的成员进行操作。
跟进成员函数查看:
由ecx寻址,并给一个地方赋值,结合上下文,大致判断是类内成员变量赋值。
20: void Show(int nID)
21: {
00247020 push ebp
00247021 mov ebp,esp
00247023 sub esp,0CCh
00247029 push ebx
0024702A push esi
0024702B push edi
0024702C push ecx
0024702D lea edi,[ebp+FFFFFF34h]
00247033 mov ecx,33h
00247038 mov eax,0CCCCCCCCh
0024703D rep stos dword ptr es:[edi]
0024703F pop ecx
00247040 mov dword ptr [ebp-8],ecx ;保存this指针
22: m_Obj_1 = 1;
00247043 mov eax,dword ptr [ebp-8] ; eax = nID
00247046 mov dword ptr [eax],1
23: printf("ID:%d Who is your God? I am!\r\n",nID);
0024704C mov eax,dword ptr [ebp+8]
0024704F push eax
00247050 push 2D5E6Ch
00247055 call 00243CEC
0024705A add esp,8
24: }
综上所述,得出以下两条经验:
- 紧盯采用ecx传参的函数调用
- 留意以ecx为指针进行数据操作的代码
2.2 识别构造函数
- 判定同一个this指针的调用。
像上一个例子一样,两次call的ecx参数都来自同一位置,那么我们就要考虑验证它的第一个call调用的是否为构造函数了
- 构造函数内部会将this指针作为返回值
00246FE5 mov eax,dword ptr [ebp-8] ;ecx的值又被当做返回值传了回去
2.3 识别析构函数
简单的类识别析构函数并不靠谱,因为只有一个特征,从调用上来说,同一this指针的最后一次被使用时调用的函数就有可能是析构函数。
代码示例
#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");
}
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;
};
int _tmain(int argc, _TCHAR* argv[])
{
CObj obj;
obj.Show(0);
return 0;
}
Debug
通过VS的反汇编发现加了析构函数后多了一个成员函数的调用。
34: int _tmain(int argc, _TCHAR* argv[])
35: {
00FD7230 push ebp
00FD7231 mov ebp,esp
00FD7233 push 0FFFFFFFFh
00FD7235 push 1063528h
00FD723A mov eax,dword ptr fs:[00000000h]
00FD7240 push eax
00FD7241 sub esp,0DCh
00FD7247 push ebx
00FD7248 push esi
00FD7249 push edi
00FD724A lea edi,[ebp+FFFFFF18h]
00FD7250 mov ecx,37h
00FD7255 mov eax,0CCCCCCCCh
00FD725A rep stos dword ptr es:[edi]
00FD725C mov eax,dword ptr ds:[01089004h]
00FD7261 xor eax,ebp
00FD7263 push eax
00FD7264 lea eax,[ebp-0Ch]
00FD7267 mov dword ptr fs:[00000000h],eax
36: CObj obj;
00FD726D lea ecx,[ebp-18h]
00FD7270 call 00FD3D78
00FD7275 mov dword ptr [ebp-4],0
37: obj.Show(0);
00FD727C push 0
00FD727E lea ecx,[ebp-18h]
00FD7281 call 00FD3B43
38: return 0;
00FD7286 mov dword ptr [ebp+FFFFFF1Ch],0
00FD7290 mov dword ptr [ebp-4],0FFFFFFFFh
00FD7297 lea ecx,[ebp-18h]
00FD729A call 00FD3BB1 ;成员函数调用
00FD729F mov eax,dword ptr [ebp+FFFFFF1Ch]
39: }
跟踪到其中
20: ~CObj() // 新添加的析构函数
21: {
00FD7030 push ebp
00FD7031 mov ebp,esp
00FD7033 sub esp,0CCh
00FD7039 push ebx
00FD703A push esi
00FD703B push edi
00FD703C push ecx
00FD703D lea edi,[ebp+FFFFFF34h]
00FD7043 mov ecx,33h
00FD7048 mov eax,0CCCCCCCCh
20: ~CObj() // 新添加的析构函数
21: {
00FD704D rep stos dword ptr es:[edi]
00FD704F pop ecx
00FD7050 mov dword ptr [ebp-8],ecx
22: printf("CObj() Destructor...\r\n");
00FD7053 push 1065E6Ch
00FD7058 call 00FD3CF1
00FD705D add esp,4
23: }
无法在这个函数中找到任何有别于其他成员函数的特征,所以无从判断。如果想以比较可靠的方式识别出一个类的析构函数,那么需要类满足以下两个条件:
- 包含有虚函数
- 包含有空间申请的操作