[toc]
一、识别程序的特征
如何识别程序的特征
- PE 工具查看链接器版本
- PE 工具查看区段信息
- 查看目标程序的二进制特征
BC++ 程序
区段特征:
导入导出表以及只读数据是分开的
链接器版本:5.0
程序的二进制特征:
1
EB 10 66 62 3A 43 2B 2B 48 4F 4F 4B 90
BC++ 编写的程序,IAT 函数的调用通常是 E8 跳转表 + FF25
- Delphi 程序
区段特征:
这三个通常是 .text,.data,.textbss等
链接器版本:2.25
- 程序的二进制特征:5个 `call` 后紧跟着一堆 0,第一个 `call` 里面调用 `GetModuleHandle` 函数
![image-20191220142750565](C:\Users\14622\AppData\Roaming\Typora\typora-user-images\image-20191220142750565.png)
![image-20191220142912321](C:\Users\14622\AppData\Roaming\Typora\typora-user-images\image-20191220142912321.png)
1
55 8B EC 83 C4 F0 B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? A1 ?? ?? ?? ?? 8B 00
VB6 程序
区段特征:
- 链接器版本:6.0
![image-20191220143608453](C:\Users\14622\AppData\Roaming\Typora\typora-user-images\image-20191220143608453.png)
- 程序的二进制特征:VB 是运行在 VB 虚拟机中的,Windows 下 VB 虚拟机是一个 DLL 模块,若存在这种形式的跳转,就可直接进行判断
![image-20191220143731780](C:\Users\14622\AppData\Roaming\Typora\typora-user-images\image-20191220143731780.png)
易语言、VC6
区段特征:
易语言:
VC6:
- 链接器版本:
![image-20191220144116354](C:\Users\14622\AppData\Roaming\Typora\typora-user-images\image-20191220144116354.png)
- 程序的二进制特征:两个 `push` 语句,一个抬高堆栈语句,既可用 `sub`,也可用 `add`,一个 `GetVersion` 函数
易语言:
![image-20191220144517946](C:\Users\14622\AppData\Roaming\Typora\typora-user-images\image-20191220144517946.png)
VC6:
![image-20191220144607426](C:\Users\14622\AppData\Roaming\Typora\typora-user-images\image-20191220144607426.png)
汇编语言:
汇编语言的链接器版本和区段特征是不固定的,也没有明确的二进制特征。可以通过文件的大小来猜测是否是一个汇编程序,程序的 OEP 部分通常直接就是逻辑代码,也可作为标识
VS 程序
- 不同版本的 VS 编写出来的程序特征是不同的
- 通常 debug 版本的程序入口点可能存在
call + call
,而 release 是call + jmp
VS 版本 链接器版本 VC 6.0 6.0 VC2003 7.0 / 7.1 VS2005 8.0 VS2008 9.0 VS2010 10.0 VS2012 11.0 VS2013 12.0 VS2015 14.0 VS2017 14.1 VS2019 14.2
二、基本数据类型的识别
常量的识别
1
2
3
4
5
6const bool bRet = true;
const int nCount = SIZE;
const char* szHello = "Hello 15PB";
const eData data = eData::enum_TYPE_1;
const float fNum = 1.5;
const sData stc = { 1,2.0,'1' };常量信息可能被保存在常量数据区和代码区
字符串常量的初始化
1
2
3
4
5
6// 字符串数组
char szStr[100] = { "szStr[100] Hello 15PB" };
// 宽字符串数组
wchar_t szWchar[100] = L"szWchar Hello 15PB";
// 普通的字符数组
char szHello[] = "szHello[] Hello 15PB";- 对于一个字符串初始化通常是:4字节拷贝 + 不足4字节拷贝 + 填充剩余的空间为0
- 若初始化数据较多,第一步和第二步会简化为一个串操作进行赋值
- 对于一个未指定大小的字符串初始化操作,没有填充剩余空间的步骤
指针和引用
1
2
3
4
5
6int* pnumber = &number;
// 00F21739 lea eax, [number]
// 00F2173C mov dword ptr[pnumber], eax
int& rnumber = number;
// 00F2173F lea eax, [number]
// 00F21742 mov dword ptr[rnumber], eax- 指针和引用在汇编层面的实现完全一致
三、分析对象
构造函数的分析
- 构造函数会使用 ecx 传递对象的首地址(this),构造函数的返回值是 this
- 一个存在继承关系且有虚函数的构造函数 = 构造父类\成员 + 初始化虚表指针 + 用户代码 + 返回值
1
2
3
4
5
6
7
8
9
10
11
12class CObj
{
public:
CObj() { printf("CObj::CObj()\n"); }
~CObj() { printf("CObj::~CObj()\n"); }
};
int main()
{
// 局部对象
CObj obj;
return 0;
}
存在继承关系的构造函数
1 | class Base |
存在虚函数
1 | class Base |
析构函数的分析
- 析构函数无返回值
- 一个存在继承关系且有虚函数的析构函数 = 重置虚表指针 + 用户代码 + 析构父类
存在继承关系的析构函数
存在虚函数
成员函数和数据成员
- 成员函数的调用会使用 ecx 传递 this 指针
- 数据成员的使用是通过 this 加上一个偏移得到的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class COBj2
{};
class CObj
{
private:
int number1 = 10;
int number2 = 20;
public:
void set(int n1, int n2)
{
number1 = n1;
number2 = n2;
printf("CObj::CObj()\n");
}
};
int main()
{
COBj2 obj2;
// 局部对象
CObj obj;
obj.set(100, 200);
return 0;
}虚函数
- 并非所有的虚函数调用都会有动态联编
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class CObj
{
public:
virtual void show()
{ printf("show\n"); }
};
void hook_show()
{
printf("hook_show\n");
}
int main()
{
DWORD OldProtect = 0;
LPVOID Addr = 0;
CObj obj;
obj.show();
CObj* pObj = &obj;
pObj->show();
// 获取虚函数表
__asm
{
mov eax, dword ptr [pObj] ; 取地对象首地址
mov eax, [eax] ; 虚函数指针
mov Addr, eax
push eax
}
VirtualProtect(Addr, 0x4, PAGE_READWRITE, &OldProtect);
__asm
{
pop eax
mov ecx, hook_show ; 替换的函数
mov [eax], ecx ; 替换
}
VirtualProtect(Addr, 0x4, OldProtect, &OldProtect);
pObj->show();
return 0;
}
四、对系统代码的识别
初始化安全
cookie
:1
2
3
4002A171E mov eax,dword ptr [__security_cookie (02AA004h)]
002A1723 xor eax,ebp
002A1725 mov dword ptr [ebp-4],eax
; 安全 cookie 是一个运行时确定的值,使用当前的 ebp 对它进行加密,并放置到 ebp - 4 的位置,如果被缓冲区溢出攻击了,那么 ebp - 4 绝对会被覆盖,覆盖后的值再次使用 ebp 异或解密就无法恢复之前的数值,导致程序结束
检查一段代码是否是用户代码:
1
2002A1728 mov ecx,offset _2A6D85CB_
002A172D call @__CheckForDebuggerJustMyCode@4 (02A1208h)
检查数组是否越界
1
2
3002A17EE lea edx,ds:[2A181Ch]
002A17F4 call @_RTC_CheckStackVars@8 (02A1235h)
;如果操作了数组,数组之后会被填充 0xCCCCCCCC,如果当前的数组越界写入了数据,就会覆盖 cc,这个函数被用于检查 cc 是否被覆盖(数组越界)
检查当前程序是否被缓冲区溢出攻击
1
2
3002A17FE mov ecx,dword ptr [ebp-4]
002A1801 xor ecx,ebp
002A1803 call @__security_check_cookie@4 (02A11DBh)
检查堆栈是否平衡
1
2
3
4002A1808 add esp,220h
002A180E cmp ebp,esp
002A1810 call __RTC_CheckEsp (02A1212h)
;检查堆栈是否平衡,如果存在函数调用就会在最后检查