-- dwt/oracle.lua -- direct-invocation oracle for the VT1200 rdebug `dwt_prf` -- challenge transform. -- -- Run as a MAME -autoboot_script. No TCP connection, no driver change, the -- ROM file is untouched. The script reproduces exactly what dwt_prf does -- around the transform: -- -- 0x3F5A9620(1) ; alloc/init tables (Ghidra 0x07EB52C4) -- 0x3F5AA2C0(0x1BE810) ; expand key schedule (Ghidra 0x07EB5458) -- 0x3F5A09C0(buf, 16) ; the transform (Ghidra 0x07EB4138) -- 0x3F5A9FA0() ; free tables (Ghidra 0x07EB53F4) -- -- by calling those ROM routines with the firmware's own stack convention -- (caller pushes A14, writes arguments upward from A14, CALLA pushes the -- return address; callee restores A14 from the stack and `rets 2` pops it). -- The harness borrows a scratch stack in unused top-of-RAM, pushes a -- synthetic return address, and stops on a breakpoint there. -- -- Inputs: VT1200_ORACLE_IN text file, one 32-hex-char (16-byte) input per -- line; '#' comments and blank lines are skipped. -- Output: VT1200_ORACLE_OUT one line per vector: -- " " -- Env: VT1200_ORACLE_START frame to hijack the CPU (default 3000) -- VT1200_ORACLE_TAG label used in log lines (default "oracle") -- -- The input is the buffer as the transform sees it: the 16 received bytes -- after dwt_prf's ntohl (i.e. each 32-bit wire word byte-swapped). The -- output is the in-place transformed buffer that dwt_prf then memcmp's -- against the host-order challenge words. local machine = manager.machine local cpu = machine.devices[':maincpu'] local dbg = machine.debugger local space = cpu.spaces['program'] if not dbg then machine:exit(); return end dbg.visible_cpu = cpu local IN_PATH = os.getenv("VT1200_ORACLE_IN") local OUT_PATH = os.getenv("VT1200_ORACLE_OUT") local START = tonumber(os.getenv("VT1200_ORACLE_START") or "3000") local TAG = os.getenv("VT1200_ORACLE_TAG") or "oracle" local INTER = os.getenv("VT1200_ORACLE_INTERMEDIATE") == "1" -- Scratch RAM: the top 1 MiB of the 4 MiB main RAM (bytes 0x300000..). -- hello-world and the earlier probes also used 0x01800000 without trouble. local ARG = 0x01800000 -- argument block (A14 target), grows upward local STACK = 0x01840000 -- scratch stack top, grows downward local CBUF = 0x01880000 -- 16-byte transform buffer local RET = 0x018C0000 -- synthetic return address (never executed) local IE = 0x200000 -- TMS34010 ST.IE (tms34010.cpp: STBIT_IE = 1<<21) local INIT = 0x3F5A9620 local EXPAND = 0x3F5AA2C0 local TRANSFORM = 0x3F5A09C0 local FREE = 0x3F5A9FA0 local SRC_BLOCK = 0x1BE810 -- 128-byte seed block expanded by EXPAND local f = 0 local stage = "boot" local idx = 1 local vectors = {} local out = nil local saved = {} local err = nil -- ---------------------------------------------------------------- utilities local function state(name) local r = cpu.state[name] return r and r.value or nil end local function hexbytes(bitaddr, n) local t = {} for i = 0, n - 1 do t[#t + 1] = string.format("%02X", space:read_u8(bitaddr + i * 8)) end return table.concat(t) end local function dump_lines(label, bitaddr, n) for off = 0, n - 1, 32 do local m = math.min(32, n - off) print(string.format("%s: %s+%03X %s", TAG, label, off, hexbytes(bitaddr + off * 8, m))) end end local function push(v) local sp = state('SP') - 0x20 space:write_u32(sp, v) cpu.state['SP'].value = sp end -- Call a firmware routine with the firmware's own convention. -- args == {} -> bare CALL (routine does `rets`, no pushed A14) -- args ~= {} -> caller pushes A14, writes args upward, CALLA local function setup_call(target, args) saved.pc = state('PC') saved.sp = state('SP') saved.a14 = state('A14') saved.st = state('ST') cpu.state['SP'].value = STACK if #args > 0 then local a = ARG for i = 1, #args do space:write_u32(a, args[i]) a = a + 0x20 end push(saved.a14) cpu.state['A14'].value = ARG + #args * 0x20 end push(RET) cpu.state['PC'].value = target cpu.state['ST'].value = saved.st & ~IE dbg:command('bclear') dbg:command(string.format('bpset %X', RET)) dbg.execution_state = 'run' end local function restore_and_exit(rc) dbg:command('bclear') if saved.pc then cpu.state['PC'].value = saved.pc cpu.state['SP'].value = saved.sp cpu.state['A14'].value = saved.a14 cpu.state['ST'].value = saved.st end if out then out:close() end print(string.format("%s: done (%s), rc=%s", TAG, stage, tostring(rc))) machine:exit() end -- ---------------------------------------------------------------- start-up if IN_PATH then local fh, e = io.open(IN_PATH, "r") if not fh then print(TAG .. ": cannot open input " .. tostring(IN_PATH) .. ": " .. tostring(e)) machine:exit() return end for line in fh:lines() do line = line:gsub("%s+", "") if #line > 0 and line:sub(1, 1) ~= "#" then if #line ~= 32 then print(TAG .. ": bad vector length " .. #line .. ": " .. line) machine:exit() return end vectors[#vectors + 1] = line:upper() end end fh:close() end print(string.format("%s: loaded %d vectors from %s", TAG, #vectors, tostring(IN_PATH))) if OUT_PATH then out = io.open(OUT_PATH, "w") if not out then print(TAG .. ": cannot open output " .. tostring(OUT_PATH)) machine:exit() return end end local function write_out(index, inhex, outhex) if out then out:write(string.format("%d %s %s\n", index, inhex, outhex)) out:flush() end end local function next_vector() if idx > #vectors then -- mirror dwt_prf's teardown, then leave stage = "free_run" setup_call(FREE, {}) return end local v = vectors[idx] for i = 0, 15 do space:write_u8(CBUF + i * 8, tonumber(v:sub(i * 2 + 1, i * 2 + 2), 16)) end stage = "transform_run" setup_call(TRANSFORM, { 16, CBUF }) end -- ---------------------------------------------------------------- main loop emu.register_frame_done(function() f = f + 1 if stage == "boot" and f >= START then print(string.format("%s: frame %d hijack pc=%08X sp=%08X a14=%08X", TAG, f, state('PC'), state('SP'), state('A14'))) stage = "init_run" setup_call(INIT, { 1 }) return end if stage == "boot" then return end -- While the debugger is halted the OSD keeps pumping frame updates -- (running_machine::run -> video.frame_update when paused), so this -- callback still runs; act once per stop. if dbg.execution_state ~= 'stop' then return end local pc = state('PC') if pc ~= RET then print(string.format("%s: unexpected stop at %08X in stage %s", TAG, pc, stage)) err = "unexpected stop" restore_and_exit(1) return end if stage == "init_run" then print(string.format("%s: init returned; @1C0200=%08X @1C0220=%08X @1C0240=%08X @1C0260=%08X", TAG, space:read_u32(0x1c0200), space:read_u32(0x1c0220), space:read_u32(0x1c0240), space:read_u32(0x1c0260))) stage = "expand_run" setup_call(EXPAND, { SRC_BLOCK }) elseif stage == "expand_run" then print(string.format("%s: expand returned; keyptr=%08X fontptr=%08X renderptr=%08X", TAG, space:read_u32(0x1c0260), space:read_u32(0x1c0220), space:read_u32(0x1c0240))) -- Evidence: the seed block and the full 128-byte expanded key schedule, -- per run. dwt_prf calls INIT(1), so @1C0220/@1C0240 stay NULL and -- both "font render" steps are memcpy (see notes/CHALLENGE_ORACLE.md). dump_lines("seed", SRC_BLOCK, 128) dump_lines("key", space:read_u32(0x1c0260), 128) dump_lines("aux", 0x1BED40, 0x2C0) stage = "vectors" next_vector() elseif stage == "transform_run" then local inhex = vectors[idx] local outhex = hexbytes(CBUF, 16) write_out(idx, inhex, outhex) idx = idx + 1 if idx % 25 == 0 or idx > #vectors then print(string.format("%s: %d/%d vectors", TAG, idx - 1, #vectors)) end next_vector() elseif stage == "free_run" then print(string.format("%s: free returned; exiting", TAG)) restore_and_exit(0) end end, "vt1200-dwt-oracle") dbg.execution_state = 'run'