CFB1 — pwn.by
Platform: Windows x86-64
Difficulty: Easy
Author: pwn.by / pwned.space
Source: crackmes.one
A serial-based crackme. The program takes a username (minimum 4 characters) and a serial key, then validates the key against a value derived from the username. The goal is to understand the derivation algorithm and generate a valid key for any username.
Initial Recon
File type
file CFB1.exe
CFB1.exe: PE32+ executable (console) x86-64, for MS Windows, 6 sections
64-bit Windows PE, console application. No packer signature or anything unusual in the header.
String analysis
strings CFB1.exe | grep -iE "enter|key|granted|denied|error|username|serial"
[+] Enter Username (min 4 chars):
[-] Error: Username is too short! Must be at least 4 characters.
[+] Enter Serial Key:
[*] Verifying key...
[+] ACCESS GRANTED! Congratulations!
You have successfully solved CFB1!
[-] ACCESS DENIED! Invalid key.
Keep reversing and try again!
Key takeaways:
- Two inputs: username (≥ 4 chars) and a serial key.
- The "verifying key" message suggests no anti-debug tricks — it runs the check in one shot.
- No hardcoded serial visible. The key is computed at runtime, likely from the username.
Static Analysis with radare2
Loading and analyzing
r2 -A CFB1.exe
-A (aaa internally) runs recursive analysis: it disassembles all reachable code, identifies functions, resolves calls, and names known library stubs. This is the starting point for any static session.
Listing functions
[0x140009f8c]> afl
0x140009f8c 22 398 -> 373 entry0
0x1400074d0 79 1605 main
0x1400066e0 9 405 fcn.1400066e0
0x140001580 ... fcn.140001580
...
afl (Analyze Functions List) prints every function radare2 found, with its address, basic-block count, size, and name. entry0 is the CRT entry point; main is 1605 bytes — large enough to hold all the program logic in one function. fcn.1400066e0 is a 405-byte function called from main; it will turn out to be the serial generator.
Disassembling main
[0x140009f8c]> pdf @main
pdf (Print Disassembly of Function) renders the full function at the given address with arrows for branches and cross-references. The address main resolves to 0x1400074d0.
Tracing Control Flow in main
main is a straight-line function with a few branches. Working top-to-bottom:
1. Banner printing — several call fcn.140001220 with string literals passed in rdx. This is a C++ stream write (operator<<). Purely cosmetic.
2. Username input and trimming:
lea rdx, str.___Enter_Username__min_4_chars_:_
call fcn.140001220 ; print prompt
call fcn.140002780 ; getline into var_39h (username string)
; --- trim leading whitespace ---
0x140007650 movzx ecx, byte [rdi]
0x140007653 call fcn.1400117b0 ; isspace()
0x140007658 test eax, eax
0x14000765a je 0x140007664
0x14000765c inc rdi
0x14000765f cmp rdi, rbx
0x140007662 jne 0x140007650
; --- trim trailing whitespace ---
0x1400076f0 lea rdi, [rbx - 1]
0x1400076f4 movzx ecx, byte [rdi]
0x1400076f7 call fcn.1400117b0 ; isspace()
...
fcn.1400117b0 is isspace(). Both leading and trailing spaces are stripped from the username before the length check.
3. Length check:
0x140007756 lea rcx, [0x140040770]
0x14000775d cmp rsi, 4 ; compare trimmed length with 4
0x140007761 jae 0x140007793 ; ok if >= 4
0x140007763 lea rdx, str.____Error:_Username_is_too_short_...
; print error and exit
The trimmed length must be at least 4. If not, the program exits immediately.
4. Serial key input — identical to the username input: getline, trim leading/trailing whitespace, store in var_59h.
5. Key generation and comparison:
0x140007957 call fcn.140001220 ; print "[*] Verifying key..."
0x140007964 lea rdx, [var_39h] ; arg2 = username string
0x140007960 lea rcx, [var_19h] ; arg1 = output buffer
0x140007964 call fcn.1400066e0 ; GENERATE expected serial → var_19h
; compare lengths first
0x14000798a cmp r8, qword [var_9h] ; user-entered length vs expected length
0x14000798e jne ACCESS_DENIED
; if length is zero, skip memcmp (vacuously equal)
0x140007993 je ACCESS_GRANTED
; compare bytes
0x140007995 call fcn.1400297a0 ; memcmp(expected, user_serial, len)
0x14000799c test eax, eax
0x14000799e je ACCESS_GRANTED
jmp ACCESS_DENIED
fcn.1400297a0 is a hand-optimised memcmp (aligns to 8-byte boundaries, then compares 8 bytes at a time). Returned zero means equal. The comparison is case-sensitive, character-exact — there is no normalisation after the trim.
The critical function is fcn.1400066e0. Everything depends on what it computes.
Key Generation: fcn.1400066e0
Inspecting the function
[0x140009f8c]> pdf @fcn.1400066e0
The function signature (reconstructed):
void generate_serial(std::string *output, const std::string *username);
rcx → output buffer, rdx → username. The function initialises a std::ostringstream, iterates over every character in the username, applies a transformation to each one, and appends the result to the stream. At the end, it calls str() and copies the result into output.
The transformation loop
0x140006712 xor ebp, ebp ; rbp = loop counter i = 0
0x140006714 cmp qword [rbx + 0x10], rbp
0x140006718 jbe EXIT ; exit if username.length() == 0
LOOP:
0x140006720 cmp qword [rbx + 0x18], 0xf
0x140006725 jbe SHORT_STRING
0x140006727 mov rcx, qword [rbx] ; long string: rcx = ptr to heap buffer
0x14000672a jmp GOT_PTR
SHORT_STRING:
0x14000672c mov rcx, rbx ; short string: rcx = ptr to SSO buffer
GOT_PTR:
0x14000672f lea eax, [rbp + 0x5a] ; eax = i + 0x5A
0x140006732 xor al, byte [rcx + rbp] ; al ^= username[i]
0x140006735 add al, 0x13 ; al += 0x13
0x140006737 movzx edi, al ; edi = (uint8_t) result
; ... format edi and append to stream ...
0x1400067ab inc rbp ; i++
0x1400067ae cmp rbp, qword [rbx + 0x10]
0x1400067b2 jb LOOP ; continue while i < length
Three instructions do all the mathematical work:
| Instruction | Operation |
|---|---|
lea eax, [rbp + 0x5a] |
eax = i + 0x5A (the loop index offset by 90) |
xor al, byte [rcx + rbp] |
XOR the low byte with the character at position i |
add al, 0x13 |
add 19, wrapping in 8-bit |
The movzx edi, al zero-extends the 8-bit result into a 32-bit register for the formatter, discarding the upper bits of eax. The arithmetic is entirely modulo 256.
SSO branch
The cmp qword [rbx + 0x18], 0xf / jbe pair is the MSVC Small String Optimisation check. If the string length ≤ 15, the characters live in the object itself (rbx); otherwise they live in a heap-allocated buffer pointed to by [rbx]. The actual character data is always reached via rcx + rbp.
Detecting the output format
Before calling the stream's operator<<, the code sets three things:
; 1. Set hex flag: clear dec (0x200) and oct (0x400), set hex (0x800)
0x140006743 and dword [rsp + rcx + 0x58], 0xfffff9ff ; ~(dec|oct)
0x14000674b or dword [rsp + rcx + 0x58], 0x800 ; hex
; 2. Set uppercase flag (0x004)
0x14000675c or dword [rsp + rcx + 0x58], 4
; 3. setw(2) — call the setw manipulator with width = 2
0x140006761 mov edx, 2
0x140006766 lea rcx, [var_130h]
0x14000676e call fcn.1400087e8 ; stores width=2 in the manipulator object
; 4. Set fill character to '0' (0x30)
0x140006797 mov byte [rsp + rcx + 0x98], 0x30
The MSVC ios_base format flag layout (relevant bits):
| Flag | Value | Meaning |
|---|---|---|
uppercase |
0x0004 |
uppercase hex digits A–F |
dec |
0x0200 |
decimal base |
oct |
0x0400 |
octal base |
hex |
0x0800 |
hexadecimal base |
Clearing dec|oct and setting hex switches the stream to hexadecimal. Setting uppercase makes the digits A–F. setw(2) with fill '0' zero-pads to exactly 2 characters.
Each byte value is therefore formatted as 2-digit zero-padded uppercase hexadecimal.
fcn.1400087e8 confirms setw:
[0x140009f8c]> pdf @fcn.1400087e8
; 18-byte function
0x1400087e8 lea rax, [0x1400087fc] ; vtable pointer for setw manipulator
0x1400087ef mov qword [rcx + 8], rdx ; store width (2) at offset 8
0x1400087f3 mov qword [rcx], rax ; store vtable
0x1400087f6 mov rax, rcx
0x1400087f9 ret
It simply constructs a setw object with width = 2.
The Algorithm
Putting it together:
for i = 0 to len(username) - 1:
value = ( (i + 0x5A) XOR ord(username[i]) ) + 0x13 (mod 256)
serial += format(value, "02X")
In pseudocode with Python types:
serial = ""
for i, c in enumerate(username):
val = ((i + 0x5A) ^ ord(c)) + 0x13
serial += format(val & 0xFF, '02X')
The key is always exactly 2 * len(username) characters long and consists only of hex digits 0–9, A–F.
Keygen
#!/usr/bin/env python3
"""
CFB1.exe keygen — pwn.by / crackmes.one
Algorithm (reversed from fcn.1400066e0):
serial = ""
for i, c in enumerate(username):
val = ((i + 0x5A) ^ ord(c)) + 0x13
serial += format(val & 0xFF, '02X')
"""
import sys
def generate_serial(username: str) -> str:
serial = ""
for i, c in enumerate(username):
val = ((i + 0x5A) ^ ord(c)) + 0x13
serial += format(val & 0xFF, '02X')
return serial
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: keygen.py <username>")
sys.exit(1)
name = sys.argv[1]
if len(name) < 4:
print("Error: username must be at least 4 characters")
sys.exit(1)
print(f"Username : {name}")
print(f"Serial : {generate_serial(name)}")
Example outputs:
| Username | Serial |
|---|---|
test |
4151423C |
admin |
4E52444743 |
angmar |
4E5244474552 |
Verification
Run the keygen, then pass the result to the crackme:
python3 keygen.py test
Username : test
Serial : 4151423C
wine CFB1.exe
===================================================
Crackme #1
[+] by pwn.by [+]
--> pwned.space <--
===================================================
[+] Enter Username (min 4 chars): test
[+] Enter Serial Key: 4151423C
[*] Verifying key...
===================================================
[+] ACCESS GRANTED! Congratulations!
You have successfully solved CFB1!
===================================================
Summary
| What | Detail |
|---|---|
| Binary | PE32+ x86-64 console, no packer |
| Input | Username (≥ 4 chars) + serial key |
| Keygen function | fcn.1400066e0 at 0x1400066e0 |
| Algorithm | ((i + 0x5A) ^ char) + 0x13 per character |
| Output format | 2-digit uppercase hex per byte, concatenated |
| Key length | 2 × len(username) characters |
| Tools used | radare2 (r2 -A, afl, pdf) |