Zenella

Reverse AMD Zen microcode updates inside Binary Ninja.

Open a microcode .bin, run one command, and the update container is typed and labelled. Zen 1 and Zen 2 payloads are disassembled and lifted to LLIL and HLIL. Zen 5 updates get their real register geometry and every 36-byte op-quad decoded in the Linear view.

Version
2.2.0
Runs on
Binary Ninja, Python 3
Targets
Zen 1, Zen 2, Zen 5
License
GPL-3.0

What the plugin does

AMD ships CPU microcode as small signed blobs. The container format (header, RSA material, options, match registers, then the microcode itself) is the same across generations, but the payload is not. Zenella reads the header, picks the right profile and applies it to the bytes in front of you.

Nothing is executed, decrypted or signed. The plugin only defines types, data variables, symbols and comments in the current database, and every apply runs in one undoable transaction, so a wrong guess costs you a Ctrl‑Z.

The decoder lives in zenella_core.py and has no Binary Ninja dependency. The same code drives zenella_inspect.py, a command-line tool that prints the structural report or JSON for a file without opening the GUI.

  • Zen 1 and Zen 2

    0xC80-byte updates. The 64 instruction packages become a Binary Ninja architecture: disassembly in the ZenUtils style, LLIL lifting, and MLIL/HLIL from there. Cross references and graph view work as for any other target.

  • Zen 5

    0x3820-byte updates. The loader ID selects the geometry. For loader 0x8015 the register area holds 31 match and 31 mask words, so the op-quads start at 0x420 and run to the zero padding. Each op-quad is four 64-bit micro-ops plus a sequence word, rendered with the opcode enum names.

  • Command line

    zenella_inspect.py prints the same regions, op-quads, sequence words and alignment diagnostics as text or JSON. Useful for diffing updates or checking a layout before touching a database.

Zen 5 update layout

Every Zen 5 update is 0x3820 bytes. The map below is the layout applied for loader 0x8015 (Zen 5c). Segment heights follow the byte sizes, with a minimum so the small ones stay readable.

  1. 0x0000 Header 32 B
  2. 0x0020 Signature 256 B
  3. 0x0120 Modulus 256 B
  4. 0x0220 Check 256 B
  5. 0x0320 Options 4 B
  6. 0x0324 Revision copy 4 B
  7. 0x0328 Match registers 31 × u32
  8. 0x03A4 Mask registers 31 × u32
  9. 0x0420 Op-quads 300 × 36 B
  10. 0x2E50 Zero padding to 0x3820
SymbolOffsetSizeTypeContents
amd_mc_header 0x0000 0x20 AMD_MC_Header Date, revision, loader ID, patch size, CPUID, flags
amd_mc_signature 0x0020 0x100 u8[256] RSA signature
amd_mc_modulus 0x0120 0x100 u8[256] Public key modulus
amd_mc_check 0x0220 0x100 u8[256] Check block, likely a Montgomery constant
amd_mc_options 0x0320 0x04 AMD_MC_UcodeOptions autorun, encrypted flag, loader ID. This copy of the loader ID selects the layout
amd_mc_rev 0x0324 0x04 u32 Second copy of the update revision
amd_mc_match_regs 0x0328 0x7C AMD_MC_MatchRegisterBlock 31 match registers
amd_mc_mask_regs 0x03A4 0x7C AMD_MC_MaskRegisterBlock 31 mask registers
amd_ucode_body 0x0420 0x2A30 AMD_Zen5_OpQuad[300] Op-quads up to the zero padding. The NOP run and the finalization sequence are carved out as amd_mc_nop_section and amd_mc_finalization_section
amd_mc_zero_padding 0x2E50 0x9D0 u8[2512] Trailing zeros

The op-quad count follows the padding, so it can differ between updates. 300 is what the two reference updates (revisions 0x0B10104E and 0x0B101054) contain. Loader 0x8010 keeps a searched equal match/mask split; 0x8004 and 0x8005 use zentool's fixed tables.

Zen 1 and Zen 2 for comparison

SymbolOffsetSizeTypeContents
amd_mc_header0x00000x20AMD_MC_HeaderSame header as Zen 5
signature, modulus, check0x00200x300u8[256] × 3Same crypto blocks as Zen 5
amd_mc_options0x03200x04AMD_Zen12_UcodeOptionsautorun, encrypted, two unknown bytes
amd_mc_revision_copy0x03240x04u32Second copy of the revision
amd_zen12_match_entries0x03280x58AMD_Zen12_MatchEntry[22]Two 13-bit match addresses per word plus enable bits
amd_zen1_ucode / amd_zen2_ucode0x03800x900AMD_Zen12_InstructionPackage[64]64 packages of four 64-bit instructions and a sequence word; mapped as code and lifted

