发布于 

面试题整理3(逆向基础)

一.汇编与逆向

  • 函数调用约定

    • cdecl:C 语言的调用约定(VS 默认)
    * int __cdecl cdecl_add(int a, int b)
    * {
    * return a + b;
    * }
    * // 从右到左使用栈传参,函数[外]平衡堆栈
    * int resultB = cdecl_add(1, 2);
    * // push 2
    * // push 1
    * // call cdecl_add (0B1123Ah)
    * // add esp,8
    * // mov dword ptr [resultB],eax
    • stdcall:标准调用约定(WINAPI)
    * int __stdcall stdcall_add(int a, int b)
    * {
    * return a + b;
    * }
    * // 从右到左使用栈传参,函数[内]平衡堆栈
    * int resultA = stdcall_add(1, 2);
    * // push 2
    * // push 1
    * // call stdcall_add(0B1118Bh)
    * // mov dword ptr[resultA], eax
    • thiscall:成员函数调用约定
    * class OBJ
    * {
    * public:
    * int thiscall_add(int a, int b) { return a + b; }
    * };
    * OBJ object;
    * // this 指针传入 ecx 寄存器,函数参数从右到左
    * // 使用栈传参,函数[内]平衡堆栈
    * int resultD = object.thiscall_add(1, 2);
    * // 静态成员函数使用的是 cdecl 的调用约定
    * int resultE = OBJ::static_add(1, 2);
    * // push 2
    * // push 1
    * // lea ecx,[object]
    * // call OBJ::thiscall_add (0EC137Fh)
    * // mov dword ptr [resultD],eax
    • fastcall:快速调用约定
    * int _fastcall fastcall_add(int a, int b, int c, int d, int e)
    * {
    * return a + b + c + d + e;
    * }
    * // 如果参数小于三个,使用ecx,edx从左向右传参,超出
    * // 两个的部分从右到左使用栈传参,函数[内]平衡堆栈
    * int resultC = fastcall_add(1, 2, 3, 4, 5);
    * // push 5
    * // push 4
    * // push 3
    * // mov edx,2
    * // mov ecx,1
    * // call fastcall_add (0B11348h)
    * // mov dword ptr [resultC],eax
    • x64call:64位程序调用约定
    * int x64call_add(int a, int b, int c, int d, int e, int f)
    * {
    * return a + b + c + d + e + f;
    * }
    * // 参数个数小于5个时,从左到右依次使用 ecx, edx, r8, r9
    * // 寄存器传参,多出部分使用栈进行传递
    * int resultE = x64call_add(1, 2, 3, 4, 5, 6);
    * // mov dword ptr [rsp+28h],6
    * // mov dword ptr [rsp+20h],5
    * // mov r9d,4
    * // mov r8d,3
    * // mov edx,2
    * // mov ecx,1
    * // call x64call_add (07FF69EED1398h)
    * // mov dword ptr [resultE],eax
  • 函数栈帧布局

    • 开辟和收回栈帧
    # 通常情况下会使用下列指令【开辟】栈帧
    * push ebp
    * mov ebp, esp
    * sub esp, 0x4
    # 其中的第三条指令可以被替换为
    lea esp, [esp-0x4]
    add esp, -0x4
    push eax
    # 通常情况下会使用下列指令【收回】栈帧
    mov esp, ebp
    pop ebp
    • 局部变量和参数的分析
    # ----------函数内的指令如下----------
    push ebp
    mov ebp, esp
    sub esp, 10h # 开栈帧
    push ebx
    push esi
    push edi # 保存寄存器环境
    lea edi, [ebp - 10h]
    mov ecx, 4
    mov eax, 0CCCCCCCCh
    rep stos dword ptr es:[edi] # 初始化局部变量为 cc
    mov eax, dword ptr [ebp+8]
    add eax, dword ptr [ebp+0Ch] # return a + b;
    pop edi
    pop esi
    pop ebx # 恢复寄存器环境
    mov esp, ebp
    pop ebp
    ret 8 # 收回栈帧并平衡堆栈
    # 函数的调用语句如下
    push 2
    push 1
    call 0BD118Bh
    # push 地址A && jmp 0BD118Bh
    [地址A]mov dword ptr [resultA],eax
    • 栈的布局结构如下
      • 在一个函数中,ebp保存的是调用方函数的ebp,ebp+4保存的是当前函数的返回值,ebp-n 到 esp中保存的是所有的局部变量,ebp+8开始的位置保存的是当前函数的参数。
  • 常见函数的识别

    • __security_cookie: GS检查,用于检查代码产生的缓冲区溢出
    # 初始化安全cookie的地方,通常位于程序开始
    mov eax,dword ptr [__security_cookie (0C3A004h)]
    xor eax,ebp
    mov dword ptr [ebp-4],eax
    # 校验安全cookie,通常位于程序结尾
    mov ecx,dword ptr [ebp-4]
    xor ecx,ebp
    call @__security_check_cookie@4 (0C311DBh)
    • CheckEsp:检查堆栈是否平衡,通常位于函数的结尾
    // 通常调用约定不正确,会检查失败
    cmp ebp,esp
    call __RTC_CheckEsp (0C31212h)
    • CheckStackVars:用于检查数组是否越界,通常在数组操作后被调用
    * mov ecx,ebp
    * push eax
    * lea edx,ds:[0C3177Ch]
    * call @_RTC_CheckStackVars@8 (0C31235h)
  • 程序特征的识别

    • 通过区段进行识别

      • 参考逆向 05 课上笔记
    • 通过连链接器版本识别

      VS 连接器版本
      VC 4.2 XXX
      VC 5.0 5.0
      VC 6.0(98) 6.0
      VS 2003 7.0 / 7.1
      VS 2005 8.0
      VS 2008 9.0
      VS 2010 10.0
      VS 2012 11.0
      VS 2003 12.0
      VS 2015 14.0
      VS 2017 14.XX
    • 通过入口特征进行识别

      • VC 6.0 & 易语言
        • sub esp, 0x58; call dword ptr [GetVersion];
      • Delphi 程序

        • 连续的5个call,紧跟着一串0,第一个call内调用了 GetModuleHandle
      • Borland C++ 程序

        • 第一条语句是JMP ,有明显的 “fb:C++HOOK” 字符串
      • C\C++ 程序:

        • debug版本为连续的两个call,release版本为 call jmp

        • main 函数的调用过程

  • 数据结构逆向

    • vector:内部实现为动态数组
    * struct MyVector
    * {
    * struct MyVector* pSelf;
    * int* pDataStart;
    * int* pDataEnd;
    * int* pBufEnd;
    * };
    • list:内部实现为双向循环链表
    * struct MyNode
    * {
    * struct MyNode* pNext;
    * struct MyNode* pPrev;
    * int nData;
    * };
    * struct MyList
    * {
    * struct MyList* pSelf;
    * struct MyNode* pRoot;
    * int nNodeCount;
    * };
    • map: 内部的实现是红黑树(平衡二叉树)
    * struct MyMapNode
    * {
    * struct MyMapNode* pLeft;
    * struct MyMapNode* pParent;
    * struct MyMapNode* pRight;
    * int unknown;
    * int nkey;
    * int nValue;
    * };
    * struct MyMap
    * {
    * struct MyMap* pSelf;
    * struct MyMapNode* pRoot;
    * int nNodeCount;
    * };
  • 常见的逆向分析方法

    • 字符串搜索
    • API 断点
    • 特征码查找(MFC)
    • 动静结合
    • 源码对比(Demo)

2. 工具的使用

  • 查壳工具
    • 用于查看和确定目标的程序版本以及是否被加壳,原理是特征码识别
    • PEID、ExeInfo、DetectitEasy
  • PE查看工具
    • 可用于解析目标程序的PE结构,包括PE头和区段、数据目录表等
    • LordPE、PEID、ExeInfo、DetectitEasy、010Editor
  • 动态调试工具
    • x64Dbg、OllyDbg、windbg、immunity debugger(方便调试漏洞)、IDA
    • 使用 OllyDbg
      • 支持的调试方式
        • DLL调试,多线程调试,附加调试,拖拽调试,菜单打开等
      • 支持的断点
        • 软件断点,硬件断点,条件断点,消息断点,记录断点,内存断点
      • 设置消息断点的步骤:
        • 怎样找到窗口回调?
          • RegisterClass(Ex)、CreateDialog、DialogBoxParam
        • 分析模块->右键假定参数->WinProc->断点->在WinProc上设置消息断点
      • 常用的快捷键,参考课上笔记逆向03
  • 静态调试工具
    • IDA 6.8 / 7.0: 支持不同平台不同架构程序的调试
    • IDA的反汇编引擎采用 递归下降 的解析方式。
    • IDA的常用快捷键:
      • Shift+F5 :打开签名窗口
      • ctrl + x 交叉引用(数据、函数)
      • ctrl + f: 人一窗口搜索数据
      • ESC:返回上一层
      • Shift+F12: 字符串窗口
      • a 转ASCII 字符串
      • h 十六进制十进制的转换
      • y 更改参数或函数的类型
      • n 修改数据或函数的名字
      • c: 将数据转成代码
      • p: 将代码转成函数

