← Back to Writeups
CUSTOMEasyReversing

patch3

XESXOR9/10/20264 min read
#reversing#custom#easy#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#windows

patch3

Platform: rev_kids20 | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: Windows | Author: XESXOR | Date: 2026-06-25 | Status: Solved 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

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 routine's final jmp to two hidden transform steps (ror 7; xor), reconstruct the full 7-step bijection, brute-force the 32-bit preimage of 0xca9d63fe to get 0x79627572 = little-endian ASCII 'REDACTED'.

patch3 — rev_kids20

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
GetStdHandle(-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
lea 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
0x40409b  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
0x404008  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
#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
import 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. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