The core principle of reverse engineering - HsuJv/Note GitHub Wiki

OllyDbg

调试器命令

     快捷键        含义
     Ctrl+F2       重新开始调试
     F7            单步步入
     F8            单步步过
     Ctrl+F9       函数内部运行直到retn, 跳出函数
     Ctrl+G        移动到指定位置, 用来查看代码或内存
     F4            执行到光标位置
     ;             添加注释
     :             添加标签
     F2            设置或取消断点
     F9            运行(若设置了断点, 则停到断点处)
     *             显示当前eip(命令指针)位置
     -             显示上一个光标位置
     Enter         若光标处有call/jmp等指令, 则跟踪并显示相关地址
     Ctrl+F7       反复执行Step Into(画面显示)
     Ctrl+F8       反复执行Step Over(画面显示)
     Ctrl+F11      反复执行Step Into(画面不显示)
     Ctrl+F12      反复执行Step Over(画面不显示)

入口点

  • EP(Entry Point 是Windows可执行文件(exe, dll, sys等)的代码入口点, 是执行应用程序时最先执行的代码的起始位置
  • 调试器打开文件时停止的地点即为文件的入口点

Base Camp

  • 每次重新运行调试器时, 调试都会返回EP处
  • 通过断点, 注释, 标签等方法设置一个Base Camp和不同的Stronghold, 可以方便调试过程

快速查找指定代码的方法

  • 代码执行法: 从Base Camp开始不断按下F8(Step Over), 直到某一步出现要查找的函数的事件, 则该call指令为函数入口点
  • 字符串检索法: 鼠标右键菜单-Seach for-All referenced text strings, 可以在程序中查找指定字符串, 双击即可定位到引用该字符串的函数内部
  • API检索法(1): 鼠标右键菜单-Seach for-All intermodular calls, 找到程序调用的api, 双击定位
  • API检索法(2): 鼠标右键菜单-Seach for-Name in all calls, 找到程序调用的api, 双击定位, 并设置断点, 然后按F9执行, 此时寄存器中esp的值是进程栈的地址, 它记录了调用这个api之后程序返回的地址

小端标记法

字节序

  • 多字节数据在计算机内存中存储或在网络传输时各个字节的存储顺序
  • 主要分: 小端序(little endian), 大端序(big endian)
Byte    b       = 0x12
Word    w       = 0x1234
Dword   dw      = 0x12345678
char    str[]   = "abcde"
Name    Size    Big Endian          Little Endian
b       1       [12]                [12]
w       2       [12][34]            [34][12]
dw      4       [12][34][56][78]    [78][56][34][12]
str     6       'a''b''c''d''e'     'a''b''c''d''e'
  • 小端序低地址存储数据低位, 高地址存储数据高位
  • 大端序低地址存储数据高位, 高地址存储数据低位
  • 当数据为单一字节时, 无论采用大端序还是小端序, 字节的存储顺序都一样

栈帧

  • 栈帧就是利用ebp寄存器访问栈内的局部变量, 参数, 函数返回地址等手段
  • 实现栈帧的汇编代码
    push    ebp         ; 函数开始(使用ebp前先保存)
    mov     ebp, esp    ; 把esp的值送到ebp中
    ...                 ; 函数体, 无论esp如何改变, ebp不变
    ...                 ; 以安全地访问局部变量, 参数
    mov     esp, ebp    ; 将函数起始地址返回esp
    pop     ebp         ; 返回前弹出之前保存的ebp
    retn                ; 函数终止

retn : (eip) = (esp), (esp) = (esp) + 4

retn N : (eip) = (esp), (esp) = (esp) + 4 + N

函数调用约定

  • Calling Convention 即函数调用约定, 它是对函数调用时如何穿参的一种约定
    • 调用函数前先把参数压入栈然后再传递给函数
    • 栈是定义在进程中的一段内存空间, 向下扩展, 其大小被记录在PE头中
    • 函数执行完成后, 栈中参数会被自然覆盖, 无须清理空间
    • 函数执行完毕后, ESP恢复到调用前, 这样可引用的栈的大小才不会缩减
    • 主要的函数调用约定如下: cdecl, stdcall, fastcall

cdecl

  • cdecl是主要用在C语言中的使用方式, 调用者负责处理栈
  • 好处在于, 它可以像C语言的printf()函数一样, 向被调用函数传递长度可变的参数, 这种长度可变的参数在其他调用约定中很难实现
push    argn
push    arg(n-1)
...
push    arg1
calls   func

stdcall

  • stdcall方式常用win32 api, 该方式由被调用者负责清理栈.
  • 如果在c语言中想使用stdcall方式编译, 使用关键字_stdcall即可
return_type     _stdcall    function_name(arg1, arg2, ...)
  • 好处在于, 被调用者内部存在着栈清理代码, 与每次调用函数时都要用add esp, xxx 命令的cdecl方式相比, 代码尺寸要小.
push    ebp
mov     ebp, esp
...
mov     esp, ebp
pop     ebp
retn    xxx     ; 用retn N指令完成栈的清理
                ; cdecl方式中该处为retn指令

fastcall

  • fastcall方式与stdcall方式基本类似, 但是该方式同常使用寄存器(而非栈内存)去传递部分参数(前两个)
  • 若函数有4个参数, 则前2个参数分别用ecx, edx传递

PE文件格式

PE(Portable Executable) 文件

  • PE文件是Windows操作系统下使用的可执行文件格式
  • PE文件指32位可执行文件, 也称PE32
  • 64位的可执行文件称为PE+或PE32+

PE文件种类

  • 可执行系列: exe, scr
  • 库系列: dll, ocx, cpl, drv
  • 驱动程序系列: sys, vxd
  • 对象文件系列: obj
  • 根据PE规范, obj也视为PE文件, 但是obj文件本身不能以任何形式执行

基本结构

  • 从DOS头(DOS header)到节区头(Section header)是PE头部分
  • 其下的节区合称PE体
  • 文件中使用偏移(offset), 内存中使用VA(Virtual Address)来表示位置
  • 各节区头定义了各节区在文件或内存中的大小, 位置, 属性等
  • PE头与各节区尾部存在一个区域, 称为NULL填充(NULL padding)

VA & RVA

  • VA指的是进程虚拟内存的绝对地址, RVA(Relative Virtual Address)指从某个基准位置(ImageBase)开始的相对地址
  • VA和RVA满足: RVA + ImageBase = VA
  • PE头内部信息大多以RVA形式存在

PE头

  • DOS头: 考虑PE文件对DOS文件的兼容性, 在PE头最前面添加了IMAGE_DOS_HEADER结构体, 用来扩展已有的DOS EXE头
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER
  • IMAGE_DOS_HEADER结构体为0x40字节, 其中有两个重要成员:
    • e_magic: DOS签名(signature, 0x5a4d = 'MZ')
    • e_lfanew: 指示NT头的偏移
  • DOS存根(stub): 在DOS头下方, 是个可选项, 且大小不固定, 由代码和数据混合而成, 在32位的Windows OS中不会运行该命令, 但在16位的DOS环境中可以执行
  • NT头
typedef struct _IMAGE_NT_HEADERS {
    DWORD                     Signature;        // PE Signature
    IMAGE_FILE_HEADER         FileHeader;
    IMAGE_OPTIONAL_HEADER32   OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
  • IMAGE_NT_HEADERS结构体为0xf8字节, 成员有:
    • Signature 其值为0x00004550("PE"00)
    • FileHeader 文件头:
    typedef struct _IMAGE_FILE_HEADER {
        WORD    Machine;
        WORD    NumberOfSections;
        DWORD   TimeDateStamp;
        DWORD   PointerToSymbolTable;
        DWORD   NumberOfSymbols;
        WORD    SizeOfOptionalHeader;
        WORD    Characteristics;
    } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
    
    • IMAGE_FILE_HEADER结构体中有如下4种重要成员, 如果它们设置不正确, 将导致文件无法正常运行:
      • Machine: 每个CPU都拥有唯一的Machine码, 兼容32位Intel x86芯片的Machine码为0x014C, 64位为0x0200
      • NumberOfSections: 用来指出文件中存在的节区数量, 该值严格大于0, 若与实际节区不等, 将发生运行错误
      • SizeOfOptionalHeader: 用来指出IMAGE_NT_HEADERS最后一个成员IMAGE_OPTIONAL_HEADER32结构体的大小(PE32+格式的文件中使用的是IMAGE_OPTIONAL_HEADER64结构体)
      • Characteristics: 标识文件属性, 文件是否是可运行的形态, 是否为DLL等文件信息, 以bit OR形式组合起来(常用值: 0x0002 - exe; 0x2000 - dll)
    • TimeDateStamp成员用来记录编译器创建此文件的时间, 但有的开发工具未提供设置该值的工具
    • Optionalheader 可选头: 是PE结构体中最大的
    typedef struct _IMAGE_DATA_DIRECTORY{
        DWORD   VirtualAddress;
        DWORD   Size;
    } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
    
    typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //
    +18h WORD    Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
    +1Ah BYTE    MajorLinkerVersion; // 链接程序的主版本号
    +1Bh BYTE    MinorLinkerVersion; // 链接程序的次版本号
    +1Ch DWORD   SizeOfCode; // 所有含代码的节的总大小
    +20h DWORD   SizeOfInitializedData; // 所有含已初始化数据的节的总大小
    +24h DWORD   SizeOfUninitializedData; // 所有含未初始化数据的节的大小
    +28h DWORD   AddressOfEntryPoint; // 程序执行入口RVA
    +2Ch DWORD   BaseOfCode; // 代码的区块的起始RVA
    +30h DWORD   BaseOfData; // 数据的区块的起始RVA
    //
    // NT additional fields. 以下是属于NT结构增加的领域。
    //
    +34h DWORD   ImageBase; // 程序的首选装载地址
    +38h DWORD   SectionAlignment; // 内存中的区块的对齐大小
    +3Ch DWORD   FileAlignment; // 文件中的区块的对齐大小
    +40h WORD    MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
    +42h WORD    MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
    +44h WORD    MajorImageVersion; // 可运行于操作系统的主版本号
    +46h WORD    MinorImageVersion; // 可运行于操作系统的次版本号
    +48h WORD    MajorSubsystemVersion; // 要求最低子系统版本的主版本号
    +4Ah WORD    MinorSubsystemVersion; // 要求最低子系统版本的次版本号
    +4Ch DWORD   Win32VersionValue; // 莫须有字段, 不被病毒利用的话一般为0
    +50h DWORD   SizeOfImage; // 映像装入内存后的总尺寸
    +54h DWORD   SizeOfHeaders; // 所有头 + 区块表的尺寸大小
    +58h DWORD   CheckSum; // 映像的校检和
    +5Ch WORD    Subsystem; // 可执行文件期望的子系统
    +5Eh WORD    DllCharacteristics; // DllMain()函数何时被调用, 默认为 0
    +60h DWORD   SizeOfStackReserve; // 初始化时的栈大小
    +64h DWORD   SizeOfStackCommit; // 初始化时实际提交的栈大小
    +68h DWORD   SizeOfHeapReserve; // 初始化时保留的堆大小
    +6Ch DWORD   SizeOfHeapCommit; // 初始化时实际提交的堆大小
    +70h DWORD   LoaderFlags; // 与调试有关, 默认为 0
    +74h DWORD   NumberOfRvaAndSizes; 
    // 下边数据目录的项数, 这个字段自Windows NT 发布以来 
    // 一直是16
    +78h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
    // 数据目录表
    } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
    
    • 重要成员:
      • Magic: 为IMAGA_OPTIONAL_HEADER32结构体时, 其值为10B; 64位时为20B
      • AddressOfEntryPoint: 持有EP的RVA值, 指出程序最先执行的代码起始地址
      • ImageBase: 进程虚拟内存的范围为0-ffffffff(32位系统). 该值指出文件的优先装入地址
        • exe, dll文件被装载到0-7fffffff
        • sys文件被载入80000000-ffffffff
        • 一般而言, 使用开发工具创建好exe后, 其值为00400000, dll文件为10000000
        • 执行PE文件时:
          • PE装载器创建进程
          • 将文件载入内存
          • 把eip值设置为ImageBase+AddressOfEntryPoint
      • SectionAlignment, FileAlignment: SectionAlignment指定了节区在内存中的最小单位, FileAlignment指定了节区在磁盘文件中的最小单位
      • Subsystem: 用来区分系统驱动文件(*.sys)与普通的可执行文件(*.exe, *.dll), Subsystem可拥有的值如下:
      值       含   义          备注
       1      Driver文件        系统驱动
       2       GUI文件          窗口应用程序
       3       GUI文件          控制台应用程序
      
      • NumberOfRvaAndSizes: 用来指定DataDirectory数组的个数, 虽然结构体定义中明确指出了数组个数为16, 但是PE装载器通过查看NumberOfRvaAndSizes值来识别数组大小, 换言之, 数组大小也可能不是16
      • DataDirectory: 有IMAGE_DATA_DIRECTORY结构体组成的数组, 每项都被定义有值
      DataDirectory[0] = EXPORT Directory
      DataDirectory[1] = IMPORT Directory
      DataDirectory[2] = RESOURCE Directory
      DataDirectory[3] = EXCEPTION Directory
      DataDirectory[4] = SECURITY Directory
      DataDirectory[5] = BASERELOC Directory
      DataDirectory[6] = DEBUG Directory
      DataDirectory[7] = COPYRIGHT Directory
      DataDirectory[8] = GLOBALPTR Directory
      DataDirectory[9] = TLS Directory
      DataDirectory[A] = LOAD_CONFIG Directory
      DataDirectory[B] = BOUND_IMPORT Directory
      DataDirectory[C] = IAT Directory
      DataDirectory[D] = DELAY_IMPORT Directory
      DataDirectory[E] = COM_DESCRIPTOR Directory
      DataDirectory[F] = Reserved Directory
      
  • 节区头: 定义了各节区属性, 是由IMAGE_SECTION_HEADER结构体组成的数组, 每个结构体对应一个节区
类别          访问权限
code          执行, 读取权限
data          非执行, 读写权限
resource      非执行, 读取权限
#define     IMAGE_SIZEOF_SHORT_NAME     8
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union   {
        DWORD   PhysicalAddress;
        DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER
  • 重要成员:
    • VirtualSize: 内存中节区所占大小
    • VirtualAddress: 内存中节区起始地址(RVA)
    • SizeofRawData: 磁盘文件中节区所占大小
    • PointerToRawData: 磁盘文件中节区起始位置
    • Characteristics: 节区属性(bit OR)
    #define IMAGE_SCN_CNT_CODE              0x00000020 // Section 
                                                          contains code
    #define IMAGE_SCN_CNT_INITIALIED_DATA   0x00000040 // Section contains
                                                          initialized data
    #define IMAGE_SCN_CNT_UNINITIALIED_DATA 0x00000080 // Section contains
                                                          uninitialized data
    #define IMAGE_SCN_MEM_EXECUTE           0x20000000 // Section is 
                                                          executable
    #define IMAGE_SCN_MEM_READ              0x40000000 // Section is
                                                          readable
    #define IMAGE_SCN_MEM_WRITE             0x80000000 // Section is
                                                          writeable
    
    • Name字段: 不以NULL结束, 没有必须使用ASCII值的限制, PE规范未明确规定节区的Name, 所以可以向其中放入任何值
  • RVA to RAW
    • PE文件加载到内存时, 每个节区都要能准确完成内存地址与文件偏移间的映射, 这种映射一般称为RVA to RAW
    • (1) 查找RVA所在节区
    • (2)使用公司计算文件偏移(RAW): RAW = RVA - VirtualAddress + PointerToRawData

映像(Image)

  • PE文件加载到内存时, 文件不会原封不动地加载
  • 而要根据节区头定义的节区起始地址, 节区大小等加载
  • 因此, 磁盘文件中的PE与内存中的PE具有不同的形态
  • 将加载到内存中的形态称为"映像"以示区别

IAT

  • DLL
    • 16位DOS的时代并不存在DLL(Dynamic Linked Library)的概念, 只有"库(Library)"
    • C语言在使用函数时编译器会先从C库中读取相应函数的二进制代码, 然后插入应用程序
    • Windows OS支持多任务, 用包含库的方式会非常没有效率
    • 于是有了DLL这一概念:
      • 不把库包含到程序中, 单独组成DLL文件, 需要时调用即可
      • 内存映射技术使加载后的DLL代码, 资源在多个进程中实现共享
      • 更新库时只要替换相关DLL文件即可
    • 加载DLL的方式有两种
      • 显示链接(Explicit Linking): 程序使用DLL时加载, 使用完后释放
      • 隐式链接(Implicit Linking): 程序开始时一同加载DLL, 终止时再释放
    • IAT提供的机制即与隐式链接有关
  • IMAGE_IMPORT_DESCRIPTOR
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    __C89_NAMELESS union {
        DWORD   Characteristics;    // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk; // INT(Import Name Table) address (RVA)
    } DUMMYUNIONNAME;
    DWORD       TimeDateStamp;
    DWORD       ForwarderChain;     // -1 if no forwarders
    DWORD       Name;               // library name string address(RVA)
    DWORD       FirstThunk;         // IAT address (RVA)
} IMAGE_IMPORT_DESCRIPTOR;

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD        Hint;               // ordinal
    BYTE        Name[1];            // function name string 
                                    // In fact it is a variable string ending 
                                    // with 0
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
  • IMAGE_IMPORT_DESCRIPTOR中的重要成员:
    • OriginalFirstThunk: INT的地址(RVA)
    • Name: 库名称字符串的地址(RVA)
    • FirstThunk: IAT的地址(RVA)
    • 执行一个程序需要导入多少库, 就存在多少个IMAGE_IMPORT_DESCRIPTOR结构体, 这些结构体形成了数组, 且数组最后以NULL结构体结束
    • PE头中提到的Table即指数组
    • INT与IAT是长整型数组, 以NULL结束
    • INT中各元素值是IMAGE_IMPORT_BY_NAME结构体指针
    • INT与IAT值应相同
  • IAT输入顺序:
    • 读取IID(IMAGE_IMPORT_DESCRIPTOR)的Name成员, 获取库名称字符串
    • 装载相应库(LoadLibrary("*.dll"))
    • 读取IID的OriginalFirstThunk成员, 获取INT地址
    • 逐一读取INT中数组的值, 获取相应的IMAGE_IMPORT_BY_NAME地址(RVA)
    • 使用IMAGE_IMPORT_BY_NAME的Hint(ordinal)或Name项, 获取相应函数的起始地址(GetProcAddress("*"))
    • 读取IID的FirstThunk(IAT)成员, 获得IAT地址
    • 将上面获得的函数地址输入相应IAT数组值
    • 重复以上步骤, 直到INT结束(遇到NULL时)
  • INT与IAT:
    • OriginalFirstThunk指向的数组(INT)在PE加载到内存中时被保留了下来且永远不会被修改
    • 但是Windows加载PE到内存后, 会重写FirstThunk所指向的数组元素(IAT)中的值
    • 使得FirstThunk指向的数组元素不再表示带有函数描述的IMAGE_IMPORT_BY_NAME结构体
    • 而是直接指向了函数地址
    • 参考资料

EAT

  • EAT使不同的应用程序可以调用库文件中提供的函数
    • 只有通过EAT才能准确求得从库函数中导出函数的起始地址
    • 与IAT一样, PE文件内的IMAGE_EXPORT_DIRECTORY结构体保存着导出信息
    • 且PE文件中仅有一个用来说明库EAT的IMAGE_EXPORT_DIRECTORY
    • 一般PE文件此项值应为0, 代表不存在这个表项, 只有库文件, 才会含有这个表项。
  • IMAGE_EXPORT_DIRECTORY
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;        // 未使用, 总为0 

    DWORD   TimeDateStamp;          // 文件创建时间戳
    WORD    MajorVersion;           // 未使用, 总为0 

    WORD    MinorVersion;           // 未使用, 总为0
    DWORD   Name;                   // 指向一个代表此DLL名字的ASCII字符串的RVA
    DWORD   Base;                   // 函数的起始序号
    DWORD   NumberOfFunctions;      // 导出函数的总数
    DWORD   NumberOfNames;          // 以名称方式导出的函数的总数
    DWORD   AddressOfFunctions;     // 指向导出函数地址数组的RVA
                                    // 有NumberOfFunctions个元素
    DWORD   AddressOfNames;         // 指向导出函数名字数组的RVA
                                    // 有NumberOfNames个元素
    DWORD   AddressOfNameOrdinals;  // 指向导出函数序号数组的RVA
                                    // 有NumberOfNames个元素
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  • 从库中获得函数地址的API为GetProcAddress()函数, 该函数引用EAT来获取指定API的地址:
    • 利用AddressOfNames成员转到"函数名称数组"
    • "函数名称数组"中存储着字符串地址, 通过字符串比较(strcmp), 查找指定的函数名称(此时数组的索引为name_index)
    • 利用AddressOfNameOrdinals成员, 转到ordinal数组
    • 在ordinal数组中通过name_index查找相应的ordinal(2 Bytes)值
    • 利用AddressOfFuntions成员转到"函数地址数组"(EAT)
    • 在"函数地址数组"中将刚才的ordinal用作索引, 获得指定函数的起始地址

运行时压缩

数据压缩

  • 无损压缩: 用来缩减文件(数据)的大小, 压缩后的文件更易保管, 移动
    • 最具代表性的无损压缩算法有Run-Length, Lempel-Ziv, Huffman等
    • 此外还有许多其他压缩算法都是在以上三种压缩算法的基础上改造而成的
  • 有损压缩: 允许压缩文件(数据)时损失一定信息, 以此换取高压缩率
    • 压缩多媒体文件(jpg, mp3, mp4)时, 大部分都是有这种有损压缩方式
    • 有损压缩的数据解压后不能完全恢复原始数据
    • 以mp3为例, 其核心算法通过删除超越人类听觉范围(20~20000Hz)的波长区段来缩减数据大小

运行时压缩器

  • 针对可执行文件(PE文件)而言的
    • 运行时压缩文件也是PE文件
    • 内部含有原PE文件与解码程序
    • 在EP代码中执行解码程序
    • 普通压缩与运行时压缩的比较:
    项目           普通压缩                      运行时压缩
    对象文件       所有文件                      PE文件(exe, dll, sys)
    压缩结果       压缩文件(zip, rar)            PE文件(exe, dll, sys)
    解压缩方式     使用专门解压缩程序            内部含有解码程序
    文件是否可执行 本身不可执行                  本身可执行
    优点           可以对所有文件以高压缩率压缩  无需专门解压缩程序便可直接运行
    缺点           需要专门解压缩软件            每次运行调用解码程序, 耗时过长
    
    • 把普通PE文件创建成运行时压缩文件的程序称为"压缩器(Packer)"
    • 经反逆项(Anti-Reversing)技术特别处理的压缩器称为保护器(Protector)
  • 压缩器
    • PE压缩器是指可执行文件的压缩器, 它是PE文件的专用压缩器
    • 使用目的:
      • 缩减PE文件的大小
      • 隐藏PE文件内部代码与资源
    • 使用现状: 现状的实用程序, "打补丁"文件, 普通程序等都广泛应用运行时压缩
    • 压缩器种类: 大致分为两类--一类单纯用于压缩普通PE文件的压缩器(UPX, ASPack等); 一类对源文件进行较大变形, 严重破坏PE头, 意图稍显不纯(用于恶意程序)的压缩器(UPack, PESpin, NSAnti等)
  • 保护器
    • PE保护器是一类保护PE文件免受代码逆向分析的应用程序, 它们应用了多种防止代码逆向分析的技术(反调试, 反模拟, 代码混乱, 多态代码, 垃圾代码, 调试器监视等), 这类保护器使压缩后的PE文件尺寸反而比源文件要大一些
    • 使用目的:
      • 防止破解
      • 保护代码与资源
    • 使用现状: 大量应用于对破解很敏感的安全程序; 另一方面, 恶性代码(Trojan, Worm)中也大量使用保护器来防止(或降低)杀毒软件的检测
    • 保护器种类: 有公用程序(UltraProtect, Morphine等), 商业程序(ASProtect, Themida, SVKP等), 还有专门供恶意代码使用的保护器等

跟踪UPX文件

  • 对于普通的运行时压缩文件, 源文件代码, 数据, 资源解压缩之后, 先设置好IAT再跳转到OEP(源文件的EP: Original Entry Point)
  • 快速查找UPX OEP方法:
    • 在POPAD指令后的JMP指令处设置断点
      • UPX压缩器的特征之一是, 其EP代码被包含在PUSHAD/POPAD指令之间
      • 跳转到OEP的JMP指令紧接着出现在POPAD指令之后
    • 在栈中设置硬件断点
      • 该方法也利用UPX的PUSHAD/POPAD指令的特点
      • EAX到EDI寄存器依次入栈后, 从Dump窗口追踪当前栈顶地址, 右键设置硬件断点
      • 硬件断点是CPU支持的断点, 最多可以设置4个. 与普通断点不同的是, 设置断点的指令执行完成后才暂停调试. 程序在执行POPAD的瞬间访问设有硬件断点的地址, 然后暂停调试, 其下方即是跳转到OEP的JMP指令

基址重定位表

PE重定位

  • PE重定位是指PE文件无法加载到ImageBase所指的位置, 而是被加载到其他地址时发生的一系列处理行为
  • 使用SDK(Software Development Kit)或Visual C++创建PE文件时, exe默认的ImageBase为00400000, dll默认的ImageBase为10000000; 使用DDK(Driver Development Kit)创建的sys文件默认的ImageBase为10000
  • 创建好进程后, exe文件会首先加载到内存, 所以在exe中无须考虑重定位问题
  • Visata之后的版本引入了ASLR安全机制, 每次运行PE文件都会被加载到随机位置
  • 系统dll都有自身固有的ImageBase, 所以系统的dll实际不会发生重定位问题
  • PE重定位时执行的操作
    • 生成(构建)exe文件时, 由于无法预测程序实际被加载到哪个地址, 所以记录硬编码地址时以ImageBase为基准
    • 在运行瞬间, 经过PE重定位后, 这些地址全部以加载地址为基准变换
  • PE重定位操作原理
    • 在应用程序中查找硬编码的地址位置
    • 读取值后, 减去ImageBase(VA->RVA)
    • 加上实际加载地址(RVA->VA)
    • 其关键是查找硬编码地址的位置
    • 查找过程中会用到PE内部的Relocation Table(重定位表), 它记录硬编码地址偏移, 在PE文件构建过程中被提供
  • 基址重定位表
    • 基址重定位表的地址位于PE头的DataDirectory数组的第六个元素
    //
    // Based relocation format.
    //
    
    typedef struct _IMAGE_BASE_RELOCATION {
        DWORD   VirtualAddress;
        DWORD   SizeOfBlock;
    //  WORD    TypeOffset[];
    } IMAGE_BASE_RELOCATION; 
    typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
    
    //
    // Based relocation types.
    //
    
    #define IMAGE_REL_BASED_ABSOLUTE        0
    #define IMAGE_REL_BASED_HIGH            1
    #define IMAGE_REL_BASED_LOW             2
    #define IMAGE_REL_BASED_HIGHLOW         3   // 常见于PE文件
    #define IMAGE_REL_BASED_HIGHDJ          4
    #define IMAGE_REL_BASED_MIPS_JMPADDR    5
    #define IMAGE_REL_BASED_MIPS_JMPADDR16  9
    #define IMAGE_REL_BASED_IA64_IMM64      9
    #define IMAGE_REL_BASED_DIR64           10  // 常见于PE+文件
    
    • IMAGE_BASE_RELOCATION结构体的第一个成员为VirtualAddress, 它是一个基准地址, 实际是RVA值
    • 第二个成员为SizeOfBlock, 指重定位块的大小
    • 最后一项以注释形式存在的TypeOffset表示该结构体之下会出现WORD型的数组, 并且该数组元素的值就是硬编码在程序中的地址偏移
    • TypeOffset的值为16位, 由4位的Type与12位的Offset合成, 高4位用作Type, 低12位和VirtualAddress合成真正的RVA
    • 恶意代码中正常修改文件代码后, 有时要修改指向相应区域的重定位表, 常常把Type的值改为0
    • TypeOffset项中指向位移的12位最大拥有的地址值为1000, 为了表示更大的地址, 要添加1个与其对应的块, 由于这些块以数字形式罗列, 故称为重定位表
    • 若TypeOffset的值为0, 则表明一个IMAGE_BASE_RELOCATION结构体结束
    • 重定位表以NULL结构体结束(即IMAGE_BASE_RELOCATION结构体成员的值全为NULL)

从可执行文件中删除.reloc节区

.reloc节区

  • 一般的exe文件中, "基址重定位表"项对运行没什么影响(exe中无须考虑重定位问题)
  • VC++中生成的PE文件的重定位节区名为.reloc, 删除该节区后文件照常运行, 且大小将缩减.
  • .reloc节区一般位于所有节区的最后

删除.reloc节区的操作

  • 整理.reloc节区头
  • 删除.reloc节区
  • 修改IMAGE_FILE_HEADER
  • 修改IMAGE_OPTIONAL_HEADER

UPack PE文件头详细分析

UPack

  • UPack(Ultimate PE压缩器)是一款PE文件的运行时压缩器, 其特点是用一种非常独特的方式对PE头进行变形
  • UPack对PE头的读条处理使大多数PE实用程序(调试器, PE View等)无法正常运行; 使用Stud_PE工具进行分析

分析UPack的PE文件头

  • 重叠文件头
    • 重叠文件头也是其他压缩器常用的技法, 借助该方法可以把MZ(IMAGE_DOS_HEADERH)文件头与PE文件头(IMAGE_NT_HEADERS)巧妙的重叠
    • 根据PE文件规范, IMAGE_NT_HEADERS的起始位置是可变的, 由e_lfanew的值决定, 一般在一个正常的程序中, e_lfanew拥有如下值: e_lfanew = MZ文件头大小(40) + DOS存根大小(可变: VC++下为A0) = E0
    • UPack中, e_lfanew的值为10, 这样就把MZ文件头和PE文件头重叠在一起
  • IMAGE_FILE_HEADER.SizeOfOptionalHeader
    • 修改IMAGE_FILE_HEADER.SizeOfOptionalHeader的值, 可以向文件头插入解码代码
    • SizeOfOptionalHeader表示PE文件头中紧接在IMAGE_FILE_HEADER下的IMAGE_OPTIONAL_HEADER结构体的长度(0xe0), UPack将该值更改为148
    • 设计SizeOfOptionalHeader的原因是IMAGE_OPTIONAL_HEADER的种类很多, 所以需要另外输入结构体的大小(比如, 64位的PE+文件中该值为0xf0), 通过该值, 能确定节区头的起始偏移
    • 修改SizeOfOptionalHeader的值之后, 就在IMAGE_OPTIONAL_HEADER与IMAGE_SECTION_HEADER之间添加了额外空间, UPack就向这个区域添加解码代码
    • 这段代码并不是PE文件头中的信息, 而是UPack中使用的代码, 若PE相关实用工具将其识别为PE文件头信息, 就会引发错误, 导致程序无法运行
  • IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSizes
    • NumberOfRvaAndSizes值用来指出紧接在后面的IMAGE_DATA_DIRECTORY结构体数组的元素个数
    • 正常文件中IMAGE_DATA_DIRECTORY该值被确定为0x10, 但在UPack中将其修改为0x0a, 这样IMAGE_DATA_DIRECTORY结构体数组的后6个元素被忽略
    • 修改后, UPakc就在这块被忽视的区域中覆写自己的代码
  • IMAGE_SECTION_HEADER
    • IMAGE_SECTION_HEADER结构体中, UPack会把自身数据记录到程序运行不需要的项目
    • 如节区头中的: PointToRelocations, PointerToLinenumbers, NumberOfRelocations, NumberOfLinenumbers成员都被覆写
  • 重叠节区
    • UPack的主要特征之一就是随意重叠PE节区与文件头
    • 以notepad.exe为例(详见教材18.5.5):
      • UPack将文件的第一节区和第三节区的RawOffset设置为0x10, 且RawSize设置为0x1f0, 但是节区的内存起始RVA(VirtualOffset)与内存大小(VirtualSize)项彼此不同, 这样就将PE文件头, 第一节区, 第三节区进行重叠
      • 根据节区头中定义的值, PE装载器会将文件偏移0-0x1ff的区域分别映射到3个不同的内存位置(文件头, 第一节区, 第三节区), 也就是说, 用相同的文件映像可以分别创建出不同位置的, 大小不同的内存映像
      • 这里, 文件头(第一/三节区)区域的大小为200, 这是非常小的, 相反, 第二节区尺寸非常大, 原文件(notepad.exe)即压缩于此
      • 内存中第一节区区域的尺寸为14000, 与原文件的SizeOfImage具有相同的值, 也就是说, 压缩在第二个节区中的文件映像会被原样解压缩到第一节区(notepad的内存映像), 另外, 原notepad.exe拥有3个节区, 它们被解压到一个节区
      • 压缩的notepad在内存的第二节区, 解压缩的同时被记录到第一节区, 重要的是, notepad.exe的内存映像会被整体解压, 所以程序能正常运行(地址一致)
  • RVA to RAW (仍以notepad.exe为例, 教材18.5.6)
    • 一般来说, 指向节区开始的文件偏移的PointerToRawData值应该是FileAlignment的整数倍
    • UPack的FileAlignment为200
    • PE装载器发现第一个节区的PointerToRawData(10)不是FileAlignment(200)的整数倍时, 会强制将其识别为整数倍(该情况下为0)
    • 这是UPack文件能正常运行, 但许多PE相关实用程序会发生错误的原因, 这也是上面为什么映射0-0x1ff而不是0x10-0x1ff的原因
  • 导入表(IMAGE_IMPORT_DESCRIPTOR array)
    • 根据PE规范, 导入表是由一系列IMAGE_IMPORT_DESCRIPTOR结构体组成的数组, 最后以一个内容为NULL的结构体结束
    • UPack把最后一个IMAGE_IMPORT_DESCRIPTOR从中截断
    • 前一部分和PE头重叠被映射到内存, 后一部分通过内存自动补全的NULL结尾
    • 详见教材18.5.7
  • 导入地址表
    • 详见教材18.5.8

内嵌补丁

  • "内嵌补丁"是"内嵌代码补丁"(Inline Code Patch)的简称
    • 常用于对象程序经过运行时压缩(或加密处理)而难以直接修改的情况
    • 典型的运行时压缩代码结构: [EP] jmp→ [OEP]
    • 在文件中另外设置被称为"洞穴代码"的"补丁代码"后: [EP] jmp→ [Patch] jmp→ [OEP]
    • EP代码解密后通过修改jmp指令, 运行洞穴代码
  • 补丁代码设置位置:
    1. 设置到文件的空白区域
    • 扩展最后节区后设置
    • 添加新的节区后设置
    • 补丁代码较少时, 使用第一种方法, 其他情况使用方法2或3, 补丁代码所在节区一定要有可写属性(节区头添加)

Windows消息钩取

消息钩子

  • 常规Windows消息流
    • 发生事件时, 消息被添加到[OS message queue]
    • OS判断哪个应用程序发生了事件, 然后从[OS message queue]取出消息, 添加到相应应用程序的[application message queue]中
    • 应用程序监视自身的[application message queue], 发现新添加的消息后, 调用相应的事件处理程序处理
  • "钩链"(Hook Chain)
    • OS消息队列与应用程序消息队列中存在一条"钩链"
    • 设置好消息钩子后, 处于"钩链"中的消息钩子会比应用程序先看的相应消息
    • 在消息钩子函数内部, 除了查看消息外, 还能修改, 拦截消息等
    • 设置多个消息钩子, 按照设置顺序依次调用, 它们组成的链条即为"钩链"
    • 像这样的消息钩子功能是Windows提供的基本功能, 其中最具代表性的是VS中提供的SPY++, 能够查看操作系统中的所有消息
  • 整个消息传递过程
    • 当事件发生时, 应用程序可以在相应的钩子Hook上设置多个钩子子程序(Hook Procedures), 由其组成一个与钩子相关联的指向钩子函数的指针列表(钩链)
    • 当钩子所监视的消息出现时, Windows首先将其送到调用链表中所指向的第一个钩子函数中
    • 钩子函数将根据其各自的功能对消息进行处理, 之后把消息传递给下一钩子函数(当然, 也可以强制终止消息的传递, 即返回一个非0值)
    • 直至到达钩链的末尾, 在钩子函数交出控制权后, 被拦截的消息最终仍将交还给窗口处理函数
  • SetWindowsHookEx()
    • 使用SetWindowsHookEx()API可以轻松实现消息钩子
    HHOOK SetWindowsHookEx(
        int         idHook,         // hook type
        HOOKPROC    lpfn,           // hook procedure
        HINSTANCE   hMod,           // hook procedure 所属的dll句柄
        DWORD       dwThreadId,     // 想要挂钩的线程id
    );
    
    • 钩子过程(hook procedure)是由操作系统调用的回调函数
    • 若dwThreadId参数被设置为0, 则安装的钩子为"全局钩子", 它会影响到所有进程
    • 使用SetWindowsHookEx()设置好钩子之后, 在某个进程中生成指定消息时, 操作系统会将相关的dll文件强制注入(injection)相应进程, 然后调用注册的钩子过程
    • 使用样例:
    // KeyHook.cpp
    #include    "windows.h"
    
    #define     DEF_PROCESS_NAME    "notepad.exe"
    
    HINSTANCE   g_hInstance = NULL;
    HHOOK       g_hHook = NULL;
    HWND        g_hWnd = NULL;
    
    BOOL APIENTRY DllMain(HMODULE   hModule,
        DWORD   ul_reason_for_call,
        LPVOID  lpReserved
    )
    {
        switch (ul_reason_for_call)
        {
        case DLL_PROCESS_ATTACH:
            g_hInstance = hModule;
            break;
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
        }
        return TRUE;
    }
    
    LRESULT CALLBACK KeyboardProc(int nCode,
        WPARAM wParam,
        LPARAM lParam
    )
    {
        char    szPath[MAX_PATH] = { 0, };
        char    *p = NULL;
    
        if (nCode == 0) {
            // bit 31: 0 = key press, 1 = key release
            if (!(lParam & 0x80000000)) { // key release
                GetModuleFileNameA(NULL, szPath, MAX_PATH);
                p = strrchr(szPath, '\\');
    
                // 比较进程名称, 如果是目标进程, 则消息不会传递
                if (!_stricmp(p + 1, DEF_PROCESS_NAME))
                    return 1;
            }
        }
    
        return CallNextHookEx(g_hHook, nCode, wParam, lParam);
    }
    
    #ifdef __cplusplus
    extern "C" {
    #endif // __cplusplus
    
        __declspec(dllexport) void HookStart() {
            g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, 0);
        }
    
    #ifdef __cplusplus
    }
    #endif // __cplusplus
    
    #ifdef __cplusplus
    extern "C" {
    #endif // __cplusplus
    
        __declspec(dllexport) void HookStop() {
            UnhookWindowsHookEx(g_hHook);
            g_hHook = NULL;
        }
    
    #ifdef __cplusplus
    }
    #endif // __cplusplus
    


// HookMain.cpp

#include    "stdio.h"
#include    "conio.h"
#include    "windows.h"

#define     DEF_DLL_NAME    "KeyHook.dll"
#define     DEF_HOOKSTART   "HookStart"
#define     DEF_HOOKSTOP    "HookStop"

typedef     void(*PFN_HOOKSTART)();
typedef     void(*PFN_HOOKSTOP)();

int main()
{
    HMODULE       hDll = NULL;
    PFN_HOOKSTART HookStart = NULL;
    PFN_HOOKSTOP  HookStop = NULL;
    char          ch = 0;
    
    // 加载KeyHook.dll
    hDll = LoadLibraryA(DEF_DLL_NAME);

    // 获取导出函数地址
    HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
    HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);

    // 开始钩取
    HookStart();

    // 等待直到用户输入"q"
    printf("press 'q' to quit!\n");
    while (_getch() != 'q');
    
    // 终止钩取
    HookStop();

    // 卸载KeyHook.dll
    FreeLibrary(hDll);
    return 0;
}
```

dll注入

dll注入

  • 实现方法:
    • 创建远程线程(CreateRemoteThread() API)
    • 使用注册表(AppInit_DLLs)值
    • 消息钩取(SetWindowsHookEx() API)

CreateRemoteThread()

  • 示例代码:
//myhack.cpp

#include    "windows.h"
#include    "tchar.h"
#include    "stdlib.h"
#include    "Urlmon.h"

#pragma     comment(lib, "Urlmon.lib")

#define     DEF_URL         (L"http://www.baidu.com")
#define     DEF_FILE_NAME   (L"baidu.html")

HMODULE     g_hMod = NULL;

DWORD __stdcall ThreadProc(LPVOID lParam) {
    TCHAR   szPath[_MAX_PATH] = { 0, };
    if (!GetModuleFileName(g_hMod, szPath, MAX_PATH))
        return FALSE;

    TCHAR   *p = _tcsrchr(szPath, '\\');
    if (!p)
        return FALSE;

    _tcscpy_s(p + 1, _MAX_PATH, DEF_FILE_NAME);

    URLDownloadToFile(NULL, DEF_URL, szPath, 0, NULL);

    return 0;
}

BOOL __stdcall DllMain(HINSTANCE hinstDLL,
    DWORD     fdwReason,
    LPVOID    lpvReserved
) {
    HANDLE      hThread = NULL;

    g_hMod = (HMODULE)hinstDLL;

    switch (fdwReason) {
    case DLL_PROCESS_ATTACH:
        OutputDebugString(L"myhack.dll Injection!!!");
        hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
        CloseHandle(hThread);
        break;
    }
    return TRUE;
}
// Dll被加载(DLL_PROCESS_ATTACH)时, 先输出一个调试字符串
// 然后创建线程调用函数(ThreadProc)
// 在ThreadProc()函数中调用URLDownloadToFile() API来下载指定网站的html文件
-----------------------------------------------------------------------------


-----------------------------------------------------------------------------
// InjectDll.cpp

#include "windows.h"  
#include "stdio.h"  
  
BOOL InjectDll(DWORD dwPID, PCHAR szDllPath)
{  
    HMODULE         hMod            = NULL;  
    HANDLE          hProcess        = NULL;  
    HANDLE          hThread         = NULL;  
    LPVOID          pRemoteBuf      = 0;  
    DWORD           dwSize          = 0;  
    LPTHREAD_START_ROUTINE pThreadProc;
 
  
    // #1. 使用dwPID获取目标进程句柄
    if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS,   // dwDesiredAccess  
                                  FALSE,                // bInheritHandle  
                                  dwPID)) )             // dwProcessId  
    {  
        printf("OpenProcess() fail : err_code = %d\n", GetLastError());  
        return FALSE;  
    }  
  
    // #2. 在目标进程内存中分配szDllName大小的内存
    dwSize = sizeof(char) * (strlen(szDllPath)+1);  
    if( !(pRemoteBuf = VirtualAllocEx(hProcess,             // hProcess  
                                      NULL,                 // lpAddress  
                                      dwSize,               // dwSize  
                                      MEM_COMMIT,           // flAllocationType  
                                      PAGE_READWRITE)) )    // flProtect  
    {  
        printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());  
        return FALSE;  
    }  
  
    // #3. 将dll路径写入分配的内存
    if( !WriteProcessMemory(hProcess,                       // hProcess  
                            pRemoteBuf,                     // lpBaseAddress  
                            (LPVOID)szDllPath,              // lpBuffer  
                            dwSize,                         // nSize  
                            NULL) )                         // [out] lpNumberOfBytesWritten  
    {  
        printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());  
        return FALSE;  
    }  
  
    // #4. 获取LoadLibraryA() API的地址
    hMod = GetModuleHandleA("kernel32.dll");
    pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(
        hMod, "LoadLibraryA");

    // #5. 在进程中运行线程
    if( !(hThread = CreateRemoteThread(hProcess,            // hProcess  
                                       NULL,                // lpThreadAttributes  
                                       0,                   // dwStackSize  
                                       pThreadProc,         // dwStackSize  
                                       pRemoteBuf,          // lpParameter  
                                       0,                   // dwCreationFlags  
                                       NULL)) )             // lpThreadId  
    {  
        printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());  
        return FALSE;  
    }  
  
    WaitForSingleObject(hThread, INFINITE);   
  
    CloseHandle(hThread);  
    CloseHandle(hProcess);  
  
    return TRUE;  
}  
  
  
int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("USAGE : %s pid dll_path\n", argv[0]);
        return 1;
    }

    // inject dll
    if (InjectDll((DWORD)atol(argv[1]), argv[2]))
        printf("InjectDll(\"%s\") success!\n", argv[2]);
    else
        printf("InjectDll(\"%s\") failed!\n", argv[2]);
    return 0;
}

// main()函数的主要功能是检测输入程序的参数, 
// 然后调用InjectDll()函数
// 
// InjectDll()函数是用来实施dll注入的核心函数
// 其功能是命令目标进程自行调用LoadLibrary("*.dll") API
// 
// hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)):
// 调用OpenProcess() API, 借助参数dwPID获取目标进程的句柄
// (PROCESS_ALL_ACCESS权限), 之后, 就可以使用获取的句柄(hProcess)
// 控制对应进程
// 
// pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwSize, 
//                      MEM_COMMIT, PAGE_READWRITE)):
// 需要把即将加载的dll文件的路径(字符串)告知目标进程
// 因为任何内存空间都无法进行写入操作, 所以先用VirtualAllocEx() API
// 在目标进程的内存空间中分配一块缓冲区
// 
// WriteProcessMemory(hProcess, pRemoteBuf, 
//                  (LPVOID)szDllPath, dwSize, NULL):
// 使用WriteProcessMemory() API将dll路径字符串写入分配所得的缓冲区
// (pRemoteBuf)地址, 这样, 要注入的dll文件的路径就被写入目标进程
// 
// hMod = GetModuleHandleA("kernel32.dll");
// pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(
//      hMod, "LoadLibraryA"):
// 调用LoadLibrary() API前要先获取其地址
// 该段代码获取的是InjectDll.exe进程中的kernel32.dll地址
// 由于系统dll都有自身固有的ImageBase, 不会发生重定位问题
// 所以进程InjectDll.exe中的kernel32.dll地址和目标进程中的地址是一样的
// 
// hThread = CreateRemoteThread(hProcess, NULL, 0, 
//          pThreadProc, pRemoteBuf, 0, NULL):
// 最后向目标进程发送命令, 让其调用LoadLibraryA() API来加载指定的dll文件
// 然而Windows并未提供执行这样命令的API, 于是另辟蹊径用CreateRemoteThread() API
// HANDLE WINAPI CreateRemoteThread(
// __in  HANDLE                 hProcess,  // 目标进程句柄
// __in  LPSECURITY_ATRIBUTES   lpThreadAttributes,
// __in  SIZE_T                 dwStackSize,
// __in  LPTHREAD_START_ROUTINE lpStartSize,// 线程函数地址
// __in  LPVOID                 lpParameter,// 线程参数地址
// __in  DWORD                  dwCreationFlags,
// __out LPDWORD                lpThreadId
// );
// 因为线程函数ThreadProc()和LoadLibrary()函数都有一个4字节的参数, 一个4字节的返回
// 所以用创建远程线程的API时, 将线程函数地址替换为LoadLibrary()地址
// 将线程参数地址替换为dll文件路径字符串所在地址
// 就可以驱使目标进程加载指定的dll文件

AppInit_DLLs

  • Windows操作系统的注册表中默认提供了AppInit_DLLs与LoadAppInit_DLLs两个项
  • 在注册表编辑器中, 将要注入的dll路径字符串写入AppInit_DLLs项, 然后把LoadAppInit_DLLs的项目值设置为1, 重启后, 指定的DLL会注入所有运行进程
  • 原理: User32.dll被加载到进程时, 会读取AppInit_DLLs注册表项, 若有值, 则调用LoadLibrary() API加载用户dll, 所以, 严格的说, 相应的dll只是加载至所有加载user32.dll的进程
  • 注: Windows XP会忽略LoadAppInit_DLLs注册表项
  • AppInit_DLLs注册表键几乎可以向所有进程注入dll文件, 若被注入的dll出现问题(bug), 则有可能导致Windows无法正常启动

SetWindowsHookEx()

dll卸载

dll卸载

  • dll卸载是将强制插入进程的dll弹出的技术, 其基本工作原理与使用CreateRemoteThread() API进行dll注入类似
  • 原理:
    • 将FreeLibrary() API的地址传递给CreateRemoteThread() API的lpStartAddress参数, 并把要卸载的dll的句柄传递给lpParameter参数
    • 从而驱使目标进程调用FreeLibrary() API
  • 引用计数
    • 每个Windows内核对象(Kernel Object)都拥有一个引用计数(Reference Count), 代表对象被引用的次数
    • 每调用一次LoadLibrary(), 相应dll的引用计数会加1; 而每调用一次FreeLibrary(), 相应dll的引用计数会减1
    • 因此, 卸载dll时要充分考虑引用计数因素
  • 使用FreeLibrary()卸载dll的方法仅适用于卸载自己强制注入的dll文件, PE文件直接导入的dll文件是无法再进程运行过程中卸载的

dll卸载实例

// EjectDll.cpp :

#include    "windows.h"
#include    "tlhelp32.h"

#define     DEF_PROC_NAME   (L"notepad.exe")
#define     DEF_DLL_NAME    (L"myhacker.dll")

DWORD FindProcID(LPCTSTR szProcessName) {
    DWORD   dwPID = 0xffffffff;
    HANDLE  hSnapShot = INVALID_HANDLE_VALUE;
    PROCESSENTRY32 pe;

    // 获取系统快照(SnapShot)
    pe.dwSize = sizeof(PROCESSENTRY32);
    hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);

    // 查找进程
    Process32First(hSnapShot, &pe);
    do {
        if (!_tcsicmp(szProcessName, (LPCTSTR)pe.szExeFile)) {
            dwPID = pe.th32ProcessID;
            break;
        }
    } while (Process32Next(hSnapShot, &pe));

    CloseHandle(hSnapShot);

    return dwPID;
}

BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege) {
    TOKEN_PRIVILEGES tp;
    HANDLE           hToken;
    LUID             luid;
    
    if (!OpenProcessToken(GetCurrentProcess(),
        TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
        &hToken)) {
        _tprintf(L"OpenProcessToke error: %u\n", GetLastError());
        return FALSE;
    }

    if (!LookupPrivilegeValue(NULL, // lookup privilege on local system
        lpszPrivilege,              // privilege to lookup
        &luid                       // receives LUID of privilege
    )) {
        _tprintf(L"LookupPrivilegeValue error: %u\n", GetLastError());
        return FALSE;
    }

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    if (bEnablePrivilege)
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    else
        tp.Privileges[0].Attributes = 0;

    // Enable the privilege or disable all privileges.
    if (!AdjustTokenPrivileges(hToken, FALSE,
        &tp, sizeof(TOKEN_PRIVILEGES),
        (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL)) {
        _tprintf(L"AdjustTokenPrivileges error: %u\n", GetLastError());
        return FALSE;
    }

    if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
        _tprintf(L"The token does not have the specified privilege. \n");
        return FALSE;
    }
    return TRUE;
}

BOOL EjectDll(DWORD dwPID, LPCTSTR szDllName) {
    BOOL    bMore = FALSE, bFound = FALSE;
    HANDLE  hSnapshot, hProcess, hThread;
    HMODULE hModule = NULL;
    MODULEENTRY32 me = { sizeof(me) };
    LPTHREAD_START_ROUTINE pThreadProc;

    // dwPID = 目标进程PID
    // 使用TH32CS_SNAPMODULE参数, 获取加载到目标进程的dll名称
    hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);

    bMore = Module32First(hSnapshot, &me);
    for (; bMore; bMore = Module32Next(hSnapshot, &me)) {
        if (!_tcsicmp((LPCTSTR)me.szModule, szDllName)
            || !_tcsicmp((LPCTSTR)me.szExePath, szDllName)) {
            bFound = TRUE;
            break;
        }
    }

    if (!bFound) {
        CloseHandle(hSnapshot);
        return FALSE;
    }
    
    if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS,   // dwDesiredAccess  
        FALSE,                // bInheritHandle  
        dwPID)))             // dwProcessId  
    {
        _tprintf(L"OpenProcess() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    hModule = GetModuleHandleA("kernel32.dll");
    pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(
        hModule, "FreeLibrary");

    if (!(hThread = CreateRemoteThread(hProcess,            // hProcess  
        NULL,                // lpThreadAttributes  
        0,                   // dwStackSize  
        pThreadProc,         // dwStackSize  
        me.modBaseAddr,      // lpParameter  
        0,                   // dwCreationFlags  
        NULL)))              // lpThreadId  
    {
        printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    WaitForSingleObject(hThread, INFINITE);

    CloseHandle(hThread);
    CloseHandle(hProcess);
    CloseHandle(hSnapshot);

    return TRUE;
}

int _tmain(int argc, TCHAR* argv[])
{
    DWORD   dwPID = 0xffffffff;

    // 查找process
    dwPID = FindProcID(DEF_PROC_NAME);
    if (dwPID == 0xffffffff) {
        _tprintf(L"There is no %s process!\n", DEF_PROC_NAME);
        return 1;
    }
    _tprintf(L"PID of \"%s\" is %d\n", DEF_PROC_NAME, dwPID);

    // 更改privilege
    if (!SetPrivilege(SE_DEBUG_NAME, TRUE))
        return 1;

    // eject dll
    if (EjectDll(dwPID, DEF_DLL_NAME))
        _tprintf(L"EjectDll(%d \"%s\") success!\n", dwPID, DEF_DLL_NAME);
    else
        _tprintf(L"EjectDll(%d \"%s\") failed!\n", dwPID, DEF_DLL_NAME);
    return 0;
}
  • 获取进程中加载的dll信息
    • hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
    • 使用CreateToolhelp32Snapshot API可以获取加载到进程的dll信息
    • 将获取的hSnapshot句柄传递给Module32First()/Module32Next()后
    • 即可设置与MODULEENTRY32结构体相关的模块信息
    typedef struct tagMODULEENTRY32W
    {
        DWORD   dwSize;
        DWORD   th32ModuleID;       // This module
        DWORD   th32ProcessID;      // owning process
        DWORD   GlblcntUsage;       // Global usage count on the module
        DWORD   ProccntUsage;       // Module usage count in th32ProcessID's context
        BYTE  * modBaseAddr;        // Base address of module in th32ProcessID's context
        DWORD   modBaseSize;        // Size in bytes of module starting at modBaseAddr
        HMODULE hModule;            // The hModule of this module in th32ProcessID's context
        WCHAR   szModule[MAX_MODULE_NAME32 + 1];
        WCHAR   szExePath[MAX_PATH];
    } MODULEENTRY32W;
    typedef MODULEENTRY32W *  PMODULEENTRY32W;
    typedef MODULEENTRY32W *  LPMODULEENTRY32W;
    
    • szModule成员表示dll名称, modBaseAddr成员表示相应dll被加载的地址(进程虚拟内存)
    • 在EjectDll()函数的for循环中比较szModule与希望卸载的dll文件名称, 找到相应的模块信息
  • 获取目标进程的句柄
    • hProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)
  • 获取FreeLibrary() API地址
    • hModule = GetModuleHandleA("kernel32.dll");
    • pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress( hModule, "FreeLibrary");
    • 利用系统dll固定加载地址的原理, 找到EjectDll.exe进程中的FreeLibrary()地址即可
  • 在目标进程中运行线程
    • hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL)
    • pThreadProc参数是FreeLibrary() API地址, me.modBaseAddr参数是要卸载的dll的加载地址

通过修改PE加载dll

准备

  1. 查看IDT是否拥有足够空间
    • IDT由IID(IMAGE_IMPORT_DESCRIPTOR)组成的数组, 以NULL结束
    • 每个导入的DLL文件都对应1个IID结构体(0x14字节)
    • 正常情况下IDT后面会紧跟其他内容, 没有空余区域来添加新的dll结构体
  2. 移动IDT
    • 要添加新的dll结构体, 得把整个IDT转移到其他更广阔的位置
    • 确定移动的目标位置, 有以下三种方式
      • 查找文件中的空白可用区域(Null-Padding)
      • 增加文件最后一节区的大小
      • 在文件末尾添加新的节区
    • 选取Null-Padding区域时, 应注意, 并不是所有的区域都会被无条件加载到进程的虚拟内存, 只有节区头中明确记录的区域才会被加载

修改

  1. 修改导入表的RVA值, 并且Size在原基础上加0x14字节
  2. 删除绑定导入表(BOUND IMPORT TABLE)
    • 绑定导入表是一种提高dll加载速度的技术
    • 该项是可选项, 所以无需修改, 仅删除即可
  3. 创建新的IDT
  4. 设置Name, INT, IAT
    • Name指向一个字符串, 是dll的名字
    • INT是一个RVA数组, 指向导入函数的Ordinal + Function Name String组成
    • IAT可以拥有和INT一样的值, 在装载时会被替换成实际函数的地址
  5. 修改IAT节区的属性值
    • 加载PE到内存时, PE装载器会修改IAT
    • 所以相关节区一定要拥有WIRTE(可写)属性
    • 原IAT一样位于没有可写属性的节区, 但是程序能正常运行, 因为在可选头中有指定IAT的RVA, 这样, 即使相应节区不具有可写属性也没关系

代码注入

代码注入

  • 代码注入是一种向目标进程插入独立运行代码并使之运行的技术
  • 一般调用CreateRemoteThread() API以远程线程形式运行
  • 所以也称线程注入

dll注入与代码注入

  • 采用dll注入技术时, 整个dll会被插入目标进程, 代码与数据共存于内存
  • 代码注入仅向目标进程注入必要的代码, 想要使注入的代码正常运行, 还必须将代码中使用的数据一同注入
  • 使用代码注入的原因
    • 占用内存少
      • 如果要注入的代码和数据较少
      • 采用代码注入能获得与dll注入相同的效果
      • 且占用的内存会更少
    • 难以查找痕迹
      • 采用dll注入方式会在目标进程的内存中留下相关痕迹
      • 采用代码注入方式几乎不会留下任何痕迹
    • 其他
      • 不需要另外的dll文件
    • 总结:
      • dll注入技术主要用在代码量大且复杂的时候
      • 代码注入技术则适用于代码量小且简单的情况

示例及分析(CodeInjection.cpp)

  • main()
int main(int argc, char* argv[])
{
    DWORD dwPID = 0;
    if (argc != 2) {
        printf("\n USAGE : %s pid\n", argv[0]);
        return 1;
    }
    // code injection
    dwPID = (DWORD)atol(argv[1]);
    InjectCode(dwPID);
    return 0;
}
* main()函数用来调用InjectCode()函数
  • typedef部分
// Thread Parameter
typedef struct _THREAD_PARAM {
    FARPROC pFunc[2];       // LoadLibraryA(), GetProcAddress()
    char    szBuf[4][128];  // "user32.dll", "MessageBoxA"
                            // "www.reversecore.com", "ReverseCore"
} THREAD_PARAM, *PTHREAD_PARAM;

// LoadLibraryA()
typedef HMODULE (WINAPI *PFLOADLIBRARY)(LPCSTR lpLibFileName);

// GetProcAddress()
typedef FARPROC (WINAPI *PFGETPROCADDRESS)(HMODULE hModule, LPCSTR lpProcName);

// MessageBoxA()
typedef int (WINAPI *PFMESSAGEBOXA)(HWND hwnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
+ 定义了用于充当线程参数的结构体THREAD_PARAM
+ 指向LoadLibraryA()起始地址的函数指针PFLOADLIBRARY
+ 指向GetProcAddress()起始地址的函数指针PFGETPROCADDRESS
+ 指向MessageBoxA()起始地址的PFMESSAGEBOXA
  • ThreadProc()函数
// Thread Procedure
DWORD WINAPI ThreadProc(LPVOID lParam) {
    PTHREAD_PARAM   pParam = (PTHREAD_PARAM)lParam;
    HMODULE         hMod = NULL;
    FARPROC         pFunc = NULL;
    // LoadLibrary("user32.dll")
    // pParam->pFunc[0] -> kernel32!LoadLibraryA()
    // pParam->szBuf[0] -> "user32.dll"
    hMod = ((PFLOADLIBRARY)pParam->pFunc[0])(pParam->szBuf[0]);

    // GetProcAddress("MessageBoxA")
    // pParam->pFunc[1] -> kernel32!GetProcAddress()
    // pParam->szBuf[1] -> "MessageBoxA"
    pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]);

    // MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK)
    // pParam->pFunc[2] -> "www.reversecore.com"
    // pParam->szBuf[3] -> "ReverseCore"
    ((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);

    return 0;
}
+ 等价于
```
hMod = LoadLibraryA("user32.dll");
pFunc = GetProcAddress(hMod, "MessageBoxA");
MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK);
```
+ 若直接将等价代码注入其他进程, 因为在CodeInjection进程中发生重定位, 以及目标进程中发生重定位的关系, 传递过去的代码(内存中实际上相关API以及参数, 字符串都是以地址的形式硬编码的)会因为地址不同而无法运行
+ 所以在ThreadProc()函数中使用THREAD_PARAM结构体来接收2个API地址与4个字符串数据
+ 其实可以不传递LoadLibraryA()与GetProcAddress()而直接传递MessageBoxA(), 但是原则上先传递LoadLibraryA()与GetProcAddress()来加载需要的dll, 再获取要用的函数地址, 这样的程序对于目标进程的适用性比较强
+ 大部分用户模式进程都会加载kernel32.dll, 所以直接传递LoadLibraryA()与GetProcAddress()的地址不会出什么问题, 但是某些系统进程不会加载, 应事前确认
  • InjectCode()函数(核心)
BOOL InjectCode(DWORD dwPID) {
    HMODULE         hMod = NULL;
    THREAD_PARAM    param = { 0, };
    HANDLE          hProcess = NULL;
    HANDLE          hThread = NULL;
    LPVOID          pRemoteBuf[2] = { 0, };
    DWORD           dwSize = 0;

    hMod = GetModuleHandleA("kernel32.dll");

    // set THREAD_PARAM
    param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
    param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
    strcpy_s(param.szBuf[0], "user32.dll");
    strcpy_s(param.szBuf[1], "MessageBoxA");
    strcpy_s(param.szBuf[2], "www.reversecore.com");
    strcpy_s(param.szBuf[3], "ReverseCore");

    // Open Process
    hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);

    // Allocation for THREAD_PARAM
    dwSize = sizeof(THREAD_PARAM);
    pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
    WriteProcessMemory(hProcess, pRemoteBuf[0], (LPVOID)&param, dwSize, NULL);

    // Allocation for ThreadProc()
    dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
    pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)ThreadProc, dwSize, NULL);


    hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0, NULL);
    WaitForSingleObject(hThread, INFINITE);

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}
+ 与dll注入类似, InjectCode()函数把THREAD_PARAM结构体变量的地址以参数形式传递给ThreadProc()线程函数
+ 函数ThreadProc()大小的计算方法: `dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;` ; 因为在MS Visual C++中使用Release模式编译程序代码后, 源代码中按照顺序编写, 所以直接用函数指针相减即可
+ 总体结构如下:
```
OpenProcess();

// data: THREAD_PARAM
VirtualAllocEx();
WriteProcessMemory();

// code: ThreadProc
VirtualAllocEx();
WriteProcessMemory();

CreateRemoteThread();
```

API钩取

钩取

  • 钩取(Hooking)是一种截取信息, 更改程序执行流向, 添加新功能的技术
  • 钩取的流程:
    • 使用反汇编器/调试器把握程序的结构与工作原理
    • 开发需要的"钩子"代码, 用于修改Bug, 改善程序功能
    • 灵活操作可执行文件与进程内存, 设置"钩子"代码
  • 钩取Win32 API的技术被称为API钩取, 与消息钩取共同广泛用于用户模式
  • 钩取技术在没有程序源代码的情况下对程序的分析非常有用

API

  • API(Application Programming Interface)
    • Windows OS中, 用户程序无法直接访问系统资源(内存, 文件, 网络等)
    • 当用户程序需要使用这些资源时, 必须向系统内核(Kernel)申请, 方法就是使用微软提供的Win32 API
    • 为运行实际的应用程序代码, 需要加载许多系统库(dll), 所有的进程默认加载kernel32.dll库, 而kernel32.dll库又会加载ntdll.dll库
    • 有一些例外情况: 如某些指定的系统进程(如smss.exe)不会加载kernel32.dll
    • 此外, gui程序中, user32.dll与gdi32.dll是必需的库
  • 用户模式的程序访问系统资源时, 由ntdll.dll向内核模式提出访问申请
    • 假设用notepad.exe打开c:\abc.txt文件
    • 首先在程序的代码中调用msvcrt!fopen() API
    • 然后引发一系列API调用: kernel32!CreateFileW() → ntdll!KiFastSystemCall() → SYSENTER → 进入内核模式

API钩取

  • 通过API钩取技术可以实现对某些Win32 API调用过程的拦截, 并获得相应的控制权限
  • 使用API钩取技术的优势:
    • 在API调用前/后运行用户的"钩子"代码
    • 查看或操作传递给API的参数或API函数的返回值
    • 取消对API的调用, 或更改执行流, 运行用户代码
  • 技术图表 TechMap
    • 静态方法针对"文件", 而动态方法针对内存, 一般的API钩取指动态方法
    静态                      动态
    文件对象                  内存对象
    程序运行前钩取            程序运行后钩取
    只需要钩取一次            每次运行时都要钩取
    用于特殊情况              常规方法
    不可脱钩!                 程序运行中可以脱钩(灵活性强)
    
    • 位置: 指出实施API钩取时应该操作哪部分(通常有3部分)
      • IAT: 将内部的API地址更改为钩取函数地址; 优点是实现简单, 缺点是无法钩取不在IAT而在程序中使用的API(如: 动态加载并使用dll时)
      • 代码: 系统库(*.dll)映射到进程内存时, 从中查找API的实际地址, 并直接修改代码
        • 该方法应用范围广泛, 具体实现通常有
        • 使用JMP指令修改起始代码
        • 覆写函数局部
        • 仅更改必需部分的局部
      • EAT: 将记录在dll的EAT中的API起始地址更改为钩取函数地址; 这种方法实现较难, 功能较弱, 并不常用
    • 技术: 大致分为调试法与注入法, 注入法又可分为代码注入与dll注入
      • 调试: 调试器拥有被调试者(目标进程)的所有权限, 所以可以向被调试者任意设置钩取函数
      • 注入
    • API: 图表最后一列给出各技术具体实现过程中要用的API

调试法: 记事本WriteFile() API钩取

关于调试器的说明

  • 功能:
    • 确认被调试者是否正确运行
    • 发现程序错误
    • 逐一执行被调试者的指令
    • 拥有对寄存器与内存的所有访问权限
  • 工作原理:
    • 调试进程经过注册后, 每当被调试者发生调试事件(Debug Event)时, OS就会暂停其运行, 并向调试器报告. 调试器处理后, 被调试者继续运行
    • 一般异常(Exception)也属于调试事件
    • 若相应进程处于非调试, 调试事件会在其自身的异常处理或OS的异常处理机制中被处理
    • 调试器无法处理或不关心的调试事件最终由OS处理
  • 调试事件
    • EXCEPTION_DEBUG_EVENT
    • CREATE_THREAD_DEBUG_EVENT
    • CREATE_PROCESS_DEBUG_EVENT
    • EXIT_THREAD_DEBUG_EVENT
    • EXIT_PROCESS_DEBUG_EVENT
    • LOAD_DLL_DEBUG_EVENT
    • UNLOAD_DLL_DEBUG_EVENT
    • OUTPUT_DEBUG_STRING_EVENT
    • RIP_EVENT
  • 上面列出的调试事件中, 与调试器相关的事件为EXCEPTION_DEBUG_EVENT, 与其相关的异常列表如下:
    • EXCEPTION_ACCESS_VIOLATION
    • EXCEPTION_ARRAY_BOUNDS_EXCEEDED
    • EXCEPTION_BREAKPOINT
    • EXCEPTION_DATATYPE_MISALIGNMENT
    • EXCEPTION_FLT_DENORMAL_OPERAND
    • EXCEPTION_FLT_DIVIDE_BY_ZERO
    • EXCEPTION_FLT_INEXACT_RESULT
    • EXCEPTION_FLT_INVALID_OPERATION
    • EXCEPTION_FLT_OVERFLOW
    • EXCEPTION_FLT_UNDERFLOW
    • EXCEPTION_ILLEGAL_INSTRUCTION
    • EXCEPTION_IN_PAGE_ERROR
    • EXCEPTION_INT_DIVIDE_BY_ZERO
    • EXCEPTION_INT_OVERFLOW
    • EXCEPTION_INVALID_DISPOSITION
    • EXCEPTION_NONCONTINUABLE_EXCEPTION
    • EXCEPTION_PRIV_INSTRUCTION
    • EXCEPTION_SINGLE_STEP
    • EXCEPTION_STACK_OVERFLOW
  • 调试器必须处理EXCEPTION_BREAKPOINT异常. 其对于汇编为 INT 3, IA-32指令为0xcc
  • 调试器实现断点的方法十分简单, 找到要设置断点的代码内存的起始地址处, 把1个字节修改为0xcc, 继续调试时, 恢复原值; 调试法钩取API就是用了这个特性

调试技术流程

  1. 对想钩取的进程进行附加操作, 使之成为被调试者
  2. "钩子": 将API起始地址的第一字节修改为0xcc
  3. 调用相应API时, 控制权移到调试器
  4. 执行需要的操作(参数, 返回等)
  5. 脱钩: 将0xcc恢复原值以正常运行API
  6. 运行API
  7. 控制权返还被调试者

示例代码

// hookdbg.cpp
#include <stdio.h>
#include <Windows.h>

LPVOID  g_pWriteFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE    g_chINT3 = 0xcc, g_chOrgByte = 0;

void DebugLoop();
bool OnCreateProcessDebugEvent(LPDEBUG_EVENT);
bool OnExceptionDebugEvent(LPDEBUG_EVENT);


int main(int argc, char* argv[])
{
    DWORD   dwPID;
    if (argc != 2) {
        printf("\nUsage: hookdbg.exe pid\n");
        return 1;
    }

    // Attach Process
    dwPID = atoi(argv[1]);
    if (!DebugActiveProcess(dwPID)) {
        printf("DebugActiveProcess(%d) failed!!\nError Code = %d\n", dwPID, GetLastError());
        return 1;
    }

    // 调试器循环
    DebugLoop();

    return 0;
}


// TODO: 类似于窗口过程函数(WndProc)
// 从被调试者处接收事件并处理, 然后使被调试者继续运行
void DebugLoop() {
    DEBUG_EVENT  de;
    DWORD        dwContinueStatus;

    // 等待被调试者发生事件
    // API: WaitForDebugEvent
    // BOOL APIENTRY WaitForDebugEvent(
    //      _Out_ LPDEBUG_EVENT lpDebugEvent,
    //      _In_ DWORD dwMilliseconds
    //  );
    // 由DEBUG_EVENT结构体de接收被调试进程的所有调试事件
    while (WaitForDebugEvent(&de, INFINITE))
    {
        dwContinueStatus = DBG_CONTINUE;
        // 被调试进程生成或者附加事件
        if (CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode) {
            OnCreateProcessDebugEvent(&de);
        }
        // 异常事件
        else if (EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode) {
            if (OnExceptionDebugEvent(&de))
                continue;
        }
        // 被调试进程终止事件
        else if (EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode) {
            // 调试器终止
            break;
        }
        // 再次运行被调试者
        // API: ContinueDebugEvent
        // BOOL APIENTRY ContinueDebugEvent(
        // _In_ DWORD dwProcessId,
        // _In_ DWORD dwThreadId,
        // _In_ DWORD dwContinueStatus
        // );
        // TODO: 使被调试者继续运行, 
        // 如果调试事件处理正常, 则dwContinueStatus应为DBG_CONTINUE
        // 若无法处理, 希望在应用程序的SEH中处理, 则dwContinueStatus应为DBG_EXCEPTION_NOT_HANDLED
        ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
    }
}

// TODO: 首先获取WriteFile() API的起始地址(调试者和被调试者具有相同的RVA)
// 然后获取被调试进程句柄, 并设置断点
bool OnCreateProcessDebugEvent(LPDEBUG_EVENT pde) {
    // 获取WriteFile() API地址
    g_pWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");

    // 通过CREATE_PROCESS_DEBUG_INFO结构体的hProcess成员获取被调试进程句柄
    memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));

    // API "钩子" - WriteFile()
    // 设置断点, 保存原始字节值(调试器拥有被调试者的调试权限)
    ReadProcessMemory(g_cpdi.hProcess, g_pWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
    WriteProcessMemory(g_cpdi.hProcess, g_pWriteFile, &g_chINT3, sizeof(BYTE), NULL);

    return true;
}

bool OnExceptionDebugEvent(LPDEBUG_EVENT pde) {
    CONTEXT     ctx;
    PBYTE       lpBuffer = NULL;
    DWORD       dwNumOfBytesToWrite, dwAddrOfBuffer, i;
    PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;

    // 是断点异常(INT 3)时
    if (EXCEPTION_BREAKPOINT == per->ExceptionCode) {
        // 断点地址为WriteFile() API地址时
        if (g_pWriteFile == per->ExceptionAddress) {
            // 1. Unhook
            // 将0xcc恢复
            WriteProcessMemory(g_cpdi.hProcess, g_pWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);

            // 2. 获取线程上下文
            ctx.ContextFlags = CONTEXT_CONTROL;
            GetThreadContext(g_cpdi.hThread, &ctx);

            // 3. 获取WriteFile()的第2, 3个参数
            // param 2 : ESP + 0x8
            // param 3 : ESP + 0xc
            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8), &dwAddrOfBuffer, sizeof(DWORD), NULL);
            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xc), &dwNumOfBytesToWrite, sizeof(DWORD), NULL);

            // 4. 分配临时缓冲区
            lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite + 1);
            memset(lpBuffer, 0, dwNumOfBytesToWrite + 1);

            // 5. 复制WriteFile()缓冲区到临时缓冲区
            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpBuffer, dwNumOfBytesToWrite, NULL);
            printf("\n# original string: %s\n", lpBuffer);

            // 6. 大小写互换
            for (i = 0; i < dwNumOfBytesToWrite; i++) {
                if (isalpha(lpBuffer[i]))
                    lpBuffer[i] ^= 0x20;
            }
            printf("\n# converted string: %s\n", lpBuffer);

            // 7. 将变换后的缓冲区复制到WriteFile()缓冲区
            WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpBuffer, dwNumOfBytesToWrite, NULL);

            // 8. 释放临时缓冲区
            free(lpBuffer);

            // 9. 将上下文的EIP更改为WriteFile()首地址
            // 由于执行完INT 3, 当前EIP比首地址大1
            ctx.Eip = (DWORD)g_pWriteFile;
            SetThreadContext(g_cpdi.hThread, &ctx);

            // 10. 运行被调试进程
            ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
            Sleep(0);

            // 11. API"钩子"
            WriteProcessMemory(g_cpdi.hProcess, g_pWriteFile, &g_chINT3, sizeof(BYTE), NULL);

            return true;
        }
    }
    return false;
}
  • 注: 虽然有DebugSetProcessKillOnExit() API可以不销毁被调试进程就退出(detach)调试器, 但是必须在终止调试器前"脱钩", 否则, 当进程调用相关API时会引发断点异常, 但又因为没有调试器而终止
  • 调用Sleep(0)函数可以释放当前线程剩余时间片, CPU会立即执行其他线程. 被调试进程的主线程处于运行态时, 会正常调用WriteFile() API; 然后经过一定时间, 控制权再次转移给hookdbg.exe, Sleep(0)后面的"钩子"代码会被调用
  • 如果没有Sleep(0), 在notepad.exe调用WriteFile() API的过程中, hookdbg.exe会尝试设置钩子, 可能导致内存访问异常

IAT钩取: 计算器显示中文数字

// SetWindowTextWHooking.dll
#include <windows.h>
#include <stdlib.h>

FARPROC g_pOrgFun;
typedef bool(*PFSETWINDOWTEXTW)(HWND, LPWSTR);

bool __stdcall hook_iat(LPCSTR, PROC, PROC);
bool __stdcall MySetWindowTextW(HWND, LPWSTR);

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
        // 保存原始api地址, 为脱钩用
        g_pOrgFun = GetProcAddress(GetModuleHandle(L"user32.dll"), "SetWindowTextW");

        // hook
        hook_iat("user32.dll", g_pOrgFun, (PROC)MySetWindowTextW);
        break;
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        // unhook
        hook_iat("user32.dll", (PROC)MySetWindowTextW, g_pOrgFun);
        break;
    }
    return TRUE;
}

bool __stdcall MySetWindowTextW(HWND hWnd, LPWSTR lpString) {
    wchar_t* pNum = L"零一二三四五六七八九";
    wchar_t temp[2] = { 0, };
    int i = 0, nLen = 0, nIndex = 0;
    
    nLen = wcslen(lpString);
    for (i = 0; i < nLen; i++) {
        if (L'0' <= lpString[i] && lpString[i] <= L'9') {
            temp[0] = lpString[i];
            nIndex = _wtoi(temp);
            lpString[i] = pNum[nIndex];
        }
    }
    return ((PFSETWINDOWTEXTW)g_pOrgFun)(hWnd, lpString);
}

bool __stdcall hook_iat(LPCSTR szDllName, PROC src, PROC dest) {
    HMODULE hMod;
    LPCSTR szLibName;
    PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
    PIMAGE_THUNK_DATA pThunk;
    DWORD dwOldProtect, dwRVA;
    PBYTE pAddr;

    // hMod, pAddr = ImageBase of calc.exe
    //               VA to MZ signature(IMAGE_DOS_HEADER)
    hMod = GetModuleHandle(NULL);
    pAddr = (PBYTE)hMod;

    // pAddr = VA to PE signature (IMAGE_NT_HEADRERS)
    pAddr += *((DWORD*)&pAddr[0x3C]);

    // dwRVA = RVA to IMAGE_IMPORT_DESCRIPTOR
    dwRVA = *((DWORD*)&pAddr[0x80]);

    // pImportDesc = VA to IMAGE_IMPORT_DESCRIPTOR Table
    pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)hMod + dwRVA);

    for (; pImportDesc->Name; pImportDesc++) {
        // szLibName = VA to IMAGE_IMPORT_DESCRIPTOR.Name
        szLibName = (LPCSTR)((DWORD)hMod + pImportDesc->Name);

        if (!_stricmp(szLibName, szDllName)) {
            // pThunk = IMAGE_IMPORT_DESCRIPTOR.FirstThunk
            //        = VA to IAT
            pThunk = (PIMAGE_THUNK_DATA)((DWORD)hMod + pImportDesc->FirstThunk);

            // pThunk->u1.Function = VA to API
            for (; pThunk->u1.Function; pThunk++) {
                if (pThunk->u1.Function == (DWORD)src) {
                    // 更改内存属性为E/R/W
                    VirtualProtect((LPVOID)&pThunk->u1.Function, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect);

                    // 修改IAT
                    pThunk->u1.Function = (DWORD)dest;

                    // 恢复内存属性
                    VirtualProtect((LPVOID)&pThunk->u1.Function, 4, dwOldProtect, &dwOldProtect);
                    
                    return TRUE;
                }
            }
        }
    }

    return FALSE;
}

API代码修改

  • 当要钩取的API不在进程的IAT中(即动态加载dll时), IAT技术会失效
  • 而API代码修改技术没有这一限制

原理

  • IAT钩取通过操作进程的特定IAT值来实现API钩取
  • 而API代码修改技术则将API代码的前5个字节修改为JMP XXXXXXXX指令来钩取API
  • 调用执行被钩取的API时, 会跳转到指定的hooking函数
  • 以向Process Explorer进程(procexp.exe)注入stealth.dll后钩取ntdll.ZwQuerySystemInformation() API为例
    • 钩取之前
      • procexp.exe的00422cf7地址处的 call dword ptr ds:[48c69c] 指令调用ntdll.ZwQuerySystemInformation() API
      • 相应API执行完毕后, 返回到调用代码的下一条指令的地址处
    • 钩取之后
      • Example
      • 在422cf7地址处调用ntdll.ZwQuerySystemInformation() API(7c93d92e)
      • 位于7c93d92e地址处的(修改后的) jmp 10001120 指令将执行流转到10001120地址处(hooking函数). 1000116a地址处的 call unhook() 指令用来将ntdll.ZwQuerySystemInformation() API的起始5个字节恢复
      • 位于1000119b地址处的 call eax(7c93d92e) 指令将调用原来的函数
      • ntdll.ZwQuerySystemInformation() 执行完毕后, 由7c93d93a地址处的 retn 10 指令返回到stealth.dll代码区域. 然后, 10001212地址处的 call hook() 指令再次钩取ntdll.ZwQuerySystemInformation()
      • stealth.MyZwQuerySystemInformation()函数执行完毕后, 由10001233地址处的 retn 10 命令返回到procexp.exe进程代码区域, 继续执行

进程隐藏

  • 由于进程是内核对象, 所以(用户模式下的程序)只要通过相关API就能检测到它们. 用户模式下检测进程的相关API通常分为如下2类
    • CreateToolhelp32Snapshot() & EnumProcess()
    HANDLE WINAPI CreateToolhelp32Snapshot(
        DWORD dwFlgs,
        DWORD th32ProcessID
    );
    BOOL EnumProcess(
        DWORD* pProcessIds,
        DWORD  cb,
        DWORD* pBytesReturned
    );
    
    上面2个API均在其内部调用了ntdll.ZwQuerySystemInformation() API
    • ZwQuerySystemInformation()
    NTSTATUS ZwQuerySystemInformation(
        SYSTEM_INFOMATION_CLASS SystemInformationClass,
        PVOID                   SystemInformation,
        ULONG                   SystemInformationLength,
        PULONG                  ReturnLength
    );
    
    借助ZwQuerySystemInformation() API可以获取运行中的所有进程信息(结构体), 形成一个链表, 操作该链表(从链表中删除)即可隐藏相关进程
⚠️ **GitHub.com Fallback** ⚠️