$ cat writeup.md…
$ cat writeup.md…
HackTheBox
The challenge provides a bootable Linux system: `bzImage` (kernel), `initramfs.cpio.gz` (filesystem), `run.sh` (QEMU launch script). The goal is to reverse a kernel module that implements password verification through a character device and extract the flag from the `.rodata` section.
We've extracted an embedded operating system running on an intercepted deep-space satellite launched by Arodor. If we can breach the secure enclave and extract their security mechanisms, we can crack their encrypted communications.
The challenge provides a bootable Linux system: bzImage (kernel), initramfs.cpio.gz (filesystem), run.sh (QEMU launch script). The goal is to reverse a kernel module that implements password verification through a character device and extract the flag from the .rodata section.
Unpacking initramfs.cpio.gz reveals the structure:
/init — boot script
/checker — ELF 64-bit statically linked stripped (userspace)
/checker.ko — Linux kernel module (not stripped)
/initramfs.cpio.gz — internal initramfs (empty/incomplete)
The /init script shows the interaction architecture:
insmod checker.ko mount -t proc none /proc mount -t sysfs none /sys mknod /dev/checker c 137 0 chmod 0666 /dev/checker exec /checker
Key point: the kernel module checker.ko is loaded, a character device /dev/checker is created with major number 137, then the userspace binary /checker is launched which communicates with the kernel through this device.
The module is not stripped — all symbols are available for analysis. It creates a character device with 4 handlers:
| Handler | Behavior |
|---|---|
| open | Single-open guard; initializes counter = 0, clears state |
| release | Resets the open flag |
| write | Copies exactly 1 byte from userspace to BSS buffer |
| read | Main verification logic — byte-by-byte XOR comparison |
The read handler implements byte-by-byte verification:
// Pseudocode of read handler if (count != 1) return -EINVAL; byte expected = rodata[0x60 + counter] ^ rodata[0x20 + counter]; if (user_byte != expected) { result = 0; // FAIL — byte mismatch } else { counter++; if (counter > 33) { result = 2; // SUCCESS — all 34 bytes matched counter = 0; } else { result = 1; // CONTINUE — need more bytes } } copy_to_user(buf, &result, 1);
Critical comparison in assembly: cmp rax, 0x21 (33) with ja (jump if above) — 34 bytes are verified (indices 0–33).
The /checker binary is a statically linked, stripped ELF. Logic:
"Please enter your security key for offline verification"/dev/checkerwrite(fd, &byte, 1) → read(fd, &result, 1)result == 0 → "[X] PASSWORD REJECTED [X]"result == 2 → "] PASSWORD VERIFIED ["Protocol: userspace sends one byte at a time via write, the kernel compares and returns status via read. This is a classic userspace↔kernel communication pattern through a char device.
Two 34-byte tables in the .rodata section of the kernel module:
Table 1 (offset 0x20):
b4 e4 e9 ab 09 36 4a c2 a5 14 e5 35 66 c3 99 14
5a 34 f1 18 91 7d 23 70 fa b5 3d fa 3d e5 00 d1
69 15
Table 2 (offset 0x60):
fc b0 ab d0 6e 44 2b a0 c7 7d 8b 52 39 a7 ad 60
6e 6b 97 6a a1 10 7c 1b c9 c7 53 c9 51 d0 70 e5
0a 26
#!/usr/bin/env python3 """ SEPC (Secure Enclave) — HackTheBox Extracting the flag by XORing two tables from .rodata of the kernel module checker.ko """ # Table 1: rodata offset 0x20 (34 bytes) t1 = bytes([ 0xb4, 0xe4, 0xe9, 0xab, 0x09, 0x36, 0x4a, 0xc2, 0xa5, 0x14, 0xe5, 0x35, 0x66, 0xc3, 0x99, 0x14, 0x5a, 0x34, 0xf1, 0x18, 0x91, 0x7d, 0x23, 0x70, 0xfa, 0xb5, 0x3d, 0xfa, 0x3d, 0xe5, 0x00, 0xd1, 0x69, 0x15 ]) # Table 2: rodata offset 0x60 (34 bytes) t2 = bytes([ 0xfc, 0xb0, 0xab, 0xd0, 0x6e, 0x44, 0x2b, 0xa0, 0xc7, 0x7d, 0x8b, 0x52, 0x39, 0xa7, 0xad, 0x60, 0x6e, 0x6b, 0x97, 0x6a, 0xa1, 0x10, 0x7c, 0x1b, 0xc9, 0xc7, 0x53, 0xc9, 0x51, 0xd0, 0x70, 0xe5, 0x0a, 0x26 ]) # XOR of two tables = expected password flag_bytes = bytes([a ^ b for a, b in zip(t1, t2)]) flag = flag_bytes.decode('ascii') print(f"Flag: {flag}}}") # HTB{grabbing_d4t4_fr0m_k3rn3l5p4c3}
The kernel module verifies 34 bytes (the flag content without the closing curly brace }). Each byte is checked as input_byte == table1[i] ^ table2[i]. A simple XOR of the two tables from .rodata yields:
HTB{grabbing_d4t4_fr0m_k3rn3l5p4c3
The closing brace } is added by the userspace binary context (the 35th character is not verified by the module).
.ko is not stripped but userspace is stripped, reverse the module first: symbols and handler structure will give you the complete picturewrite sends data to the kernel, read receives the result. Understanding this protocol is key to understanding the verification logiccmp rax, 0x21 + ja means 34 bytes (0–33), not 33. An off-by-one error can cost you the flag-s -S), set a breakpoint on the read handler and observe the XOR operationchecker.ko to output the expected bytes to dmesg instead of comparing$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar