Skip to content

第四次

约 1937 字大约 6 分钟

2024-10-08

计划

  1. 《IDA Pro权威指南》三章
  2. 《逆向工程权威指南》十章
  3. 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型数据。

3.2 x86-64

公告板

持续建设中