将晦涩难懂的技术讲的通俗易懂
分类: LINUX
2022-11-26 22:06:20
本文主要介绍一下在linux x86-64系统下一些反汇编和逆向分析的基础知识,方便在日常工作中对基础问题的分析。为了描述方便,本文采用如下测试程序demo为例。
点击(此处)折叠或打开
首先需要了解x86的栈和地址关系,如下图所示,栈是由高地址向低地址方向增长的。
在x86-64中,所有寄存器都是64位,相对32位的x86来说,标识符发生了变化,e开头变为了r开头,比如:从原来的%ebp变成了%rbp。为了向后兼容性,%ebp依然可以使用,不过指向了%rbp的低32位。X86-64寄存器的变化,不仅体现在位数上,更加体现在寄存器数量上。新增加寄存器%r8到%r15。加上x86的原有8个,一共16个通用寄存器。
让寄存器为己所用,就得了解它们的用途,这些用途都涉及函数调用,X86-64有16个64位寄存器,分别是:%rax,%rbx,%rcx,%rdx,%rsi,%rdi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15(如下图gdb中的显示)。其中:
● %rax :作为函数返回值使用。
● %rsp: 栈指针寄存器,指向栈顶
● %rbp:一般是指向栈帧的基地址,但是如果使用了gcc-fomit-frame-pointer参数优化,就不再保留栈帧,%rbp就可以作为其他使用了
● %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
如示例中的main函数对funA的调用,前面6个参数分别是以上6个寄存器(%edi即%rdi的低32位,其他类似),后面两个(7,8)参数则直接通过栈传递。并且主要函数参数入栈是从从右向左入栈的。
● %rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
● %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
除了以上16个通用寄存器外,还有一个比较关键的寄存器:
● %rip:指令寄存器,指向当前执行的指令
l disassemble
反汇编某个函数
如果disassemble不接函数名称,则反汇编的是当前堆栈执行的函数,如下图当前堆栈crash在了funC。
l info registers
查看寄存器的值
l x/nfu
打印指定地址内容
命令格式:x/nfu
如:(gdb)x/1xb 0x7fffffffd708
x : examine 的缩写
n : 表示要显示的内存单元个数
f : 表示显示方式, 可取如下值:
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
i 指令地址格式
c 按字符格式显示变量。
f 按浮点数格式显示变量。
u表示一个地址单元的长度,与n一起表示显示的地址长度
b表示单字节,
h表示双字节,
w表示四字节,
g表示八字节
● info line
你可以使用info line命令来查看源代码在内存中的地址。info line后面可以跟“行号”,“函数名”,“文件名:行号”,“文件名:函数名”,这个命令会打印出所指定的源码在运行时的内存地址(需要有debug info),如:
(gdb) info line tst.c:func
Line 5 of "tst.c" starts at address 0x8048456 and ends at 0x804845d
函数的调用过程主要分为5步:
1. 参数入栈(只有参数大于六个或者是大的结构体的情况,否则直接采用寄存器传参)
2. 函数的返回地址(调用函数后的下一个指令地址)入栈,通过callq指令完成(在调用方函数中完成)
3. push %rbp,将调用者的栈基指针入栈(在被调用方中完成)
4. mov %rsp,%rbp,将调用者的栈顶指针设置为被调用者的栈基指针(在被调用方中完成)
5. sub $0xxx,%rsp,被调用者开辟自己的栈空间(在被调用方完成)
需要理解上面每个栈帧前的那一串数字代表什么?其实是某个函数内调用被调用函数,被调用函数返回之后要执行的指令的地址(也即callq指令压栈的地址),如funB里调用了funC,在funC调用返回之后要执行的指令的地址就是0x00000000004011c3。该地址在funC被调用时,将被压入到rsp堆栈寄存器里,随后在进入funC时,funB的rbp寄存器也被压入栈,并更新rbp为当前栈顶,即funC栈帧在栈内存上的基址。
根据函数的调用过程,我们可以有以下关键结论:
l 被调用函数的rbp指向的值(不是rbp本身的值)就是调用者函数的栈基地址,即old rbp=*rbp
l ($rpb+8)即为上一级函数的地址,准确的说是上一级函数调用当前函数后的返回地址
我们在日常问题排除中经常会遇到GBD无法打印堆栈信息的情况,这种情况一般是栈被破坏,导致其中的rpb寄存器指向的内容被改写。我们将测试程序的funC函数进行如下改写,其他代码不变:
点击(此处)折叠或打开
运行后crash,gdb显示如下,可见堆栈已经被破坏:
通过bt指令或rbp寄存器回溯的方式得到调用栈已行不通。那么我们还有没有其它的方法呢?我们先来看下此时rsp栈上的内容,使用x指令来打印栈上前50个单元里的内容:
可以看到funA的rbp在栈上还可以找的到,且可以沿着这个rbp一直往下回溯出调用栈。
0x401224
0x401267
0x7ffff7e1f1b2 <__libc_start_main+242>
注意:上面打印$rsp向上的50个单元内容,左侧的地址和bt打印左侧的地址含义的区别,前者是栈空间的地址,后者是函数在代码段的地址。
因此,当在rbp寄存器或返回地址被意外篡改,导致堆栈破坏bt指令无法正常解析时,我们可以借助rsp寄存器里的内容,打印栈顶附近内存实现部分恢复。