Static Software Patching
About
This project covers a static patch of SimpleCrackme.exe, a small Windows x86 crackme from crackmes.one1. The goal here was not to recover the original password but to read the control flow well enough in Cutter2 to force the binary into its success path. I kept this one focused on the binary itself and the patch instead of re-explaining the lab setup, so if you want the broader environment first, see Building a Malware Analysis Lab and Static Malware Analysis.
Introduction
The crackme used here is lightweight, but it still has enough in it to map out the main logic quickly. The first strings that stand out are SimpleCrackme by arasreal, Enter password:, [-] Wrong password!, [+] Access Granted!, and Debugger Detected!. That was enough to confirm that the binary has both a password check and a debugger check, which was enough to start following the control flow.
Initial Recon
From Cutter’s overview, the binary identifies as a PE32 executable for x86, running as a Windows CUI program. It is not stripped, it imports the expected Visual C++ runtime libraries, and it is small enough at roughly 20.5 kB that the interesting logic should be fairly easy to isolate.
MD5: 78f38da13a69b1afa845a74e352f1cd5
SHA1: 07d4d0761cedac2d298f31292e9025433063bb42
SHA256: 56065aed8e49734a6033e1b55dc11d40caaec4509a945bc2f997d26700735062

The strings view was the first place I checked after the overview. It showed the input prompt, the success and failure messages, and the debugger-related strings. From there I focused on two areas: the password validation path and the anti-debug branch.

The data section makes those same strings easy to correlate with addresses inside the binary. str.Access_Granted sits at 0x004042b8, str.Wrong_password at 0x004042d0, and that becomes useful later when tracing the conditional jump that decides between them.

Anti-Debugging Clue
Before touching the password logic, I checked the debugger-related branch. Cutter shows a short sequence that calls IsDebuggerPresent3, checks the return value with test eax, eax4, and then conditionally jumps away if no debugger is detected.
0x0040125d call dword [IsDebuggerPresent]
0x00401263 test eax, eax
0x00401265 je 0x401297
This confirms that the crackme includes an anti-debug check and is not only doing a plain string comparison. I did not need to patch this part for the walkthrough, but it helped map the program logic early.

Finding the Password Check
The actual validation routine becomes easier to follow once the Wrong password string is used as the anchor. One block first compares what looks like the input length or a derived counter against another local value, and if those do not match it goes straight to the failure path at 0x004018ec.
0x0040187d cmp esi, dword [var_34h]
0x00401880 jne 0x4018ec
0x00401882 test esi, esi
0x00401884 je 0x4018e5
cmp esi, dword [var_34h]5 tells us there is an equality gate before the final decision. jne 0x4018ec is the first direct route to the failure message, while test esi, esi handles the zero-length edge case.

A little deeper in the block, the comparison result is normalized into eax and then used for the final success or failure branch.
0x004018dc test eax, eax
0x004018de sete al
0x004018e1 test al, al
0x004018e3 je 0x4018ec
0x004018e5 mov edx, str.Access_Granted
The flags are the important part here. sete al6 writes 1 into al when the zero flag is set, meaning the earlier comparison resolved as equal. The second test al, al refreshes the flags, and je 0x4018ec7 sends execution to str.Wrong_password only when al is zero. If the comparison succeeded, execution falls through directly into mov edx, str.Access_Granted.

The larger disassembly view shows the same result more clearly. On equality the code zeroes eax with xor eax, eax; on mismatch it produces a non-zero result with sbb eax, eax followed by or eax, 1. By the time execution reaches 0x004018dc, the only thing left is whether the code jumps to 0x004018ec or falls through into the success string.

The decompiler view is messy because of compiler-generated locals and small-string handling, but the overall structure still matches the assembly. It compares lengths first, then walks the data buffers in a loop, then leaves the final branch to the smaller block above. So even without recovering the real password, the success/failure split is visible enough to patch safely.

