反汇编与逆向分析基础

反汇编与逆向分析基础

——lvyilong316

本文主要介绍一下在linux x86-64系统下一些反汇编和逆向分析的基础知识,方便在日常工作中对基础问题的分析。为了描述方便,本文采用如下测试程序demo为例。

点击(此处)折叠或打开

    #include <stdio.h>

    void funC(int c1, int c2, int c3, int c4, int c5, int c6, int c7, int c8)
    {
        int *p = NULL;
        *p = c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8;
    }

    void funB(int b1, int b2, int b3, int b4, int b5, int b6, int b7, int b8)
    {
        printf("call funC");
        funC(b1, b2, b3, b4, b5, b6, b7, b8);
    }

    void funA(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
    {
        printf("call funB");
        funB(a1, a2, a3, a4, a5, a6, a7, a8);
    }

    int main()
    {
        printf("call funA");
        funA(1, 2, 3, 4, 5, 6, 7, 8);
        return 0;
    }

x86的地址和栈

    首先需要了解x86的栈和地址关系,如下图所示,栈是由高地址向低地址方向增长的。

x86-64常用寄存器

在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:指令寄存器,指向当前执行的指令

 

反汇编GDB常用命令

disassemble

反汇编某个函数

 

如果disassemble不接函数名称,则反汇编的是当前堆栈执行的函数,如下图当前堆栈crash在了funC。

 

info registers

查看寄存器的值

 

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,被调用者开辟自己的栈空间(在被调用方完成)

GDB的显示信息含义

 

需要理解上面每个栈帧前的那一串数字代表什么?其实是某个函数内调用被调用函数,被调用函数返回之后要执行的指令的地址(也即callq指令压栈的地址,如funB里调用了funC,在funC调用返回之后要执行的指令的地址就是0x00000000004011c3。该地址在funC被调用时,将被压入到rsp堆栈寄存器里,随后在进入funC时,funB的rbp寄存器也被压入栈,并更新rbp为当前栈顶,即funC栈帧在栈内存上的基址。

 

关键结论

根据函数的调用过程,我们可以有以下关键结论:

l 被调用函数的rbp指向的值(不是rbp本身的值)就是调用者函数的栈基地址,即old rbp=*rbp

l ($rpb+8)即为上一级函数的地址,准确的说是上一级函数调用当前函数后的返回地址

堆栈破坏后的恢复方法

我们在日常问题排除中经常会遇到GBD无法打印堆栈信息的情况,这种情况一般是栈被破坏,导致其中的rpb寄存器指向的内容被改写。我们将测试程序的funC函数进行如下改写,其他代码不变:

点击(此处)折叠或打开

    void funC(int c1, int c2, int c3, int c4, int c5, int c6, int c7, int c8)
    {
        int *tmp = 0;
        int *rbp = (int *)(&tmp + 2); //获取返回地址的位置
        *rbp = c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8; //破坏函数返回地址
    }

 运行后crash,gdb显示如下,可见堆栈已经被破坏:

 

通过bt指令或rbp寄存器回溯的方式得到调用栈已行不通。那么我们还有没有其它的方法呢?我们先来看下此时rsp栈上的内容,使用x指令来打印栈上前50个单元里的内容:

 

可以看到funA的rbp在栈上还可以找的到,且可以沿着这个rbp一直往下回溯出调用栈。

 0x401224

 0x401267

 0x7ffff7e1f1b2 <__libc_start_main+242>

注意:上面打印$rsp向上的50个单元内容,左侧的地址和bt打印左侧的地址含义的区别,前者是栈空间的地址,后者是函数在代码段的地址。

 

 

因此,当在rbp寄存器或返回地址被意外篡改,导致堆栈破坏bt指令无法正常解析时,我们可以借助rsp寄存器里的内容,打印栈顶附近内存实现部分恢复。

来源url
栏目
文章分类