64位软件逆向技术
(x64通常是指AMD64, Intel64的合称
1 寄存器
RAX, RBX, RCX, RDX, R8 ~ R15, 扩充8个128位寄存器XMM(一般用来优化代码)
寄存器是向下兼容的, 用后缀DWORD, WORD, BYTE, 例如R8(64位), R8D(32位), R8W(16位), R8B
2 函数
- (1) 栈平衡
- (2) 启动函数 一般反汇编成start, 可在start中找到main函数的调用
编译器设定多线程DLL(/MD)时, IDA会显示main符号, 可直接定位
设置为/MT多线程时, 不会显示main, 需要通过特征定位main, call cs:exit上的第一个call是main
- (3) 调用约定
x64程序只有一种调用约定, 快速调用约定. 6个参数保存在寄存器里, 其余参数从右往左入栈, 调用方负责平衡栈空间. RDI, RSI, RDX, RCX, R8, R9, 大于8或不是1,2,4,8字节的参数由引用(地址)传递. XMM用于传递浮点数参数. 如果子函数需要调用RCX, RDX, R8, R9则调用者提前申请32字节栈空间通过栈来传参.(注意这32字节是默认的预留栈空间, 但是需要调用者申请使用)
- (4) 参数传递
- 多参数传参
- 结构体的传参
- 不超过8字节
结构体直接存进寄存器, 用高低32位访问成员, 逆向分析时可以借此特征判断结构体数据 - 超过8字节
先复制结构体到栈, 再通过结构体地址当成函数参数(通过寄存器比如rcx, 不够的时候是放到栈里)传递给函数(引用传递), 被调用的函数通过结构体地址+偏移取结构体内容
不过需要注意, 预留栈空间和局部变量在栈中是有差异的, 调用一个函数, 返回地址之前的是预留栈空间, 之后的是开辟出来存局部变量的空间, 并且局部变量中也有隐含的预留空间, 即4个参数的存储空间, 比如
对应的栈结构大概如下, rsp0是main函数调用fun函数之前的栈顶
- 不超过8字节
- 多参数传参
- thiscall传递
C++成员函数调用约定, 调用时会隐含传递一个this指针 - 函数返回值
8字节返回值在RAX, 浮点数在MMX0, 大于8字节, 返回栈空间地址, 作为引用参数返回
- (3) 调用约定
3 数据结构
- 局部变量
局部变量保存在栈空间中, 注意与预留栈空间的空间关系, 预留栈空间在低地址, 局部变量在高地址 - 全局变量
编译时就会被固定, 常用固定地址访问; 另外注意, 全局变量也是先定义的在低地址, 后定义的在高地址, 据此可还原变量定义顺序 - 数组
数组中数据是从低到高地址排列(结合写C程序的经验, 是常理之中)
访问数组通常用数组访问公式, 数组下标已知会被优化成直接计算偏移量, 未知则继续用寻址公式
特征
+ [数组首地址 + n]
+ [数组首地址 + 寄存器 * n]
遇到以上特征就可大致判断为数组访问, 优化过后也可能有XMM寄存器的使用
4 控制语句
- if 语句
通常编译器会对表达式的结果取反操作, 比如 > 取成 <= 即jle - if else 语句
特征: jxx跳转, 跳转目的有jmp指令 - if else if else
特征: jxx跳转目的有jmp指令, else if跳转目的有jmp指令, else 代码结尾没有jmp指令, 并且所有jmp目的地址相同 - switch-case
分支数 < 6 时编译器用if else实现, 当分支数 >= 6时, 编译器会优化
优化后用一维数组表示case表, 用下表寻址代码块, 而且当case的间隔较小时, 会完整的列出1-n表格, 其中缺失的数字会用default地址替代
当case用if来实现时, 如果判断次数较多, 会优化成判定树形式
- 转移指令机器码计算
(1) call / jmp direct (与x86类似, 用位移量寻址)
位移量 = 目的地址 - 起始地址 - 跳转指令长度
(2) call /jmp memory direct
32位系统, 跳转指令机器码 + 绝对地址
64位系统, 跳转指令机器码 + 相对地址
5 循环语句
- do while 循环, 有一个向上跳转的jxx
- while 循环, 有jmp向上跳转, jxx向下跳转 (效率低于do while, 所以通常被优化为do)
- for 循环, 开始有一个jmp向下, 最后有一个jmp向上, 中间有个jxx取反向下跳出循环(通常被优化为if + do循环)
6 数字运算符
- 加减法, 同32位用lea加速程序; 另外常量折叠指编译器在编译时就计算了只涉及常量的加减乘法, 执行时直接拿结果计算即可
- 乘法, 同32位用lea加速计算
- 除法, 优化成等价的移位运算或乘法运算
- 无符号除法移位优化公式
x ≥ 0 , x 2 n = x > > n x < 0 , x 2 n = ( x + ( 2 n − 1 ) ) > > n x \ge 0, \frac{x}{2^n} = x >> n \\ x < 0, \frac{x}{2^n} = (x + (2^n - 1)) >> n x≥0,2nx=x>>nx<0,2nx=(x+(2n−1))>>n
实际实现时, 用到了有符号扩展+and指令的技巧, 同时概括了正负两种情况, 实现以上两个公式, 正的时候+0, 负的时候+ 2 n − 1 2^n - 1 2n−1
- 无符号除法移位优化公式
-
有符号除法
相当于在无符号除法基础上加一个求补操作
neg eax -
有符号除法, 除数正非 2 n 2^n 2n
- 优化公式1
x o = x ∗ c > > 64 ( 32 ) > > n , x ≥ 0 x o = ( x ∗ c > > 64 ( 32 ) > > n ) + 1 , x < 0 c > 0 , n 可 以 为 0 \frac{x}{o} = x * c >> 64(32) >> n, x \ge 0\\ \frac{x}{o} = (x * c >> 64(32) >> n) + 1, x < 0\\ c > 0, n可以为0 ox=x∗c>>64(32)>>n,x≥0ox=(x∗c>>64(32)>>n)+1,x<0c>0,n可以为0 - 优化公式2
x o = ( x ∗ c > > 64 ( 32 ) ) + x > > n , x ≥ 0 x o = ( ( x ∗ c > > 64 ( 32 ) ) + x > > n ) + 1 , x < 0 c < 0 \frac{x}{o} = (x * c >> 64(32)) + x >> n, x \ge 0\\ \frac{x}{o} = ((x * c >> 64(32)) + x >> n) + 1, x < 0\\ c < 0 ox=(x∗c>>64(32))+x>>n,x≥0ox=((x∗c>>64(32))+x>>n)+1,x<0c<0
n为右移总次数, o是除数, o = 2 n c o = \frac{2^n}{c} o=c2n, c是编译器为优化除法转成乘法计算出来的magic_num, 根据c的正负知道是采用的哪一个优化公式
- 优化公式1
-
有符号除法, 除以负非 2 n 2^n 2n, 区别在与c取相反符号, 即优化公式1中的c是负的, 优化公式2中的c是正的, 这样就是除以负的非 2 n 2^n 2n, 此外还有一处细微差别
- 优化公式1
x o = x ∗ c > > 64 ( 32 ) > > n , x ≥ 0 x o = ( x ∗ c > > 64 ( 32 ) > > n ) + 1 , x < 0 c < 0 , n 可 以 为 0 \frac{x}{o} = x * c >> 64(32) >> n, x \ge 0\\ \frac{x}{o} = (x * c >> 64(32) >> n) + 1, x < 0\\ c < 0, n可以为0 ox=x∗c>>64(32)>>n,x≥0ox=(x∗c>>64(32)>>n)+1,x<0c<0,n可以为0 - 优化公式2
x o = ( x ∗ c > > 64 ( 32 ) ) − x > > n , x ≥ 0 x o = ( ( x ∗ c > > 64 ( 32 ) ) − x > > n ) + 1 , x < 0 c > 0 \frac{x}{o} = (x * c >> 64(32)) - x >> n, x \ge 0\\ \frac{x}{o} = ((x * c >> 64(32)) - x >> n) + 1, x < 0\\ c > 0 ox=(x∗c>>64(32))−x>>n,x≥0ox=((x∗c>>64(32))−x>>n)+1,x<0c>0
n为右移总次数, 除数o计算公式
∣ o ∣ = 2 n 2 64 ( 32 ) − c |o| = \frac{2^n}{2^{64(32)} - c} ∣o∣=264(32)−c2n
- 优化公式1
-
无符号除法, 除以 2 n 2^n 2n
无须判断符号, shr替代除法 -
无符号除法, 除以非 2 n 2^n 2n
类似有符号除法优化公式1, 不过不用判断符号, 只考虑正数情况
注意有符号与无符号的重要区别在于, 无符号用mul, 有符号用imul
另一种优化公式
x o = ( x − ( x ∗ c > > 32 ( 64 ) ) > > n 1 ) + ( x ∗ c > > 32 ( 64 ) ) > > n 2 o = 2 32 ( 64 ) + n 2 32 ( 64 ) + c \frac{x}{o} = (x - (x *c >> 32(64)) >> n_1) + (x * c >> 32(64)) >> n_2 \\ o = \frac{2^{32(64) + n}}{2^{32(64) + c}} ox=(x−(x∗c>>32(64))>>n1)+(x∗c>>32(64))>>n2o=232(64)+c232(64)+n -
取模
通常优化为位运算和除法运算, 再经由除法运算优化- 除数为
2
n
2^n
2n
优化公式1
x % 2 n = x & ( 2 n − 1 ) , x ≥ 0 x % 2 n = ( x & ( 2 n − 1 ) ) − 1 ∣ ( ∼ ( 2 n − 1 ) ) + 1 , x < 0 x \% 2^n = x \& (2^n - 1), x \ge 0\\ x \% 2^n = (x \& (2^n - 1)) - 1 | (\sim(2^n - 1)) + 1, x < 0 x%2n=x&(2n−1),x≥0x%2n=(x&(2n−1))−1∣(∼(2n−1))+1,x<0
优化公式2
x % 2 n = x & ( 2 n − 1 ) , x ≥ 0 x % 2 n = ( ( x + ( 2 n − 1 ) ) & ( 2 n − 1 ) ) − ( 2 n − 1 ) , x < 0 x \% 2^n = x \& (2^n - 1), x \ge 0\\ x \% 2^n = ((x + (2^n - 1)) \& (2^n - 1)) - (2^n - 1), x < 0 x%2n=x&(2n−1),x≥0x%2n=((x+(2n−1))&(2n−1))−(2n−1),x<0 - 除数为非
2
n
2^n
2n
x % c = x − x / c ∗ c x \% c = x - x / c * c x%c=x−x/c∗c, 余数 = 被除数 - 除数 * 商
- 除数为
2
n
2^n
2n
7 虚函数
每个类生成一个虚表(假如存在虚函数)
封装, 继承, 多态相关细节放到C++逆向总结文章中
以上
之后要继续撰写关于windows和linux逆向相关的总结文章, 这只是开始, to be continued …