二、PE文件结构

0.基本概念

  • PE文件
    • 是一种文件格式,目前所学的 exe,dll,sys等都是PE文件
    • 通过检查DOS头的"MZ"(0x5A4D)和NT头的"PE"(0x00004550)来判断一个文件是不是有效的PE文件
  • 术语解释
    • RVA: 数据在虚拟内存中的偏移地址
    • VA: 数据在虚拟内存中的绝对地址
      • VA = 实际加载基址 + RVA
    • FOA: 数据在文件中的偏移地址
      • FOA = 需要转换的RVA - 所在的区段RVA + 所在区段的FOA

1.PE 头信息

  • IMAGE_DOS_HEADER
    • e_magic: “MZ” - 0x5A4D
    • e_lfanew:指向NT头的偏移
  • IMAGE_NT_HEADERS
    • signature: “PE” - 0x00004550
    • IMAGE_FILE_HEADER
      • NumberOfSections:区段的数量
      • SizeOfOptionalHeader: 扩展头的大小
      • Characteristics:可以判断当前是不是DLL,是不是32位,重定位已分离(表示不需要重定位)
    • IMAGE_OPTIONAL_HEADER32
      • Magic:CPU架构:x32(0x10B)x64(0x20B)
      • AddressOfEntryPoint:程序的入口点(OEP) RVA
      • ImageBase:镜像的默认加载基址
      • SectionAlignment :内存对齐粒度,通常是0x1000,要求大于文件粒度
      • FileAlignment:文件对齐粒度,通常是0x200, 拉伸
      • SizeOfImage:镜像大小,即内存中的大小, 最后一个区段的RVA + 最后一个区段的大小
      • SizeOfHeaders:整个PE头部大小,通常是 0x400
      • DllCharacteristics :特征标识0x40 有随机基址
      • NumberOfRvaAndSizes:数据目录表个数 0x10
      • DataDirectory:数据目录表
    • IMAGE_SECTION_HEADER(重要)
      • Name[8]:区段名
      • virtualSize:内存中大小
      • VirtualAddress:内存位置RVA
      • SizeOfRawData:文件中大小
      • PointerToRawData:文件位置FOA
      • Characteristics:区段属性(可读可写可执行),是否有代码

2.导入表

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
* union {
* DWORD Characteristics;
* DWORD OriginalFirstThunk; // INT
* } DUMMYUNIONNAME;
* DWORD TimeDateStamp;
* DWORD ForwarderChain;
* DWORD Name; // 模块名称
* DWORD FirstThunk; // IAT
* } IMAGE_IMPORT_DESCRIPTOR;
  • 导入表中保存的是一组 以全0为结尾 的 MAGE_IMPORT_DESCRIPTOR 结构。
    • INT:导入名称表,指向了一组以0结尾的 IMAGE_THUNK_DATA 结构,可以找到函数的ID或名称,但是有些时候其中会被填充为0,在文件和内存中它们保存的值是相同的。
      • 当 IMAGE_THUNK_DATA 字段的最高位为1,表示这是一个序号导入的函数
        • 序号的值为 0x0000??? & 0x0000FFFF = 0x0000???
      • 最高位为0,表示这是一个名称导入的函数,
        • 名称和序号保存在 0x??? 的位置 IMAGE_IMPORT_BY_NAME
  • IAT:导入地址表,在文件中存放的是INT的数据,当程序被加载到内存时,会被填充为函数的真是地址。

3. 导出表

* typedef struct _IMAGE_EXPORT_DIRECTORY {
* // 省略了无意义的字段
* DWORD Name; // 模块名RVA
* DWORD Base; // 序号的起始数值
* DWORD NumberOfFunctions; // 函数的数量
* DWORD NumberOfNames; // 有名字的函数的数量
* DWORD AddressOfFunctions; // 导出地址表的的RVA 【DWORD】
* DWORD AddressOfNames; // 导出名称表的RVA【DWORD-RVA】
* DWORD AddressOfNameOrdinals; // 导出序号表的RVA【WORD】
* } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  • 导出表只能有一个
  • 名称表的数量和序号表的数量相同
  • 导出函数的序号 = 基址(Base) + 索引(函数地址表下标)

4. 重定位表

* typedef struct _IMAGE_BASE_RELOCATION {
* DWORD VirtualAddress;
* DWORD SizeOfBlock;
* // WORD TypeOffset[1];
* } IMAGE_BASE_RELOCATION;
  • 重定位表由多组重定位块组成,每个重定位块保存的是一个分页(RVA)中需要重定位的所有数据。
  • 重定位项的个数 = (SizeOfBlock– sizeof(IMAGE_BASE_RELOCATION)) / 2
  • 每一个重定位项都是一个WORD类型的值,标识了当前数据的类型以及偏移(相对VirtualAddress)
  • 需要重定位的数据所在的位置 = 实际加载基址 + VirtualAddress + 重定位数据的偏移,其中保存的是一个偏移
  • 重定位公式:需要重定位的地址 - 默认加载基址 + 实际加载基址

5.资源表

  • 三层结构
    • 一层:资源种类;二层:此种资源的个数,名称; 三层:资源数据
    • 每一层都是 IMAGE_RESOURCE_DIRECTORY 开始,后面跟着一组IMAGE_RESOURCE_DIRECTORY_ENTRY结构体,保存的是资源的类型和名称
      • 结构体个数 = NumberOfNamedEntries + NumberOfIdentries
  • 第一层:第二层:
    • 资源的类型名称,当 NameIsString 为1时,标识这是一个自定义的名称,使用 NameOffset字段可以找到IMAGE_RESOURCE_DIR_STRING_U 字段,其中保存了字符串名称。
  • 第三层:
    • 指向 IMAGE_RESOURCE_DATA_ENTRY,保存了数据的偏移和大小

6.其他表

  • TLS表 IMAGE_TLS_DIRECTORY
    • 使用 __declspec(thread) int nNum = 99; 定义TLS变量
    • TLS 函数的原型如下,和DllMain相比,它们只有返回值类型不同
      • Void NTAPI t_TlsCallBackA(PVOID DllHandle,DWORD Reason,PVOID Red);
    • TLS 函数的调用顺序位于main函数前,通常用于进行反调试

三、加壳与脱壳

1.脱壳技巧

  • 脱壳的基本步骤
    • 找OEP -> [还原IAT] -> dump文件 -> 修复IAT
    • 具体的步骤见课上笔记

2. 加壳原理

  • 什么是加壳
    • 利用特殊的算法,对可执行文件里的资源数据或代码进行压缩加密。添加对应的Stub代码段用于对加密后的文件解密,以及充当PE加载器的角色,对目标程序执行修复重定位、修复(加密)IAT等操作。
    • 常见的手段有:伪装OEP特征,反反汇编,IAT加密,函数偷取(通常偷取OEP),代码虚拟化等
  • 手动添加区段的步骤
    • 区段头表中添加区段信息
    • 文件头表中添加区段个数
    • 修改扩展头中镜像大小
    • 修改OEP为新的区段RVA
    • 填充文件,主要是跳转原OEP的代码

四、病毒分析

1.病毒分类

  • 感染性病毒、蠕虫病毒、勒索病毒、APT 木马、RootKit、后门、间谍软件

2.常见传播方式

  • 移动介质传播、网络传播、捆绑邮件、即时通信软件、局域网共享文件夹、网络下载、漏洞 。

3.常见行为

  • 操作注册表设置开机启动
  • 设置计划任务,定时关机
  • 遍历进程,关闭杀软
  • 遍历文件,进行感染
  • 下载软件,捆绑安装
  • 连接网络,传播病毒
  • 释放文件,拷贝自身

4. 分析方式

  • 静态分析:使用IDA进行静态分析
  • 动态分析:使用OD等调试器
  • 行为分析:使用火绒剑,Process Monitor,Api Monitor等进行行为监控
  • 在线沙箱:使用微步云杀箱等获取病毒行为报告

五、漏洞利用

1. 名词解释

  • fuzz:是 Fuzz testing 的缩写,全称模糊测试,是一种通过无规则试探探测漏洞的的方式。

  • exploit:通常指用于验证漏洞的一段代码

  • shellcode:是一段利用特定漏洞的代码

  • payload:在漏洞领域,指的是实际用于攻击的那一段代码

  • GetPC:获取当前指令所在地址的一种方式

    • 缺点是会产生大量的 0,可能被截断(“\n”, “\r”," ", “\0”)
  • 使用浮点指令 FSTENV,缺点是会损坏栈的0xc大小的数据

    • 缺点是会产生一条冗余的指令,inc ebx

2.获取kernel32的基址

* mov eax, fs:[0x30]; // 获取 PEB
* mov eax, [eax + 0x0C]; // 获取 PEB_LDR_DATA
* mov eax, [eax + 0x1c]; // LIST_ENTRY.Flink -> 第一个模块 ntdll
* mov eax, [eax]; // kernelbase.dll -> 第二个模块
* mov edx, [eax + 0x08]; // _LDR_DATA_TABLE_ENTRY.DllBase;
* mov kernel32, edx; // kernel32.base

六、内核编程

1.保护模式

  • 分段基址

    • 保护模式下段寄存器变成段选择子(高速缓冲区),由 13位索引 1位TI 2位RPL 构成
      • RPL 表示当前访问段的请求权限
        • MAX(CPL,RPL)<= DPL
      • TI描述了当前使用GDT还是LDT,通常为0
        • rgdt是一个48位寄存器,gdtr 是基址 gdtl是范围
      • 索引即位于GDT或LDT中的下标
  • 分页机制

    • 没有开启PAE[10-10-12]:CR3(页目录表地址) -> 页目录项 -> 页表项 -> 物理地址

    • 开启了PAE[2-9-9-12]:CR3(页目录表指针) -> 页目录表地址->页目录表 -> 页表项 -> 物理页地址

    • 保护模式具体内容参考群文件内的保护模式笔记

2.内核编程

  • 相关术语
    • DriverEntry:驱动程序的入口函数,参数一是驱动对象,参数二是所在路径
      • 注意 DriverEntry 没有名称粉碎,C++编写需要加上 extern “C”
    • 驱动对象:DRIVER_OBJECT,类似于三环中应用程序的身份,不能进行直接通信
      • 使用其中的 DriverSection 可以遍历系统中的所有驱动对象
    • 设备对象:DEVICE_OBJECT,类似于三环中的窗口,是程序的一部分,可用作接收消息
      • 需要为设备指定符号链接名供三环访问,格式: \DosDevices\Name
    • IRP:I/O请求数据包,类似于窗口程序中的消息
    • IRQL:中断请求级别,从低到高为 PASSIVE_LEVEL 、APC_LEVEL 、DISPATCH_LEVEL
      • 如果到来的中断有更高优先级,那么当前中断被挂起,CPU处理高优先级的中断
    • SSDT:系统服务描述表,保存了一系列的内核层函数
      • 通常使用 SYSENTER 进入内核,调用号保存在 eax寄存器,除此之外msr寄存器中的0x174号负责保存CS段寄存器,0x175号负责保存SS段寄存器,0x176号负责保存eip,通常是 KiFastCall,eax保存的是调用号
    • 分页内存:保存在页交换文件中的数据,使用会产生缺页异常
    • 非分页内存:直接保存在物理内存中的数据
  • R0/R3通信的通信方式
    • 直接方式(DO_DIRECT_IO):通过irp->MdlAddress获取
    • 缓冲区方式(DO_BUFFERD_IO):通过irp->AssociatedIrp.SystemBuffer获取
    • 其他方式(NULL): 通过irp->UserBuffer获取
  • DeviceIoControl
    • 使用 CTL_CODE 定义需要发送的控制码。
    • 四种数据的传输方式
      • METHOD_BUFFERED:缓冲区
      • METHOD_IN_DIRECT \ METHOD_OUT_DIRECT:直接方式
      • METHOD_NEITHER:两者都不的方式
  • 内核Hook
    • SSDT Hook -> 替换函数
    • SYSENTER Hook -> KiFastCallEntry
    • Object Hook -> OBJECT_TYPE -> _OBJECT_TYPE_INITIALIZER(保存了用于操作对象的函数)
  • 内核重载
    • 开辟一块新内存空间,模拟PE加载器将内核模块重新加载到这段内存,用于修复被Hook的SSDT表