avatar
Cyscom
Cybersecurity Student Community of VIT Chennai
  • CTF EVENTS
  • CATEGORIES
  • TAGS
  • ARCHIVES
  • POSTS
  • ABOUT
Home Ciphercase 2026 Residual
Writeup
Cancel

Residual

Residual

Author: Manasa

This is a Breaking Bad-themed reverse-engineering CTF challenge involving a stripped Linux ELF binary, encoded runtime checks, a fake flag, self-inspection through /proc/self/exe, a custom .x9 section, and a reversible byte transformation.

The goal is to satisfy the binary’s filename, environment-variable, and command-line checks, analyze the generated dead drop, and recover the real flag.

1. What We Receive

The challenge gives us one executable:

1
memory

The flag format is:

1
CYS{...}

The challenge story tells us that the binary came from an abandoned lab and that previous analysts recovered conflicting answers. That is our first warning: the first flag we see may be a bad batch.


2. Identify the File

Start with the normal first command for an unknown binary:

1
file memory

We should see something similar to:

1
memory: ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped

This tells us that it is a 64-bit Linux ELF and that symbols have been stripped. In Ghidra, many functions will therefore appear with generic names such as FUN_... rather than useful original names.

If the executable bit was lost during download:

1
chmod +x memory

3. Run It Normally

Execute:

1
./memory

We get:

1
2
3
The cook enters the lab...
Alias accepted.
The blue formula is missing a signal.

There are already two useful clues here.

Alias accepted. suggests that the program checks the name it is running under, while missing a signal suggests another external condition is required.


4. Check Printable Strings

Run:

1
strings memory | less

Search specifically for flag-shaped strings:

1
strings memory | grep 'CYS{'

We find:

1
CYS{crystal_blue_is_too_obvious}

This is intentionally tempting, but it is a fake flag. The wording itself is a hint that an obvious blue crystal is not necessarily a clean batch.

Submitting it fails, so we need to reverse the executable rather than trust strings.

We can also check for obvious secrets:

1
2
3
strings memory | grep SIGNAL
strings memory | grep MIRROR
strings memory | grep -- '--listen'

The important values are not stored directly in plaintext.


5. Open the Binary in Ghidra

Start Ghidra:

1
ghidra

Then create a non-shared project and import memory:

1
File → Import File → memory

Accept the detected ELF format, open the program, click Yes when Ghidra asks to analyze it, and keep the default analyzers enabled.

Because the file is stripped, focus on what functions do, not their names.


6. Start From main

Locate main and inspect the decompiled control flow.

Conceptually it performs several checks:

1
2
3
4
5
6
7
8
9
print lab message
    ↓
check executable identity
    ↓
check environment
    ↓
check command-line argument
    ↓
produce dead drop

Work through those checks one at a time.


7. Recover the Expected Filename

The first validation routine uses argv[0], which normally contains the path/name used to launch the program.

Inside that routine, an encoded byte array is transformed and compared against the basename of argv[0] using strcmp().

Reversing the small transformation gives:

1
memory

The supplied challenge file already has that name, which explains:

1
Alias accepted.

We can prove the check exists:

1
2
3
cp memory test
chmod +x test
./test

Now the program says:

1
2
The cook enters the lab...
This alias does not belong in the lab.

Remove the copy:

1
rm test

8. Investigate the Missing Signal

The next failed message is:

1
The blue formula is missing a signal.

In Ghidra, follow the next validation function. It eventually calls:

1
getenv()

That tells us the program expects an environment variable.

The variable name is reconstructed from encoded bytes. Reversing that decoder gives:

1
SIGNAL

The returned value is compared against another reconstructed string:

1
MIRROR

Therefore the required environment is:

1
SIGNAL=MIRROR

Test it without permanently exporting anything:

1
SIGNAL=MIRROR ./memory

Now we get:

1
2
3
4
The cook enters the lab...
Alias accepted.
Signal locked. The batch is getting cleaner.
You are not listening to the lab carefully enough.

So the environment check is solved.


9. Recover the Required Argument

Continue through main in Ghidra.

