Fortress — Reverse Engineering Writeup
Category: Reverse Engineering / Windows PE
Binary: fortr3ss.exe (PE32+ x86-64 console, GCC 15-win32 C++, not stripped)
Flag: Africc{R3v3rs3_Eng1n33r1ng_1s_Fun}
1. Recon
$ file fortr3ss.exe
PE32+ executable (console) x86-64, for MS Windowsstrings reveals the layout right away:
Error
Africc{
Correct!
Wrong!
FLAGP4
FLAGP3r0
FLAGP2
FLAGP1rabin2 -i/r2 symbols give us the roadmap:
sym.main @ 0x1400bfe40
sym.init_apis__ @ 0x1400015d0
sym.is_bot__ @ 0x140001bd0
sym.check_part @ 0x140001cd0 (check_part(unsigned char const*, unsigned long long,
unsigned char, std::string const&))So main reads a line, splits it, and validates up to four FLAGP* sections through
check_part. init_apis__ resolves a few API pointers dynamically and is_bot__
decides which key set is used.
2. main() flow
main (disassembled with r2 -c 'aaa; s sym.main; pdf'):
init_apis()— if it fails → printError, exit.- Call
IsDebuggerPresent()(stored in0x1400f1040) — if non-zero →Error. is_bot()— returns 1 for non-interactive/automated environments (see below).- Print a prompt (an obfuscated string),
getlinethe answer into astd::string. - Validate in order:
- length must be
> 6, - first 7 chars must equal
Africc{(memcmp(s1, "Africc{", 7)), - remaining content is split on
_(0x5f) into 4 parts viasubstr+find, - each part goes to
check_part(const, len, key, part).
- length must be
andthe four return values; all non-zero →Correct!, elseWrong!.
The four check_part calls in the non-bot path:
| Call | rcx (const data) | rdx (len) | r8 (key) | arg4 (user part) |
|---|---|---|---|---|
| 1 | 0x1400c608e |
8 | 'A' |
part1 |
| 2 | 0x1400c6076 |
12 | 'B' |
part2 |
| 3 | 0x1400c605e |
3 | 'C' |
part3 |
| 4 | 0x1400c604e |
4 | 'D' |
part4 |
3. check_part() — single-byte XOR
bool check_part(unsigned char const* in, unsigned long long len,
unsigned char key, std::string const& ref)
{
std::string out; // reserved, built char by char
for (unsigned long long i = 0; i < len && in[i] != class="hljs-string">'\0'; i++)
out += (char)(in[i] ^ key); // XOR each byte with the key
return out == ref; // compare with the user's part
}So each expected part is simply:
part_n = const_bytes_n XOR key_n4. is_bot() — anti-automation key switch
bool is_bot() {
if (!_isatty(_fileno(stdin))) return true; // piped input
if (!_isatty(_fileno(stdout))) return true;
if (!getenv(class="hljs-string">"TERM") || !getenv(class="hljs-string">"USER")) return true;
if (!strcmp(getenv(class="hljs-string">"USER"), class="hljs-string">"root")) return true;
if (getenv(class="hljs-string">"PYTHONPATH") || getenv(class="hljs-string">"VIRTUAL_ENV") || getenv(class="hljs-string">"CI")) return true;
... // GetConsoleMode-based heuristic
return !ok;
}If is_bot() returns 1, main switches the keys to Q/R/S/T (0x51/52/53/54)
instead of A/B/C/D. That is an anti-scripting trap: run it with piped stdin
(echo flag | ./fortr3ss.exe) and the expected parts change to garbage. A real
interactive terminal keeps keys A/B/C/D.
5. Extracting the constants
Dump .rdata around the FLAGP* markers (r2 -c 'px 96 @ 0x1400c6040'):
0x1400c6040 5772 6f6e 6721 0000 464c 4147 5034 0231 Wrong!..FLAGP4.1
0x1400c6050 2a00 0000 0000 0000 464c 4147 5033 7230 *.......FLAGP3r0
0x1400c6070 464c 4147 5032 072c 2573 2c71 7130 732c FLAGP2.,%s,qq0s,
0x1400c6080 2500 0000 0000 0000 464c 4147 5031 1372 %.......FLAGP1.r
0x1400c6090 3772 3332 7200 0000 7r32r...The constant for each part sits right after its FLAGP* marker:
| Part | Constant bytes (hex) | Key | Decoded |
|---|---|---|---|
P1 @ 0x1400c608e |
13 72 37 72 33 32 72 00 |
A |
R3v3rs3 |
P2 @ 0x1400c6076 |
07 2c 25 73 2c 71 71 30 73 2c 25 00 |
B |
Eng1n33r1ng |
P3 @ 0x1400c605e |
72 30 00 |
C |
1s |
P4 @ 0x1400c604e |
02 31 2a 00 |
D |
Fun |
Decoding script:
parts = [
(class="hljs-string">"13 72 37 72 33 32 72 00", 0x41),
(class="hljs-string">"07 2c 25 73 2c 71 71 30 73 2c 25 00", 0x42),
(class="hljs-string">"72 30 00", 0x43),
(class="hljs-string">"02 31 2a 00", 0x44),
]
for h, k in parts:
b = bytes.fromhex(h)
print(class="hljs-string">"".join(chr(x ^ k) for x in b if x)) # stops at the null terminatorR3v3rs3
Eng1n33r1ng
1s
Fun6. Flag
Joining the four parts with _ inside the Africc{...} envelope:
Africc{R3v3rs3_Eng1n33r1ng_1s_Fun}(Running it interactively under Windows prints Correct!; a naive echo flag | wine fortr3ss.exe instead hits the Error path from the IsDebuggerPresent/init
check and the Q/R/S/T key switch — both anti-automation measures.)
7. Takeaways
- Follow the markers: the
FLAGP1..FLAGP4strings conveniently label each constant block right before its encrypted bytes. - Anti-automation ≠ anti-re:
is_bot()doesn't change the math, it only swaps the key byte, so static decoding is unaffected. - Always check
IsDebuggerPresent/init-bail paths — they printErrorbefore the flag check, which can mislead you into thinking your answer is wrong.