Reading the Assembly
A few instructions were worth slowing down for because they explain almost the whole crackme by themselves.
call dword [IsDebuggerPresent] transfers control into the Windows API anti-debugging check and returns a non-zero or zero result in eax, depending on whether a debugger is attached.3
test eax, eax does not store a new value anywhere. It simply ANDs the register with itself to update the flags, which is why it shows up so often right before conditional jumps.4
cmp esi, dword [var_34h] compares two operands and sets flags without writing the subtraction result back. In this routine it acts like an early length gate before the byte-wise comparison continues.5
sete al converts the zero flag into a byte value. In this case it turns a successful comparison into al = 1, which is then checked again by test al, al.6
je 0x4018ec is the real decision point. As long as that jump exists, the crackme can still take the failure path. Once that jump is gone, the block only has one direction left to go.7
Patching
At this point the easiest patch was not to reverse the condition but to remove it completely. Cutter already exposed the exact jump we care about at 0x004018e3, and the context menu shows both Edit Instruction and Nop Instruction, as well as the Reverse Jump plugin as an alternative.

I chose to edit the instruction at 0x004018e3 and replace it with nop8. Cutter automatically filled the remaining byte with another NOP, which is what we want because the original je is a two-byte short jump.

That means the original bytes were:
Patch notes
Address: 0x004018e3
Original instruction: je 0x004018ec
Original bytes: 74 07
Patched bytes: 90 90
Once those two bytes are replaced, the failure jump disappears. The block no longer has a branch to str.Wrong_password; it simply falls through into mov edx, str.Access_Granted.

Result
From a control-flow perspective, this is enough to prove the patch worked. Before the change, 0x004018e3 could still redirect execution to 0x004018ec and load str.Wrong_password. After the change, that two-byte branch is now just nop / nop, so the next meaningful instruction is always mov edx, str.Access_Granted.
After patching, I ran the program again and entered a test password. The binary still returned Access Granted, which matches the patched branch and confirms that the failure path is no longer being taken.

This is why static patching works well in small crackmes. Once the right branch is isolated, the actual binary modification can be very small. Here it only took two bytes to completely change the visible behavior of the crackme.
Conclusion
This crackme is small, but it still has an anti-debugging check, a structured comparison routine, and a final branch that can be patched once the flow is clear. The patch point is easy to justify from both the disassembly and the decompiler, so the change does not rely on guessing.
If I take this further, the next step is to recover the real password instead of stopping at the patch. On crackmes.one, patching can be useful during analysis, but it is usually not treated as the intended final solution unless the challenge is specifically built around patching.9 For this post, the focus was the patch itself.
-
crackmes.one is a platform built around reverse engineering practice binaries and writeups. It is useful for finding small crackmes that are safe and intentionally made to be reversed. ↩︎
-
Cutter is a graphical reverse engineering platform powered by Rizin. It made this walkthrough easier because the overview, strings, disassembly, decompiler, and patching workflow were all in one place. ↩︎
-
IsDebuggerPresentchecks whether the current process is running under a user-mode debugger. In this crackme it is used as a simple anti-debugging gate before the password logic. ↩︎ ↩︎ -
testupdates flags by ANDing its operands without storing a new result. In practice that makes it a fast way to ask whether a register is zero before branching. The instruction set reference is in the Intel Software Developer’s Manual, Volume 2. ↩︎ ↩︎ -
cmpsubtracts for flag-setting purposes without writing the result back. Here it acts like an equality test before the routine decides whether it should continue or fail. See the Intel Software Developer’s Manual, Volume 2. ↩︎ ↩︎ -
setewrites1when the zero flag is set and0otherwise. That is why it works well as a small bridge between a comparison result and a later branch decision. See the Intel Software Developer’s Manual, Volume 2. ↩︎ ↩︎ -
jetransfers control only when the zero flag is set. In this crackme it is the last branch that still allows the binary to reachstr.Wrong_password, so it becomes the natural patch point. See the Intel Software Developer’s Manual, Volume 2. ↩︎ ↩︎ -
nopperforms no state-changing operation and is commonly used in patching to neutralize instructions without shifting surrounding code. See the Intel Software Developer’s Manual, Volume 2. ↩︎ -
The crackmes.one submission rules explicitly say that patching is not a valid normal solution unless the crackme is specifically themed around patching. ↩︎