forked from arti/vt1200
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:
1 parent
041c82cb73
commit
1e5c095b72
22 files changed
+1351
No files matched your search
@@ -0,0 +1,53 @@
|
||||
-- live_check.lua -- verification with NO breakpoints and no memory writes.
|
||||
--
|
||||
-- This script only reads RAM/VRAM once per frame. There is no bpset anywhere,
|
||||
-- so the exploit cannot be attributed to any debugger action: the only thing
|
||||
-- the emulator does is report what the firmware and the payload left in memory.
|
||||
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 MAGIC1 = 0x01810000
|
||||
local MAGIC2 = 0x01810010
|
||||
local COUNT = 0x01810020
|
||||
local VRAM = 0x04019040
|
||||
local f = 0
|
||||
local done_at = nil
|
||||
local last = nil
|
||||
|
||||
emu.register_frame_done(function()
|
||||
f = f + 1
|
||||
if f % 500 == 0 then
|
||||
print(string.format("CHECK frame %d MAGIC1=%04X MAGIC2=%04X COUNT=%04X",
|
||||
f, space:read_u16(MAGIC1), space:read_u16(MAGIC2), space:read_u16(COUNT)))
|
||||
end
|
||||
if done_at == nil and space:read_u16(MAGIC1) == 0xDEAD then
|
||||
done_at = f
|
||||
local filled, holes = 0, 0
|
||||
for r = 0, 63 do
|
||||
for c = 0, 15 do
|
||||
if space:read_u16(VRAM + r * 1024 + c * 16) == 0xFFFF then
|
||||
filled = filled + 1
|
||||
else
|
||||
holes = holes + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
print(string.format("CHECK payload-ran: MAGIC1=%04X MAGIC2=%04X COUNT=%04X",
|
||||
space:read_u16(MAGIC1), space:read_u16(MAGIC2), space:read_u16(COUNT)))
|
||||
print(string.format("CHECK payload-ran: VRAM block %d/1024 words FFFF, %d holes",
|
||||
filled, holes))
|
||||
end
|
||||
if done_at and f >= done_at + 120 then
|
||||
print(string.format("CHECK exiting at frame %d (payload seen at %d)", f, done_at))
|
||||
machine:exit()
|
||||
end
|
||||
if f >= 12000 then
|
||||
print("CHECK timeout: no payload markers")
|
||||
machine:exit()
|
||||
end
|
||||
end, "vt1200-live-check")
|
||||
dbg.execution_state = 'run'
|
||||
@@ -0,0 +1,72 @@
|
||||
-- live_verify.lua -- passive verification of the remote challenge bypass.
|
||||
--
|
||||
-- No breakpoint here writes a register or memory: it only prints. In
|
||||
-- particular the memcmp at 3F5A0920 is *observed*, never forced.
|
||||
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 D = "/home/arti/code/k-space/vt1200/dwt/evidence/live-run"
|
||||
local MAGIC1 = 0x01810000
|
||||
local MAGIC2 = 0x01810010
|
||||
local COUNT = 0x01810020
|
||||
local VRAM = 0x04019040
|
||||
local f = 0
|
||||
local armed = false
|
||||
local saw_challenge = false
|
||||
local saw_memcmp = false
|
||||
local done_at = nil
|
||||
|
||||
local function report_magic(tag)
|
||||
print(string.format("LIVE2 %s: MAGIC1=%04X MAGIC2=%04X COUNT=%04X",
|
||||
tag, space:read_u16(MAGIC1), space:read_u16(MAGIC2), space:read_u16(COUNT)))
|
||||
local filled, holes = 0, 0
|
||||
for r = 0, 63 do
|
||||
for c = 0, 15 do
|
||||
if space:read_u16(VRAM + r * 1024 + c * 16) == 0xFFFF then
|
||||
filled = filled + 1
|
||||
else
|
||||
holes = holes + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
print(string.format("LIVE2 %s: VRAM block %d/1024 words FFFF, %d holes", tag, filled, holes))
|
||||
end
|
||||
|
||||
emu.register_frame_done(function()
|
||||
f = f + 1
|
||||
if f == 1 and not armed then
|
||||
armed = true
|
||||
dbg:command('bpset 3F58AD60,,{ printf "LIVE2 accept_debug_conn entry pc=%08X\\n", pc ; g }')
|
||||
dbg:command('bpset 3F59F320,,{ printf "LIVE2 dispatcher entry pc=%08X\\n", pc ; g }')
|
||||
dbg:command('bpset 3F59FDC0,,{ printf "LIVE2 dwt_prf challenge entry pc=%08X\\n", pc ; g }')
|
||||
dbg:command('bpset 3F5A0200,,{ printf "LIVE2 challenge sent a13=%08X wire=%08X.%08X.%08X.%08X host=%08X.%08X.%08X.%08X\\n", a13, d@(a13), d@(a13+0x20), d@(a13+0x40), d@(a13+0x60), d@(a13+0x100), d@(a13+0x120), d@(a13+0x140), d@(a13+0x160) ; g }')
|
||||
dbg:command('bpset 3F5A0740,,{ printf "LIVE2 pre-transform reply=%08X.%08X.%08X.%08X\\n", d@(a13+0x80), d@(a13+0xa0), d@(a13+0xc0), d@(a13+0xe0) ; g }')
|
||||
dbg:command('bpset 3F5A08E0,,{ printf "LIVE2 memcmp-args expecting=%08X.%08X.%08X.%08X got=%08X.%08X.%08X.%08X\\n", d@(a9), d@(a9+0x20), d@(a9+0x40), d@(a9+0x60), d@(a11), d@(a11+0x20), d@(a11+0x40), d@(a11+0x60) ; g }')
|
||||
-- OBSERVE ONLY: print the comparison result; never assign to a8.
|
||||
dbg:command('bpset 3F5A0920,,{ printf "LIVE2 challenge memcmp a8=%08X (0=equal)\\n", a8 ; g }')
|
||||
dbg:command('bpset 3F5A1B50,,{ printf "LIVE2 rdebug_poke entry pc=%08X\\n", pc ; g }')
|
||||
dbg:command('bpset 3F5A3890,,{ printf "LIVE2 rdebug_jump entry pc=%08X\\n", pc ; g }')
|
||||
print("LIVE2 armed read-only breakpoints")
|
||||
end
|
||||
if f % 500 == 0 then
|
||||
print(string.format("LIVE2 frame %5d word@0xAECD0=%04X field2..3=%d",
|
||||
f, space:read_u16(0xaecd0), (space:read_u16(0xaecd0) >> 2) & 3))
|
||||
end
|
||||
if done_at == nil and space:read_u16(MAGIC1) == 0xDEAD then
|
||||
done_at = f
|
||||
report_magic("payload-ran")
|
||||
end
|
||||
if done_at and f >= done_at + 120 then
|
||||
print(string.format("LIVE2 exiting at frame %d (payload seen at %d)", f, done_at))
|
||||
machine:exit()
|
||||
end
|
||||
if f >= 9000 then
|
||||
print("LIVE2 timeout: no payload markers")
|
||||
machine:exit()
|
||||
end
|
||||
end, "vt1200-live-verify")
|
||||
dbg.execution_state = 'run'
|
||||
@@ -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'
|
||||
Reference in new issue
Block a user