$ cat writeup.md…
$ cat writeup.md…
sekai2026
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.
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.full_solve.py, solve_emu.py, emu_gate.py, emu_um.py, emu_km.py, solve_chibile.py, and the final flag.png.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:
{sid, c_um, lh_um, c_km, lh_km}.typed_um and typed_km.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:
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.
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.
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:
gs:[0x60].InMemoryOrderLinks pointed at the unpacked DLL.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.
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)
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:
0x5430 in eac_nocrt_unpacked.dll:9a47d31eb8056cf283217eca4d901b665fe80ab377c429d13ca6528b14ff9d70
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.
The end-to-end solve was:
chibile.exe and recover 8875cf0267a7c5510933aebf9de827b9.chals.sekai.team:1337.CBL2 / CBR2 transport: RSA-OAEP-SHA256, AES-CBC/PKCS#7, HMAC-SHA256, stream XOR, little-endian packet length.nonce16 is shared between JSON, UM/KM attestations, and the CBL2 transport.fcn.140006640 to get the raw gate, then derive the secret-room gate with the client custom hash and string secret-room.0xa in eac_nocrt_unpacked.dll with a fake TEB/PEB/LDR list to produce UM attestation.eac_shield.sys + 0x2b00 for IOCTL 0x22e014 to produce KM attestation.{sid, c_um, lh_um, c_km, lh_km}.typed_um and typed_km.sid, typed_um, and typed_km; decrypt the CBF2 PNG response and read the overlaid flag text in flag.png.$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar