Files

249 lines
7.9 KiB
Lua

-- 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:
-- "<index> <input hex> <output hex>"
-- 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'