In this write-up
These notes document the October 2026 CTF challenge. Commands, addresses and outputs refer to the supplied challenge environment.
Challenge Summary#
- Binary:
checker— Linux x86-64, statically linked, stripped ELF - Goal: Find an input that the binary accepts
- Flag format:
tibeb{...}
Step 1 — Basic Recon#
file checker bundle.zip run.txt
cat run.txt
chmod +x checker && ./checker
run.txt says:
Linux x86-64 static ELF. chmod +x checker; ./checker
Running with test gives:
Locked.
So the binary is a password checker. A wrong input prints Locked.
Look for interesting strings:
strings -a -t x checker | grep -E 'Locked|TIBEB_XOR_V1'
Output:
77013 Locked.
a30e0 TIBEB_XOR_V1$
Two key addresses:
0x477013→"Locked."0x4a40e0→"TIBEB_XOR_V1$"
Step 2 — Find and Disassemble main#
The binary is stripped, so we need to locate main manually.
readelf -h checker | grep -iE 'entry|type'
Output:
Type: EXEC
Entry point address: 0x401660
Disassemble the entry point:
objdump -d -M intel --start-address=0x401660 --stop-address=0x401700 checker
Important instruction:
401674: mov rdi,0x401785
40167b: call 0x403df0 ; __libc_start_main
So main is at 0x401785.
Disassemble main:
objdump -d -M intel --start-address=0x401785 --stop-address=0x401900 checker
Key parts of the disassembly:
401785: push rbp
...
4017a6: call 0x404920 ; fgets(buffer, 256, stdin)
...
4017ce: call 0x4010a8 ; strcspn(buffer, "\n")
4017d3: mov BYTE PTR [rbp+rax*1-0x110],0x0 ; strip newline
...
4017db: movzx eax,BYTE PTR [rip+0xa290a] # 0x4a40ec
4017e2: movzx eax,al
4017e5: movzx edx,BYTE PTR [rip+0xa2901] # 0x4a40ed
...
401812: mov DWORD PTR [rbp-0x8],eax ; length
...
40181f: call 0x4010b8 ; strlen(buffer)
401824: mov rdx,rax
401827: mov eax,DWORD PTR [rbp-0x8] ; expected length
40182a: cmp rdx,rax
40182d: je 0x401845
40182f: lea rax,[rip+0x757dd] # 0x477013 "Locked."
...
401845: mov DWORD PTR [rbp-0x4],0x0 ; i = 0
40184c: jmp 0x401891
40184e: mov eax,DWORD PTR [rbp-0x4] ; i
401851: movzx eax,BYTE PTR [rbp+rax*1-0x110] ; input[i]
401859: xor eax,0x6d ; input[i] ^ 0x6d
40185c: mov BYTE PTR [rbp-0x9],al
40185f: mov eax,DWORD PTR [rbp-0x4]
401862: add eax,0x10 ; i + 16
401865: mov edx,eax
401867: lea rax,[rip+0xa2872] # 0x4a40e0
40186e: movzx eax,BYTE PTR [rdx+rax*1] ; target[i] at 0x4a40e0 + i + 16 = 0x4a40f0 + i
401872: cmp BYTE PTR [rbp-0x9],al
401875: je 0x40188d
401877: lea rax,[rip+0x75795] # 0x477013 "Locked."
...
40188d: add DWORD PTR [rbp-0x4],1
401891: mov eax,DWORD PTR [rbp-0x4]
401894: cmp eax,DWORD PTR [rbp-0x8] ; i < length
401897: jb 0x40184e
Logic summary:
- Read input, strip newline.
- Expected length is loaded from bytes at
0x4a40ec–0x4a40ef. - If input length ≠ expected length → print
Locked. - For each
ifrom0tolength-1:temp = input[i] ^ 0x6d- Compare
tempwithtarget[i]wheretargetstarts at0x4a40f0. - If mismatch → print
Locked.
- If all match → success.
Step 3 — Extract the Expected Length#
Dump the data section:
objdump -s -j .data checker | head -n 40
Relevant bytes at 0x4a40e0:
4a40e0 54494245 425f584f 525f5631 24000000 TIBEB_XOR_V1$...
4a40f0 19040f08 0f161502 1f320a0c 19083208 .........2....2.
4a4100 55085f5d 5a5c5c5e 0b0b550c 545f0c5d U._]Z\\^..U.T_.]
4a4110 0b5f5a10 00000000 00080000 00000000 ._Z.............
At 0x4a40ec, the bytes are:
24 00 00 00
Little-endian 0x00000024 = 36.
So the input must be exactly 36 characters.
Step 4 — Extract Target Bytes and XOR#
The target array starts at 0x4a40f0. It is 36 bytes long:
19 04 0f 08 0f 16 15 02 1f 32 0a 0c 19 08 32 08
55 08 5f 5d 5a 5c 5c 5e 0b 0b 55 0c 54 5f 0c 5d
0b 5f 5a 10
The check is:
input[i] ^ 0x6d == target[i]
Therefore:
input[i] = target[i] ^ 0x6d
Compute the flag with Python:
target = bytes.fromhex(
"19040f080f1615021f320a0c19083208"
"55085f5d5a5c5c5e0b0b550c545f0c5d"
"0b5f5a10"
)
flag = bytes(b ^ 0x6d for b in target).decode()
print(flag)
Output:
tibeb{xor_gate_e8e207113ff8a92a0f27}
Step 5 — Verify#
Run the checker and paste the flag:
./checker
tibeb{xor_gate_e8e207113ff8a92a0f27}
It should print the success message (e.g. Unlocked. or similar) instead of Locked.
Tricky / Interesting Points#
- Stripped static binary — No symbols like
main. You must findmainby looking at the entry point and the argument passed to__libc_start_main. - Obfuscated length check — The expected length is not an immediate constant. It is read from the data section at
0x4a40ecas a 4-byte little-endian integer. Those bytes happen to be24 00 00 00, which is the ASCII code for'$'(0x24 = 36). This is easy to miss. - Target array hidden next to magic string — The string
TIBEB_XOR_V1$sits at0x4a40e0. The target bytes start 16 bytes later at0x4a40f0. The disassembly uses0x4a40e0 + i + 0x10, so it’s not immediately obvious that the target is separate. - XOR key embedded in instruction — The key
0x6dappears only asxor eax,0x6din the loop. - Newline stripping — The binary uses
strcspn(buffer, "\n")to remove the trailing newline, so your input length must be 36 after stripping.
Final Flag#
tibeb{xor_gate_e8e207113ff8a92a0f27}