← Back to Writeups
HTBMediumReversing

SEPC (Secure Enclave)

XESXOR9/10/20265 min read
#reversing#htb#medium#xor_key_recovery#kernel_module_analysis#rodata_extraction#char_device_protocol_reversing#initramfs_extraction#linux

SEPC (Secure Enclave)

Platform: HackTheBox | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-02-19 | Status: Solved Techniques: xor_key_recovery, kernel_module_analysis, rodata_extraction, char_device_protocol_reversing, initramfs_extraction

Summary

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.

SEPC (Secure Enclave) — HackTheBox

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.

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:

sh
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:

c
// 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

python
#!/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{REDACTED}

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

The closing brace } is added by the userspace binary context (the 35th character is not verified by the module).

Lessons

  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

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