Four random challenges, four different techniques(nexsus)
Ran through a batch of random challenges in one sitting, writing all four up together instead of splitting them into separate posts. Each one uses a completely different technique, so it works well as one combined set of notes.
Part 1: A stack string built byte by byte
This one only got partially documented on my end — I’ve only got a single screenshot from this session, so this section covers what’s decodable from it and stops where the screenshot stops.
Opening the binary in IDA and landing near the entry point, the very first thing in the function is a long, repetitive block of single-byte stores onto the stack:
sub rsp, 40h
mov [rbp+var_1], 6Eh
mov [rbp+var_2], 65h
mov [rbp+var_3], 78h
mov [rbp+var_4], 75h
mov [rbp+var_5], 73h
mov [rbp+var_6], 7Bh
mov [rbp+var_7], 64h
mov [rbp+var_8], 31h
mov [rbp+var_9], 73h
mov [rbp+var_A], 34h
mov [rbp+var_B], 73h
mov [rbp+var_C], 73h
mov [rbp+var_D], 33h
mov [rbp+var_E], 6Dh
mov [rbp+var_F], 62h
mov [rbp+var_10], 6Ch
mov [rbp+var_11], 33h
mov [rbp+var_12], 5Fh
mov [rbp+var_13], 79h
mov [rbp+var_14], 30h
mov [rbp+var_15], 75h
mov [rbp+var_16], 72h
mov [rbp+var_17], 5Fh
mov [rbp+var_18], 64h
mov [rbp+var_19], 30h
mov [rbp+var_1A], 75h
mov [rbp+var_1B], 62h
mov [rbp+var_1C], 74h
...
This is a stack string — instead of a plaintext string literal in
.rodata where it’d show up immediately under Strings, the string gets
written onto the stack one byte at a time as immediate values baked
directly into the instructions. It defeats a plain string search but does
nothing against just reading the immediates back off, since each mov is
right there in the disassembly holding one ASCII byte.
Reading the immediates in order and converting each to its character:
6E 65 78 75 73 7B 64 31 73 34 73 73 33 6D 62 6C 33 5F 79 30 75 72 5F 64 30 75 62 74
n e x u s { d 1 s 4 s s 3 m b l 3 _ y 0 u r _ d 0 u b t}
That gives nexus{d1s4ss3mbl3_y0ur_d0ubt}, already clearly reading as
“disassemble_your_doubt(s)”. The stack allocation is sub rsp, 40h (64
bytes), so there’s more string past var_1C that this screenshot doesn’t
capture — I don’t have the rest of the byte list to finish the decode. If
I dig up the missing part of this session later I’ll come back and close
this section out properly.
Part 2: Hacking a game’s score with Cheat Engine
A little arcade-style shooter with a score counter, a “High” score, and a “Target” that isn’t realistically reachable by actually playing.
Step 1: Sizing up the target
The HUD shows:
Score: 500Target: 1,000,000High: 500
A target that far out of reach usually means the win condition is a
straightforward score >= target check somewhere in the update loop, and
the score itself is a plain variable sitting in process memory — no need
to actually earn it.
Step 2: Attaching Cheat Engine
Paused the game first, so the value can’t drift mid-scan, then attached
Cheat Engine to the process (00001BEC-chall.exe in the title bar).

Step 3: Exact Value scan
Since the current score is already known (500), this goes straight to an Exact Value scan instead of the usual unknown-initial-value → narrow-down process:
- Scan Type:
Exact Value - Value Type:
4 Bytes - Value:
500
One scan, one result: address 16F78C82118, value 500, matching both
the “Previous” and “First” columns.
Step 4: Editing the value
Double-clicked the Value field for that address and set it to 1000000,
matching the target. Cheat Engine writes it directly into the process’s
memory.
Step 5: Resuming
Unpaused the game. The next check of score >= target passes immediately,
triggering the win state and drawing the flag on screen:

nexus{ch4ng3_th3_rul35_br34k_th3_g4m3}
Part 3: An env-var-gated crackme — XOR, rotation, and base64
An ELF binary this time. No password prompt — the “input” is an environment variable the binary expects to already be set before it does anything.
Step 1: Running it cold
$ ./chall
Error: DECRYPT_KEY not set
Setting an arbitrary value gets further, but not much:
$ export DECRYPT_KEY="KVP3msmE90DFycH+x9vLwqfAyrPbw62iYfQN2A=="
$ ./chall
Invalid key
(That guess was the base64 blob from config.bin itself, just to rule out
the key and the payload being the same thing. They weren’t.)

Step 2: Decompiling main
F5 in IDA on main lays out the whole check:
v16[1] = __readfsqword(0x28u);
config_key = getenv("DECRYPT_KEY", argv, envp);
if ( config_key )
{
v4 = config_key;
v5 = j_strlen_ifunc(config_key);
v6 = 0;
if ( v5 == 20 )
{
while ( EXP[v6] == (__ROL1__(*(_BYTE *)(v4 + v6), 3) ^ 0xAA) )
{
if ( ++v6 == 20 )
{
key = fopen64("config.bin", "rb", 20, EXP);
v9 = key;
if ( !key )
return main_cold();
fseek(key, 0, 2);
v10 = ftell(v9);
fseek(v9, 0, 0);
v11 = malloc(v10 + 1);
_fread_chk(v11, v10 + 1, 1, v10, v9);
*(_BYTE *)(v11 + v10) = 0;
fclose(v9);
v12 = b64dec(v11, v10, v16);
free(v11);
v13 = v16[0];
v14 = malloc(v16[0] + 1LL);
if ( v13 )
{
for ( i = 0; i != v13; ++i )
*(_BYTE *)(v14 + i) = __ROR1__(*(_BYTE *)(v12 + i) ^ *(_BYTE *)(v4 + i % 0x14), 3);
}
*(_BYTE *)(v14 + v13) = 0;
puts(v14);
free(v12);
}
}
}
}
fwrite("Invalid key\n", 1, 12, stderr);

Broken down:
getenv("DECRYPT_KEY", ...)pulls the env var. Missing entirely → theDECRYPT_KEY not setmessage.strlen(config_key) == 20— hard length gate, same idea as the XOR crackme from before: wrong length, immediate reject.- The
whileloop validates each byte with a two-step transform:EXP[i] == (ROL(key[i], 3) ^ 0xAA). - If all 20 bytes pass, it opens
config.bin, reads it, base64-decodes it, and decrypts the result using the same key cycled every 20 bytes:output[i] = ROR(decoded[i] ^ key[i % 20], 3).
Two layers: EXP exists purely to validate the key, and config.bin is a
separate encrypted payload that only gets decrypted once the key checks
out.
Step 3: The EXP array
EXP is a hardcoded byte array sitting in .rodata:
.rodata:0048918A public EXP
EXP db 78h, 69h, 0Bh, 23h, 38h, 50h, 0D9h, 33h, 19h, 33h, 39h
db 50h, 8, 39h, 23h, 0B1h, 0Bh, 60h, 2 dup(2Bh), 1Ch dup(0)

Read out (2 dup(2Bh) = two more 0x2B bytes, 1Ch dup(0) = 28 bytes of
trailing zero padding, irrelevant since the loop only checks the first 20):
78 69 0B 23 38 50 D9 33 19 33 39 50 08 39 23 B1 0B 60 2B 2B
Step 4: Undoing the transform
Rather than doing the bit rotation by hand for 20 bytes, I ran it through two online tools.
First, undo the XOR — dcode.fr’s XOR Cipher tool, hex key AA:

That strips the ^ 0xAA, leaving ROL(key[i], 3):
D2 C3 21 89 92 FA 73 99 B3 99 93 FA A2 93 89 1B A1 CA 82 82
A left-rotate undoes with a right-rotate of the same amount — CyberChef,
Rotate right, amount 3:

5A 78 24 31 52 5F 6E 33 76 33 72 5F 54 72 31 63 34 59 50 50
Hex to ASCII:
Zx$1R_n3v3r_Tr1c4YPP

Step 5: Cleaning up the transcription
A few of those characters are ambiguous glyphs in a monospace disassembly
font — 0/8, $/4, 0/P all look close enough to misread when
copying 20 hex bytes by eye. n3v3r and Tr1c4 clearly want to read as
leetspeak “never” and “Trick”, so the $ and the trailing PP were the
likely misreads. Swapped those and tested directly against the binary:
$ export DECRYPT_KEY="Zx41R_n3v3r_Tr1c4Y00"
$ ./chall
nexus{3nv_v4r5_4r3_p0w3rful}

