第4章:逆向分析技术--64位软件逆向技术
函数
1.栈平衡
在 x64 环境下,某些汇编指令对栈顶的对齐有要求,因此在 Visual Studio 中,有时会出现申请了栈空间而不使用的情况。
2.启动函数
在 IDA 中找到启动函数的步骤: start -> _tmainCRTStartup -> 下翻找到 main 或者通过找到 exit 函数,向上翻一个 call 就是 main 函数。
3.调用约定
大部分在 《逆向工程核心原理》 中都有。此处注意一点,Debug 版本的程序,申请了栈空间后会使用 CC 填充(即 Int 3 ),而Release 版程序则不会有,并且会在一些地方做很多优化。
4.参数传递
(1) 参数为结构体时且大小不超过8字节
那么会将其放在一个栈单元内,通过一个寄存器传值进入函数。
(2) 参数为结构体且大小超过8字节
即将 [ rsp + 28 ] —— [ rsp + 34 ] 复制到 [ rsp + 50 ] —— [ rsp + 5C] ,然后
(3) thiscall 传递
它是 C++ 类的成员函数调用约定。
this 指针可以用来存储虚表。
(4) 函数返回值
RAX 返回保存函数值。浮点类型时使用 MMX0 寄存器。返回值大于8字节时,使用栈空间地址,间接保存返回值。
数据结构
(1) 局部变量
预留栈空间在低地址,局部变量在高地址。当为 Release 版时,程序会尽量使用寄存器,只有不够用时才会使用栈空间。
(2) 全局变量
先定义的在低地址,后定义的在高地址。
(3) 数组
一维数组,局部数组的寻址方式:
一维数组,全局数组的寻址方式:
在 Release 版本中,数组初始化可能会采用 XMM0 寄存器进行优化。
控制语句
(1) Switch-Case 语句
数学运算符
(1) 加法减法
通常用 lea 指令来进行优化,而不是直接用 add sub 指令。
(2) 整数的乘法
(3) 整数的除法
通常会由编译器计算出一个值,使用该值进行计算优化或者改变计算方法。
虚函数
VC++ 实现虚函数功能的方式是做表,即虚表。首先来看第一个程序:
#include "stdafx.h" class CVirtual { public: CVirtual() { m_nMember1 = 1; m_nMember2 = 2; printf("CVirtual()\r\n"); } virtual ~CVirtual() { printf("~CVirtual()\r\n"); } virtual void fun1() { printf("fun1()\r\n"); } virtual void fun2() { printf("fun2()\r\n"); } private: int m_nMember1; int m_nMember2; }; int main(int argc, char* argv[]) { CVirtual object; object.fun1(); object.fun2(); return 0; }
this 指针用作构建虚表,不同的类虚表不同,相同的类对象共用同一个虚表。构造函数内部:
接下来调用类成员函数:
析构函数:
C++ 语法规定,在实例化对象时会自动调用构造函数,对象作用域会自动调用析构函数(在对象作用域以外时调用)。
构造函数执行完后会返回 this 指针。析构函数会重新赋值虚表,C++ 规定,析构函数需要调用函数的无多态性。
第二个程序:单重继承虚表
#include "stdafx.h" class CBase { public: CBase() { m_nMember = 1; printf("CBase()\r\n"); } virtual ~CBase() { printf("~CBase()\r\n"); } virtual void fun1() { printf("CBase::fun1()\r\n"); } private: int m_nMember; }; class CDerived :public CBase { public: CDerived() { m_nMember = 2; printf("CDerived()\r\n"); } ~CDerived() { printf("~CDerived()\r\n"); } virtual void fun1() { printf("CDerived::fun1()\r\n"); } virtual void fun2() { printf("CDerived::fun2()\r\n"); } private: int m_nMember; }; int _tmain(int argc, _TCHAR* argv[]) { CBase *pBase = new CDerived(); pBase->fun1(); delete pBase; return 0; }
首先,调用 new 函数,申请一块空间,返回首地址并存在 rax . 对象数据成员只有8字节,还需要8字节的虚表空间和8字节的内存对齐,因此总共申请了18字节。
检测返回值,存在则调用构造函数。
可以看到,在构造函数中,先是基类的表占用了 this 指针,然后是派生类占用了 this 指针。但是他们的成员( m_nMember )都相邻,共用一个虚表中。
若 this 指针不为 NULL ,则调用 this 指针第一个函数,而虚表的第一个函数是析构函数:
delete 函数有两个参数,一个是 this 指针,一个是 bool 值,分别由 ecx 和 edx 传入,而 edx 在调用析构函数后就被置为1。若 edx 为 1则不调用 delete 函数,否则调用。这是为了防止在调用析构函数之后再次调用 delete 函数,造成空间重复释放。
第三个程序:多重继承虚表
#include "stdafx.h" class CBase1 { public: CBase1() { m_nMember = 1; printf("CBase1()\r\n"); } ~CBase1() { printf("~CBase1()\r\n"); } virtual void fun1() { printf("CBase1::fun1()\r\n"); } private: int m_nMember; }; class CBase2 { public: CBase2() { m_nMember = 2; printf("CBase2()\r\n"); } ~CBase2() { printf("~CBase2()\r\n"); } virtual void fun2() { printf("CBase2::fun1()\r\n"); } private: int m_nMember; }; class CDerived :public CBase1, public CBase2 { //继承两个基类 public: CDerived() { m_nMember = 2; printf("CDerived()\r\n"); } ~CDerived() { printf("~CDerived()\r\n"); } virtual void fun1() { printf("CDerived::fun1()\r\n"); } virtual void fun3() { printf("CDerived::fun3()\r\n"); } private: int m_nMember; }; int _tmain(int argc, _TCHAR* argv[]) { CDerived derievd; return 0; }
函数总体结构比较统一:
最先进行基类的构造(同类则按照顺序进行构造),然后完成对派生类的构造(派生类新增的虚函数挂在第一个虚表后面):
第四个程序:菱形继承虚表
#include "stdafx.h" class A { public: A() { m_nMember = 1; printf("A()\r\n"); } ~A() { printf("~A()\r\n"); } virtual void fun1() { printf("A::fun1()\r\n"); } private: int m_nMember; }; class B :virtual public A{ public: B() { m_nMember = 2; printf("B()\r\n"); } ~B() { printf("~B()\r\n"); } virtual void fun2() { printf("B::fun2()\r\n"); } private: int m_nMember; }; class C :virtual public A{ public: C() { m_nMember = 3; printf("C()\r\n"); } ~C() { printf("~C()\r\n"); } virtual void fun3() { printf("C::fun3()\r\n"); } private: int m_nMember; }; class BC :public B, public C { public: BC() { m_nMember = 4; printf("BC()\r\n"); } ~BC() { printf("~BC()\r\n"); } virtual void fun3() { printf("BC::fun3()\r\n"); } virtual void fun4() { printf("BC::fun4()\r\n"); } private: int m_nMember; }; int _tmain(int argc, _TCHAR* argv[]) { BC theBC; return 0; }
依次是对 sub_140001041 、sub_14000102D 的调用。上图可知,首先对 A 基类进行了构造。因为 A 已经执行过构造函数,因此跳过,而 B 虚继承 A 基类,因此会取两次虚表,并且有一次重复的对 A 虚表的赋值,注意存储两个虚表的地址不同。
下图中,接下来会对 C 类进行构造,在 C 类的内部,若跳转失败,则会执行 A 基类的构造,因此 C 类也是继承的 A 。而 BC 类的构造中,会重复对 B、A 虚表进行同地址的重复存储,并将自己的表写入。程序中有一个 偏移表 需要好好注意一下,在存储虚表时会发挥作用。
第五个程序:抽象类虚函数
C++ 中,含有纯虚函数的类称为抽象类,它不能实例化对象,通常用抽象类给子类规范接口。
#include "stdafx.h" class IBase { public: IBase() { m_nMember = 1; printf("IBase()\r\n"); } virtual void fun1() = 0; virtual void fun2() = 0; private: int m_nMember; }; class CDerived :public IBase { public: CDerived() { printf("CDerived()\r\n"); } virtual void fun1(){}; virtual void fun2() {}; }; int _tmain(int argc, _TCHAR* argv[]) { IBase *pBase = new CDerived(); delete pBase; return 0; }