Menu commands

Everything sits under Plugins → AMD Microcode. Each apply command comes in two forms: at file start for a standalone update and at cursor for an update embedded in a larger image, where the cursor marks the header.

Auto-detect and apply

Reads the header, decides between Zen 1, Zen 2 and Zen 5, and runs the matching apply. This is the one to start with.

Zen1 › Apply layout + LLIL/HLIL

Zen2 › Apply layout + LLIL/HLIL

Types the container, maps the 64 packages as executable code under the Zen 1 or Zen 2 architecture and lets Binary Ninja lift it. Pick the generation yourself if auto-detect has no opinion.

Zen1-Zen2 › Show ZenUtils-style disassembly

Prints a plain-text listing of the match registers and all packages in the format ZenUtils users know, as a report tab.

Zen5 › Apply structural layout

Applies the confirmed loader 0x8015 layout: 31 match and 31 mask registers at 0x328, op-quads from 0x420 to the zero padding. Defines the types, creates the data variables and symbols from the table above, and verifies the result against the bytes before the transaction commits. Other loader IDs get their own geometry.

Zen5 › Experimental

Four alternative Zen 5 geometries for research. None of them is the default and none carries documented evidence; they exist so a hypothesis can be applied and compared quickly.

  • Apply exact-fit 0x418/370 (no tail): zentool's boundary, 60 register words and 370 op-quads.
  • Apply best-scoring body offset (scan): tries register-area sizes and keeps the one whose sequence words look sane.
  • Apply tail match-mask model: metadata before the op-quads, registers after them.
  • Set match/mask register counts: type the counts yourself and move the body boundary.

Without Binary Ninja

The report the plugin builds is also available from a shell. Only the standard library is used.

python3 zenella_inspect.py update.bin                  # regions, op-quads, sequence words
python3 zenella_inspect.py update.bin --json           # everything as JSON
python3 zenella_inspect.py update.bin --layout exact   # force an alternative geometry
python3 zenella_inspect.py image.bin --base 0x1000     # embedded update

Types the plugin defines

All structures are packed. Names from Zenella 1.2 are kept, so scripts and existing databases keep working; new names are prefixed with the generation.

AMD_MC_Header 0x20 bytes, shared by all generations

u16year
u8day
u8month
u32update_revision
AMD_MC_LoaderIdTagloader_id
u16size_of_patch
u32minimum_patch_level
u16nb_ven
u16nb_dev
u16sb_ven
u16sb_dev
AMD_MC_CpuIdproc_sig
u8bios_revision
u8flags
u8reserved
u8reserved2

The CPUID in proc_sig is expanded and commented with the matching processor description from cpuid_descriptions.json.

AMD_Zen5_MicroOp64 8 bytes, bitfields

BitsField
0–15imm16
16–20imm_flags
21–25rt
26–30rs
31–35rd
36–41flags
42–44size
45load
46store
47–54opcode (AMD_Zen_Opcode)
55–58mid
59–61exec_unit (spec, br, ld, stn, st, regx, reg)
62–63hi

AMD_Zen5_OpQuad 36 bytes

AMD_Zen5_MicroOp64uop0 … uop3
u32sequence_word

The Linear view shows each micro-op with its opcode name, class and a zentool-style operand projection. The sequence word is annotated, not lifted.

AMD_MC_Patch 0x3820 bytes, Zen 5 container

AMD_MC_Headerheader
u8[256]signature
u8[256]modulus
u8[256]check
AMD_MC_UcodeOptionsoptions
u32rev
AMD_MC_MatchRegisterBlockmatch_regs
AMD_MC_MaskRegisterBlockmask_regs
AMD_Zen5_OpQuad[300]body
u8[2512]zero_padding

Also registered under the generation-specific name AMD_Zen5_Patch. The array length and padding size follow the update.

AMD_MC_UcodeOptions 4 bytes

u8autorun
u8encrypted
u16loaderid

AMD_MC_LoaderIdTag u16 enum

0x8004AMD_MC_LOADER_8004
0x8005AMD_MC_LOADER_8005
0x8010AMD_MC_LOADER_8010
0x8015AMD_MC_LOADER_8015
0x8016AMD_MC_LOADER_8016

AMD_MC_MatchRegisterBlock 124 bytes

u32[31]match_reg

AMD_MC_MaskRegisterBlock 124 bytes

u32[31]mask_reg

