第四次
约 1937 字大约 6 分钟
2024-10-08
计划
- 《IDA Pro权威指南》三章
- 《逆向工程权威指南》十章
- C++ 类有空就看
《逆向工程权威指南》
第一部分 指令讲解
第一章 CPU 简介
这一张没讲什么太重要的,介绍了一下 cpu 架构啥的,然后说明了本书重点要讲的架构
重要
三类 arm 指令集:arm 指令集、thumb 模式指令集、arm64 指令集
虽然这些指令集之间有着千丝万缕的联系,需要强调的是:不同的指令集分别属于不同的指令集架构;一个指令集绝非另一个指令集的变种。
第二章 最简函数
返回预定常量的函数,已经算得上是最简单的函数了。
int f()
{
return 123;
}
2.1 x86
f:
mov eax, 123
ret
#这个函数仅由两条指令构成:第一条指令把数值123存放在EAX寄存器里;根据函数调用约定①,后面一条指令会把EAX的值当作返回值传递给调用者函数,而调用者函数(caller)会从EAX寄存器里取值,把它当作返回结果
2.2 ARM
f PROC # PROC过程标识符,作用就是声明
MOV r0,#0x7b ; 123
BX lr
ENDP #表示结束
# proc是定义子程序的伪指令,位置在子程序的开始处,它和endp分别表示子程序定义的开始和结束两者必须成对出现。
2.3 MIPS
重要
为什么赋值指令LI和转移指令J/JR的位置反过来了?这属于RISC精简指令集的特性之一——分支(转移)指令延迟槽(Branch delay slot)的现象。简单地说,不管分支(转移)发生与否,位于分支指令后面的一条指令(在延时槽里的指令),总是被先于分支指令提交。这是RISC精简指令集的一种特例,我们不必在此处深究。总之,转移指令后面的这条赋值指令实际上是在转移指令之前运行的。
在MIPS指令里,寄存器有两种命名方式。一种是以数字命名($0$31),另一种则是以伪名称(pseudoname)命名($V0VA0,依此类推)。在GCC编译器生成的汇编指令中,寄存器都采用数字方式命名。
j $31
li $2,123 # 0x7b
提示
j
是MIPS汇编语言中的跳转指令(Jump),用于无条件跳转到指定的地址执行代码。$31
是MIPS寄存器集中的特殊寄存器,名为ra
(Return Address),通常用于存储子程序返回时的地址。li
是MIPS汇编语言中的加载立即数(Load Immediate)指令的简写,用于将一个立即数(即直接写在指令中的数)加载到指定的寄存器中。$2
是MIPS寄存器集中的一个通用寄存器。
jr $ra
li $v0, 0x7B
提示
$v0
是MIPS寄存器集中的一个通用寄存器,但在MIPS的ABI(Application Binary Interface)中,它经常被用作系统调用的返回值寄存器。然而,在普通的MIPS汇编代码中,它也可以像其他通用寄存器一样被使用。j
指令直接跳转到指定的地址或标签,而jr
指令跳转到寄存器中存储的地址。j
指令更适用于在程序中无条件跳转到任意位置,而jr
指令通常用于函数调用的返回,通过跳转回$ra
寄存器中保存的返回地址来实现。
第三章 Hello,world!
现给出最著名的程序
#include <stdio.h>
int main(){
printf("hello, world\n");
return 0;
}
3.1 x86
3.1.1 MSVC
CONST SEGMENT
$SG3830 DB 'hello, world', 0AH, 00H ; 字符串 'hello, world' 加上换行符和字符串结束符
CONST ENDS
PUBLIC _main
EXTRN _printf:PROC ; 声明外部过程 _printf
_TEXT SEGMENT
_main PROC
push ebp ; 保存旧的 ebp
mov ebp, esp ; 设置新的栈帧
push OFFSET $SG3830 ; 将字符串的地址压入栈中
call _printf ; 调用 printf 函数
add esp, 4 ; 清理栈(这里假设 _printf 消耗了 4 字节的栈空间)
xor eax, eax ; 设置 eax 为 0,表示程序正常退出
pop ebp ; 恢复旧的 ebp
ret ; 返回
_main ENDP
_TEXT ENDS
END ; 表示文件结束
提示
OFFSET
是一个操作符,用于获取某个标签(label)、变量名或常量在内存中的偏移地址(offset address)。我们会发现编译器在字符串常量的尾部添加了十六进制的数字0,即00h。依据C/C++字符串的标准规范,编译器要为这个字符串常量添加结束标志(即数值为零的单个字节)。
上述程序的源代码等效于:
#include <stdio.h>
// 声明一个指向 const char 的指针,指向字符串 "hello, world\n"
const char *SG3830 = "hello, world\n";
int main() {
// 使用 printf 函数打印字符串
printf("%s", SG3830);
// 或者更简单地,因为 SG3830 已经是一个指向字符串的指针
// printf(SG3830);
return 0;
}
3.1.2 GCC
在 IDA 中观察到的汇编指令
Main PROC NEAR
var_10 = dword ptr -10h
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 10h
mov eax, offset aHelloWorld ; "hello, world\n"
mov [esp+10h+var_10], eax ; 注意:这行代码的意图可能不清晰或错误
call _printf
mov eax, 0
leave
retn
Main ENDP
; 假设aHelloWorld是在其他地方定义的字符串常量
; aHelloWorld DB 'hello, world\n', 0 ; 字符串定义通常包括末尾的0作为结束符
提示
and esp, 0FFFFFFF0h
这条指令是用来对栈指针寄存器ESP
进行位与(AND)操作的,目的是将ESP
的值向下调整到离它最近的16字节,说白了就是 2 进制下低四位清零。如果地址位没有对齐,那么CPU可能需要访问两次内存才能获得栈内数据。虽然在8字节边界处对齐就可以满足32位x86 CPU和64位x64 CPU的要求,但是主流编译器的编译规则规定“程序访问的地址必须向16字节对齐(被16整除)”。人们还是为了提高指令的执行效率而特意拟定了这条编译规范。- LEAVE 指令,等效于“ MOV ESP, EBP ”和“ POP EBP ”两条指令。可见,这个指令调整了数据栈指针 ESP,并将 EBP 的数值恢复到调用这个函数之前的初始状态。
3.1.3 GCC:AT&T 语体
AT & T语体同样是汇编语言的显示风格。这种语体在UNIX之中较为常见。
去除掉大量的汇编宏得到的程序
.LC0: .string "hello, world\n"
main:
pushl %ebp ; 保存旧的基指针
movl %esp, %ebp ; 将栈指针的值复制到基指针,设置新的栈帧
andl $-16, %esp ; 将栈指针向下调整到16字节的边界
subl $16, %esp ; 为局部变量和可能的函数调用预留空间
movl $.LC0, (%esp) ; 将字符串的地址压入栈中,作为printf的参数
call printf ; 调用printf函数打印字符串
movl $0, %eax ; 设置返回值0,表示程序成功执行
leave ; 恢复旧的栈帧(mov %ebp, %esp; pop %ebp)
ret ; 返回调用者
重要
运算表达式(operands,即运算单元)的书写顺序相反。
Intel 格式:<指令><目标><源>。
AT & T 格式:<指令><源><目标>
AT & T语体中,在寄存器名称之前使用百分号(%)标记,在立即数之前使用美元符号($)标记。AT & T语体使用圆括号,而Intel语体则使用方括号。
AT & T语体里,每个运算操作符都需要声明操作数据的类型:
-q-quad(64位)
-l指代32位long型数据。
-w指代16位word型数据。
-b指代8位byte型数据。