二进制漏洞挖掘与利用入门

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

二进制漏洞挖掘与利用

1. pwn概述及基本术语补充

1.1 pwn概述

    pwn可以指:
        1.  破解、利用成功(程序的二进制漏洞)
        2.  攻破了设备、服务器
        3.  控制了设备、服务器
    (简单理解)pwn漏洞指的就是:已经编译成机器码的二进制程序(可执行程序)相关的漏洞。

1.2 ELF

    ELF指的就是linux系统下可执行文件的格式。

1.3 ida

    ida是非常优秀的反编译器和反汇编器。
    ida可以将机器码反编译成汇编语言甚至C语言。

1.4 exploit(EXP)

    exploit指的是用于攻击的脚本与方案。

1.5 payload(攻击载荷)

    payload指的就是:攻击者精心构造一段恶意数据发送给服务器,服务器解析这段数据之后,导致服务器当中程序的控制流被攻击者劫持。

1.6 shell

    shell可以简单理解为:linux系统下用户与操作系统进行交互的接口(界面)。通过shell,用户可以跟操作系统进行交互去完成一些事情(使用命令去查看磁盘中存储的文件,创建文件等)。

1.7 shellcode

    shellcode指的是:攻击者利用漏洞,导致进一步调用攻击目标的shell的代码。

2. 二进制基础

2.1 程序的编译与链接-前置知识

    在Windows中,通过后缀名来识别一个文件的格式。
    但是在Linux中,通过文件头来识别一个文件的格式。我们可以通过file命令来查看一个文件的格式(file命令内部会查看一个文件的文件头)。
    我们可以通过vim当中的%!xxd来查看一个文件的二进制内容(十六进制格式)。
    我们可以通过vim当中的%!xxd -r将文件的二进制内容还原为原本内容。

img

    上述图片展示了某个c程序的二进制内容。左侧表示的是该行距离文件头有多少字节。中间就是二进制内容(十六进制格式)。最后就是用户看到的c程序。

2.1 程序的编译与链接-gcc

    gcc是GNU组织的一个c程序的编译器。
    当我们使用gcc example.c去编译c程序时,会生成一个example.out文件。这个文件就是该c程序的二进制文件,这个文件可以直接被加载到内存当中执行。
    我们可以通过gcc -S example.c 将c程序编译成汇编语言。会生成example.s文件。
    我们可以通过file命令查看这些文件的格式:
    example.c -> ASCII Text
    example.s -> ASCII Text
    example.out -> ELF
    gcc既可以充当编译器,也可以充当汇编器。

2.1 从C源代码到可执行文件的生成过程

    一个C程序要经过编译生成汇编代码,再通过汇编生成机器码,再通过将多个机器码的目标文件(库函数等)进行链接成一个可执行文件后,才可以放到内存中执行。

img
img

2.2 Linux下的可执行文件格式ELF

    什么是可执行文件?
        1.  广义:文件中的数据是可执行代码的文件
            .out、.exe、.sh、.py
        2.  狭义:文件中的数据是机器码的文件
            .out、.exe、.dll、.so
    可执行文件的分类:
        1.  Windows:PE(Portable Executable)
            可执行程序:.exe
            动态链接库:.dll
            静态链接库:.lib
        2.  Linux:ELF(Executable and Linkable Format)
            可执行程序: .out
            动态链接库: .so
            静态链接库:.a
    ELF文件格式(内存):
        1.  ELF header(ELF文件头表)
            记录了ELF文件的组织结构(整体信息),操作系统通过分析ELF header来为ELF创建一个进程映像。
        2.  程序头表/段表(Program header table)
            用于表明进程映像不同部分权限(例如:代码段不可写,数据段可写等)。
            用于标识ELF中各种各样的段是如何组织起来的。
            告诉系统如何创建进程
            生成进程的可执行文件必须拥有此结构
            重定位文件(链接库文件)不一定需要
        3.  节头表(Section header table)
            节头表是用来组织ELF文件存储在磁盘上各个节的信息。(一个段中有可能有多个节)
            记录了ELF文件的节区信息
            用于链接的目标文件必须拥有此结构
            其他类型目标文件不一定需要
        4.  代码段(CODE)
            记录了代码主体
        5.  数据段(DATA)
            用来存放ELF文件在运行时所需要的数据。
        6.  Sections' names
            用来存放各个节的名称。

