逆向基础特征分析
- 栈帧
通常开始需要开辟栈帧,初始化安全cookie, (j_CheckDebuggerJustMyCode)
通常一个函数的结束需要对eax即对返回值赋值,恢复寄存器环境,检测安全cookie是否被改变,平衡堆栈(esp+xx),检查(CheckEsp),关闭栈帧
开栈操作 通常初始化安全cookie(ebp-4),保存非易失性寄存器(如ebp,esi,edi),初始化栈帧
开辟栈帧的作用是把ebp作为基址调用参数和变量 - 参数
通常大于8字节的参数,先开辟栈帧在赋值
小于四字节的参数会被隐式转换为4个字节,但地址还是保持在开头位置(扩展填充) - 变量
变量之间通常用0xCC隔开,连续的相同类型变量可以猜测为数组,不同类型为结构体
变量之间的间隔通常为两个0xCCCCCCCC
在编译通常会自动对齐 align 0x10 - 常量
大多数常量保存在opcode中,浮点数常量保存在常量区,以全局变量的方式保存,使用浮点数寄存器如xmm0寄存器操作,先赋值到寄存器中才赋值给变量
常量字符串保存在常量去,使用首地址
bool的本质就是0,1 - 跳转指令
E8 call offset
E9 jmp offset
ff15 call 全局地址
ff25 borland (ff15)call特征 - 字符串操作
较小数据每次使用mov DWORD ptr 0xxxxxxxx为单位初始化,不足使用WORD,BYTE,剩下未赋值的使用memset设置为0
较大数据使用串操作指令优化,不足的使用movsw/b,剩下的使用memset设置为0,rep movsd
未指定大小的,没有多余的空间使用memset赋值 - 堆空间
未初始化0xcdcdcdcd 结尾0xfdfdfdfd - 对象操作
_thiscall:使用ecx传递this指针
构造函数:首先将ecx保存的this指针赋值到变量(第一个变量ebp-8),返回值为eax,初始化后的对象,通常调用完设置返回码mov exit_code,0
构造函数使用 ecx 传递对象的地址,在其中进行初始化
初始化当前对象的虚函数表指针
会在执行具体的逻辑代码前,调用父类的构造函数
父类构造>对象成员构造>子类构造
会将返回值设置为当前的 this 指针
析构函数:首先将ecx保存的this指针赋值到变量,没有返回值,调用完析构设置返回值,mov eax,exit_code
在执行逻辑代码后,函数退出前调用父类的析构
子类析构>对象成员析构>父类析构
调用成员函数第一个参数都需要将this指针赋值给ecx,在复制给eax使用eax作为基址
当一个类对象什么都不实现会被优化,没有继承,没有虚函数
; 取出一个对象的地址进行初始化,大概率是指针或引用 - 存在虚函数的情况
在链接的时候初始化虚函数表,复制父类的虚函数表,相同的重写不同的添加
只有同时存在虚函数表和指针或引用的情况下才属于动态联编
调用按照虚函数表首地址+偏移,和变量的使用方式类似
构造函数
mov eax, [ebp+this]mov dword ptr [eax], offset ??_7CObj@@6B@ ;; 如果存在虚函数,那么会在构造函数中初始化虚表指针指向虚函数表; 虚函数表的初始化由链接器完成,父类构造函数会出初始化自己的虚; 表指针,表内只保存了自己的虚函数,子类初始化的指针指向的表中; 除了父类继承的以及重写的虚函数外还会添加自己的虚函数,注意, ; 子类的虚函数和父类的虚函数位置是相同
析构函数
没有返回值,会再次复制虚函数表,局部对象在函数返回前析构
在执行析构函数逻辑代码前会再次将虚函数表地址赋值给对象
- 变量成员及成员函数的初始化
任何一个动态联编调用虚函数以及访问数据的地方都会使用 this
使用 [ecx(基址) + (对象内偏移)] 的方式操作数据成员
mov eax, [ebp+this]mov ecx, [ebp+arg_0]mov [eax], ecxmov eax, [ebp+this]mov ecx, [ebp+arg_4]mov [eax+4], ecxmov eax, [ebp+this]
初始化列表赋值,再构造函数内赋值,如果构造函数并未对变量赋值则使用变量的初始化
- 全局对象
全局对象的构造函数调用
全局对象的构造函数:CObjA::CObjA() 行 8 C++
调用全局对象的构造函数:dynamic initializer for ‘g_object1’’() 行 24 C++
所有的全局的构造函数:_initterm(void()() * first, void()() * last) 行 22 C++
- 函数
// 全局函数 -> 友元函数 -> 成员函数 -> 静态函数 -> 内联函数
// 全局函数: push + call
// 友元函数: 实现上和全局函数一致,语法上可以访问私有
// 成员函数: 实现上就是全局函数限定参数一为this
// 虚函数: 十分的复杂,自己体会
// 静态函数: 实现上和全局函数一致,语法上作用域在类内
// 内联函数: 通常是经过优化的函数,没有函数调用语句(call)
// 逆向的时候可能猜不出这是一个内联函数(strlen\strcpy) - 指针和引用
引用本质上就是带语法限制的指针
const在后面就是指向不可变,在前面就是指向的元素不可变 - mov与lea
当是一个变量使用lea计算地址(在编译时未确定的地址使用lea,不能使用offset)
全局地址,(指针)使用mov - 特征运行时库函数
CheakEsp:检测堆栈是否平衡,检查esp的值,不平衡程序直接崩溃
cmp ebp, espcall j_CheckEsp
通常涉及数组操作,检测数组是否越界访问,在数组结尾添加0xCCCCCCCC,判断是否被修改
防止缓冲区溢出攻击
push edxmov ecx, ebp ; ; 有效 1push eaxlea edx, dword_0Xxxxxxx ; ; 有效 2call j_CheckStackValue ; ; 有效 3
pop eax
pop edx
获取一个全局变量放到ecx中调用函数,vs2019运行时函数特征,JMC选项中关闭,仅调试我的代码
mov ecx, offset 0xxxxxxxcall j_CheckForDebuggerJustMyCode ;(特征call GetCurrentThreadId)
获取初始化安全cookie与ebx异或保存到ebp+4(第一个变量中)
mov eax, ___security_cookiexor eax, ebpmov [ebp+xor_security_cookie], eax
作用:检测栈
mov ecx, [ebp+xor_security_cookie]xor ecx, ebpcall j_@__security_check_cookie@4 ; ; 判断当前安全 cookie 是否被修改,如果被修改就表示栈的内 ; 容被修改了,可能被利用了缓冲区溢出技术进行攻击
- 常见函数
printf(format_s,arg1,arg2)
scanf_s(format_s,&buf,size)
memset(&addr,0,size)
strlen(s)
strcmp(s1,s2)
VirtualProtect(&addr,size,newProt,&OldProt)