Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
ReverseMedium

To Jmp Or Not Jmp

CTF writeup for To Jmp Or Not Jmp from Backdoor

//To jmp or not jmp

Author: v1bh475u (challenge)

>TL;DR

  • Binary is a 64-bit PIE ELF that expects a flag in printf/iostream style. After a length check, a stream cipher (RC4-like) XORs and validates the buffer against a stored ciphertext.
  • The keystream was recovered by running the binary with a known input (all 'A's) and dumping the processed buffer; the keystream was derived as processedByte XOR 0x41.
  • Using the keystream, XOR the ciphertext from .rodata to retrieve the flag.

Target flag: flag{$t0p_JUmp1n9_@R0uNd_1!k3_A_F00l_4nd_gib3_M3333_7H@t_f14g!!!!}


>Environment / Tools used

  • Linux (same environment as challenge worked on)
  • objdump (disassembly)
  • gdb (interactive runtime inspection; used with set disable-randomization on to avoid address randomization)
  • ltrace (library trace to quickly see strlen, getline)
  • strings (quick text scan)
  • python3 (to craft keystream/rc4 and compute flag)

Note: tools like radare2 or ghidra would speed up some static analysis but were not necessary.


>What I saw first — quick checks

Start with a basic file check:

bash

$ file challenge

challenge: ELF 64-bit LSB pie executable, x86-64, ... stripped

Run it (add exec bit if needed):

bash

$ chmod +x challenge

$ ./challenge

Enter the flag:

Wrong! Try again.

A quick strings reveals the prompted text:

bash

$ strings -n 4 challenge | grep -E "Enter the flag|Correct|Wrong"

Enter the flag:

Correct! You got the flag!

Wrong! Try again.

So it's checking the flag and printing success/failure.


>Step 1 — inspect rodata for ciphertext and key

The .rodata segment contains the strings and some binary blobs that are likely constants (key, ciphertext). Use objdump (or xxd) to inspect it:

bash

$ objdump -s -j .rodata challenge | sed -n '1,120p'

# or with xxd

$ xxd -g1 -s 0x2020 -l 0x60 challenge

From that dump we find a small printable sequence followed by two important binary chunks, e.g.:

  • A 16 byte ASCII sequence at .rodata offset 0x2020; this looks like a key: !a1 a&\\r9a+\\r 1fsR (hex: 2161312061260d39612b0d2031667352).
  • A binary blob at .rodata offset 0x2040 and length 0x42 that looks random — this is likely the ciphertext.

We now suspect the program uses this key and ciphertext to check the flag.


>Step 2 — examine disassembly to find algorithm (KSA/PRGA)

Use objdump -d -M intel to disassemble and identify the flow and comparisons.

We see a loop which:

  • Gets a buffer index variable (compares with a fixed max),
  • Uses a bunch of movzbl/xor/add/mov operations that read from a table and write into a destination buffer.

This key-scheduling + PRGA-like logic strongly matches an RC4-style XOR stream cipher. Important observations:

  • The algorithm reads a key slot and mixes into a state array
  • Then a keystream byte is produced and XORed with the input bytes to produce a buffer which is compared against the stored ciphertext
  • The comparisons occur sequentially for a fixed number of bytes (0x42 bytes)

Because the binary is PIE (position-independent), we used gdb with ASLR disabled to get deterministic addresses (set disable-randomization on).


>Step 3 — confirm behaviour dynamically (trace input)

A quick library-level trace (ltrace) shows a getline call to accept the input and a strlen call for length. This confirms there's a length check, so we need the input to match expected length.

bash

$ ltrace ./challenge <<< "flag{test}"

# We saw length = 10 for `flag{test}`

Disassembly and debug indicated a length check that ensures input is long enough (the expected count is 0x42, i.e., 66 bytes) before comparing each resulting byte.


>Step 4 — keystream recovery trick (easiest approach)

Instead of fully reversing the cipher or reimplementing the KSA to produce the keystream by reading S-box operations, we used a simple oracle trick: provide a known input (e.g., A x 66) and dump the processed buffer before comparison. That processed buffer = plaintext XOR keystream.

  • The keystream for a position i is keystream[i] = processed_buffer[i] XOR input[i]. For A (0x41), keystream[i] = processed[i] XOR 0x41.
  • The stored ciphertext (from .rodata) is ciphertext[i] = flag[i] XOR keystream[i], so flag[i] = ciphertext[i] XOR keystream[i].

