基本栈溢出
1. 缓冲区溢出概念以及种类
本质是向定长的缓冲区写入了超长的数据,造成超出的数据覆写了合法内存区域。
缓冲区溢出的种类:
1. 栈溢出
最常见,漏洞比例最高,危害最大的二进制漏洞
在CTF PWN 中往往是漏洞利用的基础。
2. 堆溢出
现实中的漏洞占比不高
堆管理器复杂,利用方式很多
CTF PWN 中的常见题型
3. BSS溢出
现实中与CTF比赛中占比均不高
攻击效果依赖于BSS上存放了何种控制数据
栈溢出的中心思想就是要将栈帧的返回地址篡改为自身的攻击代码的首地址,来进一步实现攻击。
2. PWN工具介绍
2.1 IDA Pro
如今最强大的反编译和反汇编工具,常用于对静态的二进制文件进行分析。
注意:
如果分析的是32位程序,那么ida也需要32位
如果分析的是64位程序,那么ida也需要64位
f5用于开启ida的反编译功能
2.2 pwntools
python的用于进行pwn的包,里面封装了很多用于pwn的对象和方法。
2.3 gdb
用于动态调试c文件的工具。(需要有c源代码)
所谓动态调试:实际上就是对正在运行的c文件进行分析。
2.4 pwndbg
它是对gdb的封装,在没有c源代码的情况下,用于对正在运行的c文件进行分析。(专用于pwn)
我们可以在动态调试时,使用vmmap 查看虚拟内存的具体情况(空间分布)。
2.5 checksec
用于获取某个二进制文件的保护机制的工具。通过查看该文件的保护机制,我们就可以针对该保护机制来实现进一步的攻击。(checksec是pwn的第一步)
接下来介绍一些linux下的保护机制:
The NX bits(the No-eXecute bits)
程序与操作系统的防护措施,编译时决定是否生效,由操作系统实现。
通过在内存页的标识中增加执行位,可以表示该内存页是否可以执行,若程序代码的EIP执行至不可运行的内存页,则CPU将直接拒绝执行"指令"造成程序崩溃。
ASLR(Address Space Layout Randomization)
系统的防护措施,程序装载时生效.ASLR使得程序每次运行时,某些缓冲区的起始地址均不相同。
/proc/sys/kernel/randomize_va_space = 0:没有随机化。即关闭 ASLR
/proc/sys/kernel/randomize_va_space = 1:保留的随机化。共享库、栈、mmap() 以及 VDSO 将被随机化
/proc/sys/kernel/randomize_va_space = 2:完全的随机化。在randomize_va_space = 1的基础上,通过 brk() 分配的内存空间也将被随机化。(堆分配的内存空间)
PIE (Position-Independent Executable)
程序的防护措施,编译时生效
随机化ELF文件映像的映射地址
开启 ASLR 之后,PIE 才会生效
ELF文件映像:指的是ELF文件没有运行时就存在的区域。(.data、.text等)(heap、stack这样的区域都是ELF文件运行时才有的,故不算做ELF文件映像)
Canary
程序的防护措施,编译时生效
在刚进入函数时,在栈上放置一个标志canary(里面是随机值),在函数返回时检测其是否被改变(因为只要进行栈溢出,一定会将Canary的值覆盖)。以达到防护栈溢出的目的。
canary长度为1字长,其位置不一定与ebp/rbp存储的位置相邻,具体得看程序的汇编操作。
2.6 ROPgadget
用于查看程序当中可用于ROP的代码片段的。
2.7 one_gadget
可以直接帮你找到获取目标机器的shell的代码片段,然后帮你把这些片段组合在一起,方便进行攻击。
3. 基本栈溢出-ret2text
3.1 目的
篡改栈帧上的返回地址为程序中已有的后门函数。
3.2 步骤
1. 首先我们使用checksec来查看该程序有哪些保护措施?
发现没有开启canary保护机制,没有开启地址随机化。但是开启了栈不可执行。
2. 使用ida来进行反编译,查看源代码。发现有一个函数vulnerable()
3. 查看该函数内容,发现gets()函数,漏洞在于此。(可以输入大于8个字节的数据来达到缓冲区溢出)
4. 再次查看该ELF文件,发现有一个函数get_shell(),该函数可以获得本机的shell。如果这个文件部署在远端,那么利用好这个漏洞,就可以获得远端的shell,达到黑入远端服务器的效果。
5. 入侵的思路就是:通过gets函数进行溢出,当vulnerable()函数返回时,将return address修改为get_shell()函数的首地址,进而可以利用该漏洞进行攻击。
通过ida来计算地址,进行溢出。
经过分析,可以发现:get_shell()的首地址为:0x08048522
通过gdb进行动态调试,进入到vulnerable()函数,计算地址。
当我们输入AAAAAAAA后,我们发现数组buffer的首地址为:0xffffd188
ebp的地址为0xffffd198
这两个地址相差的字节数:0xffffd198 - 0xffffd188 = 16(十进制)
那么,根据函数调用栈的流程,ebp指向上一个函数的栈底地址,ebp在往上就是该函数的返回地址return address。
因此,payload为:我们需要填充16 + 4(覆盖上一个函数的栈底地址,四字节)个垃圾数据,之后将0x08048522填入到return address即可。
这样的话,就可以得到shell了。
我们根据如上内容,书写python脚本,进行攻击。
需要注意的是:如果我们想要攻击远端服务器,那么需要把process()函数改为remote()函数。
代码如下:
执行该脚本,我们发现已经成功获得shell,本题到此结束。
4. 基本栈溢出-ret2shellcode
4.1 目的
在现实情况下,目标程序中根本不可能专门写一个可以获得shell的后门函数(get_shell())。因此,shellcode需要我们自己书写。
篡改栈帧上的返回地址为攻击者手动传入的shellcode所在的缓冲区地址
初期往往将shellcode直接写入栈缓冲区
目前由于 the NX bits 保护措施的开启,栈缓冲区不可执行,故当下的常用手段变为向bss缓冲区写入shellcode或向堆缓冲区写入shellcode并使用mprotect赋予其可执行权限。
bss缓冲区默认有可执行权限。
我们可以声明一个全局变量,但是不给他赋值。那么该全局变量运行时就会放到bss缓冲区,只要我们对该全局变量写入shellcode,再通过栈溢出将返回地址给到该全局变量的起始地址,这样就可以实现攻击了。
怎么将shellcode写入到全局变量?shellcode一般是机器码,我们不可能直接写机器码。因此,我们通常借助pwntools中的shellcraft来实现。
我们使用shellcraft.sh()函数来获得:获得对方shell的汇编代码。
asm()函数代表将汇编代码转换成机器码。
但是需要注意:在执行64位程序攻击时,需要提前指定架构
context.arch = "amd64"
32位架构:
asm(shellcraft.sh())
64位架构:
asm(shellcraft.amd64.sh())
4.2 步骤
1. 使用checksec来查看该程序的安全措施。
查看上述内容发现:canary机制没有打开,随机地址没有打开,栈不可执行没有打开且栈段可读可写可执行。这是非常危险的(表明可以将shellcode写入到栈)。
2. 使用ida来查看该程序源代码
通过查看源代码我们发现:
1. 仍是有gets函数,那么我们可以推断此处有漏洞。
2. 用gets为s赋值之后,将其写入到了buf2。但是buf2并不是局部变量,因此该变量应为全局变量。未初始化的全局变量应该存放到bss段中。
3. bss段的权限为可读可写可执行,因此我们可以将shellcode写入到这里。
4. buf2的首地址为0x0804A080,作为篡改之后的返回地址。
那么,根据上述内容,我们总结一下攻击的思路:
1. 通过gets函数将shellcode(需要自己生成)和垃圾数据填充到s。
2. 通过strcpy将shellcode放到buf2当中。
3. 在填充的过程中,将return address修改为buf2的起始地址即可。这样的话就可以获得到对方机器的shell。
3. 通过gdb对该程序进行动态调试(b main 代表给main函数打断点,run 代表运行该程序),计算地址。
当我们输入了AAAAAAAA之后,数据(数组s)的首地址为:0xffffd13c
ebp的地址为:0xffffd1a8
那么这两个地址的差值为:
0xffffd1a8 - 0xffffd13c = 108(十进制)
又因为:ebp存放的是上一个函数的栈底地址,因此我们在构造payload的时候,还需要加上四个字节。
覆盖完上一个函数的栈底地址之后,我们要用buf2的起始地址来覆盖return address。这样的话,当main函数返回的时候,就直接执行buf2当中的内容了(eip当中的地址改为了buf2的首地址)。
payload = 108 + 4 + buf2的首地址。
4. 书写攻击脚本,实现漏洞利用。
注意:
1. 生成完shellcode之后,shellcode是汇编语言,需要asm转换成机器指令。
2. ljust函数代表将该对象(shellcode)填充到112字节,不够的话用'A'填充。
3. 如果需要攻击远程服务器,将process改为remote即可。
5. 运行该攻击脚本,发现已经获得了shell,本题结束。
5. 基本栈溢出-ret2syscall
5.1 目的
篡改栈帧上自返回地址开始的一段区域为一系列的gadget的地址,最终调用目标系统调用,实现攻击。
这道题的主要目的是要执行execve函数,调用shell。
execve的汇编代码:
mov eax,0xb
mov ebx, ["/bin/sh"]
mov ecx, 0
mov edx, 0
int 0x80
我们需要一系列的gadget,来离散的执行以上代码,结合起来就等价于执行了execve函数。
5.2 步骤
1. 首先查看ret2syscall的安全机制。
发现:并没有打开canary,没有地址随机,但是开启了栈不可执行。这就意味着我们无法将shellcode写入到栈内。
2. 查看该文件的类型,发现该文件是静态链接的文件。这就意味着该文件需要的所有库函数的具体实现均写入了这个文件当中(意味着有很多可以利用的gadget)。
3. 使用ida,反编译出该文件的源代码。
我们又发现了gets函数,这就证明我们可以依据这一点来实现溢出。但是,这道题我们无法手写shellcode,也没有后门函数,那么我们应该如何去做?这就需要用到ROP了。
4. 我们的终极目的是为了获得对方机器的shell,那么获得shell的系统调用就是execve()函数。只要我们能从这个文件里面找到execve()函数所需要的gadget,那么就可以实现攻击。
接下来我们依照:
execve的汇编代码:
mov eax,0xb
mov ebx, ["/bin/sh"]
mov ecx, 0
mov edx, 0
int 0x80
来寻找gadget。
5. 寻找给eax赋值且可以ret的gadget(gadget需要满足:pop和ret)(pop通过弹栈赋值,ret可以进行跳转,便于到下一个gadget)
ROPgadget --binary ret2syscall --only "pop|ret" | grep eax
我们选择0x080bb196开始的gadget。
6. 寻找给ebx,ecx,edx赋值且可以ret的gadget
ROPgadget --binary ret2syscall --only "pop|ret" | grep ebx
我们选择0x0806eb90这个gadget。
7. 接下来寻找进行系统调用的int 0x80指令的位置(0x08049421)。
ROPgadget --binary ret2syscall --only "int"
8. 找到了指令之后,我们接下来需要寻找这些指令的参数。(/bin/sh)
我们在ida中,使用shift+f12,打开字符串窗口,再通过摁下ctrl+f,来检索这个文件中所有出现过的字符串。
对于其余参数,直接手写即可。
我们发现地址为:0x080be408
10. 接下来,我们通过动态调试来计算地址。(需要填充多少垃圾数据,之后构造payload)
当我们输入AAAAAAAA之后,从图中可以看出ebp和AAAAAAAA的起始地址之间的字节为:
0xffffd1b8 - 0xffffd14c = 108(十进制)
ebp举例return address还有四个字节,具体原因这里就不赘述了。
因此,我们需要填充108 + 4 = 112个垃圾数据。
从返回地址开始,我们需要依次填充(具体流程请参考pwn的第二部分的笔记,这里就不赘述了):
1. 0x080bb196(eax gadget的起始地址)
2. 0xb (需要赋值eax的参数)
3. 0x0806eb90 (ebx,ecx,edx的起始地址)
4. 0 (edx的参数)
5. 0 (ecx的参数)
6. 0x080be408 (ebx的参数,也是"/bin/sh"的首地址)
7. 0x08049421 (执行 int 0x80指令,开始执行系统调用,获得shell)
11. 编写脚本,实现攻击。
12. 执行该脚本,获得了shell,本题到此结束。(如果攻击远端服务器,直接将process改为remote即可)
6. 基本栈溢出-ret2libc1
6.1 目的
篡改栈帧上自返回地址开始的一段区域为一系列gadget的地址,最终调用libc中的函数获取shell。
\x00代表字符串末尾结束符。
6.2 步骤
1. 首先使用checksec来查看该程序的保护机制。
我们可以发现:没有打开canary, 没有打开地址随机化,但是打开了栈不可执行。
2. 之后使用ida来反编译程序的源代码
查看源代码发现:gets()函数,老朋友了。我们可以在这里使用栈溢出。
虽然可以使用栈溢出,但是我们如何获得shell?
我们首先可以查看一下这道题有没有后门函数?
经过查看发现,有一个后门函数secure,但是查看源代码发现:这个后门函数虽然调用了system()尝试获取shell,但是参数不正确。并且根据这个函数的逻辑,我们也无法正常的执行到这个system()函数。
我们还有没有别的方法?
3. 我们来查看一下该程序是静态链接的程序还是动态链接的程序。
我们发现这个是动态链接的程序,这就意味着我们可利用的gadget很少。
但是,由于这个程序里面存在后门函数secure(),而这个函数又调用了system()。那么,根据动态链接的过程可知,system()函数存放在.plt节中。
因此,我们可以想办法让程序的执行流执行到system()函数,再为该函数传参即可。
4. 根据上述过程,我们首先查看一下这个程序里面有没有/bin/sh这个字符串(参数)?
经过查看发现,程序当中存在这个参数。那么,我们在调用system()时,直接指定这个字符串的地址即可。
5. 接下来就是最后的问题:我们如何构造payload来实现劫持程序控制流,执行system()函数并传参,最终实现攻击?
首先,在执行这个函数时,我们通过gets()函数填充垃圾数据,在返回地址处,我们填入system@plt表项内的首地址(之后根据动态链接的过程,他会解析出system函数的真实地址,并调用)。接下来就是要传参,函数的参数存放在父函数的栈帧中。具体的位置是在:子函数栈帧的local variable区域上方两个字节的位置(调用规定)。
最终,payload就是:垃圾数据 + system@plt + 四个字节的垃圾数据 + /bin/sh这个字符串的首地址(用于传参)。
为什么这么构造?
假设,我们已经完成了溢出,程序执行到了return address处,并解析到了system的真实地址。那么,程序就会开始执行system函数,该函数的汇编代码首先就会向栈中压入ebp,然后压入system函数所需的局部变量(就如下图所示)
那么这样的话,从local var开始上方两个字节处,就是system函数的参数。当执行system函数时,就会获取此参数,执行。
system函数的汇编代码如下:
system:
push ebp
mov ebp, esp
...(execute command)
ret
6. 有了上述的分析之后,我们来通过动态调试进行地址的计算。
输入AAAAAAAA后,我们发现local var 距离ebp的距离:
0xffffd1b8 - 0xffffd14c = 108
108 + 4 = 112
故我们需要填充112个字节的垃圾数据。
上方展示了system@plt的地址
7. 我们来实现攻击脚本的编写,实现攻击。
8. 执行完攻击脚本后,我们发现已成功实现攻击,拿到了shell,本题结束。
7. 基本栈溢出-ret2libc2
7.1 目的
这道题跟ret2libc1非常相似,这里不再赘述。
7.2 步骤
这道题比ret2libc1难一点的地方在于:没有了/bin/sh这个字符串的首地址。
咋办?
1. 查看一下程序的保护措施
2. 查看程序的源代码
3. 直接说结论:由于程序中调用了gets函数,并且程序当中还有一个未初始化的全局变量buf2。因此,我们可以劫持控制流先到gets函数,输入字符串到buf2。再劫持控制流到system函数,将参数通过buf2给到system函数最终实现攻击。
4. 书写攻击脚本,具体逻辑均在脚本上,这里不再赘述。
5. 执行攻击脚本,获得shell,本题结束。
注意:这里有一种通用性更强的payload的构造方式,遵循用完即丢的原则。可以根据函数调用的逻辑来手动模拟一遍。
8. 基本栈溢出-ret2libc3
8.1 目的
目的跟前两题是一样的,只不过这次更难了。
在程序中没有/bin/sh,没有system函数的plt(那就意味着没有system函数的got)。
那么我们怎么办?
8.2 步骤
1. 使用checksec来查看程序的安全措施
2. 使用ida来反编译程序的源代码。
通过查看程序的逻辑,我们可以发现如下内容:
1. 我们可以通过第一个read函数来给出地址(str类型),该程序会通过See_something函数来给出该地址对应的内容(以地址形式打出%p)
这就意味着我们可以将某一个函数的got表项地址给入程序,该程序就会将该函数的真实地址(got表项的内容)给你。(前提:要保证程序执行到该函数后)
2. 第二个read函数代表你要给程序输入一些内容,该程序接收内容后,返回给你。这里存在漏洞。因为:我们给buf最多可以写入0x100u个字节,但是dest数组最多只可以存入56字节。这样的话,当我给buf数组输入了超过56个字节数据的话,就会发生溢出(在Print_message函数内发生溢出)。
下图表示了执行该程序时调用print_message时,栈的分布情况。
从图中清晰的看出:如果将src的内容复制到dest,将会产生溢出。
3. 那么,我们的目的就是要通过栈溢出将print_message的返回地址(上图的return address)进行覆盖。
覆盖什么?由于程序中没有system@plt,同理肯定没有system@got。但是,该程序在动态链接时,使用了一个libc-2.31.so。我们可不可以从这个文件中进行分析?
4. 我们可以使用ida反编译libc-2.31.so(这里使用pwntools查看),发现该动态链接库内部存在着system函数,也存在着/bin/sh字符串。这就说明,我们可以从该动态链接库下手,找到system函数和/bin/sh字符串的位置(并不是真实地址,只是偏移量)。
由于程序在动态链接时,会将动态链接库原封不动的放到虚拟内存空间中。因此,当我们运行该程序时,一直可以从该程序的虚拟内存空间中找到system函数和/bin/sh的真实地址(通过偏移量进行偏移来实现)。
那么该地址如何获取?
5. 我们可以先求出puts函数在该动态链接库的位置,再分别求出system和/bin/sh距离puts函数的差值。由于程序在动态链接时,会将动态链接库原封不动的放到虚拟内存空间中。因此,这个差值不会改变。
之后,我们再将puts函数在该程序的got表项给入程序,得到puts函数在该程序的真实地址。
得到真实地址之后,我们用真实地址对上述求得的差值进行运算,就可以得到system和/bin/sh在该程序的真实地址。
得到真实地址之后,我们就可以构造payload,直接进行攻击了。(关于地址的计算,这里就不再赘述了。)
左边是动态链接库视角,右图是程序中的虚拟内存空间。
6. 所编写的攻击脚本如下:
7. 执行完攻击脚本后,得到了shell,本题结束。