Zenella

Reverse AMD microcode updates inside Binary Ninja.

Open a microcode .bin, run one command, and the update container is typed and labelled. K8 and K10 patches (families 0x0F–0x12) have their obfuscated triads decoded and disassembled. 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
K8, K10, Zen 1, Zen 2, Zen 5
License
GPL-3.0

What the plugin does

AMD ships CPU microcode as small blobs. Zen updates share one signed container (header, RSA material, options, match registers, then the microcode itself) while the payload changes from generation to generation. The older K8 and K10 patches use a plain 0x40-byte container: a header, eight match registers and an array of obfuscated 28-byte triads. 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 decoders live in zenella_core.py (Zen) and prezen_core.py (K8/K10) and have no Binary Ninja dependency. The same code drives zenella_inspect.py and the prezen_core.py command line, which print the structural report, the triad listing or JSON for a file without opening the GUI. The K8/K10 decoder renders the same text as the GhidraAmdMicrocode SLEIGH specification, and its triad de-obfuscation is a port of the amd-ucode-patch reference parser.

  • K8 and K10 (families 0x0F–0x12)

    0x40 + 28·N-byte patches with loader 0x8000 or 0x8003. The N triads, three 64-bit micro-ops and a sequence word each, are stored scrambled; the plugin de-obfuscates them, mirrors them into an executable section at patch RAM address uc_0c00 and disassembles them under the amd_prezen_ucode architecture. Match registers become handler functions, jcc and sequence-word branches become edges. The text matches Ghidra's AMDuCode K8/K10 languages instruction for instruction.

  • 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. An experimental report follows the sequence words and lists the micro-ops in execution order, assembly or raw hex.

  • Command line

    zenella_inspect.py prints the same regions, op-quads, sequence words and alignment diagnostics as text or JSON, and with --flow the sequence-ordered micro-op stream. 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

K8 and K10 (pre-Zen, families 0x0F–0x12)

SymbolOffsetSizeTypeContents
amd_prezen_header0x00000x20AMD_PreZen_HeaderBCD date, patch level, loader ID (0x8000 K8/K10, 0x8003 family 0x12), triad count, init flag, triad checksum, cpuid equivalence ID
amd_prezen_match_regs0x00200x20AMD_PreZen_MatchRegister[8]Low 16 bits name the ROM triad that register i redirects to patch triad uc_0c00 + 2i; 0xFFFF means unused
amd_prezen_triads_raw0x004028 × NAMD_PreZen_TriadObfuscated[N]N obfuscated triads as stored in the file; each carries the decoded text as a comment
uc_0c00 … uc_0c00+Nregion + 0x1500028 × N.zenella_prezen_ucode_<base>Executable mirror of the triads at patch RAM address 0xC00 × 28; functions at every match-register handler and branch target

The file keeps the scrambled bytes. Each 32-bit word of a triad is rotated by an amount that depends on the triad index, swapped with its neighbour and inverted; the architecture undoes this per micro-op from its own eight bytes and the address, so a saved database keeps disassembling after it is reopened. Patch size is always 0x40 + 28·N and the header checksum is the 32-bit word sum of the triad array.

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 K8/K10, Zen 1, Zen 2 and Zen 5, and runs the matching apply. This is the one to start with.

Pre-Zen (K8/K10) › Apply layout + disassembly

Types the 0x40-byte container and the obfuscated triad array, then mirrors the triads into an executable section at uc_0c00 under the amd_prezen_ucode architecture. Match registers get handler functions, jcc and sequence-word branches get edges; micro-ops are disassembled, not lifted.

Pre-Zen (K8/K10) › Show decoded triad listing

Prints the header, the match registers and every de-obfuscated triad (three micro-ops and the sequence word, with the raw words) as a report tab, in the same text Ghidra's AMDuCode languages produce.

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.

Two further commands are read-only reports that follow the sequence words instead of the file order. They never touch the database or the structural layout; the rules below are inferred from decrypted updates and labelled as such in the report header.

  • Show sequence-ordered op-quad listing (file start / cursor): one flat stream, one line per micro-op in execution order, assembly when the operand model applies and the raw 64-bit word otherwise. Sequence word 1 continues with the next op-quad, word 7 returns, a 13-bit target inside 0x1f00 … 0x1f00 + quads continues in the patch (the patch RAM base observed in every decrypted sample), a target equal to a match-register address re-enters at that hook's quad (m1 → quad 2N, m2 → quad 2N+1), and any other ROM target ends the run with a comment line. Every op-quad is printed exactly once.
  • Show sequence-ordered op-quad chains (detailed) (file start / cursor): the same order grouped per hook, with quad headers, the rule table and the reason each chain ends.

Expect many short runs rather than one program: a Zen 5 update is a set of hooks (two per match register, 58 in a typical patch), and most of them end by jumping back into the CPU's ROM microcode, which is not part of the file.

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
python3 zenella_inspect.py update_dec.bin --flow        # experimental: micro-ops in sequence-word order (decrypted Zen 5)
python3 zenella_inspect.py update_dec.bin --flow-detail # same order as chains with headers and end reasons
python3 zenella_inspect.py update_dec.bin --flow --patch-base none   # or auto / 0x1f00: how ROM targets map into the patch
python3 prezen_core.py k10_patch.bin                   # K8/K10: header, match registers, decoded triads

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, and the K8/K10 types with AMD_PreZen_.

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_PreZen_Header 0x20 bytes, K8/K10

u32date (BCD 0xMMDDYYYY)
u32patch_level (family = 0xF + top byte)
AMD_PreZen_LoaderIdTagloader_id
u8op_triad_count
u8init_flag
u32op_triad_checksum
u32nb_dev_id
u8[4]unknown0
u16cpuid (equivalence ID)
u8[6]unknown1

AMD_PreZen_LoaderIdTag u16 enum

0x8000PREZEN_K8_K10_0x8000
0x8003PREZEN_FAMILY12_0x8003

AMD_PreZen_MatchRegister 4 bytes

u32raw (low 16 bits = ROM triad, 0xFFFF unused)

AMD_PreZen_TriadObfuscated 28 bytes, as stored

u32[7]obfuscated_word

AMD_PreZen_Triad 28 bytes, decoded

u64op0, op1, op2
u32sequence_word

Pre-Zen micro-op fields 64-bit word, bit 0 = LSB

54–62operation (9 bits)
59–62hi4 (5 marks a jcc)
54–58cc (32 condition codes)
523op (three-operand form)
46–51dst (64 register names)
30–31size (.b .w .d .q)
24–29src1
23regmode (1 = register, 0 = imm16)
9–14src2reg
0–15imm
0–11braddr (jcc target triad)

Sequence word 32-bit, bits 14–16 = action

0sw_next fall through to the next triad
1sw_complete return
2sw_branch to triad in bits 0–11
othersw_raw shown as stored

Named operations: add, or, adc, sbb, and, sub, xor, cmp, test, rll, rrl, sll, srl, writepc, mov, mul, imul, muleh, mulel, div1, div2, ld, st, lea, bswap, not and crash; every other operation number is shown as unk_op. Registers are eax…edi, t1d…t56d, the regmd result registers and pcd. Names, fields and text follow the GhidraAmdMicrocode AMDuCodePreZen.sinc specification.

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

    K8 and K10: jump to uc_0c00 or follow the handler addresses in the match-register comments, and read the triads in Disassembly or graph view (HLIL shows micro-ops as unimplemented because only control flow is lifted). 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. For execution order instead of file order, run the experimental Zen5 → Experimental → Show sequence-ordered op-quad listing on a decrypted update.

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. K8 and K10 micro-ops are likewise disassembled but not lifted: the text and the control flow follow Ghidra's AMDuCode specification, which carries no data-flow semantics either.