diff options
| author | user <user@clank> | 2026-10-09 16:11:31 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-10-09 16:11:31 +0200 |
| commit | bc43149d914325e15dc0490dee9d982e21287623 (patch) | |
| tree | a632cf70de742d5a60d01c9c0d806bf5b9257538 /fw/verify_gcp_loggates_memcpy_path.py | |
| parent | GCP->ASD 0x403: ASD-internal chain PROVEN end-to-end in emulator (real handlers) (diff) | |
| download | amd-psp-re-main.tar.gz amd-psp-re-main.tar.xz amd-psp-re-main.zip | |
Diffstat (limited to 'fw/verify_gcp_loggates_memcpy_path.py')
| -rw-r--r-- | fw/verify_gcp_loggates_memcpy_path.py | 118 |
1 files changed, 118 insertions, 0 deletions
diff --git a/fw/verify_gcp_loggates_memcpy_path.py b/fw/verify_gcp_loggates_memcpy_path.py new file mode 100644 index 0000000..aee47a4 --- /dev/null +++ b/fw/verify_gcp_loggates_memcpy_path.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Offline constraint on the GCP .77 memcpy fault in the log-gate attempt. + +Checks signed ARM instructions and the two literal planned copy spans. This +EXCLUDES an intact planned copy from the observed conditional store, but does +NOT reconstruct the actual caller/registers or authorize a hardware retry. +""" +from __future__ import annotations + +import ast +import argparse +import struct +from pathlib import Path + +from capstone import Cs, CS_ARCH_ARM, CS_MODE_ARM, CS_MODE_THUMB + +ELF = Path(__file__).resolve().parents[1] / 'psp_out/versions_win/elf_all/gcp_22.0.0.77_7c426e7f.elf' +FRAME_PC, FRAME_FAR = 0x4011D8, 0x42CFA0 + + +def executable_offset(data: bytes, va: int) -> int: + assert data[:6] == b'\x7fELF\x01\x01' + phoff = struct.unpack_from('<I', data, 28)[0] + entsize, count = struct.unpack_from('<HH', data, 42) + for i in range(count): + typ, off, base, _, size, _, flags, _ = struct.unpack_from('<8I', data, phoff+entsize*i) + if typ == 1 and flags & 1 and base <= va < base+size: + return off+va-base + raise ValueError(hex(va)) + + +def literals(path: Path) -> dict[str, int]: + # AST only: NEVER import/execute the host/card probe. + body = ast.parse(path.read_text()).body + out: dict[str, int] = {} + def value(node: ast.expr) -> int: + if isinstance(node, ast.Constant) and type(node.value) is int: + return node.value + if isinstance(node, ast.Name): + return out[node.id] + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): + return value(node.left)+value(node.right) + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Sub): + return value(node.left)-value(node.right) + raise ValueError(f'unsupported literal expression: {ast.dump(node)}') + wanted = {'PARAMS','STACK_DST','SRC_OFF','MLEN','DUMPDST','DUMPLEN','DUMP_OFF','MSGA_OFF','SLOT'} + for node in body: + if isinstance(node, ast.Assign) and len(node.targets)==1: + target=node.targets[0] + if isinstance(target, ast.Name) and target.id in wanted: + out[target.id] = value(node.value) + assert wanted <= out.keys(), wanted-out.keys() + # Bind the claimed two offsets to the actual top-level qcs invocation, + # not merely to constants with similar names elsewhere in the script. + qcs_def=next(n for n in body if isinstance(n,ast.FunctionDef) and n.name=='qcs') + calls=[n for n in ast.walk(ast.Module(body=body,type_ignores=[])) + if isinstance(n,ast.Call) and isinstance(n.func,ast.Name) and n.func.id=='qcs'] + assert len(calls)==1 + args=calls[0].args + assert len(args)==3 + for pos,expected in ((1,out['SLOT']-0x1c),(2,out['SLOT']-4)): + expr=args[pos] + assert isinstance(expr,ast.Call) and isinstance(expr.func,ast.Name) and expr.func.id=='disp' + assert value(expr.args[0])==expected + writes={} + for n in ast.walk(qcs_def): + if isinstance(n,ast.Call) and isinstance(n.func,ast.Attribute) and n.func.attr=='pack_into': + if len(n.args)>3 and isinstance(n.args[2],ast.Constant): + writes[n.args[2].value]=n.args[3] + assert 'dA' in ast.unparse(writes[0x840]) and 'dB' in ast.unparse(writes[0x838]) + assert value(writes[0x81c])==0x40 # eight-byte selector-4 copies + return out + + +def main() -> None: + ap=argparse.ArgumentParser(description=__doc__) + ap.add_argument('--probe',type=Path,default=Path('/tmp/gcp_rop_sysdrv_loggates_readonly.py')) + v=literals(ap.parse_args().probe) + image=ELF.read_bytes() + md=Cs(CS_ARCH_ARM, CS_MODE_ARM) + for ea,mnem in ((0x40111C,'cmp'),(0x401154,'beq'),(0x4011A8,'subs'), + (0x4011C8,'lsls'),(FRAME_PC,'stmmi')): + off=executable_offset(image,ea) + ins=next(md.disasm(image[off:off+4],ea,count=1)) + assert ins.address==ea and ins.mnemonic==mnem,(hex(ea),ins.mnemonic) + planned=((v['STACK_DST'],v['PARAMS']+v['SRC_OFF'],v['MLEN']), + (v['DUMPDST'],v['STACK_DST']+v['DUMP_OFF'],v['DUMPLEN'])) + for dest,src,length in planned: + assert dest%16==src%16==length%16==0,(hex(dest),hex(src),hex(length)) + assert not (dest <= FRAME_FAR < dest+length) + print(f'planned copy dst=0x{dest:x} src=0x{src:x} len=0x{length:x}: ' + 'aligned/16-multiple; cannot execute STMMI tail with intact arguments') + # FE0A selector 4 performs TWO 8-byte copies to a host-displaced R0. + # The first's nominal destination, unlike the two ROP copies above, + # differs from FAR by exactly a page-aligned 0x11000. At this length, + # ARM 0x40111C can enter 0x4011A4 and execute the STMMI tail. + thumb=Cs(CS_ARCH_ARM,CS_MODE_THUMB) + for ea,mnem in ((0x404D44,'add.w'),(0x404D4E,'blx'), + (0x404D52,'add.w'),(0x404D5C,'blx')): + off=executable_offset(image,ea) + ins=next(thumb.disasm(image[off:off+4],ea,count=1)) + assert ins.address==ea and ins.mnemonic==mnem,(hex(ea),ins.mnemonic) + first=v['SLOT']-0x1C + second=v['SLOT']-4 + delta=(FRAME_FAR-first)&0xffffffff + assert delta==0x11000 and (FRAME_FAR-second)&0xfff != 0 + inferred_base=(v['PARAMS']+delta)&0xffffffff + print(f'selector-4 first 8-byte chosen-write nominal dst=0x{first:x}; ' + f'FAR=0x{FRAME_FAR:x} implies params VA=0x{inferred_base:x} ' + f'(old assumed 0x{v["PARAMS"]:x} + 0x{delta:x}).') + print('Second chosen-write would need a non-page-aligned base shift; ' + 'first is a strong MATCH, not measured R0/R6 or proof of caller.') + print('The actual command-buffer VA must be verified independently; ' + 'NO hardware retry authorized by this offline inference.') + + +if __name__=='__main__': + main() |
