Sunday, March 29, 2026

LAB 8: x86 Assembly Programming and Patching

 Introduction

In this lab, we will write a simple crackme program in x86 32-bit assembly using the SASM IDE, compile it into a working Windows executable, and then crack our own program using the Cutter disassembler.

We will go through 3 main parts:

  1. Building our own CrackMe in SASM
  2. Analyzing our Binary in Cutter
  3. Patching our Binary in Cutter


1. Building our own CrackMe in SASM

 We programmed our CrackMe with the help of AI, specifically Google's Gemini AI. It was a constant attempt of trial and error (and asking questions to understand) but the end result was this code:


extern printf

extern scanf


section .data

    prompt_user db "Enter Username: ", 0

    prompt_pass db "Enter Password: ", 0

    success_msg db 10, "ACCESS GRANTED! Welcome to the system.", 10, 0

    denied_msg  db 10, "ACCESS DENIED! Invalid credentials. Try again.", 10, 10, 0 

    format_str  db "%s", 0

    real_user   db "admin", 0

    real_pass   db "SuperSecret2026", 0


section .bss

    in_user resb 50

    in_pass resb 50


section .text

global main          ; Removed the underscore

main:                ; Removed the underscore

    push ebp

    mov ebp, esp 


.login_prompt:

    ; --- GET USERNAME ---

    push prompt_user

    call printf

    add esp, 4

    

    push in_user

    push format_str

    call scanf

    add esp, 8


    ; --- GET PASSWORD ---

    push prompt_pass

    call printf

    add esp, 4

    

    push in_pass

    push format_str

    call scanf

    add esp, 8


    ; --- MANUAL USERNAME CHECK ---

    mov esi, in_user      

    mov edi, real_user    

.user_loop:

    mov al, [esi]         

    mov bl, [edi]         

    cmp al, 0

    je .user_check_end

    cmp al, bl            

    jne .denied           

    inc esi               

    inc edi

    jmp .user_loop        

.user_check_end:

    cmp bl, 0               

    jne .denied           


    ; --- MANUAL PASSWORD CHECK ---

    mov esi, in_pass      

    mov edi, real_pass    

.pass_loop:

    mov al, [esi]         

    mov bl, [edi]         

    cmp al, 0

    je .pass_check_end

    cmp al, bl            

    jne .denied           

    inc esi               

    inc edi

    jmp .pass_loop        

.pass_check_end:

    cmp bl, 0

    jne .denied           

    jmp .granted          


.denied:

    push denied_msg

    call printf

    add esp, 4

    jmp .login_prompt      


.granted:

    push success_msg

    call printf

    add esp, 4


    ; Cleanup and exit

    mov esp, ebp

    pop ebp

    xor eax, eax          

    ret


Our code starts in the data section, where we defined all the strings for the prompts and the messages along with the hardcoded credentials. We also used the bss section to reserve 50 bytes of space for the username and password inputs. For the logic we used extern declarations to call printf and scanf from the C library because the lab required it. This meant we had to push the arguments onto the stack and then clean up the stack with add esp after every call. The most important part is the manual comparison loop where we check the input against the real credentials byte by byte using the esi and edi registers. We chose to write our own loop instead of using a built in function to meet the extra challenge requirements. If a character does not match it uses a conditional jump to go to the denied section which tells the user to try again. If the input matches perfectly it jumps to the granted section to print the success message and then exits the program by restoring the stack and returning zero.


After writing the code we needed to put it into SASM, which is a lightweight platform meant for begginers to write assembly. This can be seen in the picture down below:


We then tried to put in the false and correct initials inside the SASM terminal to see if everything works properly, and as can be seen below, it did.




2. Analyzing our Binary in Cutter



2.1 Assembly Literacy

Looking at the graph view for our program we can see exactly how the code branches and handles the data through the different blocks. 

In the first large block at address 0x00401393 the program uses the instruction push str.Enter_Username to put the memory address of our prompt string onto the stack. This is required because the printf function needs that specific address as an argument to know what text to display on the screen. 

Further down in that same block at 0x004013a0 we see push 0x405020 which pushes the address of the in_user buffer we reserved so that scanf knows exactly where in memory to save the characters the user types in. 

After the input is stored the program uses mov esi, 0x405020 at 0x004013d1 to load that buffer address into the esi register. This sets up esi as a pointer so we can start our manual loop and read the input character by character.

 Inside the loop block at 0x004013db the instruction mov al, byte [esi] is used to grab a single byte from the memory address held in esi and move it into the al register. This is a key step because it lets the cpu compare that specific letter against the hardcoded password.

 Finally at 0x004013e5 we see the jne 0x401411 instruction which creates the main fork in our graph. This is a conditional jump that triggers if the characters do not match and as we can see from the red arrow it sends the program to the failure block to print the denied message.


2.2 Function Prologue and Epilogue




Looking at the top of our graph we can see the function prologue which consists of the instructions push ebp and mov ebp esp. This pattern is essential because push ebp saves the base pointer of the previous function so that we do not lose track of where we came from. The next instruction mov ebp esp sets our current base pointer to the top of the stack which creates a new stack frame for our main function to use for local variables and arguments. 

At the very end of our program in the bottom right block we can see the function epilogue starting at address 0x00401430. This part uses mov esp ebp to restore the stack pointer followed by pop ebp which brings back the original base pointer we saved at the start.

 Finally the ret instruction is used to jump back to the calling code and finish the program execution. These patterns exist because every function needs its own organized space in memory to work correctly without interfering with other parts of the system and the epilogue ensures that everything is cleaned up before we exit.


2.3 Conditional Logic Identification

We can see the key conditional check in our disassembly at address 0x004013e3 where the program uses the instruction cmp al bl followed by jne 0x401411.  


the bl register holds a byte from our credential string and this instruction specifically checks if we have reached the null terminator to ensure the entire password was matched. the cmp instruction compares the value to zero and the jne instruction creates a fork in the graph where it will jump to the failure routine at address 0x401411 if the condition is not met. in the decompiler view this logic is represented by the statement if pcVar2 == 0 which is the high level way of checking for that same null character. if the condition is true the decompiler shows the program entering the block that prints the access granted message and returns zero to exit. if the condition is false the code skips the success block and jumps straight to the label for code r0x00401411 which matches the exact same failure jump target we see in our assembly graph.




Part 3: Patching the Binary in Cutter

To ensure the program always grants access regardless of the input, we implemented a comprehensive patching strategy that redirects the control flow at every critical decision point. Instead of just neutralizing a single check, we targeted three specific memory addresses to force a jump directly to the success message located at 0x00401423. This "Global Redirection" approach ensures that whether the program is checking the username length, comparing password characters, or making its final validation decision, every logical path is hijacked and sent to the "ACCESS GRANTED" block.

The first modification was made at address 0x004013e1. In the original binary, this was a conditional jump that checked for the end of a string or a specific comparison result. By right-clicking this instruction in Cutter and selecting "Edit" > "Patch Instruction," we changed the original code to a jmp 0x00401423. This acts as our "Early Exit" bypass; as soon as the program begins its validation routine, it is immediately forced to leapfrog over the entire comparison logic and land directly on the success message. This effectively cracks the program before it even has a chance to evaluate the password characters.

To make the patch even more robust, we applied a second redirection at address 0x0040140d. This location originally held a jne instruction that served as the final gatekeeper before the failure message. By overwriting this with another jmp 0x00401423, we ensured that even if the program somehow bypassed our first patch, it would still be forced into the success path at the final moment. Finally, we patched the address 0x00401411, which is the actual entry point of the "ACCESS DENIED" code block. By changing this to a jump as well, we created a safety net: even if the program "thinks" the user failed and attempts to show an error, the very first instruction it hits will simply bounce the execution back to the "Access Granted" message.




The reason this three-point strategy is so effective is that it removes the need to understand every nuances of the comparison loops. In reverse engineering, if you can identify the "Convergence Point"—the memory address where the success message begins—you can turn any conditional instruction into an unconditional gateway. By replacing the logic at 0x004013e1, 0x0040140d, and 0x00401411 with direct jumps to 0x00401423, we have fundamentally rewritten the software's destiny. The original security model is now completely bypassed, making any combination of username and password technically "correct."

Additionally, what is worth mentioning, since we changed everything, even the graph changed. 




No comments:

Post a Comment

LAB 10: APK Security Scanner - Multi-Agent Static Analysis pt.2

Taking Android Apps Apart: Building an AI-Powered Security Scanner Part 2: The Analysis, Execution and Findings In Part 1 we defined our obj...