Chibile
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
| Port | Service | Version | Notes |
|---|---|---|---|
| <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
- Custom_hash_emulation
- Dynamic_unpacking
- Key_extraction
- Protocol_reimplementation
- 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
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
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.dllandeac_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 finalflag.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:
- Phase1 sends JSON with a nonce, user-mode attestation, kernel-mode attestation, PID, timestamp, and client build.
- The server returns
{sid, c_um, lh_um, c_km, lh_km}. - The client derives two 12-character secrets,
typed_umandtyped_km. - Phase2 sends
sid,typed_um, andtyped_km, and receives aCBF2PNG 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
InMemoryOrderLinkspointed 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
0x5430ineac_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:
- Reverse the host obfuscation VM in
chibile.exeand recover8875cf0267a7c5510933aebf9de827b9.chals.sekai.team:1337. - Reimplement the
CBL2/CBR2transport: RSA-OAEP-SHA256, AES-CBC/PKCS#7, HMAC-SHA256, stream XOR, little-endian packet length. - Ensure
nonce16is shared between JSON, UM/KM attestations, and the CBL2 transport. - Emulate
fcn.140006640to get the raw gate, then derive the secret-room gate with the client custom hash and stringsecret-room. - Emulate ordinal 1 command
0xaineac_nocrt_unpacked.dllwith a fake TEB/PEB/LDR list to produce UM attestation. - Emulate driver handler
eac_shield.sys + 0x2b00for IOCTL0x22e014to produce KM attestation. - Send Phase1 JSON through CBL2 and parse
{sid, c_um, lh_um, c_km, lh_km}. - Use the baked UM/KM half-keys, not the attestation outputs, to derive
typed_umandtyped_km. - Send Phase2 JSON with
sid,typed_um, andtyped_km; decrypt theCBF2PNG response and read the overlaid flag text inflag.png.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR