January 31 2025
Reverse Reverse Reverse
Stage 3 Area 2
This challenge contains three reverse-engineering steps: a Java file, an obfuscated Python check, and a compiled ELF executable, each one unlocking the next.
Walkthrough
00:19 - Unpacking the Java archive
02:56 - Deobfuscating the Python stage
07:21 - Patching the ELF executable
Patching next.o
The video walkthrough covers all three steps; this section focuses on patching the final ELF executable.
Conditions
cpu_func- XORs every 4 bytes of the vendor string.
(a ^ b) ^ c. - The CPU vendor must be
NereusTechPC, since the result is later used when generating the flag.
- XORs every 4 bytes of the vendor string.
time_func- Uses
argv[2]to undo an XOR-obfuscated string, yieldingsystem_ok_f. - Calls
__xstatonsystem_ok_fto verify it exists and to retrieve file metadata. - Converts the file timestamp to UTC using
gmtime_r, then readstm_year(years since 1900). - Checks
(tm_year + 1900)equalsargv[1]. - Adds
tm_yearto the first 4 bytes ofsystem_ok_f, storing the result for use byseed_func.
- Uses
In order to successfully patch the binary, we need to do two things: bypass the comparison checks so execution continues, and ensure cpu_func/time_func still produce the expected values so the final flag is generated correctly.
def u32_le(b: bytes) -> int:
return struct.unpack("<I", b)[0]
# cpu_func expected XOR
vendor = b"NereusTechPC"
w0, w1, w2 = struct.unpack("<III", vendor) # 3x uint32 (little-endian)
cpu_expected = (w0 ^ w2) ^ w1
print(hex(cpu_expected)) # 0x43767e58
# time_func expected value
name = b"system_ok_f"
year_offset = 0x102 # tm_year (2158 - 1900)
expected_value = (u32_le(name[:4]) + year_offset) & 0xFFFFFFFF
print(hex(expected_value)) # 0x74737a75Finding the dynamic function offsets
Since the binary uses an indirect call (call rdx) for both functions, radare2 can't resolve the address in static analysis. To patch it, we need the function's offset/address. During debugging, once you have analysed and named the functions, find the offset using one of:
dmj | jq
dmi @ func_name
iPh | grep baddr
# OFFSET = FUNC_ADDR - BASE_ADDR
?v FUNC_ADDR - BASE_ADDRcpu_func
The CPU check compares the cpuid vendor string against the hardcoded value NereusTechPC via memcmp. To bypass this, I patched the stored vendor string to my machine’s vendor (GenuineIntel).
However, this function also XORs the vendor bytes and stores the result for later use (the value written at mov dword [0x00202074], eax). So I also patched the code just before that store to load the expected XOR value directly into eax, padding with nops to keep the instruction count the same.
r2 -w ./next_patched
af cpu_func 0x9B5
# Patch expected vendor string so memcmp passes
s 0xe93
wz GenuineIntel
# Patch the computed XOR result (used later for flag generation)
s 0x00000a5b
pa mov eax, 0x43767e58 # b8587e7643
wx b8587e7643909090time_func
time_func calls gmtime_r and then performs a year check. The value in var_cch is tm_year (years since 1900).
To bypass this check, I patched the two mov eax, dword [var_cch] loads to instead load the expected value into eax. This forces the subsequent comparison/path to behave as intended.
r2 -w ./next_patched
af time_func 0xA89
# Replace: mov eax, dword [var_cch]
# With: mov eax, 0x102 ; nop
# (pad with NOP to match original instruction length)
s 0x00000b49
wx b80201000090
s 0x00000b73
wx b80201000090main
For convenience, I hardcoded the two required CLI arguments in main by overwriting what value is stored.
# Always assume "argc > 2"
s 0x000008fd # jg 0x915
pa jmp 0x915 # eb16
wx eb16
# Hardcode argv[1] -> 2158
s 0x00000934 # call sym.imp.atoi
pa mov eax, 0x86e
wx b86e080000
# Hardcode argv[2] -> 50661104
s 0x00000946 # call sym.imp.atoi
pa mov eax, 0x30506f0
wx b8f0060503Result
With both checks patched (and the expected values preserved for seed_func), the binary prints the flag!
./next_patched 2158 50661104
Executing subsystem 3
Passed: Central processing unit check
Passed: System file
Printing diagnostics:
Subsystem 1......... Active
Subsystem 2......... Active
Subsystem 3......... Active
Environment checks.. Passed
Flag................ <redacted>
Fuel pumps.......... Frozen (ERROR)
Incoming transmission.....
<<
I'm not going to congratulate you Pithy human.
You'll thaw your fuel pumps by a reboot of your
SuperSpace 3.0 and ASRM. You'll find what's
seemingly unrelated is almost always connected.
>>Video walkthrough: Two quick clarifications -
var_cch (rbp-0xcc)is writable during debugging (stack local variable), but not something you can "set" via static patching; and thetm_yearaddition interprets the first 4 bytes of"system_ok_f"as little-endianuint32.