x86 Assembly CrackMe and Binary Patching
About
This post covers a small x86 32-bit crackme written in SASM1, then analyzed and patched in Cutter2. The ouroboros theme stays in the banner and naming, while the password check itself is a direct byte-by-byte comparison using pointer increments. The password vorares is stored in plaintext in the .data section, which makes the strings view useful right away.
Introduction
I kept this one small and easy to follow. The crackme prints the ouroboros banner, reads one input string, and walks it against a hardcoded secret one byte at a time. From there I moved on to Cutter to isolate the mismatch branch and patch it.
Part 1: Building the CrackMe
The Code
extern _printf
extern _scanf
section .data
banner db 10
db " 10000", 10
db " 0101000011", 10
db " 0011110000110", 10
db " 000110111011101010110", 10
db " 0101101111100010011111110110000", 10
db " 111111000000011100010110001001000010", 10
db " 0111011010101000011111111 00110001101100", 10
db " 00111101000 01010111111 00101100110", 10
db " 010011010 000011010 01010110", 10
db " 1001100000 011100000 00011101", 10
db " 0100001000 000 1010010", 10
db " 0000001010 10 101111", 10
db " 000001010 010 000001", 10
db " 00010000 0 0 01010010", 10
db " 00100000 00010010", 10
db " 10010100 0101110", 10
db " 100011000 1011000", 10
db " 11000101 01011000", 10
db " 110000000 0101010", 10
db " 000110101 1110011", 10
db " 001001000 000111000", 10
db " 00000010010 10100111", 10
db " 00001010000 0101000100", 10
db " 010 01100000 01001000110", 10
db " 0010000100000 0101001000", 10
db " 0010010100000 010001000000", 10
db " 1100000100001100 00 000000010001010", 10
db " 00010011001000001001000000110111101", 10
db " 00010101010010101101100110100000100", 10
db " 00010000010000000101000001000", 10
db " 000001111100000", 10
db 10
db " [ O U R O B O R O S ]", 10
db 10, 0
prompt db " > enter the cycle : ", 0
fmt_in db "%31s", 0
secret db "vorares", 0
msg_open db 10
db " the cycle completes.", 10
db " you may pass.", 10, 10, 0
msg_break db 10
db " the ring is broken.", 10, 10, 0
section .bss
input_buf resb 32
section .text
global main
main:
push ebp
mov ebp, esp
push banner
call _printf
add esp, 4
push prompt
call _printf
add esp, 4
push input_buf
push fmt_in
call _scanf
add esp, 8
mov esi, input_buf
mov edi, secret
eat_tail:
mov al, [esi]
mov bl, [edi]
cmp al, bl
jne broken
test al, al
je complete
inc esi
inc edi
jmp eat_tail
complete:
push msg_open
call _printf
add esp, 4
jmp done
broken:
push msg_break
call _printf
add esp, 4
done:
mov esp, ebp
pop ebp
xor eax, eax
ret
The flow stays simple. main prints the banner, prints the prompt, reads input with scanf, then loads the input buffer into esi and the secret into edi. From there the compare loop walks both strings one byte at a time.
Inside the loop, mov al, [esi] reads the current input byte and mov bl, [edi] reads the current secret byte. The real gate is cmp al, bl, followed by jne broken, which sends execution to the failure message on any mismatch.34 If the bytes match, test al, al checks whether the current byte is the null terminator, and je complete ends the loop only when both strings have matched all the way to the end.5
The data section holds the banner, the prompt, the format string, the secret, and both result messages. In this version the secret contains vorares directly as plaintext. The bss section reserves input_buf as a 32-byte buffer, which matches the %31s limit and leaves room for the terminating zero. The text section keeps the program inside main, using the usual cdecl convention with caller stack cleanup after each function call. The syntax follows NASM style, which is the form used here in SASM.6
One extra thing I wanted here was the ASCII art banner. It is meant to be an ouroboros, the snake eating its own tail, because it fit the whole idea of the crackme with the cycle, the ring, and the loop in the password check. I also had to keep it inside an 80-character width, so I could not make it too detailed. That is why the ASCII loses some quality in places, but for a small console crackme it was enough and it still gave the program the look I wanted.
Running the Program
I wrote the crackme in SASM1 first and checked the editor view before moving on to the binary.

Then I ran it with vorares and confirmed it reached the success message.

After that I used a wrong input and confirmed it dropped into the failure path.

Part 2: Analyzing the Binary in Cutter
The compiled exe was easy to locate from SASM’s build output. Once it was built, the final file was just ouroboros.exe, which is the same binary I moved into Cutter2 for the rest of the analysis.
![]()
From there I checked the strings view first. vorares, the prompt, and both result strings stand out right away, which confirms that the password is present in plaintext in the data section.

Graph View
Even before zooming into individual instructions, the graph view of main already shows the whole shape of the crackme. The setup sits at the top, the compare loop is in the middle, and the success and failure blocks split off clearly from the same condition.

Assembly Literacy
The main routine is still easy to follow in disassembly because the pointer setup and the compare loop sit close together.
main prologue push ebp
main prologue+1 mov ebp, esp
... mov esi, input_buf
... mov edi, secret
... mov al, byte ptr [esi]
... mov bl, byte ptr [edi]
mismatch check cmp al, bl
failure branch jne broken
... test al, al
... je complete
... inc esi
... inc edi
... jmp eat_tail
The first setup pair, mov esi, input_buf and mov edi, secret, loads the user buffer and the hardcoded password into the registers that drive the rest of the loop.

Here mov al, byte ptr [esi] pulls the current byte from the user input, while mov bl, byte ptr [edi] pulls the matching byte from the secret. Since both registers are working one byte at a time, the compare loop stays small and easy to follow.
cmp al, bl is the actual equality check inside the loop. It compares the current input byte against the current secret byte and only updates the flags, without writing any result back.3
jne broken is the part that matters most. If the bytes do not match, execution goes straight to the failure block. That is the instruction that really decides whether the loop keeps going or dies there.4
test al, al comes right after the successful compare. It checks whether the current byte is zero, which means the loop has reached the string terminator.5
je complete takes the success path only when the zero flag is set by that test, which means the entire string matched and the loop reached the end cleanly.5
inc esi and inc edi advance both pointers by one byte before the loop jumps back. That keeps the input pointer and the secret pointer aligned on each iteration.

Conditional Logic
The important conditional block inside eat_tail is small enough to isolate immediately.
... mov al, byte ptr [esi]
... mov bl, byte ptr [edi]
mismatch check cmp al, bl
failure branch jne broken
... test al, al
... je complete
The first decision here is the mismatch gate. cmp al, bl checks the current pair of bytes, and jne broken takes the failure path on the first difference. If the bytes match, execution falls through to test al, al and then je complete, which only ends the loop once the null terminator is reached.

Part 3: Patching the Binary
Once the compare loop was isolated, the patch point was small. The branch to focus on is the single jne at 0x004013d9, right after cmp al, bl at 0x004013d7. In the file itself that instruction sits at offset 0x000009d9, and its original bytes are 75 17.
Replacing those two bytes with 90 90 turns the branch into nop / nop.7 After that, the loop no longer exits on mismatches. Execution falls through into test al, al and keeps advancing both pointers until the input terminator is reached, which sends control to complete. Since scanf with %31s writes a null-terminated string into the 32-byte buffer, any normal input still reaches that terminator.
Before patching, jne 0x4013f2 is still there, so it still redirects execution into the failure block that prints the ring is broken.

After patching, that same spot becomes nop / nop, which forces the code to fall through into the rest of the loop instead of branching away on the first mismatch.

In graph view the result is also easy to see. Once the conditional edge is gone, the loop only keeps walking forward until the null terminator reaches the je 0x4013e3 success path.

Conclusion
This crackme stays simple by design. vorares is visible in the strings view, the validation logic is a standard byte-by-byte loop, and the patch only needs one jne to disappear before the program starts accepting wrong input. If I take it further, the next step is to obfuscate the password so it does not stand out in strings and the comparison becomes less obvious on a first pass.

© Credits: The writing and images on this page are the original work of the page author unless a source is explicitly cited.
-
SASM is the IDE used here to write and build the
32-bitassembly source. Documentation: https://dman95.github.io/SASM/english.html ↩︎ ↩︎ -
Cutter is the reverse engineering frontend used here for disassembly, decompiler output, and byte patching. Documentation: https://cutter.re/docs/ ↩︎ ↩︎
-
Intel Software Developer’s Manual, Volume 2 documents
cmp, which updates flags from a subtraction without writing a result back. Documentation: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html ↩︎ ↩︎ -
Intel Software Developer’s Manual, Volume 2 documents
jne, which branches when the zero flag is clear after the compare. Documentation: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html ↩︎ ↩︎ -
Intel Software Developer’s Manual, Volume 2 documents
testandje, which are what make the loop stop only when it reaches the terminating zero byte. Documentation: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html ↩︎ ↩︎ ↩︎ -
NASM documentation covers the syntax used in this source file. Documentation: https://www.nasm.us/doc/ ↩︎
-
Intel Software Developer’s Manual, Volume 2 documents
nop, which is what makes the patched branch disappear without changing control flow in any other way. Documentation: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html ↩︎