-
Notifications
You must be signed in to change notification settings - Fork 0
MK1 IoStore and DataTable
MK1 uses UE4.27's IoStore (Zen storage) instead of UE3's .xxx archives. Assets are distributed across PAK files (legacy) and IoStore containers (UTOC + UCAS pairs).
pakchunk0-WindowsNoEditor.utoc — Table of contents (10MB, indices + directory)
pakchunk0-WindowsNoEditor.ucas — Content archive (5.7GB, compressed + encrypted data)
pakchunk0-WindowsNoEditor.pak — Legacy PAK (4.2GB, not yet supported)
global.utoc / global.ucas — Engine globals (small, special handling needed)
MK1 ships with 26 UTOC/UCAS pairs. pakchunk0 alone contains 18,765 files (16,401 .uasset).
1. Parse UTOC header (144 bytes)
2. Read section tables (ChunkIds, OffsetAndLengths, CompressedBlocks)
3. Skip signatures section (if Signed flag — hash_size×2 + block_count×20 bytes)
4. Decrypt directory index (AES-256-ECB)
5. Walk directory tree to build virtual path → entry index mapping
6. For each requested file:
a. Look up entry index → offset + length in UCAS
b. Compute compression block range
c. Read blocks from UCAS, align to 16 bytes
d. AES-ECB decrypt each block
e. Oodle v9 decompress (if method > 0)
f. Trim to exact offset within first block + requested length
The UTOC section layout was initially parsed incorrectly because the signatures section was not accounted for. When ContainerFlags & 0x04 (Signed), a variable-length signature section sits between CompressionMethods and DirectoryIndex:
[4 bytes: hash_size]
[hash_size bytes: TOC signature]
[hash_size bytes: block signature]
[compressed_block_count × 20 bytes: per-block SHA-1 hashes]
For pakchunk0, hash_size = 512, adding ~5.5MB of signature data. Skipping this was the key to finding the correct directory index offset.
While implementing the DFP cipher (unrelated to MK1), a critical bug was found in the stream cipher's counter logic. The shellcode does check → maybe_reset → increment, but the Python port initially did increment → check → maybe_reset. This caused the counter to trigger state doubling one iteration early, corrupting all output after the 17th byte. The fix was verified against 5 reference files.
MK1's inventory and game configuration data is stored in UE4 DataTable assets. These are .uasset files with export type 0x0B.
Export type is encoded in the low byte of ObjectFlags:
-
0x0B= DataTable (inventory, config, game data) -
0x39= Unknown secondary type (also parseable)
DataTable exports start with an ObjectProperty containing a RowStruct reference:
FName "RowStruct"
FName "ObjectProperty"
u64 size (= 4)
u8 padding
i32 object_reference
FName object_super (typically "None")
i32 class_reference (signed, into name table)
u32 children_count
For each child:
FName row_key (e.g., "SubZero_Fatality001")
[Tagged properties until "None"]
Each row is a struct with tagged UE4 properties. The row key is the primary identifier.
MK1 uses UE4's self-describing tagged property format. Each property:
FName property_name → FName property_type → type-specific data
| Type | Header (non-array) | Data |
|---|---|---|
| BoolProperty | u64 size + u8 value + u8 pad | — |
| IntProperty | u64 size + u8 pad | int of size bytes |
| FloatProperty | u64 size + u8 pad | float/double |
| StrProperty | u64 size + u8 pad | i32 len + string |
| NameProperty | u64 size + u8 pad | FName (u32+u32) |
| EnumProperty | u64 class_id + FName class + u8 value_id + FName value | dict |
| StructProperty | u32 size + u32 dup_id + FName type + u8 + u64 + u64 | recursive |
| ArrayProperty | u64 size + FName element_type + u8 + u32 count | elements |
| MapProperty | u64 size + FName key_type + FName val_type + u8 + u32 unk + u32 count | entries |
| ObjectProperty | u64 size + u8 pad + i32 ref | special dispatch by element_name |
| TextProperty | u64 size + u16 unk + u32 unk2 | 3 strings (or empty) |
| SoftObjectProperty | u64 size + u8 pad | FName path + u32 subpath |
Key difference from UE3: Container types (Array, Map) serialize their element types inline. No whitelist lookup needed.
u32 name_index (into name table)
u32 name_suffix (0 = no suffix, N = append "_{N-1}")
| File | Contents | Rows |
|---|---|---|
| SubZero_Fatalities | Fatality moves | 2 |
| SubZero_Gear | Equipment items | 35 |
| SubZero_Skins | Costume variants | 24 |
| SubZero_Character | Character config | 1 |
| Announcer | Voice pack entries | 1 |
| Bundles | DLC/store bundles | 18 |
| Consumables | Consumable items | 6 |
{
"RowStruct": {
"SubZero_Fatality001": {
"Title": ["", "C5CD3A29...", "(Anywhere) Hairline Fracture"],
"Rarity": {
"class": "EMKInventoryItemRarity",
"value": "EMKInventoryItemRarity::Rarity1"
},
"bIsDefaultItem": true,
"MaxCount": 1,
"Tags": ["Fatality", "SubZero"],
"PreviewIcon": "/Game/Disk/Char/SubZero/UI/UIFatalities/SubZero_A.SubZero_A",
"BuyPriceDefinition": {
"CurrencyType": { "DataTable": "...", "RowName": "None" },
"PricePerItem": 100
}
}
}
}-
Standalone
.uasset: Parsed directly as midway -
.utoccontainer: Parsed to extract directory index → all contained files listed as exports in the browse tree
- User adds MK1 game path → auto-resolves to
MK12/Content/Paks/ - File detector finds
.utocfiles (mountable) and.ucasfiles (companions, filtered) - Mounting a
.utoccreates a flat list of 18,765 virtual file entries - Tree shows
MK1 / Engine / ...andMK1 / MK12 / Content / ...(../../../prefix stripped) -
Single-click on a
.uassetnode: extracts from container, parses midway, shows DataTable JSON in right pane -
Double-click on a
.uassetnode: expands tree node with internal exports as children
NRS Asset Manager
Architecture
Game Formats
Game Documentation
- Injustice 2 (DCF2)
- Mortal Kombat X (MK10)
- Mortal Kombat 11 (MK11)
- Mortal Kombat 1 (MK12)
Export Handlers
Reference