← Back to Writeups
HTBN/AReversing

Chibile

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

Chibile

Platform: Sekai2026 | Category: Reversing | Type: Challenge | Difficulty: Hard | OS: Windows | Author: D3v0o0Nu11 | Date: 2026-06-28 | Status: Solved Techniques: custom_hash_emulation, dynamic_unpacking, key_extraction, protocol_reimplementation, unicorn_emulation

Summary

Task: Windows game client with custom anti-cheat attestations and encrypted CBL2/CBR2 flag protocol. Solution: reimplemented transport, emulated user/kernel attestations, used baked half-keys, and fetched the PNG flag.

Recon

Port scan

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

Enumeration highlights

  • Event: sekai2026 | ID: 20260628_sekai2026_chibile
  • Tags: windows_pe, custom_protocol, anti_cheat, kernel_driver, unicorn_emulation, crypto_protocol
  • Indicators: obfuscated host and port in Windows client, CBL2 and CBR2 encrypted packet magic, DeviceIoControl code 0x22e014, ordinal 1 command 0xa in unpacked DLL, PROTOCOL.md derives 12-character typed secrets
  • Source: 20260628_sekai2026_chibile.md

Foothold

Vulnerability / Misconfiguration

  1. Custom_hash_emulation
  2. Dynamic_unpacking
  3. Key_extraction
  4. Protocol_reimplementation
  5. Unicorn_emulation
<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

  • custom_hash_emulation
  • dynamic_unpacking
  • key_extraction
  • protocol_reimplementation
  • unicorn_emulation
  • Tags: windows_pe, custom_protocol, anti_cheat, kernel_driver, unicorn_emulation, crypto_protocol

Original Writeup

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

Description

Original organizer description was not available in the saved task context.

English summary: the task provided a Windows chibi game client and anti-cheat components. The goal was to reverse the hidden secret-room protocol, compute the required user-mode and kernel-mode typed secrets, and retrieve the flag image from the remote server.

Relevant files in tasks/sekai2026/Chibile/rev_chibile/:

  • chibile.exe — main game client.
  • eac_nocrt.dll and eac_nocrt_unpacked.dll — user-mode anti-cheat DLL.
  • eac_shield.sys — kernel anti-cheat driver.
  • PROTOCOL.md — partial documentation for Phase1 secret derivation.
  • Local helper scripts included full_solve.py, solve_emu.py, emu_gate.py, emu_um.py, emu_km.py, solve_chibile.py, and the final flag.png.

Analysis

The visible game was a front-end for a hidden secret-room flow. Static analysis of chibile.exe showed that initialization deobfuscated a remote endpoint and prepared an anti-cheat context. The recovered endpoint was:

8875cf0267a7c5510933aebf9de827b9.chals.sekai.team:1337

The secret-room path calls Phase1 first, then Phase2:

  1. Phase1 sends JSON with a nonce, user-mode attestation, kernel-mode attestation, PID, timestamp, and client build.
  2. The server returns {sid, c_um, lh_um, c_km, lh_km}.
  3. The client derives two 12-character secrets, typed_um and typed_km.
  4. Phase2 sends sid, typed_um, and typed_km, and receives a CBF2 PNG containing the flag.

The Phase1 JSON format was:

{
  "phase": 1,
  "nonce": "...",
  "um_attest": "...",
  "km_attest": "...",
  "pid": 1234,
  "ts": 1234567890,
  "client_build": "CHIBILE-R3-20260615"
}

The transport was a custom CBL2 request / CBR2 response wrapper using:

  • RSA-OAEP-SHA256 for a 0x70-byte key block.
  • AES-CBC with PKCS#7 padding for the payload.
  • HMAC-SHA256 for packet authentication.
  • A stream XOR layer before AES encryption and after AES decryption.

The most important transport pitfall was nonce reuse by design: the same 16-byte nonce16 is used as the JSON nonce, as input to UM/KM attestations, and as the CBL2 stream transport nonce. Generating a separate transport nonce causes the server to close the connection.

Gate derivation

fcn.140006640 computes a raw 32-byte gate:

81928d9335c6452d2588b681d6ef98075a79b38684840fac5198c40e4842c0de

However, the secret-room path does not pass this raw gate directly to the user-mode attestation. The client derives a secret-room-specific gate by hashing the raw gate plus the string secret-room through the client's custom hash helpers (fcn.140007110, fcn.140008860, fcn.140006ab0). The gate actually used for UM attestation is:

78a39767f1c301728aa1f169ffe9b4863207805ad6f2396c606c86bc32c1d31e

This correction was critical. Using the raw gate gives valid-looking local values but does not match the server's expected secret-room flow.

UM and KM attestations

For UM attestation, eac_nocrt_unpacked.dll exports ordinal 1. Command 0xa reaches fcn.180004030. The function measures the module containing ordinal 1 via fcn.18000a12f, walks a fake PEB/LDR module list, and hashes executable sections with a custom SHA-like implementation.

The local emulation setup used Unicorn with:

  • Fake TEB/PEB at gs:[0x60].
  • A fake loader list whose InMemoryOrderLinks pointed at the unpacked DLL.
  • The unpacked DLL mapped as the measured module.

