0x01 basic knowledge
what?
1.1 逆向分析技术
- 通过软件使用说明和操作格式
- 静态分析
- 动态分析
- 粗跟踪:大块跟踪,遇到 CALL、REP、LOOP等一般不跟入。了解Win32 API 根据情况合适选择断点。
- 细跟踪关键部分:粗跟踪获取核心模块和程序段,再进行具体跟踪。
1.2 文本字符
1.2.1 ASCII 与 Unicode
Ascii 是7位编码标准,00h~7Fh,26个小写字母和26个大写字母、10个数字、32个符号、33个控制代码及空格,共128个代码。厂商扩充128附加字符,但其中部分不统一。如 ANSI、Symbol、OEM 等字符集,ANSI 是系统预设的标准文字存储格式。
Unicode 是 ASCII 字符编码扩展,在 win 中用两Byte进行编码,称为宽字符集widechars。Unicode 是双字节编码机制的字符集,0~65535 双字节无符号整数对每个字符进行编码。Unicode 所有字符都是16位,7位的 ASCII 码被扩充为16位。
1 | #ascii |
Intel 处理器在内存中将一个字存入存储器要占用相继的2字节,按 Little-endian (低字节)方式存入,高位字节存入高地址。
1.2.2 字节存储顺序
- Big-endian:高位字节存入低地址,低地址存入高地址。
- Little-endian:低位字节存入低地址,高位地址存入高地址。
例如:12345678h 写入以 1000h 开始的内存中
一般 x86 系列 CPU 都是 Little-endian 字节序,PowerPC 通常是 Big-endian 字节序。网络协议也采用 Big-endian 方式传输数据,所以称为网络字节序。
1.3 Windows
1.3.1 API
Application Programming Interface(应用程序编程接口)。提供窗口管理、图形设备接口、内存管理等服务功能,以函数库形式组织在一起,形成 Windows 应用程序编程接口,Win API。Win API 子系统负责讲 API 调用转换成 Windows 操作系统的系统服务调用。API 下面是 Windows 操作系统核心,上面是 Windows 应用程序。
16位 Windows 的 API 称作“Win16”,用于32、64位 Windows 的 API 称作“Win32”,其中64位 API 的名称和基本功能没变,只是用64位代码实现。Win API 是基于 C 语言接口,但可由不同语言编写的程序调用。
32位系统调用 Win16 函数要经过转换层转换位 Win32 函数,反之相同。
Windows 运作核心是 DLL,16位 win 中位于 \WINDOWS\SYSTEM;32位 win 中位于 \SYSTEM 和 \SYSTEM32 中。早期只需在三个 DLL 中实现 Win 主要部分:
- Kernel(Kernel32.DLL):操作系统核心功能服务,进程与线程控制、内存管理、文件访问等。
- User(USER32.DLL):处理用户接口,包括键鼠输入、串口和菜单管理等。
- GDI(GDI32.DLL):图像设备接口,允许程序在屏幕和打印机上显示文本和图形。
向 Windows 函数传一个 ANSI 字符串,会先将 ANSI 字符串转换成 Unicode 字符串,再传给操作系统;若希望函数返回 ANSI 字符串,系统会先将 Unicode 字符串转换成 ANSI 字符串,然后再返回程序。
Win32 API 函数字符集中,A 表示 ANSI,W 表示 Widechars(Unicode)。前者是常使用的单字节方式;后者是宽字节方式,以便处理双字节字符。每个以字符串位参数的 Win32 函数在操作系统中都有这两种方式的版本。
1.3.2 WOW64
Windows-on-Windows64-bit 是64位 Windows 上的子系统,可使 32 位应用程序在不修改的情况下运行在64位操作系统上。64位系统文件放在 System32 文件夹中,还增加了 \Windows\SysWOW64 文件夹来存储32位系统文件。
加载32位程序时,WOW64 建立32位 ntdll.dll 要求的启动环境,切换 CPU 模式至32位,开始执行32位加载器。WOW64 会对32位 ntdll.dll 的调用重定向 ntdll.dll(64位),而不是发出原生32位系统调用指令。WOW64 转换到原生64位模式,获取系统调用有关参数,发出对应原生64位系统调用。
1.3.3 Windows 消息机制
Windows 是一个消息驱动式系统。Windows 消息提供通信手段,应用程序想要实现的功能由消息触发,通过对消息的响应和处理来完成。
- 系统消息队列
- 应用程序 消息队列
所有输入设备由 Windows 监控。事件发生时,WIndows先将输入的消息放入系统消息队列,再将输入的消息复制到相应的应用程序队列中,应用程序中的消息循环在它的消息队列中检索每个消息并发送给对应的窗口函数。无论事件急缓,总按先后排队(一些系统消息除外),可能导致外部实时事件得不到及时处理。
1.3.4 虚拟内存
32位系统地址空间为 4GB 以内,00000000h~FFFFFFFFh,代码和数据都放在同意地址空间中,不区分代码段和数据段。
虚拟内存 virtual memory 通过映射 map 的方法使可用虚拟地址 virtual address 达到 4GB,每个应用程序都可以获得 2GB 的虚拟地址,剩下 2GB 系统自用。
应用程序不会直接访问物理地址,虚拟内存管理器通过虚拟地址的访问请求来控制所有物理地址访问。每个应用程序都有独立的 4GB 寻址空间,彼此隔离。DLL 程序没有私有空间,总时背映射到其他应用程序地址空间中,作为其他应用程序的一部分运行,DLL 不与其他程序处于同一地址空间,应用程序就无法调用它。
0x02 动态分析技术
2.1OD 调试器
2.1.3 基本操作
2.1.4 常用断点
1.INT3
bp 命令或者 F2 快捷键来设置/取消断点。执行 INT3 断点,该地址内容被 INT3 指令替换。INT3 指令机器码是 0xCC,被调试进程执行 INT3 指令导致一个异常时,调试器会捕捉到这个异常,从而停在断点处,然后将断点处指令恢复原来指令。
优点是可以设置无数个断点,缺点是改变原程序机器码,易被软件检测。为防范 API 被下断点,一些软件会检测 API 首地址是否为 0xCC。将断点设在函数内部或末尾可绕过该检测方法。
1 | FARPROC Uaddr; |
2.硬件断点
与 DRx 调试寄存器有关。
- DR0~DR3:调试地址寄存器,用于保存需要监视的地址,如设置硬件断点。
- DR4~DR5:保留
- DR6:调试寄存器组状态寄存器。
- DR7:调试寄存器组控制寄存器。
硬件断点原理是使用 DR0~DR3 设定地址,并使用 DR7 设定状态,最多设置 4 个断点。硬件执行断点与 CC 断点作用一样,但不会修改指令首字节,更难检测,速度更快。
3. 内存断点
原理是对所设的地址赋予不可访问/不可写属性,当访问/写入的时候就会产生异常。OD 截获异常后,比较是不是断点地址,是就中断。考虑执行效率,只能下一个内存断点。在内存也可对代码右键下内存断点。
4. 内存访问一次性断点
alt+m 显示内存,set break-on-access(访问上设置断点) 用于对整个内存块设置该类断点,该断点是一次性的,当所在段被读取或执行时就会中断。向捕捉调用或返回某个模块(如脱壳)就显得很有用。
5. 消息断点
当某个特定窗口函数接收到特定消息时,消息断点将程序中断。只有在窗口被船舰之后才能被设置并拦截消息。
用户在窗口的操作,消息会发送给当前窗体。所有发送的消息有4个参数,包括1个窗口句柄 hwnd,一个消息编号 msg 和2个 long 的参数。win 可通过句柄来标识它代表的对象。
查看 od 中的 windows 窗口,列出相关参数,可右键选中执行 Message breakpoint on ClassProc,再根据消息类型设置拦截的消息。之后回到进程界面进行对应操作会中断在 Windows 系统代码中,此时可在 .text 段下内存断点可回到应用程序代码。
6. 条件断点
满足一定条件才会中断称为条件断点,可按寄存器、存储器、消息等设断点。调试器遇到此类断点时,计算表达式的值,结果非零或者表达式有效,则中断。
- 按寄存器条件中断:快捷键 shift+F2,输入 eax==04000000 或 bp addr eax==04000000
- 按存储器条件中断:根据对应地址存储的对应内容来判断中断,如 bp CreateFileA,[STRING [esp+4]]==”c:\1212.txt”
7. 条件记录断点
除了条件断点作用,还能记录断点处函数表达式或参数的值,也可设置通过断点的次数,每次负荷中断条件,计数器值减一。shift+F4 打开条件记录,可设置条件
2.1.5 插件
2.1.8 调试符号
被调试程序二进制信息与源程序信息之间的桥梁,在编译器将源文件编译为可执行程序过程中为支持调试而摘录的调试信息,包括变量、类型、函数名、源代码行等。
0x03 静态调试
IDA
3.3.16 IDC 脚本
在IDA中按下 shift + F2 可调出脚本编辑器,可以简单的学习一下IDA IDC脚本的编写与使用,IDC脚本借鉴的C语言的语法,具体参考 IDA 中的IDC脚本编写笔记
0x04 逆向分析技术
4.1 32位软件逆向
4.1.1 启动函数
编写 Win32 应用必须在源码实现一个 WinMain 函数。但并不是从 WInMain 函数开始执行,首先被执行的是启动函数的相关代码,这段由编译器生成。启动代码初始化进程完成后,才会调用 WinMain 函数。
C/C++ 程序运行时,启动函数作用基本相同,包括检索指向新进程的命令行指针、检索指向新进程的环境变量指针、全局变量初始化和内存栈初始化等。
4.1.2 函数
1. 函数的识别
编译器使用 call 和 ret 指令来调用函数及返回调用位置。call 指令给出被调用函数的起始地址:ret 指令用于结束函数的执行。可通过两者来识别函数。
2. 函数的参数
栈方式、寄存器方式、全局变量进行隐含参数传递的方式。
栈传递参数
esp 指向栈中第一个可用数据项,调用函数时,参数依次入栈,再调用函数。调用后,再栈中取得数据并计算。计算结束后,由调用者或者被调用者平衡栈数据。
详情可见 常见函数调用约定。
函数对参数的存取及局部变量都是通过栈来定义的,非优化编译器用一个专门的寄存器(通常是 ebp)对参数进行寻址。
- 调用者将函数执行完毕时返回的地址、参数入栈。
- 子程序使用 “ebp指针+偏移量” 对栈中的参数进行寻址并取出,完成操作。
- 子程序使用 ret 或 retf 指令返回。CPU 将 eip 置为栈中保存的返回地址,并继续执行他。
stdcall 约定 test2(Par1, Par2),汇编代码大致如下:
1 | push par2 |
enter 指令相当于 push ebp; mov ebp, esp
leave 指令相当于 add esp, xxx; pop edp
寄存器传递参数
如 Fastcall,VC++ 中,左边两个不大于4字节的参数分别放在 ecx 和 edx 中,之后的参数从右往左压栈,由被调用函数清理栈。
Thiscall 是 C++ 中非静态成员函数的默认调用对象,对象的每个函数隐含接收 this 参数。参数从右往左顺序入栈,由被调用函数清理栈,仅通过 ecx 传递 this 指针。
1 | class Csum |
对应汇编代码为
1 | push ebp |
名称修饰约定
为了允许使用操作符和函数重载,编译器会改写每一个入口点的符号名,从而允许同一个名字有多个用法且不会破坏现有基于 C 的链接器。
3. 函数返回值
return
一般函数返回值放在 eax 寄存器中返回,处理结果大小超过 eax 寄存器容量,其高32位就会放到 edx 寄存器中。
参数按传引用方式返回值
传值调用会建立参数一份副本,在调用函数中修改参数值复本不会影响原始变量值。传引用调用允许调用函数修改原始变量的值。调用某个函数,当吧变量地址传递给函数时,可以在函数中用间接引用运算符修改调用函数内存的单元中该变量的值。
4.1.3 数据结构
1.局部变量
用栈存放
用 sub esp, 8
语句位局部变量分配空间,用ebp-xxxx
寻址调用这些变量,而参数调用相对于 ebp 偏移量是正的ebp+xxxx
,在逆向时比较容易区分。编译器优化模式时,通过 esp 寄存器直接对局部变量和参数进行寻址。用add esp, 8
来平衡栈,释放局部变量内存。
在被调用函数中,不存在 sub esp, n
,而是通过 push ecx
来开辟一块栈,然后[esp-04]
来访问这个局部变量。局部变量的起始值是随机的,是其他函数执行后留在栈中的垃圾数据,因此需要对其进行初始化:一是 mov 位变量赋值,二是 push 直接压栈。
寄存器
除了栈占用两个寄存器,剩下6个寄存器尽可能有效存放局部变量,可以少产生代码,提高效率。寄存器不够用就会放到栈中。
2. 全局变量
一直存放在全局变量的内存区中。通常位于数据区块 .data 一个固定地址处,一般用一个固定硬编码地址直接对内存进行寻址。编译器一般会将全局变量放到可读写的区块里(若只可读,则就是一个常量)。在程序整个执行过程中占用内存单元。
1 | mov eax, dword ptr [4084C0h] ;4084C0h是全局变量地址 |
全局变量可被同一文件中所有函数修改,若某个函数改变了全局变量的值,就能影响到其他函数。
3. 数组
相同数据类型元素的集合,在内存中按顺序连续存放在一起,一般通过基址加变址来寻址访问。
4.14 虚函数
虚函数地址只能在调用即将进行时确定,所有对虚函数 的引用通常放在虚函数表 VTBL 中,数组的每个元素中存放的就是类中虚函数的地址。调用时,先取出虚函数表指针 VPTR,得到虚函数表地址,根据这个地址到虚函数表中取出该函数地址,最后调用该函数。
1 | class CSum |
VC++6.0,maximize Speed下,汇编为:
1 | 00401000: push esi |
调用 new 函数分配 class 所需的内存。调用成功后,eax 保存分配到内存的指针,然后将对象实例指向 CSum 类虚函数表 VTBL 004050A0h。
1 | [VTBL]=401040h ;Add() |
4.1.5 控制语句
1. if-then-else
整数用 cmp 指令进行比较,浮点值用 fcom、fcmp 等比较。通常为
1 | cmp a, b ;影响几个标志位:零标志位、进位标志、符号标志位、溢出标志位 |
实际上,编译器使用 test、or 或 dex 这种较短逻辑指令替换 cmp。
1 |
|
VC++6.0,maximize Speed下,汇编为:
1 | push ecx ;等价于 sub esp, 4 |
2. SWITCH-CASE
多个 IF-THEN 语句的嵌套组合
3. 转移指令机器码计算
- Short Jump:无条件转移和条件转移机器码均为 2 字节,转移范围是 -128~127 字节。
- Long Jump:无条件转移机器码为 5 字节,条件转移码为 6 字节(条件转移:2字节标识转移类型;其他4字节表示转移偏移量;无条件转移:1字节 jmp,4字节偏移量)。
- call:一类调用类似于 Long Jump;另一类调用参数涉及寄存器、栈等,如
call dword ptr [eax+2]
短转移指令机器码计算
1 | 4010000: jmp 401005 |
CPU 执行完 jmp 401005,后 eip = 4010002h,执行 eip=eip+偏移量,就跳转 401005 处。所以 jmp 401005 机器码为 EB 03
。
长转移指令机器码计算
1 | 4010000: jmp 401005 |
位移量为 00402398h - 00401000h - 5h(长转移指令机器码长度) = 00001393h
所以机器码为 E9 93 13 00 00
若向前转移的无条件指令
1 | 4010000: xor eax, eax |
位移量为 401000h - 402398h - 5h = FFFFEC63h(取后32位)
机器码为 E9 63 EC FF FF
4. 条件设置指令 SETcc
指令形式是 SETcc r/m8
,r/m8 表示表示8位寄存器或单字节内存单元。
1 | c = (a < b)? c1, c2; |
使用条件设置指令,编译器不产生包含条件分支的逻辑判断代码
1 | xor eax, eax |
5. 纯算法实现逻辑判断
4.1.6 循环语句
高级语言中进行反向引用的一种语言形式。确定某段代码是循环代码,就可分析其计数器,一般位 ecx 。
2021.1.10 先8学这本了