Sizes shown are for loader 0x8015. Other loaders get scoped block types of their own width.

AMD_Zen12_InstructionPackage 36 bytes

u64uop0 … uop3
u32sequence_word

AMD_Zen12_MatchEntry 4 bytes

u32raw

AMD_Zen12_Patch 0xC80 bytes

AMD_MC_Headerheader
u8[256] × 3signature, modulus, check
AMD_Zen12_UcodeOptionsoptions
u32revision_copy
AMD_Zen12_MatchEntry[22]match_entries
AMD_Zen12_ExecutablePayloadpayload

AMD_Zen_Opcode u16 enum, bits 47–54 of a micro-op

Names come from the published ZenUtils research. Values above 0xFF are synthetic: a load/store op is identified by its class, not by the opcode bits.

Class dependent
0x00AMD_ZEN_UOP_LD_ST_00
0x100AMD_ZEN_LD
0x101AMD_ZEN_ST
0x05AMD_ZEN_BR_JMP
Special
0xFFAMD_ZEN_SPEC_NOP
0xDEAMD_ZEN_TYPE5_READ
Register ops
0x19AMD_ZEN_REG_NSUB
0x30AMD_ZEN_REG_AND
0x40AMD_ZEN_REG_SHL
0x41AMD_ZEN_REG_BLL
0x42AMD_ZEN_REG_ROL
0x44AMD_ZEN_REG_RLC
0x46AMD_ZEN_REG_RRD
0x47AMD_ZEN_REG_SRC
0x48AMD_ZEN_REG_SHR
0x4AAMD_ZEN_REG_ROR
0x4CAMD_ZEN_REG_RRC
0x4FAMD_ZEN_REG_SRD
0x50AMD_ZEN_REG_SUB
0x52AMD_ZEN_REG_SBB
Register ops, continued
0x55AMD_ZEN_REG_NADD
0x5CAMD_ZEN_REG_ADD2
0x5DAMD_ZEN_REG_ADC
0x5EAMD_ZEN_REG_ADD3
0x5FAMD_ZEN_REG_ADD
0x6FAMD_ZEN_REG_VZEROUPPER_64B
0x70AMD_ZEN_REG_POPCNT
0x72AMD_ZEN_REG_SBIT
0x7FAMD_ZEN_REG_VZEROUPPER_32B
0x93AMD_ZEN_REG_MOV2
0xA0AMD_ZEN_REG_MOV_SREG
0xA9AMD_ZEN_REG_BSWAP
0xB5AMD_ZEN_REG_XOR
0xBEAMD_ZEN_REG_OR

Install

Zenella is a plugin folder, not a single file. It needs nothing beyond the Python that ships with Binary Ninja.

  1. Find the plugin folder

    In Binary Ninja choose Plugins → Open Plugin Folder…, or go straight there:

    ~/Library/Application Support/Binary Ninja/plugins/   # macOS
    %APPDATA%\Binary Ninja\plugins\                       # Windows
    ~/.binaryninja/plugins/                               # Linux
  2. Copy the repository into it

    Clone or download ercihan/zenella and place the whole directory in the plugin folder. Remove any older single-file copy of amd_zen_ucode.py first; two copies would both register the menu.

    cd ~/Library/Application\ Support/Binary\ Ninja/plugins/
    git clone https://github.com/ercihan/zenella.git
  3. Restart Binary Ninja

    The AMD Microcode submenu appears under Plugins. The log shows the loaded version and the path it was loaded from, which is handy if a stale copy is still around.

What you need

  • Binary Ninja desktop with Python scripting enabled
  • Python 3 from the Binary Ninja install, no extra packages
  • For the command-line tool, any Python 3.8 or newer

A typical session

  1. Open the update

    Load the .bin as a raw file. For an update inside a firmware image, open the image and place the cursor on the update header.

  2. Run auto-detect

    Plugins → AMD Microcode → Auto-detect and apply at file start, or the cursor variant. The log tells you which profile was chosen and why.

  3. Read the header

    In Linear view amd_mc_header shows date, revision, loader ID and the expanded CPUID with the processor name as a comment.

  4. Work through the payload

    Zen 1 and Zen 2: jump into the lifted packages and use graph or HLIL view. Zen 5: scroll amd_ucode_body; every op-quad is decoded, and the NOP run and the finalization sequence are labelled so you can skip them.

Applying the layout to a Zen 5 update.

Zen 5 micro-op semantics are undocumented. The plugin names opcodes and fields from the published research and shows sequence words as stored; it does not claim to execute or lift Zen 5 code. The experimental menu exists precisely because some of the geometry is still being worked out.