patch3
patch3
Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: exhaustive_32bit_bruteforce, iat_thunk_following, little_endian_dword_to_ascii, magic_constant_compare, runtime_patch_reconstruction, self_modifying_code_analysis, static_analysis, win32_io_pattern_recognition
Summary
Walkthrough of the CUSTOM Reversing patch3. Task: tiny hand-written MASM PE32 crackme that reads stdin, transforms the first DWORD, and compares to a magic constant. Solution: spot the self-modifying patch (sub [op],0xf) that redirects the hash
Description
task3.exe — the flag is the stdin input that makes the binary print "OK".
We are given task3.exe, a 3584-byte hand-written MASM PE32 console executable
(Intel 80386, MS Windows, no symbols). It reads input from stdin, runs a
transform over the first 4 bytes (first DWORD) of the input, compares the result
to a magic constant, and prints OK on match or NO otherwise. The flag is the
exact input that triggers OK. This is the rev_kids20 "patch" family (sibling
of "Deadbeef"); the distinguishing trick here — hence patch3 — is
self-modifying code.
Analysis
Imports (IAT jump thunks @ 0x401000, image base 0x401000)
jmp [0x402008] -> GetStdHandle
jmp [0x402000] -> ReadFile
jmp [0x402004] -> WriteFile
Control flow — entry0 @ 0x404015
asm read-only • copy allowedGetStdHandle(-0xb) ; STD_OUTPUT_HANDLE -> saved @ 0x403000 sub dword [0x4040a1], 0xf ; <-- SELF-MODIFYING PATCH (the core trick) GetStdHandle(-0xa) ; STD_INPUT_HANDLE -> saved @ 0x403004 ReadFile(stdin, buf=[ebp-0x64], 0x64, &[0x403008], 0) ; read 100 bytes mov eax, [lpBuffer] ; first DWORD X of the input push eax call hashfn @ 0x40409b cmp eax, [0x404004] ; magic = 0xca9d63fe (bytes: fe 63 9d ca) jne fail WriteFile(stdout, 0x404000 "OK", 2, ...) fail: WriteFile(stdout, 0x404002 "NO", 2, ...)
Output strings live in the CSEG at 0x404000 = "OKNO" ("OK" @ 0x404000,
"NO" @ 0x404002). The compare constant dd 0xca9d63fe sits at 0x404004.
The self-modifying code trick
At startup entry0 runs:
asm read-only • copy allowedlea ebx, [0x4040a1] sub dword [ebx], 0xf
0x4040a1 is the 4-byte immediate operand of the lea edi, [0x404017]
instruction inside the hash routine. Statically the bytes read 17 40 40 00
(= 0x00404017). Subtracting 0xf rewrites them to 0x00404008. So at runtime
the hash routine loads edi = 0x404008, stores it to [0x40300c], and its final
jmp dword [0x40300c] jumps to 0x404008 — not the 0x404017 a naive static
disassembly shows. The code at 0x404008 contains two additional transform
steps that a static read of the function misses entirely.
hash routine @ 0x40409b (statically visible part)
asm read-only • copy allowed0x40409b pop edx ; saved return address 0x40409c push ebp 0x40409d push ecx 0x40409e push edi 0x40409f lea edi, [0x404017] ; patched at runtime to [0x404008] 0x4040a5 mov [0x40300c], edi 0x4040ab xor eax, 0x36478241 0x4040b0 bswap eax 0x4040b2 xor eax, 0x3a29e87f 0x4040b7 bswap eax 0x4040b9 xor eax, 0xe5e5e5e5 0x4040be jmp dword [0x40300c] ; -> 0x404008 at runtime
Continuation @ 0x404008 (overlaps entry0 bytes, revealed only after the patch)
asm read-only • copy allowed0x404008 ror eax, 7 0x40400b xor eax, 0x13373389 0x404010 pop edi 0x404011 pop ecx 0x404012 pop ebp 0x404013 push edx ; edx = saved return address 0x404014 ret
Full transform on the first DWORD X
1. X ^= 0x36478241
2. bswap
3. ^= 0x3a29e87f
4. bswap
5. ^= 0xe5e5e5e5
6. ror 7
7. ^= 0x13373389
result must equal 0xca9d63fe
The transform is a bijection (only XORs, byteswaps and a rotate), so there is
exactly one solution. Note: ignoring the self-modifying patch (steps 6–7) yields
the wrong, non-printable answer 0x66d72d60.
Solution
Brute-force the 32-bit preimage of the magic constant using the full 7-step transform.
c read-only • copy allowed#include <stdint.h> #include <stdio.h> static uint32_t bswap(uint32_t x){ return __builtin_bswap32(x); } int main(void){ uint32_t T = 0xca9d63fe; for(uint64_t i = 0; i < 0x100000000ULL; i++){ uint32_t X = (uint32_t)i; uint32_t e = X ^ 0x36478241; e = bswap(e); e ^= 0x3a29e87f; e = bswap(e); e ^= 0xe5e5e5e5; e = (e >> 7) | (e << 25); // ror 7 e ^= 0x13373389; if(e == T){ unsigned char *b = (unsigned char*)&X; printf("%08x '%c%c%c%c'\n", X, b[0], b[1], b[2], b[3]); } } return 0; }
Output:
79627572 'REDACTED'
X = 0x79627572, little-endian bytes 72 75 62 79 = ASCII REDACTED.
Python verification:
python read-only • copy allowedimport struct def bswap(x): return struct.unpack('>I', struct.pack('<I', x))[0] X = int.from_bytes(b'REDACTED', 'little') e = X ^ 0x36478241; e = bswap(e); e ^= 0x3a29e87f; e = bswap(e); e ^= 0xe5e5e5e5 e = ((e >> 7) | (e << 25)) & 0xffffffff; e ^= 0x13373389 assert (e & 0xffffffff) == 0xca9d63fe # passes
Feeding REDACTED to the binary on stdin makes it print OK.
Key Takeaways / Lessons
- Mastering exhaustive_32bit_bruteforce, iat_thunk_following, little_endian_dword_to_ascii, magic_constant_compare, runtime_patch_reconstruction, self_modifying_code_analysis, static_analysis, win32_io_pattern_recognition was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-06-25 • CUSTOM • patch3 • Difficulty: Easy