PWN的另一个名字是二进制漏洞审计
Pwn和逆向工程一样,是操作底层二进制的,web则是在php层面进行渗透测试
我是从re开始接触CTF的,有一点二进制基础,本文可能会忽略一些基础知识的补充
”Pwn”是一个黑客语法的俚语词 ,是指攻破设备或者系统。发音类似“砰”,对黑客而言,这就是成功实施黑客攻击的声音——砰的一声,被“黑”的电脑或手机就被你操纵了。
Pwn在CTF竞赛是体现技术实力的关键部分,也是最难的部分。
下面讲解一下Pwn有关的知识
——Your computer,my access!
Pwn的目的
最终目的:获得一个shell
Shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。
它接收用户输入的命令并把它送入内核去执行。
实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。
获得一个shell意味着获取了一个服务器的控制台,我们就可以随心所欲地操作服务器中的数据。
基础知识
程序的编译和链接
编译过程
编译->汇编->链接
file命令
可以查看文件类型
gcc使用的vim查看底层二进制
在vim中查看底二进制,在命令行中输入:%!xxd
还原为字符文件 %!xxd -r
gcc直接编译
gcc 要编译的文件
逐步使用gcc编译
gcc -S 源代码文件 得到汇编文件
gcc 汇编语言文件 得到可执行文件
./可执行文件 可以执行可执行文件
可执行文件
Windows:PE
可执行程序
exe
动态链接库
dll
静态链接库
lib
Linux:ELF
可执行程序
out
动态链接库
so
静态链接库
a
需要对PE文件和ELF文件有一定的了解
堆栈
这里讲解一下汇编语言的堆栈机制,读完不能很好理解的读者可以自行学习汇编语言
堆栈数据结构
堆栈数据结构就是我们数据结构课中学到的栈
栈顶添加新元素,删除元素在栈顶删除
后进去先出FILO
本文要讲的是运行时堆栈
运行时堆栈
运行时堆栈是内存数组,程序的数据存放在内存上,运行时用抽象数据结构中栈的形式来处理数据
ESP寄存器
ESP寄存器(extended stack pointer,扩展堆栈指针)
可以理解为抽象数据结构栈中指向栈顶的指针
存放某个位置的32位偏移量
基本上不会被程序员修改
用CALL,RET,PUSH,POP指令间接修改
入栈操作
32位入栈操作是把栈顶指针减4,再将数值复制到栈顶指针指向的位置
运行时堆栈在内存中是向下生长的,从高地址向低地址扩展
出栈操作
从堆栈删除元素,站定指针减小
下面介绍一些汇编指令
PUSH指令
首先减少ESP的值,将操作数复制到堆栈
POP指令
将ESP指向的堆栈元素复制到一个操作数当中,增加ESP的值
PUSHFD指令
将32位EFLAGS寄存器的内容压入堆栈
POPFD指令
将栈顶元素弹出到32位EFLAGS寄存器
PUSHAD指令
按照EAX,ECX,EDX,EBX,ESP,EBP,ESI,EBI的顺序
将所有32位通用寄存器压入堆栈
POPAD指令
按照与PUSHAD相反的顺序将其弹出堆栈
堆栈帧
堆栈参数
堆栈帧是一块堆栈保留区域
存放
- 被传递的实际参数
- 子程序的返回值
- 局部变量
- 被保存的寄存器
创建步骤
- 将被传递的实际参数压入堆栈
- 当子程序被调用时,该子程序的返回值压入堆栈
- 子程序开始执行的时候,EBP被压入堆栈
- 设置EBP等于ESP,EBP成为子程序所有参数的引用基址
- 如果有局部变量,修改ESP在堆栈中为其预留空间
- 需要保留的寄存器,将它们压入堆栈
Fastcall调用方式
顾名思义,是一种希望快速的调用方式
我们来分析一下这个调用方式:
调用自过程的时候,需要首先将参数传入EAX,EBX,ECX,EDX,少数情况还会传入ESI,EDI
我们知道寄存器是CPU内部的原件,堆栈在内存上,寄存器调用明显更快
但是我们知道通用寄存器很少,很多都有特定的功能,乘法需要用到EAX,还有许多寄存器用来循环数值和参与计算的操作数
因此寄存器不可能一直存放传递给过程的参数
在过程调用之前, 存放参数的寄存器需要首先入栈,然后向其分配过程参数
但是这些额外的入栈操作会让代码变得混乱,还有可能消除性能优势
值传递
一个参数通过数值传递时,该值的副本会被压入堆栈
.data
val1 DWORD 3
val2 DWORD 6
.code
push val2
push val1
call AddTwo
引用传递
通过引用来传递的参数包含的是对象的地址
push OFFSET val2
push OFFSET val1
传递数组
将数组的地址压入堆栈
不愿意采用将每个数组元素压入堆栈的原因是这样很慢而且浪费堆栈空间
访问堆栈的参数
1.将传递的参数压入堆栈,调用子过程
2.EBP寄存器存放的是原来栈帧的基址,我们需要现将EBP压入栈保存
3.然后将当前的ESP作为新的栈帧的基址
示例
int AddTwo(int x,int y)
{
return x+y;
}
将EBP入栈,设置ebp位esp的值
AddTwo PROC
push ebp
mov ebp,esp
ADD(5,6)
6 | [EBP+12] |
5 | [EBP+8] |
返回地址 | [EBP+4] |
EBP | mov ebp,esp |
这样通过当前EBP和偏移量就能访问传入的参数和原来的ebp(返回地址)
显式的堆栈参数
堆栈参数的引用表达式形如[esp+8],称它们为显式的堆栈参数
清除堆栈
子程序返回时,必须将参数从堆栈中删除
否则会导致内存泄露,堆栈会被破坏
C调用方式-cdecl
用于C和C++语言
子程序的参数按逆序入栈
解决了运行时堆栈的问题
在调用子过程后,紧跟一条语句让堆栈指针ESP加上一个数,该数的值即为子程序参数所占的堆栈空间
main PROC
push 6
push 5
call AddTwo
add esp,8
ret
main ENDP
能将参数从堆栈中删除
STDCALL调用规范
给RET指令添加了一个参数,使程序在返回调用过程的时候,ESP会加上这个参数
这个添加的整数和过程参数占用的堆栈空间字节数相等
AddTwo PROC
push ebp
mov ebp,esp
mov eax,[ebp+12]
add eax,[ebp+8]
pop ebp
ret 8
AddTwo ENDP
局部变量
在子过程中创建的变量
局部变量在ebp下
void Mysub()
{
int X=10;
int Y=20;
}
每个变量的存储大小都要向上取整保存为4的倍数
两个局部变量一共保留8个字节
MySub PROC
push ebp
mov ebp,esp
sub esp,8
mov DWORD PTR [ebp-4],10
mov DWORD PTR [ebp-8],20
mov esp,ebp
pop ebp
ret
MySub ENDP
从堆栈中删除局部变量,只需要执行:
mov esp,ebp
esp向上移动=内存释放
可以给局部变量的偏移量定义一个符号,在代码中使用这些符号
X_local EQU DWORD PTR [ebp-4]
Y_local EQU DWORD PTR [ebp-8]
MySub PROC
push ebp
mov ebp,esp
sub esp,8
mov X_local,10
mov Y_local ,20
mov esp,ebp
pop ebp
ret
MySub ENDP
保存和恢复寄存器
子程序在修改寄存器之前将它们的当前值保存到堆栈
通常在ebp入栈,设置ebp等于esp之后,相关寄存器入栈
栈 | 解释说明 |
传递的参数 | [EBP+8] |
返回地址 | [EBP+4](原来栈帧的EBP) |
EBP | 当前栈帧的EBP |
ECX | |
EDX | 当前ESP指向的位置 |
EBP被初始化之后,整个过程中它的值将保持不变
ECX,EDX入栈并不影响EBP按照原来的偏移量访问传递的参数
引用参数
引用参数通常是基址-偏移量寻址方式进行访问
每个引用参数都是一个指针
.data
count=100
array WORD count DUP(?)
.code
push OFFSET array
push count
call ArrayFill
ArrayFill PROC
push ebp
mov ebp,esp
数组偏移量 |
数组长度 |
返回地址 |
EBP |
下面我们来进入与Pwn的正文
StackOverflow
StackOverflow是一种常见的Pwn的手段
同时与有与之同名的网站StackOverflow是全球最大的编程问答社区
C语言函数调用栈
复习和补充一下函数调用栈的内容
-
函数调用栈是值程序运行时内存一段连续的区域
-
用来保存函数运行时的状态信息
-
称之为“栈”是因为发生函数调用的时候,调用函数的状态被保存在栈内
-
在函数调用结束之后,栈顶的函数状态被弹出,栈顶恢复到调用函数的状态
-
函数调用栈在内存中从高地址向低地址扩展
栈帧结构
previous stack frame pointer |
---|
arguments |
return address |
stack frame pointer |
callee saved registers |
local variable |
arguments会倒序压入栈
StackOverflow原理
我们要控制程序执行流
就要控制EIP,RIP这种PC寄存器
只要EIP寄存器能写入我们想要的值,整个程序执行流就会被我们劫持
能传给EIP值的位置只有return address这里
缓存区溢出
本质是向定长的缓存区中写入了超长的数据,造成超出的数据覆写了合法内存区域
栈溢出
-
最常见,漏洞比例最高,危害最大的二进制漏洞
-
在CTF PWN中往往是漏洞利用的基础
堆溢出
-
现实中的漏洞占比不高
-
堆管理器复杂,利用花样繁多
-
CTF PWN中的常见题型
BSS溢出
-
现实中与CTF比赛中占比都不高
-
攻击效果依赖于BSS上存放了何种控制数据
什么样的代码会发生缓存区溢出
#include<stdio.h>
int main(){
char str[8];
read(0,str,24);
return 0;
}
上述代码企图在8个元素的数组中输入24个数据
编译执行结果如上图,我们在运行的时候输入了超过8个字符
在一些Linux中这种缓存区溢出会导致程序崩溃
如果要是输入一些精心构造的数据,就能实现劫持
我们输入的数据位置和存储原函数EBP的位置是相邻的
我们可以利用StackOverflow修改存储原函数的EBP的值
这样函数返回的时候,原函数的EBP被我们修改了,我们就修改了PC,劫持了这个程序执行流程
PWN必备的工具
-
IDA pro
-
pwntools
-
pwndbg
-
checksec
-
ROPgadget
-
one_gadget
未完待续