The next check operates on argv[1]. Another encoded array is transformed and compared against the argument.

Reversing the transformation gives:

1
--listen

So the intended execution is:

1
SIGNAL=MIRROR ./memory --listen

Run it:

1
2
3
4
5
The cook enters the lab...
Alias accepted.
Signal locked. The batch is getting cleaner.
Cook complete. Package the evidence.
Dead drop: b37a61397fc78d045c6a702fb50ddc0b029817065e948361683f165d

We have reached the correct execution path, but the dead drop is hexadecimal data rather than the flag.


10. Find the Dead-Drop Function in Ghidra

Search Ghidra for the string:

1
Dead drop:

Use either the Defined Strings window or:

1
Search → For Strings

Find Dead drop: and follow its XREF. An XREF tells us which function references that string.

That function builds several internal fragments, combines them, passes the result through a final byte transformation, and prints each transformed byte as two hexadecimal digits.

This is the function we need to understand.


11. Recover the First Flag Fragment

One small function contains these bytes:

1
52 4b 40 6f

The decompiled loop is equivalent to:

1
out[i] = data[i] ^ (0x11 + i);

Decode them:

1
2
3
4
0x52 XOR 0x11 = 0x43 = C
0x4b XOR 0x12 = 0x59 = Y
0x40 XOR 0x13 = 0x53 = S
0x6f XOR 0x14 = 0x7b = {

So fragment 1 is:

1
CYS{

That confirms we have found the real flag reconstruction path.


12. Recover the Second Fragment

Another function contains:

1
6b 64 69 78 6d 85 7c 7c 79 8c 8e

The transformation is:

1
out[i] = data[i] - ((i * 3) + 2);

Instead of doing all 11 bytes manually, reproduce it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cat > part2.py <<'PY'
data = [
    0x6b, 0x64, 0x69, 0x78, 0x6d, 0x85,
    0x7c, 0x7c, 0x79, 0x8c, 0x8e
]

result = []
for i, value in enumerate(data):
    result.append(value - ((i * 3) + 2))

print(bytes(result).decode())
PY

python3 part2.py

Output:

1
i_am_the_on

So far:

1
CYS{i_am_the_on

13. Notice That the Third Fragment Depends on Something Else

The third fragment function contains:

1
45 7e 51 4f 4b 7f 4a 48 48 47 4b 52 5b

But unlike the first two fragments, its key depends on the return value of another function.

Follow that function.

You will encounter imports such as:

1
2
3
4
readlink
fopen
fread
fseek

The program is reading a file and parsing data from it.


14. Discover That the Binary Reads Itself

The path passed to readlink() is itself reconstructed from encoded bytes.

Decoding it gives:

1
/proc/self/exe

On Linux, /proc/self/exe points to the currently running executable.

So the cook is literally inspecting its own recipe.

The program opens its own ELF file and begins parsing ELF structures.


15. Find the Custom ELF Section

The self-reading routine reads:

1
2
3
ELF header
section headers
section-name string table

It reconstructs another short string and searches the section table for it.

That section name is:

1
.x9

Confirm it from the terminal:

1
readelf -S memory | grep '\.x9'

Then dump the section:

1
readelf -x .x9 memory

The important 32 bytes are:

1
2
3
4
91 2f c4 77 18 a3 5d e1
42 9b 06 d8 31 af 64 be
73 0c f1 55 28 99 b2 47
da 11 6e 83 3c f7 20 51

They are not text. They are input to a custom rolling mixer.


16. Reproduce the .x9 Mixer

The relevant decompiled function behaves like:

1
2
3
state = 0;
for each byte:
    state = (state * 33) ^ byte;

The state is one byte, so keep only the low 8 bits.

Create:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat > x9.py <<'PY'
data = [
    0x91, 0x2f, 0xc4, 0x77, 0x18, 0xa3, 0x5d, 0xe1,
    0x42, 0x9b, 0x06, 0xd8, 0x31, 0xaf, 0x64, 0xbe,
    0x73, 0x0c, 0xf1, 0x55, 0x28, 0x99, 0xb2, 0x47,
    0xda, 0x11, 0x6e, 0x83, 0x3c, 0xf7, 0x20, 0x51
]

state = 0
for value in data:
    state = ((state * 33) ^ value) & 0xff

print("self key =", hex(state))
print("low nibble =", hex(state & 0x0f))
PY

python3 x9.py

Output:

1
2
self key = 0xaa
low nibble = 0xa

Therefore:

1
2
self_key = 0xAA
derived = 0x0A

17. Decode the Third Fragment

The decoder uses:

1
key = (0x2A + (i % 5)) XOR derived

with:

1
derived = 0x0A

Create:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cat > part3.py <<'PY'
data = [
    0x45, 0x7e, 0x51, 0x4f, 0x4b, 0x7f, 0x4a,
    0x48, 0x48, 0x47, 0x4b, 0x52, 0x5b
]

derived = 0x0a
base = 0x2a
result = []

for i, value in enumerate(data):
    key = (base + (i % 5)) ^ derived
    result.append(value ^ key)

print(bytes(result).decode())
PY

python3 part3.py

Output:

1
e_who_knocks}

The internal message therefore becomes:

1
CYS{i_am_the_one_who_knocks}

But the program does not print this plaintext. It passes it through one more transformation before producing the dead drop.


18. Understand the Final Transformation

The final routine uses the .x9 key again:

1
self_key = 0xAA

The seed is:

1
seed = ((self_key XOR 0x63) + 7) & 0xFF

Calculate it:

1
2
3
4
5
python3 - <<'PY'
self_key = 0xaa
seed = ((self_key ^ 0x63) + 7) & 0xff
print(hex(seed))
PY

Output:

1
0xd0

For each plaintext byte at index i, the binary then performs:

1
2
3
1. XOR with seed + (i * 5)
2. rotate left by 3 bits
3. add (i XOR 0x17)

Encoding direction:

1
plaintext → XOR → ROL 3 → addition → dead-drop byte

To recover the plaintext, reverse the order:

1
dead-drop byte → subtraction → ROR 3 → XOR → plaintext

19. Write the Final Solver

Use the dead drop printed by the program:

1
b37a61397fc78d045c6a702fb50ddc0b029817065e948361683f165d

Create:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cat > solve.py <<'PY'
transmission = "b37a61397fc78d045c6a702fb50ddc0b029817065e948361683f165d"

data = bytes.fromhex(transmission)
self_key = 0xaa
seed = ((self_key ^ 0x63) + 7) & 0xff


def ror8(value, bits):
    return ((value >> bits) | (value << (8 - bits))) & 0xff


result = []

for i, value in enumerate(data):
    value = (value - (i ^ 0x17)) & 0xff
    value = ror8(value, 3)
    value ^= (seed + (i * 5)) & 0xff
    result.append(value)

print(bytes(result).decode())
PY

python3 solve.py

Output:

1
CYS{i_am_the_one_who_knocks}

Final Flag

1
CYS{i_am_the_one_who_knocks}

Full Intended Solve Flow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
memory
  ↓
file / strings
  ↓
CYS{crystal_blue_is_too_obvious}  ← fake batch
  ↓
Ghidra
  ↓
argv[0] → memory
  ↓
getenv() → SIGNAL=MIRROR
  ↓
argv[1] → --listen
  ↓
SIGNAL=MIRROR ./memory --listen
  ↓
Dead drop: b37a...
  ↓
follow Dead drop XREF
  ↓
self-reading through /proc/self/exe
  ↓
ELF section .x9
  ↓
rolling mixer → 0xAA
  ↓
reverse final byte transformation
  ↓
CYS{i_am_the_one_who_knocks}

The challenge is designed so both static and dynamic approaches are valid. A strong reverser can reproduce every deterministic value without relying on hidden server-side secrets.

Flag

CYS{i_am_the_one_who_knocks}
Edit on GitHub
Trending Tags
Admin Bot AES-GCM Algorithm Confusion authentication Broken Access Control CRT ECC ECDH ELF Ghidra

© 2026 Cyscom. Some rights reserved.

Using the Jekyll theme Chirpy.

A new version of content is available.