Initial commit

This commit is contained in:
2026-10-09 02:48:24 +02:00
commit e05d973a8d
133 changed files with 101507 additions and 0 deletions

183
ackit/verify/debugger.py Normal file
View File

@@ -0,0 +1,183 @@
"""Windows only. Unattended game run under cdb (WinDbg console debugger) with logging breakpoints.
Launch method (tested, see NOTES.md): with Steam running, BatmanAC.exe can be started directly
with SteamAppId=200260 in the environment (no relaunch through Steam). SteamStub decrypts .text
at runtime, so we wait until a known code signature is readable, then attach cdb non-intrusively
except for int3 breakpoints placed on already-decrypted code.
"""
import ctypes
import ctypes.wintypes as wt
import os
import re
import subprocess
import time
APPID = 200260
def find_cdb():
"""x86 cdb.exe from the WinDbg Store package (its path changes on every auto-update)."""
if os.environ.get("ACKIT_CDB"):
return os.environ["ACKIT_CDB"]
out = subprocess.run(["powershell", "-NoProfile", "-Command",
"(Get-AppxPackage Microsoft.WinDbg).InstallLocation"],
capture_output=True, text=True).stdout.strip()
for base in out.splitlines()[::-1]:
p = os.path.join(base.strip(), "x86", "cdb.exe")
if os.path.exists(p):
return p
for p in (r"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe",):
if os.path.exists(p):
return p
raise FileNotFoundError("x86 cdb.exe not found (install WinDbg or set ACKIT_CDB)")
# Runtime addresses (image base 0x400000; ASLR off for this exe). See docs/decoder_re.md.
ADDR = dict(
get_or_create_setup=0x010B3400, # __thiscall(cache, AkSrcInfo*, ...) ; info+0x9c hash
parse_setup=0x010B4D10, # WwiseVorbisParseSetupHeader(info, channels, reader, pool) -> 0 ok
parse_setup_ret=0x010B351A, # in get_or_create_setup, right after the call (eax = result)
sig_site=0x010B347B, # mov ebx,[ebp+0x94] (decryption probe)
)
SIG = bytes.fromhex("8B9D94000000")
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
PROCESS_VM_READ = 0x0010
PROCESS_QUERY_INFORMATION = 0x0400
k32.OpenProcess.restype = wt.HANDLE
k32.ReadProcessMemory.argtypes = [wt.HANDLE, wt.LPCVOID, wt.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
def read_mem(pid, addr, n):
h = k32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
if not h:
return None
try:
buf = ctypes.create_string_buffer(n)
got = ctypes.c_size_t()
if not k32.ReadProcessMemory(h, ctypes.c_void_p(addr), buf, n, ctypes.byref(got)):
return None
return buf.raw[:got.value]
finally:
k32.CloseHandle(h)
u32 = ctypes.WinDLL("user32", use_last_error=True)
WM_KEYDOWN, WM_KEYUP = 0x0100, 0x0101
VK = dict(enter=0x0D, space=0x20, esc=0x1B, up=0x26, down=0x28, left=0x25, right=0x27, e=0x45)
def game_windows(pid):
"""Top-level windows of the game process."""
found = []
cb_t = ctypes.WINFUNCTYPE(wt.BOOL, wt.HWND, wt.LPARAM)
def cb(hwnd, _):
p = wt.DWORD()
u32.GetWindowThreadProcessId(hwnd, ctypes.byref(p))
if p.value == pid and u32.IsWindowVisible(hwnd):
found.append(hwnd)
return True
u32.EnumWindows(cb_t(cb), 0)
return found
def post_key(pid, key, hold=0.08):
"""Post a key press to the game's window only (never global SendInput)."""
vk = VK[key] if isinstance(key, str) else key
sc = u32.MapVirtualKeyW(vk, 0)
n = 0
for hwnd in game_windows(pid):
u32.PostMessageW(hwnd, WM_KEYDOWN, vk, 1 | (sc << 16))
time.sleep(hold)
u32.PostMessageW(hwnd, WM_KEYUP, vk, 1 | (sc << 16) | (3 << 30))
n += 1
return n
def running_game_pids():
out = subprocess.run(["tasklist", "/FI", "IMAGENAME eq BatmanAC.exe", "/FO", "CSV", "/NH"],
capture_output=True, text=True).stdout
return [int(m.group(1)) for m in re.finditer(r'"BatmanAC\.exe","(\d+)"', out)]
def kill_game():
for pid in running_game_pids():
subprocess.run(["taskkill", "/F", "/PID", str(pid)], capture_output=True)
def launch_game(exe, extra_args=()):
"""Start BatmanAC.exe directly (Steam must be running; SteamAppId makes steam_api accept it)."""
env = dict(os.environ, SteamAppId=str(APPID), SteamGameId=str(APPID))
return subprocess.Popen([exe] + list(extra_args), cwd=os.path.dirname(exe), env=env)
def wait_decrypted(pid, timeout=60):
t0 = time.time()
while time.time() - t0 < timeout:
if read_mem(pid, ADDR["sig_site"], len(SIG)) == SIG:
return time.time() - t0
time.sleep(0.05)
raise TimeoutError("code never decrypted (signature not found)")
def cdb_script(extra_bps="", dump_info=False):
"""Debugger command file: log codec setup creation + parse result; log 2nd-chance crashes."""
a = ADDR
dump = "dd poi(@esp+4) L0x30; " if dump_info else ""
return "\n".join([
".echotime",
"sxd av",
'sxd -c2 ".echo ACKIT_CRASH; .echotime; r; kv 30; .ecxr; q" av',
"sxn ch", "sxn sse", "sxn eh", "sxn clr", "sxn ld", "sxn ud",
"sxi ibp",
# stream codec creation: log the AkVorbisInfo fields of the stream being opened
'bp %#x ".printf \\"ACKIT_SETUP hash=%%08x frames=%%u alloc=%%u ch=%%u rate=%%u\\\\n\\", '
'poi(poi(@esp+4)+0x9c), poi(poi(@esp+4)+0x78), poi(poi(@esp+4)+0x94), '
'wo(poi(@esp+4)+0x5a), poi(poi(@esp+4)+0x5c); %sgc"' % (a["get_or_create_setup"], dump),
# setup parse result (0 = OK)
'bp %#x ".printf \\"ACKIT_PARSE ret=%%x\\\\n\\", @eax; gc"' % a["parse_setup_ret"],
extra_bps,
".echo ACKIT_ATTACHED",
"g",
])
class Run:
def __init__(self, exe, log_path, script_extra="", dump_info=False):
self.exe = exe
self.log_path = log_path
self.script_extra = script_extra
self.dump_info = dump_info
self.game = None
self.dbg = None
def start(self):
kill_game()
self.t0 = time.time()
self.game = launch_game(self.exe)
pid = self.game.pid
dt = wait_decrypted(pid)
script = self.log_path + ".cdbcmds"
open(script, "w").write(cdb_script(self.script_extra, self.dump_info))
self.dbg = subprocess.Popen([find_cdb(), "-p", str(pid), "-pd", "-logo", self.log_path,
"-y", "", "-cf", script],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
return dict(pid=pid, decrypt_s=dt)
def alive(self):
return self.game.poll() is None
def stop(self):
kill_game()
if self.dbg:
try:
self.dbg.wait(15)
except subprocess.TimeoutExpired:
self.dbg.kill()
def log(self):
try:
return open(self.log_path, encoding="utf-8", errors="replace").read()
except FileNotFoundError:
return ""