Steps to do this:

  1. Run the binary with a 66-character known input and instrument the comparison break.
bash

# Disable ASLR for consistent addresses, then run in gdb to break at the byte comparison loop address

$ gdb -q ./challenge

(gdb) set disable-randomization on

(gdb) starti  # run to `_start` then pause

(gdb) b *0x5555555555b3  # this was the comparison instruction's address in my run

(gdb) run < <(printf 'A%.0s' {1..66})

(gdb) x/66bx 0x555555558420  # the buffer where processed bytes were written
  1. Dumped bytes look random in general; these are the processed bytes. Example (first 16):
0x555555558420: 0xa8 0x1b 0xef 0x5b 0x3e ...
  1. Compute keystream: keystream[i] = processed[i] XOR 0x41 (since 'A' is 0x41).
  2. Decrypt ciphertext (from offset .rodata, bytes at 0x2040) as: flag[i] = ciphertext[i] XOR keystream[i].

>Step 5 — decrypt with Python 🐍

Here are the reproducible python steps (works in one shot):

bash

# Read constants from binary and decrypt using Python

# We will: (1) extract the ciphertext bytes from file offsets, (2) use the captured processed buffer (observed via gdb), (3) compute keystream, (4) compute flag

python3 - <<'PY'

from pathlib import Path

# ciphertext read from .rodata offset 0x2040 length 0x42

ct = bytes.fromhex('8f36cf7d048e35ac0fe83f538b87ac26185b13c7ffa61d9229b762afa9b0cf74d2994e5547a9773b6728cb52749047241594e14e4df257ad7f5d221705088b2aedf1')

# processed buffer observed during unencrypted run (with 'A'*66)

buf = bytes([

 0xa8,0x1b,0xef,0x5b,0x3e,0xeb,0x00,0xdd,0x3e,0xf6,0x34,0x47,0xa7,0xb6,0xdc,0x09,

 0x60,0x45,0x12,0xd4,0x8e,0x92,0x12,0xb7,0x37,0xc7,0x02,0x85,0xdb,0xae,0xcf,0x6a,

 0xd5,0xe8,0x3f,0x78,0x59,0xdc,0x58,0x1e,0x79,0x0e,0xe3,0x71,0x06,0x8e,0x4b,0x56,

 0x67,0xe6,0x93,0x50,0x3b,0xfb,0x56,0x98,0x61,0x7a,0x52,0x62,0x23,0x68,0xeb,0x4a,

 0x8d,0xcd

])

ks = bytes([b ^ 0x41 for b in buf])  # keystream because input was 'A' (0x41)

flag = bytes([c ^ k for c, k in zip(ct, ks)])

print(flag)

print(flag.decode())

PY

You should get the flag string printed out. Example output:

flag{$t0p_JUmp1n9_@R0uNd_1!k3_A_F00l_4nd_gib3_M3333_7H@t_f14g!!!!}

>Extra verification — key and cipher approach

We also found the key bytes near $RODATA + 0x2020 (ASCII string !a1 a&\\r9a+\\r 1fsR). A full KSA/PRGA RC4 re-implementation using this key would produce the exact keystream. However, since we already recovered the keystream by the known-plaintext trick, reimplementing the full KSA/PRGA was not necessary.

If you want to implement KSA/PRGA to recreate the keystream programmatically, here's a short script to do it from the key:

python

# RC4 sample (if you'd like to rebuild key->keystream):

def rc4_keystream(key, length):

    S = list(range(256))

    j = 0

    # KSA

    for i in range(256):

        j = (j + S[i] + key[i % len(key)]) % 256

        S[i], S[j] = S[j], S[i]

    # PRGA

    i = j = 0

    out = bytearray()

    for _ in range(length):

        i = (i + 1) & 0xff

        j = (j + S[i]) & 0xff

        S[i], S[j] = S[j], S[i]

        out.append(S[(S[i] + S[j]) & 0xff])

    return bytes(out)

Then decrypt ct with flag[i] = ct[i] XOR keystream[i].


>Summary & final flag

  • The program checks a 66-byte long flag. It does an RC4-like encryption/keystream XOR and compares to a ciphertext embedded in .rodata.
  • I ran the program with the A * 66 test input, dumped the processed buffer, derived the keystream, and decrypted the ciphertext to reveal the flag.

Final flag is:

flag{$t0p_JUmp1n9_@R0uNd_1!k3_A_F00l_4nd_gib3_M3333_7H@t_f14g!!!!}