dwt: reverse the dwt_prf challenge; remote poke/jump now works

The firmware transforms the reply (16-round Feistel per 8-byte record) and
compares it with the host-order copy of the challenge, so the answer is
swap4(inverse_transform(swap4(challenge))).  The font stages are memcpy in
this path because dwt_prf initialises only mode 1.

challenge.py implements the transform and its inverse using the firmware's
fixed key/SP table; solve.py answers the challenge over TCP using the
unmodified exploit client.  320 forward vectors, the firmware's own inverse
and three live sessions validate it.  A live run with no breakpoints poked
the 82-byte hello-world payload and ran it: MAGIC DEAD/BEEF/0400, VRAM
1024/1024.  Notes and evidence under dwt/notes and dwt/evidence.
This commit is contained in:
SDD committed 2026-10-18 02:36:23 +03:00
1 parent 041c82cb73
commit 1e5c095b72
22 files changed
+1351

No files matched your search

+197
View File
@@ -0,0 +1,197 @@
-- oracle_call.lua -- drive the firmware's own dwt_prf transform from the
-- debugger, with no TCP connection.
--
-- At frame 1000:
-- 0x3F5A9620(1) dwt_prf's allocator/init (Ghidra 0x07EB52C4)
-- 0x3F5AA2C0(0x1BE810) key schedule from the boot-time block
-- then, for each vector in VECTORS:
-- 0x3F5A09C0(BUF, 16) the transform dwt_prf applies to the reply
-- and prints the 16 output bytes.
--
-- Optionally (INVERSE=1) it calls 0x3F5AADA0 (the per-record companion that
-- looks like the inverse) on the vectors instead.
--
-- Output lines: ORACLE <inhex> <outhex>
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 ARG = 0x01800000
local STACK = 0x01840000
local RET = 0x018c0000
local BUF = 0x01810000
local IE = 0x200000
local INIT = 0x3F5A9620
local SCHED = 0x3F5AA2C0
local XFORM = 0x3F5A09C0
local XFORM1 = 0x3F5AADA0
local INVERSE = (os.getenv("ORACLE_INVERSE") or "") ~= ""
local COUNT = tonumber(os.getenv("ORACLE_COUNT") or "0")
local f = 0
local started = false
local phase = "init" -- init | sched | vectors | dump | done
local saved = {}
local idx = 0
local vectors = {}
local cur = nil
-- deterministic pseudo-random vectors plus the all-zero and ff cases
local function make_vectors()
vectors[1] = string.rep("\0", 16)
vectors[2] = string.rep("\255", 16)
local x = 0x12345678
for i = 3, COUNT do
local t = {}
for j = 1, 16 do
x = (x * 1103515245 + 12345) & 0x7fffffff
t[j] = string.char((x >> 16) & 0xff)
end
vectors[i] = table.concat(t)
end
end
local function state(name)
local r = cpu.state[name]
return r and r.value or nil
end
local function push(v)
local sp = state('SP') - 0x20
space:write_u32(sp, v)
cpu.state['SP'].value = sp
end
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
else
push(saved.a14)
cpu.state['A14'].value = ARG
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()
dbg:command('bclear')
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
local function hexbytes(addr, n)
local t = {}
for i = 0, n - 1 do
t[#t + 1] = string.format("%02X", space:read_u8(addr + i * 8))
end
return table.concat(t)
end
local function writebytes(addr, s)
for i = 1, #s do
space:write_u8(addr + (i - 1) * 8, s:byte(i))
end
end
local function next_vector()
idx = idx + 1
if idx > #vectors then
phase = "done"
restore()
machine:exit()
return
end
cur = vectors[idx]
writebytes(BUF, cur)
if INVERSE then
-- per-record inverse companion; call it twice, one 8-byte record each
phase = "vec_inv_lo"
setup_call(XFORM1, { BUF })
else
phase = "vec_fwd"
setup_call(XFORM, { 16, BUF })
end
end
emu.register_frame_done(function()
f = f + 1
if f == 1000 and not started then
started = true
if COUNT < 2 then COUNT = 2 end
make_vectors()
print(string.format("ORACLE start frame=%d p200=%08X p400=%08X",
f, space:read_u32(0x1c0200), space:read_u32(0x1c0400)))
phase = "init"
setup_call(INIT, { 1 })
return
end
if not started then return end
if f % 100 == 0 then
print(string.format("ORACLE trace frame=%d phase=%s state=%s pc=%08X sp=%08X",
f, phase, tostring(dbg.execution_state),
state('PC') or 0, state('SP') or 0))
end
if f > 1600 and phase ~= "done" then
print("ORACLE watchdog: exiting")
phase = "done"
restore()
machine:exit()
return
end
if dbg.execution_state ~= 'stop' then return end
if phase == "init" then
print(string.format("ORACLE init done @1C0200=%08X @1C0220=%08X @1C0240=%08X @1C0260=%08X @1C0400=%08X",
space:read_u32(0x1c0200), space:read_u32(0x1c0220),
space:read_u32(0x1c0240), space:read_u32(0x1c0260), space:read_u32(0x1c0400)))
phase = "sched"
setup_call(SCHED, { 0x1be810 })
elseif phase == "sched" then
local kp = space:read_u32(0x1c0260)
local fp = space:read_u32(0x1c0200)
print("ORACLE KP=" .. string.format("%08X", kp) .. " FP=" .. string.format("%08X", fp))
if kp ~= 0 then print("ORACLE KEY " .. hexbytes(kp, 128)) end
if fp ~= 0 then print("ORACLE FTAB " .. hexbytes(fp, 2048)) end
-- boot-time inputs the schedule reads
print("ORACLE SEED " .. hexbytes(0x1be810, 128))
print("ORACLE R1 " .. hexbytes(0x1bed40, 0x38))
print("ORACLE R2 " .. hexbytes(0x1bef00, 0x80))
print("ORACLE R3 " .. hexbytes(0x1bef80, 0x80))
print("ORACLE R4 " .. hexbytes(0x1c0280, 0xE0))
next_vector()
elseif phase == "vec_fwd" then
print("ORACLE " .. cur:gsub(".", function(c) return string.format("%02X", c:byte()) end)
.. " " .. hexbytes(BUF, 16))
next_vector()
elseif phase == "vec_inv_lo" then
phase = "vec_inv_hi"
setup_call(XFORM1, { BUF + 0x40 })
elseif phase == "vec_inv_hi" then
print("ORACLE_INV " .. cur:gsub(".", function(c) return string.format("%02X", c:byte()) end)
.. " " .. hexbytes(BUF, 16))
next_vector()
end
end, "vt1200-oracle-call")
dbg.execution_state = 'run'