← Back to Writeups
HTBN/AReversing

SEPC

XESXOR8/23/20265 min read
#reversing#htb#n/a

SEPC

Platform: HackTheBox | Category: Reverse | Difficulty: Medium Date: 2026-02-19 | Status: Solved

Description

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.

Solution Approach

The challenge provides a bootable Linux system: bzImage (kernel), initramfs.cpio.gz (filesystem), run.sh (QEMU launch script).

Techniques: xor key recovery, kernel module analysis, rodata extraction, char device protocol reversing, initramfs extraction.

Steps

Analysis

Step 1: Extracting the Filesystem

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.

Step 2: Analyzing the Kernel Module (checker.ko)

The module is not stripped — all symbols are available for analysis. It creates a character device with 4 handlers:

HandlerBehavior
openSingle-open guard; initializes counter = 0, clears state
releaseResets the open flag
writeCopies exactly 1 byte from userspace to BSS buffer
readMain 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).

Step 3: Analyzing the Userspace Binary (checker)

The /checker binary is a statically linked, stripped ELF. Logic:

  1. Prints "Please enter your security key for offline verification"
  2. Reads user input
  3. Opens /dev/checker
  4. For each input byte: write(fd, &byte, 1)read(fd, &result, 1)
  5. result == 0"[X] PASSWORD REJECTED [X]"
  6. 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.

Step 4: Extracting XOR Tables from .rodata

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

Solution

Flag Extraction Script

#!/usr/bin/env python3
"""
SEPC (Secure Enclave) — HackTheBox
Extracting the flag by XORing two tables from .rodata of the kernel module checker.ko
"""

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
])

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
])

flag_bytes = bytes([a ^ b for a, b in zip(t1, t2)])
flag = flag_bytes.decode('ascii')

print(f"Flag: {flag}}}")

Explanation

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{REDACTED}` is added by the userspace binary context (the 35th character is not verified by the module).

### Alternative Approaches

- **Dynamic analysis** — run the system in QEMU, attach GDB to the kernel (`-s -S`), set a breakpoint on the read handler and observe the XOR operation
- **Module patching** — modify `checker.ko` to output the expected bytes to `dmesg` instead of comparing
- **Bruteforce via QEMU** — send all 256 variants byte by byte, determine the correct byte by the response (0 vs 1). Slow, but doesn't require reversing the module

## Flag

HTB{REDACTED}


## Lessons Learned

1. **Start with the init script** — it shows the entire architecture: which modules are loaded, which devices are created, what runs in userspace
2. **Kernel module > userspace binary** — if the `.ko` is not stripped but userspace is stripped, reverse the module first: symbols and handler structure will give you the complete picture
3. **Char device protocol** — `write` sends data to the kernel, `read` receives the result. Understanding this protocol is key to understanding the verification logic
4. **XOR in .rodata** — if you see two arrays of equal length and an XOR operation between them, it's almost certainly a key/flag. Extract the tables and XOR them offline
5. **Count the bytes** — `cmp rax, 0x21` + `ja` means 34 bytes (0–33), not 33. An off-by-one error can cost you the flag