IDEH is a Moroccan CTF competition hosted by INPT. During this event, I solved many challenges while playing solo lmao, and it was an amazing experience overall. The competition was intense, fun, and full of learning opportunities. I also had the chance to meet and connect with many great people from the community big thanks to the organizers and especially to the challenge authors Have fun reading, and see you next year.
📎 Attachments
IDEH CTF — Exfil (Android Reverse Engineering)
Category: Mobile / Reverse Engineering
Flag format: IDEH{…}
1) Challenge Overview
We are given an Android APK named exfil.apk. The goal is to analyze the application and recover a hidden secret stored inside it.
Initial assumption: The application hides sensitive data and exposes some functionality through a WebView JavaScript bridge.
Step 1 — Static analysis
First, decompile the APK.
jadx exfil.apk -d outThis gives us:
out/ ├── resources/ └── sources/Searching for JavaScript bridges:
grep -R "JavascriptInterface" -n out/sourcesOutput:
sources/com/cit/ideh/exfil/DocsActivity.javasources/i1/a.javaThe interesting class is:
sources/i1/a.javaThis file is injected into a WebView as the JavaScript object IDEH.
Step 2 — Inspecting the exposed interface
Opening sources/i1/a.java:
nano sources/i1/a.javaWe find:
@JavascriptInterfacepublic final String readVault() { try { return a(); } catch (Exception unused) { return "error"; }}And the real logic is inside function a().
Step 3 — Understanding the crypto
The method a() does the following:
- Loads encrypted blob
InputStream is = getResources().openRawResource(R.raw.flag_blob);This corresponds to:
resources/res/raw/flag_blob- Splits the blob
nonce = blob[0..11]ciphertext = blob[12..]So the file is:
[ 12 bytes nonce | encrypted data + GCM tag ]- Builds AES key from APK signature
Signature sig = packageManager.getPackageInfo(...).signingInfo.getApkContentsSigners()[0];byte[] digest = SHA256(sig.toByteArray());SecretKeySpec key = new SecretKeySpec(digest[0:16], "AES");So:
AES key = first 16 bytes of SHA256(apk signing certificate)- Uses package name as AAD
cipher.updateAAD("com.cit.ideh.exfil".getBytes());So the authenticated data is:
AAD = "com.cit.ideh.exfil"- Decrypts using AES-GCM
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");cipher.init(DECRYPT, key, new GCMParameterSpec(128, nonce));plaintext = cipher.doFinal(ciphertext);The output is directly returned as a string.
Step 4 — Extracting the flag
To decrypt, we need:
| Element | Source |
|---|---|
| Encrypted blob | res/raw/flag_blob |
| Nonce | First 12 bytes |
| Ciphertext | Remaining bytes |
| AES key | SHA256(signing cert)[0:16] |
| AAD | "com.cit.ideh.exfil" |
After reproducing this logic and decrypting the blob, we obtain:
IDEH{m4ster_0f_4ndro1d}
IDEH CTF — Frozen Truth
📎 Attachments
Category: Reverse Engineering
Format: IDEH{…}
1) Executive Summary
The challenge provides a Linux binary named frozen_truth acting as a flag checker. Initial execution fails due to a GLIBC version mismatch, which strongly suggests that static reverse engineering is required.
By identifying the binary as a PyInstaller-packed Python 3.8 executable, extracting its contents, and reversing the embedded challenge.pyc, we recover a simple Caesar-style decoding routine with a constant shift.
This leads directly to the final flag.
Step 1 — File Identification
file frozen_truthOutput:
frozen_truth: ELF 64-bit LSB executable, x86-64, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0Trying to execute it:
./frozen_truthResult:
libpython3.8.so.1.0: version `GLIBC_2.38' not foundSo instead of running it, we move to static analysis.
Running strings reveals:
PYZ-00.pyzpyi-runtime-tmpdirpyiboot01_bootstraplibpython3.8.so_MEIPASSThis is a classic PyInstaller executable.
Step 2 — Extracting the PyInstaller Archive
We use pyinstxtractor-ng:
python3 -m pip install --user pyinstxtractor-ngpyinstxtractor-ng frozen_truthThis creates:
frozen_truth_extracted/Listing files:
challenge.pycPYZ.pyzPYZ.pyz_extracted/libpython3.8.so.1.0...The file of interest is clearly:
challenge.pycStep 3 — Decompiling the Python Bytecode
We use uncompyle6:
python3 -m pip install --user uncompyle6uncompyle6 challenge.pycRecovered Python source (simplified):
SHIFT = 3
encoded_flag = b'...'
def decode(data): return bytes([(b - SHIFT) % 256 for b in data])
def main(): user = input("Enter flag: ").encode() if user == decode(encoded_flag): print("Correct!") else: print("Wrong!")
if __name__ == "__main__": main()Key observations:
The flag is not dynamically computed
It is stored as encoded_flag
Each byte is decoded with:
decoded_byte = encoded_byte - 3Simple Caesar shift on raw bytes.
Step 4 — Recovering the Flag
We dump encoded_flag and decode it:
encoded = b"..."print(bytes([(b - 3) % 256 for b in encoded]))Output:
IDEH{compiling_wi7h_pyins7all3r_is_no7_s3cur3}
IDEH CTF – Insecured IoT Device
📎 Attachments
Category: Forensics
Flag format: IDEH{}
Executive Summary
In this challenge, we are given a raw binary dump extracted from an insecure IoT device. The goal is to analyze the firmware, extract its filesystem, locate suspicious components, and recover a hidden flag.
By carving the firmware image, mounting the embedded filesystem, and analyzing internal scripts and data files, we discover that the flag is split across several locations and encoded in Base64. After decoding and reassembling these parts, we recover the final flag.
1) Provided File
router_dump.binThis appears to be a full flash dump / firmware image from an IoT router-like device.
Step 1 – Initial Recon
First, identify the file:
file router_dump.binoutput :
router_dump.bin: dataNo clear format, so next step is firmware carving.
Step 2 – Firmware Analysis with binwalk
We scan the dump to locate embedded filesystems and compressed sections.
binwalk -e router_dump.binResult shows something similar to:
DECIMAL HEXADECIMAL DESCRIPTION------------------------------------------------65536 0x10000 Squashfs filesystem, little endianBinwalk automatically extracts it into:
_extracted/└── squashfs-root/Step 3 – Exploring the Filesystem
Navigate into the extracted filesystem:
cd _extracted/squashfs-rootlsTypical embedded Linux layout:
bin/ etc/ lib/ usr/ var/ sbin/ tmp/This confirms the presence of a full Linux-based IoT firmware.
Step 4 – Hunting for Interesting Files
We search for scripts, configs, or suspicious data:
find . -type f | grep -i "telemetry\|state\|device\|flag"Interesting files discovered:
./etc/device.id./bin/print./var/lib/telemetry/state.cacheStep 5 – Static Analysis of Files
1️⃣ /etc/device.id
cat etc/device.idOutput:
SURFSGtjMDBsDecode:
echo SURFSGtjMDBs | base64 -dResult:
IDEH{c00l2️⃣ /bin/print
Checking the file:
strings bin/printWe find an embedded Base64 string:
X2Yxcm13NHJlDecode:
echo X2Yxcm13NHJl | base64 -dResult:
_f1rmw4re3️⃣ /var/lib/telemetry/state.cache
cat var/lib/telemetry/state.cacheOutput:
X2FuNGx5czFzfQ==Decode:
echo X2FuNGx5czFzfQ== | base64 -dResult:
_an4lys1s}Step 6 – Rebuilding the Flag
Collected parts:
IDEH{c00l_f1rmw4re_an4lys1s}Final flag:
IDEH{c00l_f1rmw4re_an4lys1s}
IDEH Rev — “This is the end.”
📎 Attachments
Category: Reverse Engineering
Flag format: IDEH{…}
1) Executive Summary
The binary prompts for input, enforces a strict length of 26, then runs a small stack-based virtual machine over a bytecode program stored in .rodata. The VM computes a sum of 26 byte-sized checks of the form:
(bi⊕bi+1)⊕ciand succeeds only if the sum is 0, which (because the sum can’t overflow 32-bit) forces every term to be 0, giving direct XOR constraints between consecutive characters. Using the known flag prefix IDEH{ resolves the full flag.
2) Basic Recon
file chall# chall: ELF 64-bit LSB executable, x86-64, statically linked, stripped3) Locating main via String Xrefs
The prompt string “oui?” lives in .rodata. We locate it and then find its xref in .text.
Using offsets (from readelf -S):
.rodata vaddr: 0x47f000
.rodata file offset: 0x7f000
The prompt is at vaddr 0x47f010, and in the disassembly we find:
401825: lea rdi, [rip+0x7d7e4] # 0x47f010 -> "oui?"401849: call puts40185d: call fgets...401887: cmp rax, 0x1a # length check == 2640189c: call 0x401a30 # VM validator(program, size, input)4018a5: je success4018a7: lea rdi, "non..."4018ae: call putsSo the flow is:
Print prompt “oui?”
Read input (up to 0x100 bytes)
Strip newline
Enforce length == 26
Run validator sub_401a30(program_ptr, program_len, input)
If validator returns 0 → success message, else failure
4) Understanding the Validator VM (0x401a30)
4.1 Bytecode location
The VM is called with:
rdi = 0x4849a0 (bytecode program)
esi = 0x1d9 (program size = 473 bytes)
rdx = input
So the bytecode begins at .rodata:0x4849a0.
4.2 Instruction set
The VM is a stack machine holding 32-bit integers in a local array.
It dispatches on opcode 0..5 via a jump table. The table resolves to handlers:
| Opcode | Meaning | Notes |
|---|---|---|
0 | PUSH_CONST imm32 | 5-byte instruction |
5 | PUSH_INPUT idx32 | pushes input[idx] as integer, 5-byte instruction |
2 | XOR | 1-byte instruction |
1 | ADD | 1-byte instruction |
3 | (unused here) SUB | present in VM but not used by this program |
4 | (unused here) MUL | present in VM but not used by this program |
Important detail: this VM uses variable-length instructions:
PUSH_* = 1 opcode byte + 4 imm bytes (total 5)
arithmetic ops = opcode only (total 1)
5) Decompiling the Bytecode Program Logic
When decoding the bytecode, we see:
PUSH_INPUT appears 52 times
XOR appears 52 times
ADD appears 26 times
PUSH_CONST appears 27 times
This strongly suggests 26 independent checks being summed.
Reconstructing the stack expression reveals the VM computes:

and success requires result == 0.
5.1 Why this becomes 26 independent constraints
So the sum is at most:
26×255=6630
No 32-bit overflow can occur, so:

Thus for each i:

6) Extracted Constants
From the bytecode, the constants are:
c[0..25] =[13, 1, 13, 51, 50, 22, 55, 91, 112, 40, 2, 7, 49, 19, 124, 95, 0, 32, 57, 69, 108, 9, 27, 120, 72, 52]So:
b1 = b0 XOR 13
b2 = b1 XOR 1
…
b25 = b24 XOR 72
and finally the cycle condition matches automatically (b25 XOR b0 == 52)
This means the whole string is determined by b0.
7) Recovering the Flag
We know the required format begins with IDEH{, so:
b0 = ‘I’
Generate the chain:
b[0] = 'I'b[i+1] = b[i] ^ c[i]8) Solver Script (Extracts from the Binary)
This script:
extracts the program bytes from .rodata at 0x4849a0
decodes the VM bytecode (variable-length)
reconstructs the c[i] constants
rebuilds the flag using b0=‘I’
#!/usr/bin/env python3from pathlib import Path
BIN = "chall"
RO_VA = 0x47F000RO_OFF = 0x7F000
PROG_VA = 0x4849A0PROG_SIZE = 0x1D9
def va_to_off(va): return RO_OFF + (va - RO_VA)
def decode_vm(prog, size): pc = 0 ins = [] while pc < size: op = prog[pc] if op in (0, 5): imm = int.from_bytes(prog[pc+1:pc+5], "little") ins.append((pc, op, imm)) pc += 5 else: ins.append((pc, op, None)) pc += 1 return ins
def main(): blob = Path(BIN).read_bytes() prog = blob[va_to_off(PROG_VA):va_to_off(PROG_VA) + PROG_SIZE + 4] ins = decode_vm(prog, PROG_SIZE)
stack = [] def push(x): stack.append(x) def pop(): return stack.pop() def XOR(a, b): if a[0] == "const" and b[0] != "const": a, b = b, a return ("xor", a, b) def ADD(a, b): ta = a[1] if a[0] == "sum" else [a] tb = b[1] if b[0] == "sum" else [b] return ("sum", ta + tb)
for _, op, imm in ins: if op == 0: push(("const", imm & 0xFFFFFFFF)) elif op == 5: push(("byte", imm)) elif op == 2: b = pop(); a = pop() push(XOR(a, b)) elif op == 1: b = pop(); a = pop() push(ADD(a, b))
final = stack[-1][1] c = [None] * 26
for t in final: left, right = t[1], t[2] ci = right[1] & 0xFF a, b = left[1], left[2] i, j = a[1], b[1] if j == (i + 1) % 26: c[i] = ci else: c[j] = ci
b = [0] * 26 b[0] = ord("I") for i in range(25): b[i+1] = b[i] ^ c[i]
print("".join(map(chr, b)))
if __name__ == "__main__": main()Final Flag:
IDEH{I_h3Ckin_L0ooOv3_VM5}
Verify:
echo -n "IDEH{I_h3Ckin_L0ooOv3_VM5}" | ./chall# oui?# OUIOUIOUIOUIOUIOUIOUIOUIOUIOUIOUIOUIOUIOUIOUIOUIOUIOUIIDEH — Deception.exe
Category: Malware Analysis / Reverse Engineering / Misc
Flag format: IDEH{...}
Binary: Deception.exe
📎 Attachments
1) Executive Summary
Our SOC team detected a suspicious Windows executable named Deception.exe.
Static analysis revealed that the binary uses process-injection related APIs and contains an embedded encrypted payload.
By reversing the binary, we identified an RC4 decryption routine used to decrypt a hidden buffer stored in the .rdata section. Re-implementing this routine allowed us to recover the flag.
Step 1 — Initial Triage
Identify the file:
file Deception.exeOutput:
PE32+ executable (console) x86-64, for MS WindowsHash for tracking:
sha256sum Deception.exe50ecfaaefcd8b5b169123618169d9a0696dcbfa81f7991edb93bbb4128d676f8PE inspection (DIE / PE-Studio / CFF Explorer):
Architecture: x64
Compiler: MinGW (GCC)
19 sections
Contains
.debug_*DWARF sections
Unsigned binary
Step 2 — Suspicious Indicators
Imports analysis:
rabin2 -i Deception.exe
Interesting APIs:
CreateProcessA
ReadProcessMemory
WriteProcessMemory
VirtualProtect
VirtualQuery
NtQueryInformationProcess
ResumeThread
Strong indicators of process hollowing / injection malware.
Strings:
strings Deception.exeNotable strings:
C:\Windows\System32\WerFault.exe-u -p %d -s %dMasquerading as Windows Error Reporting.
Step 3 — Reverse Engineering
Loaded into IDA.
Main findings:
main()calls a function initializing a 256-byte state array.
Performs byte swaps and modular indexing.
XORs a buffer with a generated keystream.
This exactly matches the RC4 algorithm (KSA + PRGA).
Step 4 — Extracting Key and Ciphertext
From .rdata, referenced directly by the RC4 function:
RC4 Key (9 bytes)23 82 71 87 12 23 12 FF EEEncrypted Buffer (27 bytes)
13 39 97 51 CA 44 49 D0 4B 89 10 F0 61 8A 27 7450 F9 B6 52 9C 26 1E B4 E2 29 EDStep 5 — Decryption Script
def rc4(key, data): 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 b in data: i = (i + 1) % 256 j = (j + S[i]) % 256 S[i], S[j] = S[j], S[i] out.append(b ^ S[(S[i] + S[j]) % 256])
return bytes(out)
key = bytes.fromhex("23827187122312ffee")ct = bytes.fromhex("13399751ca4449d04b8910f0618a277450f9b6529c261eb4e229ed")
print(rc4(key, ct))Output:
IDEH{D3c3pt10n_h1d35_tru7h}have fun … always ON TOP?!

