{"uuid": "4f61be06-68b6-4431-bfb2-43add418bfbd", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2023-27566", "type": "seen", "source": "https://gist.github.com/Chace-Berry/a9e3a535347617dbe96d8cf5114501f5", "content": "# moc3 v6: first documentation, and three corrections to the public spec\n\nLive2D's `.moc3` is an undocumented binary format. The community has mapped\nversions 3 through 5 well. This adds **version 6**, which nothing public\ndescribes, and corrects three things the existing references get wrong.\n\nEverything here was derived without installing Live2D software, without\ndisassembling any binary, and without anyone accepting a Live2D licence\nagreement. Method and reproduction steps are at the bottom.\n\n---\n\n## TL;DR\n\n| | |\n|---|---|\n| **moc3 v6 documented** | Cubism 5.3 (Jan 2026) writes moc3 v6. Nothing public covers it. 167 sections vs v5's 152. |\n| **The offset table is 480 entries, not 160** | Every public reference models 160. Harmless for readers, wrong for writers. In v6 it is 1440. |\n| **`DrawableFlags` is 1 byte, not 4** | The reference declares `u32`; real files disagree. |\n| **`XY` / `UV` counts are individual floats** | Not coordinate pairs. Halves every size prediction if you get it wrong. |\n\nVerified by byte-identical round-trip across five models and three format\nversions.\n\n---\n\n## 1. The Section Offset Table is not 160 entries\n\nEvery public description of moc3, including OpenL2D's spec and its ImHex\npattern, models the offset table as **160 `u32` entries** at `0x40`.\n\nIt isn't. Its real extent is defined by slot 0, which points at the count\ntable and therefore marks where the offset table ends:\n\n```\nsot_entry_count = (offset_of_slot_0 - 0x40) / 4\n```\n\nMeasured across five files:\n\n```\nv4.02   1920 bytes    480 entries    0x40 -&gt; 0x7C0\nv5.00   1920 bytes    480 entries    0x40 -&gt; 0x7C0\nv6      5760 bytes   1440 entries    0x40 -&gt; 0x16C0\n```\n\nEntries past the highest defined slot are zero, which is why nobody noticed:\nin v4 and v5 everything above 160 is zero. **In a v6 file entries 160-166 are\npopulated**, and a reader hardcoded to 160 silently loses seven sections.\n\nThis also explains a gap that had been reported as unaccounted padding between\nthe end of the table and the first section: `1920 - 640 = 1280`. There was\nnever a gap. It is the reserved tail of the table itself.\n\nFor reading, the 160 assumption is mostly harmless, you index by slot and\nnever look further. **For writing it is not.** A file emitted with a 640-byte\ntable is not shaped like one Cubism produces.\n\n---\n\n## 2. moc3 v6\n\nCubism Editor 5.3 shipped in January 2026 and introduced advanced blend modes.\nModels using them export as moc3 **version 6**, and older SDKs refuse the file\noutright rather than degrading.\n\n```\n              sections   highest slot   SOT entries   count table\nv4.02            137          136           480         128 B (32 u32)\nv5.00            152          151           480         256 B (64 u32)\nv6               167          166          1440         256 B (64 u32)\n```\n\n**The layout is strictly additive.** v4.02 is a byte-exact prefix of v5, which\nis a prefix of v6. A v4.02 section map validated against a v6 file produced\n**zero mismatches** across 103 checked sections. Nothing was moved or removed\nbetween versions; slots were appended and the tables grew.\n\nOne reader serves all three, branching on exactly two things: the count table\nsize, and the highest defined slot.\n\n### What v5 added (slots 137-151)\n\n```\n137, 138   warp deformer keyforms       multiply / screen colour begin indices\n139, 140   rotation deformer keyforms   multiply / screen colour begin indices\n141, 142   art mesh keyforms            multiply / screen colour begin indices\n143-151    blend shapes (parts, rotation deformers, glue)\n           3 sections each: target indices, keyform-binding begin indices, counts\n```\n\n### What v6 added (slots 152-166)\n\n```\n152        per part        (29 parts x 4 B in a rich specimen)\n153        per art mesh    (176 art meshes x 4 B, exact)\n154-160    present, mostly zero-length in both specimens examined\n161-166    empty in both specimens\n```\n\n**Structure measured; semantics not yet determined.** Both v6 models available\nwere light users of the new features, so slots 154-166 carry little or no data.\nA v6 model that heavily exercises Cubism 5.3's advanced blend modes would\nsettle it.\n\n---\n\n## 3. Two element-size corrections\n\nBoth found by predicting section sizes from `count x element_size` and finding\nthe prediction exactly wrong by a factor of two, or wrong by 4x.\n\n**`DrawableFlags` is `u8`.** The reference declares `u32`. Real file: 356 art\nmeshes, section is 384 bytes. `356 x 1 = 356` (+28 padding). `356 x 4 = 1424`\ndoes not fit.\n\n**`XY` and `UV` counts are individual floats, not pairs.**\n\n```\nkeyformPositions = 570608\npredicted at 8 B/entry (XY pair):  4,564,864\nactual section size:               2,282,432   = 570608 x 4\n```\n\nSame for `uvs`. The count is of `f32` values, not of coordinates.\n\n---\n\n## 4. Method\n\nNo Live2D software was installed. No binary was disassembled. Nobody involved\naccepted the Cubism SDK licence or the Cubism Editor licence.\n\n**Inputs**\n\n1. Live2D's own **published** JSON specifications (`model3.json`, `cdi3.json`,\n   `physics3.json`, `motion3.json`, github.com/Live2D/CubismSpecs).\n2. Live2D's **published** Standard Parameter List, used as an oracle.\n3. **Model files downloaded under their creators' terms.** Not redistributed.\n4. **OpenL2D's `moc3.hexpat`**, whose README states explicitly: *\"you are free\n   to use the included model files and pattern files to create your own MOC3\n   readers and writers. No license will restrict your right to do so.\"*\n\n**Technique**\n\nThe published JSON sidecars contain the same ID strings the binary does. Find\nthose strings in the binary, and their addresses and stride identify which\noffset-table slot holds them and which count sizes them. From one such anchor,\n`count x element_size` classifies most remaining sections mechanically; a\nsecond model with different counts eliminates coincidences.\n\nField meanings were then confirmed against Live2D's published parameter list:\n\n```\n                   min     default    max      published\nParamAngleX      -30.00      0.00    30.00     -30 / 0 / 30\nParamEyeBallX     -1.00      0.00     1.00      -1 / 0 /  1\nParamMouthOpenY    0.00      0.00     1.00       0 / 0 /  1\n```\n\n12 of 12 standard parameters matched exactly, which names those three sections\nwith no ambiguity.\n\n**Verification**\n\nEvery section is decoded into typed values and re-encoded from those values,\nthen compared against the original file. Byte-identical output proves the shape\nmap accounts for every byte, a stronger claim than preserving unparsed bytes,\nwhich round-trips regardless of whether you understood them.\n\n```\nv4.02   2,919,872 B    BYTE-IDENTICAL\nv4.02     265,472 B    BYTE-IDENTICAL\nv5.00     787,072 B    BYTE-IDENTICAL\nv6         25,856 B    map validates, 0 mismatches\nv6        931,200 B    map validates, 0 mismatches (103 sections checked)\n```\n\n---\n\n## 5. A security note\n\nCubism Core performs no bounds checking on offsets read from the file. This is\nCVE-2023-27566, and it means an entire `.moc3` is a write-what-where primitive:\na malformed model can write to arbitrary memory near the load address.\n\nVTuber models are routinely downloaded from strangers on Booth, Discord and\nitch, and opened without any verification. There is no integrity check in the\npipeline.\n\nAny parser written from this document should therefore:\n\n- bounds-check every offset against the file length **before** dereferencing\n- verify `count x element_size &lt;= section size` before reading any section\n- treat index fields as **signed** (`-1` means \"none\"; read unsigned it becomes\n  4294967295)\n- bound string reads to 64 bytes rather than trusting a NUL to appear\n- return typed errors, never abort the process\n\n`OpenL2D/moc3ingbird` ships a deliberately malformed model, explicitly licensed\nfor this purpose. It is a good first fuzz seed: a correct parser rejects it\ncleanly.\n\n---\n\n## Credits\n\nThis builds directly on **[OpenL2D](https://github.com/OpenL2D)**, whose\n`moc3ingbird` repository contains `src/moc3.hexpat`, a complete, named ImHex\npattern for moc3 v3.00 through v5.00. That work named the fields; this adds v6\nand corrects the table size and two element widths. The security analysis in\n[Ronsor Labs' \"Live2D: A Security Trainwreck\"](https://undeleted.ronsor.com/live2d-a-security-trainwreck/)\nis the reference for the CVE.\n\n`moc3-rs` (MahouTechnologies) is a clean-room Rust implementation built from\nOpenL2D's documentation and is worth reading.\n\nTwo other implementations, `py-moc3` and `moc3-reader-re`, are more complete\nthan either in some respects but derive from a decompilation of Cubism's own\nexporter. They are noted here so readers can make an informed choice; they were\nnot consulted for this work.\n\nLive2D and Cubism are trademarks of Live2D Inc. This is an independent\ninteroperability effort, not affiliated with or endorsed by Live2D Inc.\n", "creation_timestamp": "2026-09-08T21:28:11.348497Z"}