二进制漏洞挖掘与利用-第二部分

发布时间 2023-11-11 20:57:26作者: gao79138

二进制漏洞挖掘与利用-第二部分

1. ROP概念

    ROP全名为:返回导向编程
    ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。
    当你想要执行一段攻击代码(代码)时,发现这段代码不是连续的,那么你就可以操控eip来不断地执行分离的片段(gadget),最终达到执行攻击代码的目的。(ROP)
    参考:
    https://www.yijinglab.com/html/news/news.html?newsId=NEWS-3fa-bbe4-4014-802a-21cafc832a7d
    ROP就是通过寻找gadget,进行组合(跳转),最终达成攻击的这样的一种思想。
    gadget一般都是以ret结尾的代码片段,方便进行跳转。
    查找pop或者ret的指令片段。
    ROPgadget --binary ret2syscall --only "pop|ret"

2. 系统调用

2.1 系统调用的定义

    操作系统提供给用户的编程接口
    是提供访问操作系统所管理的底层硬件的接口
    本质上是一些内核函数代码,以规范的方式驱动硬件。
    x86 通过 int 0x80 指令进行系统调用
    amd64 通过 syscall 指令进行系统调用

2.2 执行系统调用的流程

    假设你想写一个my_puts()函数,将数组当中的信息输出到屏幕上,那么你应该在函数内部调用了动态链接库当中的write()函数。
    假设你已经完成了这个操作,接下来我们来讲述一下具体的流程。
    首先,main函数调用my_puts("Hello World!")函数,随后进入到my_puts("Hello World!")函数,执行到了write(1,&"Hello world!",12)函数,由于该函数是动态链接库当中的函数。因此,程序执行流会导向虚拟内存当中的shared libraries区域中,寻找这个函数。
    找到这个函数之后,这个函数会执行内部封装好的系统调用sys_write()函数。只不过在系统调用前,会先传递sys_write()的系统调用号到eax中(假设为4),然后会将参数一一传递到ebx,ecx等寄存器中。(ebx = 1;ecx = &"Hello World!";edx = 12)
    传递完之后,会执行int 0x80(int本质上是中断,x86通过中断来进行系统调用(0x80代表是系统调用)) 来启动这个系统调用(sys_write())。
    启动完系统调用后,在执行系统调用时(内核代码),操作系统就会将字符串显示到屏幕上。
    所以说:系统调用通常都会封装在库函数(并不是所有)中,当用户调用库函数时,实际上是执行了系统调用,让操作系统驱动硬件来完成用户的需求。
    补充:
        1.  ldd命令,查看一个可执行文件的所有的动态链接库。
        2.  动态链接库装载器会将一个二进制文件需要的所有动态链接库装载到虚拟内存当中的shared libraries 区域当中。 
        3.  一个动态链接库装载器:
            /lib64/ld-linux-x86-64.so.2
        4.  动态链接库装载器是不会有漏洞的。
        5.  动态链接库也是一个可执行文件。执行它会输出动态链接库的相关信息。
        6.  execve()系统调用会执行第一个参数当中的命令(以字符串形式)。
        7.  system()函数内部封装了execve()系统调用。
        8.  系统调用本质上就是一堆指令的集合。

3. ROP详解

3.1 ROP的执行流程

img

    假设,我们通过栈溢出,将返回地址之上的内容进行了覆盖(一系列gadget的地址)。
    首先,我们执行ret,ret的作用是将栈顶的值给到eip。此时,eip = 0x08052318。同时,esp + 1(字长)。此时,eip到达了图上的位置。

img

    此时,执行pop %edx。这条指令的作用是将栈顶的值给到edx寄存器中。此时edx = value。esp + 1。
    再执行ret指令,该指令的作用就是将栈顶的值给到eip,esp + 1。 eip = 0x0809951f,此时eip到达了图上的位置。

img

    将eax寄存器的值和自身进行异或,最终的结果为将eax的值清0。
    执行ret,此时eip = 080788c1,esp + 1。此时eip到达了图上的位置

img

    将eax寄存器的值给到edx寄存器
    执行ret,此时eip = 0x41414141,esp + 1。

4. 动态链接

4.1 静态链接与动态链接的区别

    1.  如果对同一个.c文件进行编译,静态链接比动态链接占用的空间大。
    2.  静态链接会将ELF文件所需要的所有库函数(完整实现)全部都写到ELF文件本身(位于.text节)。动态链接会将ELF文件所需要的库函数进行标记(符号),当使用时,在从对应的动态链接库中进行调用(需要.plt节解析库函数在内存中的真实地址,然后再根据该地址进行调用)(此时动态链接库也在内存(shared libraries区域))。

img

4.2 动态链接相关结构

    1.  .dynamic节提供了动态链接的相关信息
    2.  link_map保存了进程载入的动态链接库的链表。
    3.  dl_runtime_resolve:装载器中用于解析动态链接库中函数的实际地址的函数(仅在第一次调用库函数时执行(.plt会调用它),之后会将解析后的真实地址保存在.got.plt节中)。
    4.  .got节为全局偏移量表,保存了整个程序虚拟地址空间中各个符号所需的全部的偏移量(地址)。
    5.  .got保存的是变量的地址,.got.plt保存的是函数的地址。
    6.  .plt和.got.plt会保存到代码段中。
    7.  .plt是一个表(每一个表项对应着动态链接的库函数),.got.plt仍是一个表(每一个表项对应着动态链接的库函数的真实地址)。
    8.  当第一次调用时,.got.plt还没有该函数的真实地址。此时存放的是.plt表项的首地址的下一个地址。因此,如果没有,会直接跳回该表项的下一条语句。

img

4.3 动态链接过程

img

    假设我们已经编写好了一个程序,该程序的内部调用了动态链接库的foo函数。那么,接下来我们来详细解释一下程序调用foo函数时都发生了什么?

img
img

    此时会跳转到.plt中的foo表项,执行该表项内的汇编代码。首先,.plt中的代码会立即跳转到.got.plt中记录的地址。

img

    但是,由于进程是第一次调用foo,因此.got.plt中记录的地址是.plt表项中首地址的下一个地址。因此会跳回到.plt表项中,执行push index。

img

    接下来会执行jmp PLT0

img

    接下来会执行push *(GOT + 4)
    以上的push的两个值均为解析真实地址的函数的参数。
    index代表你要解析的是.plt表项的第几个函数。(索引)
    第二个参数代表你要从第几个动态链接库中找到这个库函数?

img

    之后跳转到dl_runtime_resolve函数,开始解析真实地址。

img
img

    解析之后,会填入到.got.plt的对应表项中。
    之后就可以按照真实地址调用库函数了。

img
img

    等到第二次解析时,就可以直接通过.plt表项再到.got.plt获取到该库函数的真实地址。之后执行即可。

img

    上图为第一次调用foo函数的情况。

img

    上图为第二次调用foo函数的情况。

img

    补充知识点:
        1.  .init节存储着初始化的相关代码。(只在动态链接中存在)
        2.  在pwndbg中我们可以使用plt命令来查看plt的内容,got命令来查看got表的内容。