第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;
}

 

posted @ 2020-09-23 14:06  Rev_omi  阅读(768)  评论(0编辑  收藏  举报