Step 6: Full math, start to finish
key[i] = ROR( EXP[i] ^ 0xAA, 3 ) # → "Zx41R_n3v3r_Tr1c4Y00"
config.bin (base64) → decoded bytes[i]
flag[i] = ROR( decoded[i] ^ key[i % 20], 3 ) # → decrypted flag
EXP: 78 69 0B 23 38 50 D9 33 19 33 39 50 08 39 23 B1 0B 60 2B 2B
key: Zx41R_n3v3r_Tr1c4Y00
config.bin (base64): KVP3msmE90DFycH+x9vLwqfAyrPbw62iYfQN2A==
decrypted: nexus{3nv_v4r5_4r3_p0w3rful}
Part 4: A flag built from word/byte pairs, reordered using a notes file
This one comes with a companion notes.txt sitting next to the binary
(tapi) — a list of made-up fantasy-style words, followed by the flag
itself in plaintext at the bottom:
Crerceon
Ezains
Ummufh
Zonnu
Vinzo
Cuzads
Myrrh
Ohols
Tarquts
Azuris
Mohtus
Pheilons
Throrqiek
Khehlan
Gozreth
Lendrens
Munis
Inphas
Thauvi
Pakroith
Drathis
Throquels
Krolkel
Orthis
Creiqex
Zimil
Zohir
Honzor
Falnain
Ukteils
nexus{th3_c0d3_l135_t0_1ts3lf}
So the flag is already sitting right there. What’s more interesting is that the binary independently reconstructs the exact same flag if you process it correctly, which is worth documenting since it’s the actual mechanism behind the challenge.
Step 1: Looking at main
tapi is an unstripped ELF, so main is easy to read directly. It’s a
long chain of calls, each one loading a word string and a single byte
before calling a helper:
lea rax, aXaroth ; "Xaroth"
mov rsi, rax
mov edi, 5Ah ; 'Z'
call func_01
lea rax, aDrathis ; "Drathis"
mov rsi, rax
mov edi, 74h ; 't'
call func_02
lea rax, aCuzads ; "Cuzads"
mov rsi, rax
mov edi, 7Bh ; '{'
call func_03
lea rax, aBelthor ; "Belthor"
mov rsi, rax
mov edi, 51h ; 'Q'
call func_04
lea rax, aMyrkul ; "Myrkul"
mov rsi, rax
mov edi, 57h ; 'W'
call func_05
lea rax, aThrorqiek ; "Throrqiek"
mov rsi, rax
mov edi, 64h ; 'd'
call func_06
lea rax, aEzains ; "Ezains"
...

Each call passes one word plus one raw byte into what IDA guesses is a
process_char(word, byte)-style helper, called once per character.
Step 2: Not every word belongs to the flag
Further down, more of these calls show up:
call func_11
lea rax, aVelkor ; "Velkor"
mov rsi, rax
mov edi, 41h ; 'A'
call func_12
lea rax, aKrolkel ; "Krolkel"
mov rsi, rax
mov edi, 5Fh ; '_'
call func_13
lea rax, aOhols ; "Ohols"
mov rsi, rax
mov edi, 68h ; 'h'
call func_14
lea rax, aUkteils ; "Ukteils"
mov rsi, rax
mov edi, 7Dh ; '}'
call func_15
lea rax, aZephron ; "Zephron"
mov rsi, rax
mov edi, 4Bh ; 'K'
call func_16
lea rax, aThauvi ; "Thauvi"
mov rsi, rax
mov edi, 35h ; '5'
call func_17
lea rax, aQorthis ; "Qorthis"
mov rsi, rax
mov edi, 4Dh ; 'M'
call func_18

Cross-referencing the words used in main against notes.txt: we just compare them with names in the notes.txt with the word bytes and yea
Step 3: The actual ordering
The call order in main isn’t the right order to read the bytes in —
Cuzads ({) gets called third in main, but it’s the 6th word in
notes.txt. The correct approach is:
- For every call whose word also appears in
notes.txt, record the(word, byte)pair. - Ignore calls whose word doesn’t appear in
notes.txtat all — those are just extra, unrelated calls. - Sort the recorded pairs by the position of their word in
notes.txt, top to bottom, not by call order inmain. - Concatenate the bytes as ASCII in that order.
notes.txt has 31 words, but one of them — the lone t on its own line —
doesn’t correspond to any real character and gets skipped, leaving exactly
30 words to fill the 30 characters of nexus{...}.
Checking that against the known word/byte pairs confirms it lines up exactly with the word’s position in the list:
| word rank in notes.txt | word | byte | flag position | flag char |
|---|---|---|---|---|
| 6 | Cuzads | 7Bh | 5 | { |
| 9 | Ohols | 68h | 7 | h |
| 14 | Throrqiek | 64h | 12 | d |
| 20 | Thauvi | 35h | 18 | 5 |
| 22 | Drathis | 74h | 20 | t |
| 24 | Krolkel | 5Fh | 22 | _ |
| 31 | Ukteils | 7Dh | 29 | } |
Filling in the rest of the word/byte pairs the same way and reading them
off in notes.txt order reconstructs:
nexus{th3_c0d3_l135_t0_1ts3lf}
— matching the plaintext copy already sitting at the bottom of
notes.txt.
Wrap-up
nexus{d1s4ss3mbl3_y0ur_d0ubt...} (partial — stack string)
nexus{ch4ng3_th3_rul35_br34k_th3_g4m3} (Cheat Engine memory edit)
nexus{3nv_v4r5_4r3_p0w3rful} (env-var XOR + ROL chain)
nexus{th3_c0d3_l135_t0_1ts3lf} (word list / byte reordering)
Four different techniques: reading immediates straight out of a stack string, live memory editing with no static analysis at all, a two-layer XOR/rotate cipher with a base64 payload behind it, and a lookup-and-reorder puzzle across a set of word/byte pairs.
Special Thanks Towards the creator @Zx41R