Table of Contents


1. Introduction {#introduction}

In this report, I implemented a simple password verification program using x86 32-bit assembly in SASM, then analyzed and modified its behavior using Cutter. The goal was to understand how a basic authentication mechanism works at the assembly level and how it can be altered through binary patching.

The program simulates a login system that prompts the user for a password, compares it against a hardcoded credential, and displays either a success or failure message. After building and testing the program, I loaded the compiled executable into Cutter, identified the verification logic, and patched it so that access is always granted regardless of the input.

This exercise demonstrates both low-level program design and practical reverse engineering techniques.

Task Overview

2. Environment Setup {#environment}

The development and analysis environment used for this task:

Component               Tool
────────────────────────────────────────────
Operating System        Windows
Assembler               SASM (NASM x86)
Compiler / Linker       MinGW (via SASM)
Reverse Engineering     Cutter

The program was written and executed in SASM, which allows integration with C standard library functions such as printf, scanf, and strcmp.

3. Program Implementation {#implementation}

The program is structured using three sections: .data, .bss, and .text.

.data section -> stores all constant strings:

motd          →  "Welcome to the Secure Login System"
prompt        →  "Enter password: "
input_fmt     →  "%63s"   (format string for scanf)
correct_pw    →  "rit123" (hardcoded password)
success_msg   →  "Access granted. Correct password."
failure_msg   →  "Access denied. Incorrect password."

.bss section -> reserves memory for runtime input:

user_input    resb 64    (64-byte buffer for entered password)

.text section -> contains the program logic:

1. Print welcome message (motd)
2. Print input prompt
3. Read user input via scanf into user_input buffer
4. Compare user_input with correct_pw using strcmp
5. Check eax result:
     eax == 0  →  strings match  →  print success
     eax != 0  →  strings differ →  print failure

4. Assembly Source Code {#source}

global main
extern printf
extern scanf
extern strcmp

section .data
    motd        db "Welcome to the Secure Login System", 10, 0
    prompt      db "Enter password: ", 0
    input_fmt   db "%63s", 0

    correct_pw  db "rit123", 0

    success_msg db "Access granted. Correct password.", 10, 0
    failure_msg db "Access denied. Incorrect password.", 10, 0

section .bss
    user_input  resb 64

section .text
main:
    push ebp
    mov ebp, esp

    push motd
    call printf
    add esp, 4

    push prompt
    call printf
    add esp, 4

    push user_input
    push input_fmt
    call scanf
    add esp, 8

    push correct_pw
    push user_input
    call strcmp
    add esp, 8

    cmp eax, 0
    jne wrong_password

correct_password:
    push success_msg
    call printf
    add esp, 4
    jmp program_end

wrong_password:
    push failure_msg
    call printf
    add esp, 4

program_end:
    mov eax, 0
    mov esp, ebp
    pop ebp
    ret

The key logic is at the end of main: strcmp returns 0 if the strings match. The cmp eax, 0 + jne wrong_password pair is the single instruction controlling which branch executes, this is what I will target during patching.

5. Program Execution {#execution}

The program was tested in SASM using both correct and incorrect inputs.

Correct input (rit123):

SASM correct password input showing "Access granted. Correct password."

Incorrect input (test):

SASM incorrect password input showing "Access denied. Incorrect password."

Both cases confirm that the program correctly handles input and routes execution based on the strcmp result.

6. Reverse Engineering in Cutter {#reverse-engineering}

After compiling the program in SASM, I opened the generated login.exe in Cutter for static analysis.

Loading the binary:

Cutter load options login.exe loaded with auto-analysis enabled

Cutter auto-analyzed the binary and populated the Functions panel with all identified functions.

Navigating to main:

I located main in the Functions panel and opened it in the disassembly view. The full program logic is contained here.

Cutter disassembly main function with full program logic visible

Identifying the authentication logic:

The critical section is the password comparison block:

push  str.rit123          ; correct_pw
push  0x405020            ; user_input
call  sub.msvcrt.dll_strcmp
add   esp, 8

cmp   eax, 0              ; is result zero? (strings equal?)
jne   0x4013e5            ; if NOT equal → jump to failure path
Control flow:

  strcmp result → eax

  cmp eax, 0
      ├── eax == 0 (match)  → fall through → success path → print "Access granted"
      │                                     → jmp program_end
      └── eax != 0 (no match) → jne → 0x4013e5 → failure path → print "Access denied"

The jne 0x4013e5 at address 0x004013d4 is the single instruction that enforces the password check. Removing it means execution always falls through to the success path.

7. Binary Patching {#patching}

To bypass the authentication check, I targeted the jne instruction at 0x004013d4.

Editing the instruction:

I right-clicked the jneEditInstruction. The edit dialog showed:

Cutter edit instruction dialog jne 0x4013e5 at 0x004013d4 with "Fill remaining bytes with NOP opcodes" checked

I replaced jne 0x4013e5 with nop and checked Fill all remaining bytes with NOP opcodes. Since jne rel8 encodes as 2 bytes (75 0f), this fills both bytes with 0x90 (NOP).

Result in disassembly after patch:

Cutter disassembly after patching jne replaced with two NOP instructions at 0x4013d4 and 0x4013d5

; Before patch:
0x004013d4   jne  0x4013e5     ; 75 0f  -> jumps to failure on wrong input

; After patch:
0x004013d4   nop               ; 90
0x004013d5   nop               ; 90     -> no jump, execution always continues

Effect of the patch:

Execution now always falls through to the success path.
The failure block at 0x4013e5 remains in the binary but is unreachable.

After applying the patch, I saved the modified binary from Cutter using File → Save.

8. Patched Program Execution {#patched-execution}

The patched executable was tested from the command line using both wrong and correct inputs to confirm the patch worked in both cases.

Command prompt output wrong input, rit123, and "Patched" all result in "Access granted. Correct password."

Run 1:  Input = wrong      →  Access denied. Incorrect password.  (original binary)
Run 2:  Input = rit123     →  Access granted. Correct password.   (original binary)
Run 3:  Input = Patched    →  Access granted. Correct password.   (patched binary)

Any input including completely wrong ones, now returns the success message. The authentication check no longer exists in the execution path.

9. Conclusion {#conclusion}

This task demonstrated how a simple authentication mechanism works at the assembly level and how it can be analyzed and bypassed using reverse engineering tools.

By writing the program myself first, I understood exactly which instruction controlled the outcome: a single jne following a cmp eax, 0 after strcmp. That one instruction is the entire enforcement point of the password check.

Replacing it with two nop bytes removed the conditional entirely and permanently redirected execution to the success path regardless of input.

Key takeaways:

  Authentication at the binary level often reduces to a single branch instruction
  jne / je are the most common targets in password bypass patching
  NOP patching is simple and effective when protections are minimal
  Understanding your own code first makes reverse engineering far easier

This also reinforces why compiled binaries should not be treated as tamper-proof. Without additional protections such as code signing, integrity checks, or obfuscation, any conditional branch in a binary can be trivially removed with a hex editor or a tool like Cutter.