二进制漏洞挖掘与利用
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将文件的二进制内容还原为原本内容。
上述图片展示了某个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程序要经过编译生成汇编代码,再通过汇编生成机器码,再通过将多个机器码的目标文件(库函数等)进行链接成一个可执行文件后,才可以放到内存中执行。
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
用来存放各个节的名称。
左图是一个ELF文件存储在磁盘上,还没有运行时的情况。(节视图)(代表该ELF文件以节的形式进行组织)
右图是一个ELF文件加载到内存中执行时的情况。(段视图)(代表该ELF文件以段的形式进行组织)
从左图->右图,操作系统会将具有相同权限的节聚合成一个段。
objdump -s elf 用于查看该elf文件在磁盘上的结构
cat /proc/pid/maps 用于查看该elf文件在内存上的结构(前提需要确保该进程没有消亡)
参考资料:https://blog.csdn.net/weixin_43942316/article/details/131051625
2.3 进程虚拟地址空间
虚拟地址空间的定义和必要性:
虚实地址分开,建立一种从虚地址空间映射到物理内存的机制。
对于每一个进程来说,它们都拥有完整的虚拟地址空间。操作系统将内存的物理地址空间和虚拟地址空间进行分页(页大小相等)。对于每一个进程来说,虚拟地址空间是连续的。但是,对于物理地址空间来讲,每个进程所分配的页都是离散存储的。
如果分配给进程的物理页不够怎么办?采用对换技术,在外存中建立一片对换区,将不常用的页换到外存,将所需的页换入到内存(局部性原理)。
虚拟存储原理使得:可以支持大于实际物理内存的编程空间,使系统更加安全。
地址以字节编码,常以十六进制表示
虚拟内存每个进程的用户空间,每个进程独占一份。
虚拟内存内核空间(操作系统代码)所有进程共享一份
虚拟内存mmap段中的动态链接库仅在物理内存中装载一份。
2.3 进程虚拟地址空间-关于段和节的进一步阐述
我们可以简单记忆:
段视图规定了ELF文件在内存中如何执行(包括权限划分等)。
节视图规定了ELF文件在磁盘中如何存储。(包括文件结构的组织等)
在ELF文件执行时,会将相同权限的节组合成一个段。
代码段包含了代码与只读的数据:
.text节存储着程序的代码
.plt节用于解析动态链接函数的实际地址。
.rodata节用于保存ELF文件中的只读数据。
数据段包含了可读可写数据:
.got.plt节用于保存.plt节解析的实际地址。
.bss节只在内存中占用空间,并不在磁盘中占用空间。用于保存ELF文件中未初始化的变量。
2.3 进程虚拟地址空间-进程执行时各部分内容如何存储?
当一个程序装载到内存执行时,该程序的各个部分是怎么存储在该进程的虚拟地址空间当中的?
我们对上图的内容进行解释:
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 大端序与小端序
小端序:
1. 低地址存放数据低位,高地址存放数据高位。
2. 小端序比大端序更容易被利用。
大端序:
1. 低地址存放数据高位,高地址存放数据低位。
2.4 程序的装载与进程的执行
在不同的架构下,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()函数并不是真正意义上程序的入口。
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函数执行之前和执行之后的事情
在这里,讲一下ld.so是什么?
ld.so是动态链接器,由于动态链接程序在运行的时候,需要动态链接库的函数,那么我们需要动态链接器来完成这个事情。
2.5 x86&amd64汇编简述
MOV DEST,SRC 将源操作数传送给目标
LEA REG,SRC 把源操作数的有效地址传送给指定的寄存器
PUSH VALUE 把目标值压栈,同时SP指针-1字长
POP DEST 将栈顶的值弹出至目的存储位置,同时SP指针+1字长。
注意:进程的栈空间是从高地址往低地址增长,其余空间均从低地址往高地址增长。
LEAVE 在函数返回时,恢复父函数栈帧的指令。
RET 在函数返回时,控制程序执行流返回父函数的指令
栈帧保存着被调用函数的状态。需要注意的是:PUSH/POP操作的对象是值而不是栈帧。
3. C语言函数调用栈的过程
3.1 函数调用栈的基本流程
栈用来保存函数运行时的状态信息。包括函数参数和局部变量等
当发生函数调用时,调用函数(caller)的状态保存在栈中,被调用函数(callee)的状态被压入调用栈的栈顶。当父函数调用子函数时,就会将子函数的栈帧压入到栈顶。
当函数调用结束时,栈顶的函数(callee)状态被弹出,栈顶恢复到调用函数(caller)的状态。
栈从高地址往低地址增长,所以栈顶地址对应的内存地址,压栈时变小,退栈时变大。
EBP/RBP寄存器保存上一个栈帧的栈底地址。(CPU架构:32位/64位)
ESP/RSP寄存器保存当前栈帧的栈顶地址。(同上)
栈的空间远远小于堆的空间。
3.2 栈帧结构概览
在上图中,%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随之指向相邻的下一条指令,如此反复,程序就可以连续执行指令。
接下来我们来阐述一下函数调用栈的详细过程。
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压栈(记录下上一个函数栈帧的栈底,便于返回)。
对于上述图片,虚线上方是父函数栈帧,虚线下方是子函数栈帧。
标签:文件,入门,二进制,程序,ELF,调用函数,漏洞,地址,函数
From: https://www.cnblogs.com/gao79135/p/17794078.html