Both the ordinal wrapper and direct fcn.180004030 call agreed, confirming the ABI.

For KM attestation, the client calls:

DeviceIoControl(
    hDevice,
    0x22e014,
    SystemBuffer = UM32 || nonce16,
    inputLen = 0x30,
    outputLen = 0x20
)

Driver dispatch maps 0x22e014 to eac_shield.sys base +0x2b00. The handler reads SystemBuffer[0:32] as the UM attestation and SystemBuffer[0x20:0x30] as the nonce, then overwrites the first 32 bytes of SystemBuffer with the KM attestation. The driver also uses a custom SHA-like hash, so plain hashlib.sha256(...) was not byte-compatible; emu_km.py emulated the driver code instead.

CBL2 transport pseudocode

The packet construction matched the client after fixing the shared nonce:

def cbl2_request(json_bytes, nonce16):
    aes_key = randbytes(32)
    hmac_key = randbytes(32)
    stream_key = randbytes(32)

    # The key block encrypted with the embedded RSA public key.
    key_block = aes_key + hmac_key + stream_key + nonce16
    rsa_ct = rsa_oaep_sha256_encrypt(pubkey, key_block)

    xored = stream_xor(json_bytes, stream_key, nonce16)
    iv = randbytes(16)
    aes_ct = aes_cbc_pkcs7_encrypt(aes_key, iv, xored)

    pkt = b"CBL2" + nonce16 + rsa_ct + iv + aes_ct
    mac = hmac_sha256(hmac_key, pkt)
    return u32le(len(pkt) + 32) + pkt + mac

The stream XOR uses HMAC blocks keyed by the stream key:

def stream_xor(data, stream_key, nonce16):
    out = bytearray(data)
    counter = 0
    for off in range(0, len(out), 32):
        block = hmac_sha256(stream_key, nonce16 + u64le(counter))
        for i, k in enumerate(block[:len(out) - off]):
            out[off + i] ^= k
        counter += 1
    return bytes(out)

Phase1 typed secret derivation

PROTOCOL.md documented the server response format and formulas. For each side, Phase1 returns ciphertext bytes C and local hash LH. The derived key is:

DK = HMAC_SHA256(BK, LH)
KS = HMAC_SHA256(DK, tag + u32le(block_index))

Then the user and kernel secrets are decoded differently:

def derive_user_secret(C, LH, BK):
    DK = hmac_sha256(BK, LH)
    out = bytearray()
    for i, c in enumerate(C):
        ks = hmac_sha256(DK, b"UM-KS" + u32le(i // 32))
        out.append(rotl8(c ^ ks[i % 32], 3) ^ BK[i % 32])
    return bytes(out).decode()

def derive_kernel_secret(C, LH, BK):
    DK = hmac_sha256(BK, LH)
    out = bytearray()
    for i, c in enumerate(C):
        ks = hmac_sha256(DK, b"KM-KS" + u32le(i // 32))
        out.append(rotr8(c ^ DK[(i * 7) % 32], 3) ^ ks[i % 32])
    return bytes(out).decode()

The final major pitfall was the meaning of BK. It is not the UM/KM attestation output. The Phase1 secret derivation uses baked half-keys embedded in the anti-cheat components:

  • UM baked half-key at file offset 0x5430 in eac_nocrt_unpacked.dll:
9a47d31eb8056cf283217eca4d901b665fe80ab377c429d13ca6528b14ff9d70
  • KM baked half-key at driver RVA 0x1420:
4e912cd763ba18ef057ac3398651fd20a8146dbf429ed037cb60851cf32974ae

With a valid Phase1 response, these formulas produced 12-character secrets over the alphabet ABCDEFGHJKLMNPQRSTUVWXYZ23456789. One successful run produced values with the expected shape:

typed_um = MJ82C5WWH366
typed_km = TSW6VHK649PX

The exact values vary per Phase1 response because c_* and lh_* are session-dependent.

Solution chain

The end-to-end solve was:

  1. Reverse the host obfuscation VM in chibile.exe and recover 8875cf0267a7c5510933aebf9de827b9.chals.sekai.team:1337.
  2. Reimplement the CBL2 / CBR2 transport: RSA-OAEP-SHA256, AES-CBC/PKCS#7, HMAC-SHA256, stream XOR, little-endian packet length.
  3. Ensure nonce16 is shared between JSON, UM/KM attestations, and the CBL2 transport.
  4. Emulate fcn.140006640 to get the raw gate, then derive the secret-room gate with the client custom hash and string secret-room.
  5. Emulate ordinal 1 command 0xa in eac_nocrt_unpacked.dll with a fake TEB/PEB/LDR list to produce UM attestation.
  6. Emulate driver handler eac_shield.sys + 0x2b00 for IOCTL 0x22e014 to produce KM attestation.
  7. Send Phase1 JSON through CBL2 and parse {sid, c_um, lh_um, c_km, lh_km}.
  8. Use the baked UM/KM half-keys, not the attestation outputs, to derive typed_um and typed_km.
  9. Send Phase2 JSON with sid, typed_um, and typed_km; decrypt the CBF2 PNG response and read the overlaid flag text in flag.png.
</details>

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

signed by XESXOR