*CTF 2021 Reverse*5
Contents
Translation
This post is also available in Simplified Chinese.
The endless exams finally ended, and after half a year I returned to competing with my AAA teammates—and to staying up all night. We solved five of the six reverse-engineering challenges. The remaining one, RL_Env, appeared to involve machine learning; I did not even understand what it was asking us to do.
stream
This was a Rust challenge, fortunately with symbols. After opening it in IDA, the first argument to std::rt::lang_start_internal identifies the Rust main function.
| |
After the file is read, the main encryption occurs in this loop. A quick Rust test showed that rand_chacha::guts::{init_chacha, refill_wide} are not public, so the compiler must have inlined them. The guts.rs source shows that refill_wide takes the round count as its second argument; the ten-round variant corresponds to ChaCha20Rng. IDA debugging shows that generated bytes fill the buffer at v22, while *(_BYTE *)(v3 + v6) ^= LOBYTE(v22[0]); XORs only the first generated byte into the plaintext. The inlined init_chacha comes from ChaCha20Rng::from_seed, using the 32 bytes beginning at v9 as its key. *((_BYTE *)&v13 + (v4 & 0x1F)) = *(_BYTE *)(v3 + v6); inserts one plaintext byte into the key on each iteration, then XORs the resulting PRNG byte with that same byte. Each round therefore has only 256 possibilities and can be brute-forced. Plaintext bytes are selected in the order 4, 11, 18, and so on. Initially the key is all zeroes; insert and brute-force the first byte, then continue successively. Restricting candidates to ASCII resolves later ambiguities.
Since I did not know Rust and could not find a suitable equivalent implementation, I wrapped the random-number generator in Rust and wrote the main solver in Python.
| |
| |
flag: *ctf{EbXZCOD56vEHNSofFvRHG7XtgFJXcUXUGnaaaaaa}
wherekey
I installed a libc 2.31 signature and changed the opaque jnz in main to jmp, allowing IDA to disassemble it. The program even calls listen and send to transmit data to itself, purely to increase the reversing workload.
| |
The OR operations above are the expanded FD_SET(fd, set) macro, and the AND operations below are FD_ISSET; both accompany select. Data received from the TTY is passed to sub_402072(), revealing five packets of five bytes each. sub_40223C() handles socket packets and forwards them to sub_4022DE(__int64 a1). Each five-byte group is multiplied by bytes extracted from 'flag{are_you_sure_friend}', forming five linear systems whose coefficient matrix is the constant string. After removing the opaque branches, the comparison target is unk_4C5150. I reused an old Gaussian-elimination implementation to invert the matrix and multiplied it by each of the five vectors to recover the plaintext.
| |
flag: *CTF{Ha23_f0n_9nd_G0od-1uck-OH}
ChineseGame
This binary is also statically linked, so I applied libc and libstdc++ signatures. It maintains a ten-element linked list whose nodes store numbers; only whether each number exceeds 100 matters. Writing values above 100 as + and those below as -, the initial state is +-++++++++ and the target is all -. The input is a binary string selecting between two operations, reminiscent of the Chinese Rings puzzle. Each operation takes a position from the table at dword_5D5140. If all following nodes have state +---…—or no following node exists—the selected position is set to + or -; otherwise nothing happens. A teammate noticed that the positions in dword_5D5140 already describe the correct solution sequence, so we only needed to determine whether each operation flips + to - or vice versa.
| |
flag: *CTF{4ncient_G4me_Fr0m_4ncient_Ch1na!}
Favourite Architecure flag0
My teammates had already reversed part of this challenge before I joined. The flag is 0x59 bytes long. Its first 0x29 bytes use ChaCha20, recognizable from the key-expansion constant 'expand 32-byte k'; other D. J. Bernstein designs such as Salsa20, Ed25519, and Ed448 likewise contain distinctive strings. Extracting the fixed key and ciphertext is enough to decrypt this part. The remaining 0x30 bytes use TEA modified to 16 rounds. Wikipedia provides the following reference implementation:
| |
In decrypt, sum=0xC6EF3720 is the accumulated value of adding delta during every encryption round. With only 16 rounds, change it to sum=(0x9e3779b9*16)&0xffffffff=0xe3779b90 and decrypt normally.
flag: flag{have_you_tried_ghidra9.2_decompiler_if_you_have_hexriscv_plz_share_it_with_me_thx:P}
1rep
This program was produced by compiling Perl with perlcc. I compiled a small Perl script myself and found that all such programs call the following functions from main in order:
| |
My compiled sample showed that v20 is a PerlInterpreter*. Unlike Cython, perlcc does not translate the script into equivalent C; it prepares variables and bytecode, then hands everything to the Perl VM represented by this object. My first idea was to use a debugger’s appcall mechanism to extract bytecode from the VM. libperl exposes perl_dump_all(), so I broke at perl_run in gdb, changed RIP to perl_dump_all, and continued. It dumped a huge optree that was still unreadable. I then discovered Perl’s B::Deparse module and, following a Stack Overflow answer, used Perl_eval_pv to inject and execute code inside the PerlInterpreter.
| |
That injection decompiled only the small initial check. After removing its wrapper, the flag contains 16 bytes validated by kwvIJu. I injected additional B::Deparse functionality to decompile specific functions:
| |
For unknown reasons, kwvIJu itself would not decompile. Other functions revealed the pattern: each checks the flag’s first hexadecimal character, switches among 16 branches, and calls another function to process the remainder. Near .rodata:00000000032E6CAF, IDA showed the string Correct. Disassembling the preceding afRNDz revealed that it prints Correct, so the task became finding which switch function calls it. The preceding xmecUK calls afRNDz; following callers upward recovered the final 14 flag bytes. I could not find the function calling the beginning of this chain, so I brute-forced the first two bytes and obtained the flag.
| |
flag: *ctf{7a4bb0982b39baa2}