1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
#!/usr/bin/env python3
"""Characterise the AMD "F32" microcode (SDMA, RLC) that has no public disassembler.
This exists so the "F32 is a blocker" row in FIRMWARE-COVERAGE-MATRIX.md rests on a
measurement rather than an inherited claim. It reports, for each image:
* container handling, including the nested $PS1 PSP header (code starts at +0x200)
* the most common 32-bit words -- the NOP encoding shows up here
* per-bit entropy across the word, to look for a low-entropy opcode field
* how many distinct values the top 4..8 bits take, i.e. whether an opcode field
of that width is plausible
* capstone coverage across every architecture it supports, as a negative control
Interpretation notes (2026-09 run on Navi 31 blobs):
SDMA `sdma_6_0_0.bin` NOP 0x90000000 (x132), top-4 bits take all 16 values
RLC `gc_11_0_0_rlc.bin` NOP 0x90000000 (x191), top-4 bits take all 16 values
No capstone architecture fits: the best score is Thumb at 98%, which is
meaningless because Thumb decodes almost any byte string. So: fixed-width 32-bit,
a NOP of 0x90000000, and no known ISA -- consistent with the tree's long-standing
"AMD proprietary F32, no public disassembler" call, now verified rather than
assumed. Deriving the encoding would need paired execution traces, which is a
project in itself.
usage: f32_probe.py <image> [<image> ...]
"""
import collections
import math
import struct
import sys
try:
import capstone as cs
except ImportError:
cs = None
ARCHES = [('riscv64', 'CS_ARCH_RISCV', 'CS_MODE_RISCV64'), ('arm', 'CS_ARCH_ARM', 'CS_MODE_ARM'),
('thumb', 'CS_ARCH_ARM', 'CS_MODE_THUMB'), ('xtensa', 'CS_ARCH_XTENSA', 'CS_MODE_XTENSA_ESP32'),
('mips32el', 'CS_ARCH_MIPS', 'CS_MODE_MIPS32'), ('ppc32', 'CS_ARCH_PPC', 'CS_MODE_32'),
('sparc', 'CS_ARCH_SPARC', None), ('tricore', 'CS_ARCH_TRICORE', None),
('sh', 'CS_ARCH_SH', None), ('m68k', 'CS_ARCH_M68K', None),
('hppa', 'CS_ARCH_HPPA', None), ('alpha', 'CS_ARCH_ALPHA', None)]
def payload(path):
d = open(path, 'rb').read()
ucode_size, ucode_off = struct.unpack_from('<II', d, 0x14)
nested = d[ucode_off + 0x10:ucode_off + 0x14] == b'$PS1'
return d[ucode_off + (0x100 if nested else 0):ucode_off + ucode_size], nested
def bit_entropy(words):
n = len(words) or 1
out = []
for bit in range(32):
ones = sum(1 for w in words if w >> bit & 1)
p = ones / n
out.append(0.0 if p in (0.0, 1.0) else -(p * math.log2(p) + (1 - p) * math.log2(1 - p)))
return out
def main():
for path in sys.argv[1:]:
b, nested = payload(path)
words = [struct.unpack_from('<I', b, i)[0] for i in range(0, len(b) - 3, 4)]
cnt = collections.Counter(words)
print('=== %s payload 0x%x bytes, nested $PS1=%s, %d words' % (
path, len(b), nested, len(words)))
print(' most common words: %s' % ' '.join(
'0x%08x(x%d)' % kv for kv in cnt.most_common(5)))
ent = bit_entropy(words)
print(' per-bit entropy 31..0: %s' % ''.join(
'%d' % min(9, int(e * 9.99)) for e in reversed(ent)))
for width in (4, 5, 6, 7, 8):
top = collections.Counter(w >> (32 - width) for w in words)
print(' top-%d bits: %d/%d distinct values, most common %s' % (
width, len(top), 1 << width,
' '.join('0x%x(x%d)' % kv for kv in top.most_common(3))))
if cs is None:
print(' (capstone unavailable; skipping the negative control)')
continue
nz = next((i for i in range(0, max(1, len(b) - 63), 4) if b[i:i + 64] != bytes(64)), 0)
rows = []
for name, arch_n, mode_n in ARCHES:
arch = getattr(cs, arch_n, None)
if arch is None:
continue
mode = getattr(cs, mode_n, 0) if mode_n else 0
if name == 'mips32el':
mode |= cs.CS_MODE_LITTLE_ENDIAN
try:
md = cs.Cs(arch, mode)
except Exception: # noqa: BLE001
continue
md.skipdata = True
cov = tot = 0
for x in md.disasm(b[nz:nz + 0x2000], 0):
tot += x.size
if x.mnemonic not in ('.byte', '.db', 'unimp', 'c.unimp', 'ill',
'illegal', 'undefined'):
cov += x.size
rows.append((cov / max(1, tot), name))
print(' capstone coverage: %s' % ', '.join(
'%s=%.0f%%' % (n, 100 * c) for c, n in sorted(rows, reverse=True)[:6]))
print(' NOTE: a high Thumb score is not evidence -- Thumb decodes almost '
'anything. Only a coherent, structured decode counts.')
if __name__ == '__main__':
main()
|