[toc]

一、识别程序的特征

  1. 如何识别程序的特征

    • PE 工具查看链接器版本
    • PE 工具查看区段信息
    • 查看目标程序的二进制特征
  2. BC++ 程序

    • 区段特征:

      image-20191220140824560

      导入导出表以及只读数据是分开的

    • 链接器版本:5.0

      image-20191220141134080

  • 程序的二进制特征:

    image-20191220141407316

    1
    EB 10 66 62 3A 43 2B 2B 48 4F 4F 4B 90

    BC++ 编写的程序,IAT 函数的调用通常是 E8 跳转表 + FF25

    image-20191220142007230

    image-20191220142045694

    1. Delphi 程序
    • 区段特征:

      image-20191220142410874

      这三个通常是 .text,.data,.textbss等

    • 链接器版本:2.25

      image-20191220142633733

- 程序的二进制特征: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
  1. VB6 程序

    • 区段特征:

      image-20191220143455127

- 链接器版本: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)
  1. 易语言、VC6

    • 区段特征:

      易语言:

      image-20191220144038624

      VC6:

      image-20191220144204962

- 链接器版本:

  ![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)
  1. 汇编语言:

    汇编语言的链接器版本和区段特征是不固定的,也没有明确的二进制特征。可以通过文件的大小来猜测是否是一个汇编程序,程序的 OEP 部分通常直接就是逻辑代码,也可作为标识

  2. 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. 常量的识别

    1
    2
    3
    4
    5
    6
    const 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' };

    常量信息可能被保存在常量数据区和代码区

    image-20191220151908251

  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
    • 若初始化数据较多,第一步和第二步会简化为一个串操作进行赋值
    • 对于一个未指定大小的字符串初始化操作,没有填充剩余空间的步骤

    image-20191220163014617

  1. 指针和引用

    1
    2
    3
    4
    5
    6
    int* 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
    • 指针和引用在汇编层面的实现完全一致

三、分析对象

  1. 构造函数的分析

    • 构造函数会使用 ecx 传递对象的首地址(this),构造函数的返回值是 this
    • 一个存在继承关系且有虚函数的构造函数 = 构造父类\成员 + 初始化虚表指针 + 用户代码 + 返回值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class CObj
    {
    public:
    CObj() { printf("CObj::CObj()\n"); }
    ~CObj() { printf("CObj::~CObj()\n"); }
    };
    int main()
    {
    // 局部对象
    CObj obj;
    return 0;
    }

    image-20191220164524065

    image-20191220164936511

存在继承关系的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
class Base
{
public:
Base() { printf("Base::Base()\n"); }
~Base() { printf("Base::~Base()\n"); }
};
class CObj : public Base
{
public:
CObj() { printf("CObj::CObj()\n"); }
~CObj() { printf("CObj::~CObj()\n"); }
};

image-20191220165933403

存在虚函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base
{
public:
virtual void show1() { printf("Base::show1()\n"); }
Base() { printf("Base::Base()\n"); }
~Base() { printf("Base::~Base()\n"); }
};
class CObj : public Base
{
public:
virtual void show2() { printf("CObj::show2()\n"); }
CObj() { printf("CObj::CObj()\n"); }
~CObj() { printf("CObj::~CObj()\n"); }
};

image-20191220172213240

  1. 析构函数的分析

    • 析构函数无返回值
    • 一个存在继承关系且有虚函数的析构函数 = 重置虚表指针 + 用户代码 + 析构父类

    image-20191220165206484

存在继承关系的析构函数

image-20191220170040678

存在虚函数

image-20191220173000068

  1. 成员函数和数据成员

    • 成员函数的调用会使用 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
    23
    class 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;
    }

    image-20191220185500576

    image-20191220185532171

    image-20191220185603972

  2. 虚函数

    • 并非所有的虚函数调用都会有动态联编
    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
    #include <stdio.h>
    #include <Windows.h>
    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;
    }

    image-20191220191452621

image-20191220191617853

四、对系统代码的识别

  1. 初始化安全 cookie

    1
    2
    3
    4
    002A171E  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. 检查一段代码是否是用户代码:

    1
    2
    002A1728  mov         ecx,offset _2A6D85CB_
    002A172D call @__CheckForDebuggerJustMyCode@4 (02A1208h)
  1. 检查数组是否越界

    1
    2
    3
    002A17EE  lea         edx,ds:[2A181Ch]  
    002A17F4 call @_RTC_CheckStackVars@8 (02A1235h)
    ;如果操作了数组,数组之后会被填充 0xCCCCCCCC,如果当前的数组越界写入了数据,就会覆盖 cc,这个函数被用于检查 cc 是否被覆盖(数组越界)
  1. 检查当前程序是否被缓冲区溢出攻击

    1
    2
    3
    002A17FE  mov         ecx,dword ptr [ebp-4]  
    002A1801 xor ecx,ebp
    002A1803 call @__security_check_cookie@4 (02A11DBh)
  1. 检查堆栈是否平衡

    1
    2
    3
    4
    002A1808  add         esp,220h  
    002A180E cmp ebp,esp
    002A1810 call __RTC_CheckEsp (02A1212h)
    ;检查堆栈是否平衡,如果存在函数调用就会在最后检查