← Back to Writeups
HTBN/AReversing

SEPC (Secure Enclave)

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

SEPC (Secure Enclave)

Platform: HackTheBox | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: D3v0o0Nu11 | 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.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: HackTheBox | ID: 20260219_hackthebox_sepc
  • Tags: xor, linux_kernel_module, char_device, qemu, initramfs, kernel_reversing, elf_static, cpio
  • Indicators: bzImage + initramfs.cpio.gz + run.sh (QEMU boot), insmod .ko in init script, char device with custom major number, XOR of two rodata tables, byte-by-byte write/read protocol through /dev/ device
  • Source: 20260219_hackthebox_sepc.md

Foothold

Vulnerability / Misconfiguration

  1. Xor_key_recovery
  2. Kernel_module_analysis
  3. Rodata_extraction
  4. Char_device_protocol_reversing
  5. Initramfs_extraction
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • xor_key_recovery
  • kernel_module_analysis
  • rodata_extraction
  • char_device_protocol_reversing
  • initramfs_extraction
  • Tags: xor, linux_kernel_module, char_device, qemu, initramfs, kernel_reversing, elf_static, cpio

Original Writeup

<details><summary>Click to expand original content</summary>

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:

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
"""

# 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 protocolwrite 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 bytescmp 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
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR