I Guess Bro

Eh4x CTFby smothy

i guess bro - Writeup

CTF: EH4X CTF 2026 Category: Reverse Engineering Points: 500 Author: benzo Solves: First blood baby lets gooo


TL;DR

RISC-V crackme with fake flags, anti-debug, XOR decryption, checksum, and hash validation. Basically benzo said "let me ruin someone's weekend" and chose violence.

Flag: EH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}


Initial Recon - "what the hell is this"

Downloaded the file, ran file on it and...

$ file chall chall: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), statically linked, stripped

RISC-V?? RISC-V in 2026??

Okay fine. Statically linked + stripped = no symbols, no easy life. Classic RE pain.

But hey, at least we can emulate it:

bash
$ sudo apt install qemu-user   # if you dont have it
$ chmod +x chall
$ echo "test" | qemu-riscv64 ./chall
╔═══════════════════════════════════╗ ║ RISC-V Crackme Challenge ║ ║ 'I Guess Bro' - Hard Mode ║ ╚═══════════════════════════════════╝ Can you guess the flag? I guess not... Enter the flag: Wrong length! Keep guessing...

Alright so its a crackme. Takes input, checks if its the flag. Classic stuff.


Step 1: String Hunting - "the audacity of this binary"

First thing in any RE challenge - check the strings:

bash
$ strings chall | grep -iE "flag|correct|wrong|EH4X|guess"
'I Guess Bro' - Hard Mode Can you guess the flag? I guess not... Enter the flag: Wrong length! Keep guessing... Correct! You guessed it! Flag: %s Wrong! I guess you need to try harder... EH4X{n0t_th3_r34l_fl4g} <--- LMAOOO EH4X{try_h4rd3r_buddy} <--- BRO REALLY??

Two fake flags sitting right there in plaintext. Benzo really said "here, fall for this."

Don't be that guy. If a flag is sitting in strings output for a 500pt RE challenge... it's bait.


Step 2: Finding the Flag Length

Before we go deep into disassembly, let's figure out the expected length:

bash
for len in $(seq 10 50); do
    input=$(python3 -c "print('A'*$len)")
    result=$(echo "$input" | qemu-riscv64 ./chall 2>&1)
    if ! echo "$result" | grep -q "Wrong length"; then
        echo "Length $len passes length check!"
    fi
done
Length 35 passes length check!

Flag is 35 characters. Since format is EH4X{...}, thats 5 + 29 + 1 = 35. Makes sense.


Step 3: Decompilation with Ghidra - "time to suffer"

Since this is RISC-V, we can't just throw it in x86 tools. Ghidra handles RISC-V well though.

I used Ghidra headless mode because I'm a terminal enjoyer:

bash
# Import and analyze
/usr/share/ghidra/support/analyzeHeadless /tmp ghidra_project \
    -import ./chall -overwrite

# Then run a decompilation script
/usr/share/ghidra/support/analyzeHeadless /tmp ghidra_project \
    -process chall -noanalysis -postScript DecompileScript.java

Pro tip: If Ghidra doesn't find xrefs for RISC-V, its because RISC-V uses auipc + addi for address loading (PC-relative), and Ghidra sometimes doesn't resolve these properly. You might need to trace manually or use dynamic analysis.

The Validation Architecture

After decompilation, here's what the binary does:

Validation Architecture

Benzo really put 5 layers of validation on this thing. Respect the grind honestly.


Step 4: Breaking Down Each Check

Check 1: Length (trivial)

c
if (strlen(input) != 35) {
    puts("Wrong length! Keep guessing...");
    exit(1);
}

We already got this from bruteforce. Moving on.

Check 2: Anti-Fake-Flag (sneaky)

c
// At function 0x10732
if (strcmp(input, "EH4X{n0t_th3_r34l_fl4g}") == 0 ||
    strcmp(input, "EH4X{try_h4rd3r_buddy}") == 0) {
    puts("Nice try, but that's not it!");
    exit(1);
}

This literally catches people who just run strings and submit. Brutal.

Check 3: Anti-Debug Timing (annoying)

c
// Pseudocode
t1 = clock_gettime(CLOCK_PROCESS_CPUTIME_ID);
// ... computation loop ...
t2 = clock_gettime(CLOCK_PROCESS_CPUTIME_ID);

if ((t2 - t1) > 49840) {
    // Debugger detected, bail out
    exit(1);
}

If you try to single-step through this in a debugger, the timing check catches you and the binary exits.

How to bypass: Either patch out the timing check, or just do static analysis. You don't actually need to debug this if you can read the decompiled code.

Check 4: XOR Decryption - THE MAIN EVENT

This is where the real flag lives. Function at 0x105cc:

c
// Pseudocode of the core validation
void check_flag(char *input) {
    unsigned char encrypted[] = { /* 35 bytes at 0x57bc8 */ };
    int d = 0;

    for (int i = 0; i < 35; i++) {
        char decrypted = encrypted[i] ^ (d & 0xFF) ^ 0xA5;
        if (decrypted != input[i]) {
            return FAIL;
        }
        d += 7;
    }
    return PASS;
}

The algorithm:

  1. There are 35 encrypted bytes stored in the binary at address 0x57bc8
  2. Each byte is XOR'd with a rolling counter (d & 0xFF) and the constant 0xA5
  3. Counter d starts at 0 and increments by 7 each iteration
  4. The result must match your input byte-for-byte

XOR Decryption

Check 5: Format + Checksum

c
// Verify EH4X{...} format
if (input[0] != 'E' || input[1] != 'H' || input[2] != '4' ||
    input[3] != 'X' || input[4] != '{' || input[34] != '}') {
    return FAIL;
}

// ASCII sum check
int sum = 0;
for (int i = 0; i < 35; i++) {
    sum += input[i];
}
if (sum != 3243) {
    return FAIL;
}

The sum of all ASCII values must equal exactly 3243. This is a neat integrity check that catches random brute force attempts.

Check 6: Custom Hash

A custom hash function at 0x10574 processes all 35 bytes through XOR, shifts, and multiplications, then compares against a hardcoded constant. This is the final boss.


Step 5: Extracting the Flag

The easiest way to get the flag is to just decrypt the embedded bytes ourselves:

python
#!/usr/bin/env python3
"""
i_guess_bro solver - EH4X CTF 2026
Decrypts XOR-encrypted flag from embedded data
"""

# 35 encrypted bytes extracted from address 0x57bc8
# (extracted via: qemu-riscv64 + gdb, or Ghidra hex view)
encrypted = [
    # ... bytes dumped from binary ...
]

flag = ""
d = 0
for i in range(35):
    decrypted = encrypted[i] ^ (d & 0xFF) ^ 0xA5
    flag += chr(decrypted)
    d += 7

print(f"Flag: {flag}")

# Verify checksum
ascii_sum = sum(ord(c) for c in flag)
print(f"ASCII sum: {ascii_sum} (expected: 3243)")
assert ascii_sum == 3243, "Checksum mismatch!"
assert flag.startswith("EH4X{") and flag.endswith("}")
print("All checks pass!")

But honestly the fastest approach was just dynamic analysis with qemu-riscv64: dumping the decrypted bytes from memory after the XOR loop runs.


Verification

bash
$ echo "EH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}" | qemu-riscv64 ./chall
╔═══════════════════════════════════╗ ║ RISC-V Crackme Challenge ║ ║ 'I Guess Bro' - Hard Mode ║ ╚═══════════════════════════════════╝ Can you guess the flag? I guess not... Enter the flag: Correct! You guessed it! Flag: EH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}

500 points, first blood. GG benzo.


Lessons Learned (for the educational homies)

1. Never Trust Strings Output

If you see a flag in strings for a high-point RE challenge, it's probably bait. Always verify by actually submitting or tracing the validation logic.

2. RISC-V is Not Scary

RISC-V is actually cleaner than x86. Fewer instructions, more predictable patterns. Tools like Ghidra and radare2 handle it just fine. Use qemu-user to run RISC-V binaries on your x86 machine.

3. XOR Encryption is Reversible

XOR is its own inverse: A ^ B ^ B = A. If you can find the key schedule (in this case: counter * 7, XOR 0xA5), you can decrypt without ever running the binary.

4. Anti-Debug is Not Anti-Static-Analysis

Timing checks, ptrace detection, etc. only stop debuggers. They don't stop you from reading the decompiled code and reimplementing the logic in Python.

5. Multiple Validation Layers

Real-world software (and good CTF challenges) use defense in depth:

  • Length check (fast reject)
  • Known-bad check (anti-honeypot)
  • Core validation (XOR compare)
  • Integrity check (checksum)
  • Hash verification (final confirmation)

Understanding this pattern helps in both CTF and real reverse engineering.

6. Useful Tools for RISC-V RE

ToolWhat For
qemu-riscv64Run RISC-V binaries on x86
qemu-riscv64 -g 1234Run with GDB server
gdb-multiarchDebug any architecture
GhidraDecompile RISC-V to C
radare2 / r2CLI disassembly + analysis
strings / hexdumpQuick initial recon

Flag

EH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}

GG benzo. That was a solid challenge.


Written while vibing to lofi beats and questioning my life choices at 3am. If you learned something, drop a star. If you didn't, skill issue tbh.