AGES Mk2 FCD Audio Decryption

逆向工程 Android 算法AGES Mk2
浏览数 - 177发布于 - 2025-12-14 - 02:13

0x0 Background

The FPD packet encoding algorithm and epk decryption algorithm have both been reverse engineered, but the FCD audio has not been reverse engineered.

List of games using this engine: https://vndb.org/r?f=fwAGES_0Mk2-

Identify (Windows) PixPin_2025-12-14_00-25-46.jpg Identify (Android) PixPin_2025-12-14_00-26-48.jpgNo iOS version found.

0x1 Parsing FCD header

  • Validate magic

    The loader checks the first 3 bytes:

    buf[0..2] == b"FCD"

PixPin_2025-12-14_01-18-58.jpgHeader fields:

  • Processing and parsing file headers

    Swap the 16-bit version at offset 0x04

    Swap the 16-bit count at offset 0x06

  • Then it decides how to decode payload offset (the 4 bytes at 0x08) based on the swapped version:

    If version == 1
    payloadOffset is stored as 2 bytes: payloadOffset = (buf[0x08] << 8) | buf[0x09]

    If version == 3
    payloadOffset is stored as 32-bit swapped: payloadOffset = bswap32(*(u32 at 0x08))

  • It writes the result back to *(a1+8).

Entry table:

Entry i is at: a1 + 0x10*(i+1)

For each entry, it byteswaps the entry type dword (at entry offset +0)

Then, depending on the type (after swap):

  • If type == 1: byteswap three dwords at offsets +4, +8, +0xC

  • If type == 3: byteswap one dword at offset +4

  • If type == 2: no swapping of the 12-byte payload/tag

Finally it sets *a1 = 0 as a “normalized already” flag.

PixPin_2025-12-14_01-28-57.jpg

0x2 Key derivation

MD5 context is initialized with standard MD5 IVs:

A=0x67452301, B=0xEFCDAB89, C=0x98BADCFE, D=0x10325476

Then it computes:

key16 = MD5( tag12 || md5_addition_blob )

Where:

  • tag12 is exactly 12 bytes from the type=2 entry (no per-word swapping)
  • md5_addition_blob is exactly 0x4000 bytes

The resulting 16-byte digest is used as the Blowfish key material.

PixPin_2025-12-14_01-59-27.jpg

0x3 Blowfish

This section only explains the modified parts.

fz::sound::util::BlowFish::init

  • Load S material from the embedded blowfish_key table

    The code copies 0x400 words from blowfish_keystarting at index 0x12 * 4, and writes them in 4 passes:

    pass0 writes to ctx[0x12 .. 0x12+0xFF]

    pass1 writes to ctx[0x16 .. 0x16+0xFF]

    pass2 writes to ctx[0x1A .. 0x1A+0xFF]

    pass3 writes to ctx[0x1E .. 0x1E+0xFF]
    So S0/S1/S2/S3 are not 4 independent arrays; they are 4 “views” into one shared window.

  • Fill S, overlapped

    rbp = ctx + 0x4C

    4 passes; each pass:

    does 0x80 iterations, each writes two dwords using a stride of 2 dwords per iteration

    after each pass, rbp += 0x10

PixPin_2025-12-14_02-03-11.jpgsub_180046290

  • S indexing uses the overlapped “window bases”

    S0[a] ≈ ctx[0x12 + a]

    S1[b] ≈ ctx[0x16 + b]

    S2[c] ≈ ctx[0x1A + c]

    S3[d] ≈ ctx[0x1E + d]

  • F function combine differs

    Standard Blowfish: F = ((S0+S1) ^ S2) + S3
    This binary uses: F = (S0+S1) ^ S2 ^ S3

PixPin_2025-12-14_02-08-43.jpgfz::sound::util::BlowFish::decode

  • Tail handling

    If nbytes % 8 != 0:

    It packs remaining bytes into an integer by shifting left

    Decrypts that “padded” block once via sub_180046290

    Writes back only the remaining bytes by repeatedly storing the low byte and shifting (rbx >>= 8) from the end down to the tail start.

PixPin_2025-12-14_02-11-24.jpgMD5:

It's a standard MD5 hash, without any modifications.

0xFF

PixPin_2025-12-14_02-12-52.jpg

本文版权遵循 CC BY-NC 协议 本站版权政策

1 条回复

bfloat16
发布于 2025-12-16 - 03:07

(。>︿<。) 已经一滴回复都不剩了哦~