img

    左图是一个ELF文件存储在磁盘上,还没有运行时的情况。(节视图)(代表该ELF文件以节的形式进行组织)
    右图是一个ELF文件加载到内存中执行时的情况。(段视图)(代表该ELF文件以段的形式进行组织)
    从左图->右图,操作系统会将具有相同权限的节聚合成一个段。

img
img

    objdump -s elf 用于查看该elf文件在磁盘上的结构
    cat /proc/pid/maps 用于查看该elf文件在内存上的结构(前提需要确保该进程没有消亡)
    参考资料:https://blog.csdn.net/weixin_43942316/article/details/131051625

2.3 进程虚拟地址空间

    虚拟地址空间的定义和必要性:
        虚实地址分开,建立一种从虚地址空间映射到物理内存的机制。
        对于每一个进程来说,它们都拥有完整的虚拟地址空间。操作系统将内存的物理地址空间和虚拟地址空间进行分页(页大小相等)。对于每一个进程来说,虚拟地址空间是连续的。但是,对于物理地址空间来讲,每个进程所分配的页都是离散存储的。
        如果分配给进程的物理页不够怎么办?采用对换技术,在外存中建立一片对换区,将不常用的页换到外存,将所需的页换入到内存(局部性原理)。
        虚拟存储原理使得:可以支持大于实际物理内存的编程空间,使系统更加安全。
    地址以字节编码,常以十六进制表示
    虚拟内存每个进程的用户空间,每个进程独占一份。
    虚拟内存内核空间(操作系统代码)所有进程共享一份
    虚拟内存mmap段中的动态链接库仅在物理内存中装载一份。

img
img

2.3 进程虚拟地址空间-关于段和节的进一步阐述

    我们可以简单记忆:
        段视图规定了ELF文件在内存中如何执行(包括权限划分等)。
        节视图规定了ELF文件在磁盘中如何存储。(包括文件结构的组织等)
        在ELF文件执行时,会将相同权限的节组合成一个段。
    代码段包含了代码与只读的数据:
        .text节存储着程序的代码
        .plt节用于解析动态链接函数的实际地址。
        .rodata节用于保存ELF文件中的只读数据。
    数据段包含了可读可写数据:
        .got.plt节用于保存.plt节解析的实际地址。
        .bss节只在内存中占用空间,并不在磁盘中占用空间。用于保存ELF文件中未初始化的变量。

img

2.3 进程虚拟地址空间-进程执行时各部分内容如何存储?

    当一个程序装载到内存执行时,该程序的各个部分是怎么存储在该进程的虚拟地址空间当中的?

img

    我们对上图的内容进行解释:
        1. Text(Code)段:存放了sum函数具体实现的机器码,main函数具体实现的机器码,只读数据:"Hello world!"
        2. Data段:用于存放已进行初始化的全局变量:str
        3. Bss段:用于存放未进行初始化的全局变量:glb
        4. 由于main()函数使用了malloc动态分配了空间,因此这部分空间以及存储在其中的数据会放在Heap当中。
        5. 当进行函数调用时,该函数内部的局部变量以及函数的执行状态等相关数据会存放在Stack当中。(t是sum函数的局部变量,ptr是main函数的局部变量,分配存放在不同的栈帧中)
        6.  关于函数形参的存储位置,不同的架构存储位置不同。如果是32位,则形参也存储在栈当中,当调用函数之前,会把该函数的形参压入栈。如果是64位,则形参会存储在寄存器中,当函数执行时,直接从寄存器当中取值即可。

2.3 大端序与小端序

img

    小端序:
        1.  低地址存放数据低位,高地址存放数据高位。
        2.  小端序比大端序更容易被利用。
    大端序:
        1.  低地址存放数据高位,高地址存放数据低位。

2.4 程序的装载与进程的执行

img

    在不同的架构下,PC有不同的名称:
        1.  x86架构:EIP
        2.  x86-64/amd64架构:RIP
    在amd64架构下,部分寄存器的功能:
        1.  RIP
        2.  RSP:存放当前栈帧的栈顶地址
        3.  RBP:存放当前栈帧的栈底地址
        4.  RAX:通用寄存器,存放当前函数返回值。
    静态链接程序与动态链接程序之间的区别:
        1.  静态链接程序不需要动态链接库,该程序运行所需的所有内容都存储在了ELF文件中,静态链接程序可以独立执行。
        2.  动态链接程序在运行时需要动态链接库的支持,该程序运行所需的所有内容并不是都存储在了ELF文件中。当程序编译时会先记录下该程序所需要的动态链接库,等到运行时再从动态链接库中寻找所需的库函数。动态链接程序不可以独立执行,需要相应库函数的支持。

2.4 静态链接的程序,main函数执行之前和执行之后的事情

    有人说,main()函数是用户自定义程序的入口。为什么?main()函数执行之前和执行之后做了什么事情?
    总的来说,一个进程在执行main()函数之前,系统需要为该进程分配所需的空间,分配之后再去调用main()函数。因此,main()函数并不是真正意义上程序的入口。

img

    shell程序执行fork()函数,用于将自己的虚拟地址空间(用户虚拟地址空间)拷贝给当前进程。
    之后会调用execve()函数,它是系统调用sys_execve()的封装。(user mode)
    之后会执行系统调用sys_execve()、do_execve()、search_binary_handler()、load_elf_binary(),尝试将系统资源分配给当前进程,并将拷贝的虚拟地址空间重写为当前进程的内容。(kernel mode)
    之后会返回user mode,执行_start代码,注意:_start代码是一段汇编程序,它才是真正的程序入口。
    _start会调用当前程序的main()函数,此程序开始。

2.4 动态链接的程序,main函数执行之前和执行之后的事情

img

    在这里,讲一下ld.so是什么?
        ld.so是动态链接器,由于动态链接程序在运行的时候,需要动态链接库的函数,那么我们需要动态链接器来完成这个事情。

2.5 x86&amd64汇编简述

    MOV DEST,SRC 将源操作数传送给目标
    LEA REG,SRC 把源操作数的有效地址传送给指定的寄存器

img

    PUSH VALUE 把目标值压栈,同时SP指针-1字长
    POP DEST 将栈顶的值弹出至目的存储位置,同时SP指针+1字长。

img

    注意:进程的栈空间是从高地址往低地址增长,其余空间均从低地址往高地址增长。
    LEAVE 在函数返回时,恢复父函数栈帧的指令。
    RET 在函数返回时,控制程序执行流返回父函数的指令

img

    栈帧保存着被调用函数的状态。需要注意的是:PUSH/POP操作的对象是值而不是栈帧。

img

3. C语言函数调用栈的过程

3.1 函数调用栈的基本流程

    栈用来保存函数运行时的状态信息。包括函数参数和局部变量等
    当发生函数调用时,调用函数(caller)的状态保存在栈中,被调用函数(callee)的状态被压入调用栈的栈顶。当父函数调用子函数时,就会将子函数的栈帧压入到栈顶。
    当函数调用结束时,栈顶的函数(callee)状态被弹出,栈顶恢复到调用函数(caller)的状态。
    栈从高地址往低地址增长,所以栈顶地址对应的内存地址,压栈时变小,退栈时变大。
    EBP/RBP寄存器保存上一个栈帧的栈底地址。(CPU架构:32位/64位)
    ESP/RSP寄存器保存当前栈帧的栈顶地址。(同上)
    栈的空间远远小于堆的空间。

img

3.2 栈帧结构概览

img

    在上图中,%ebp和%esp寄存器之间的就是当前函数的栈帧。
    return address 为返回地址
    stack frame pointer 为上一个栈帧的栈底(上一个栈帧的%ebp)的值(方便恢复父函数的栈底指针)
    local variables 为局部变量(保存在对应函数的栈帧内)
    arguments 在32位架构中,子函数的形参并不是保存在自身的栈帧中,而是保存在父函数栈帧的末尾位置。这里的arguments是子函数的形参。

3.3 C语言函数调用栈的详细过程

    函数状态主要涉及到三个寄存器:esp,ebp,eip。
    esp用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。
    ebp用来存储当前函数状态的基地址,在函数运行时不变,可以用来索引确定函数参数或局部变量的位置。
    eip用来存储即将执行的程序指令的地址,cpu按照eip的存储内容读取指令并执行,eip随之指向相邻的下一条指令,如此反复,程序就可以连续执行指令。

img
img
img
img
img
img
img

    接下来我们来阐述一下函数调用栈的详细过程。
        1.  首先将被调用函数(callee)的参数按照逆序依次压入栈内。如果被调用函数没有参数,则此步骤省略。这些参数仍会保存在调用函数(caller)的函数状态(栈帧)内(即:被调用函数的参数保存在父函数的栈帧内,而不是当前函数栈帧内),之后压入栈的数据都会作为被调用函数(callee)的函数状态来保存。
        2.  然后将调用函数(caller)进行调用之后的下一条指令地址作为返回地址压入栈内。这样调用函数(caller)的eip(指令)信息得以保存。返回地址的意义在于当子函数执行完之后,就可以返回父函数的返回地址处继续执行。
        3.  再将当前的ebp寄存器的值(也就是调用函数的基地址)压入栈内,并将ebp寄存器的值更新为当前栈顶的地址。这样调用函数(caller)的ebp(基地址)信息得以保存。同时,ebp被更新为被调用函数(callee)的基地址。  
        4.  再之后是将被调用函数(callee)的局部变量等数据压入到栈内。
        5.  在压栈的过程中,esp寄存器的值不断减小。压入栈的数据包括:调用参数、返回地址、调用函数的基地址、以及局部变量,其中调用参数以外的数据共同构成了被调用函数(callee)的状态。在发生调用时,程序还会将被调用函数(callee)的指令地址存在eip寄存器内,这样程序就可以依次执行被调用函数的指令了。
        6.  当子函数执行完成后,我们将栈帧恢复到调用函数的状态。
        7.  首先将被调用函数的局部变量会从栈内直接弹出,栈顶会指向被调用函数(callee)的基地址。(只需要更改栈顶指针)
        8.  然后将基地址存储的调用函数(caller)的基地址从栈内弹出,并存到ebp寄存器内。这样调用函数(caller)的ebp(基地址)信息得以恢复(leave指令,需要esp和ebp并不指向同一个地址的情况下。如果esp和ebp指向同一个地址,那么直接使用pop ebp即可)。此时栈顶会指向返回地址。
        9.  再将返回地址从栈内弹出,并存储到eip寄存器内(ret指令)。这样调用函数(caller)的eip(指令)信息得以恢复。
        10. 之后调用函数(caller)的函数状态就全部恢复了,之后就可以继续执行调用函数的指令。
    需要注意:
        1.  在x86架构中,使用栈来传递参数,使用eax寄存器来存放返回值。
        2.  在amd64架构中,前6个参数依次存放于rdi,rsi,rdx,rcx,r8,r9寄存器中,第7个以后的参数存放于栈中。
        3.  一个函数在被调用以及返回时,一定会有如下四行汇编代码:
        push ebp
        mov esp ebp
        ...
        leave(可能有)
        ret
        4.  call指令不同于jump指令,jump指令只是单纯跳转目标位置。但是call指令会跳转到被调用函数的开头,并记录返回地址。
        5.  因此,当父函数调用子函数时,它会将子函数的参数压入到自己的栈帧中,同时会将返回地址压入到自己的栈帧中。因此:返回地址存在于父函数的栈帧。
        6.  调用子函数时,子函数的汇编代码首先就是将ebp压栈(记录下上一个函数栈帧的栈底,便于返回)。

img

    对于上述图片,虚线上方是父函数栈帧,虚线下方是子函数栈帧。