diff --git a/handoff/NOTES_LEVEL.md b/handoff/NOTES_LEVEL.md index c058bbf..271cfcf 100644 --- a/handoff/NOTES_LEVEL.md +++ b/handoff/NOTES_LEVEL.md @@ -2422,3 +2422,16 @@ scripts/fnwatch.py: watch на слово кернела (bin43.re) от мом Гипотезы: потребитель читает КОПИЮ кернела; либо строится однократно до армирования; либо выравнивание доступа мимо слова. Следующий шаг: watch нескольких слов + старт с рождения хоста + контроль факта рендера (wav mtime). + +### 23j (короткое): мульти-watchpoint — попаданий нет при валидном рендере +fnwatch4.py: 4 DR одновременно {слот-указатель FIR (оба dword), слово кернела, +слово scratch} → затем позитивный контроль {overlap-слово, track[43], R[43], +лок 0x2404dc}. Конфигурация: render_fresh=True (wav@1.49 c), armed=8/8 тредов +(INTERRUPT→wait→arm→CONT на resweep обязателен — армирование бегущего треда +молча не работает), WATCH_DUR=30. **0 попаданий везде**, включая лок и +аудио-переполнение, которые ОБЯЗАНЫ трогаться при обработке. +ВЫВОД: аппаратные data-watchpoints под wine-тредами НЕ доставляются tracer'у +в этом окружении (kernel/wine взаимодействие) — маршрут закрыт. Потребителя +искать статически: скан disp32 из диапазона таблицы [0x540600..0x540900] +(семейство байт `?? 06 54 00`) → функции, итерирующие слоты по индексу; +плюс декод 52e1c0/534580-окрестностей отвергнут (аллокаторы). diff --git a/scripts/fnwatch4.py b/scripts/fnwatch4.py new file mode 100644 index 0000000..9ae38a4 --- /dev/null +++ b/scripts/fnwatch4.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +"""fnwatch4.py — 4 hardware watchpoints simultaneously: + DR0/DR1: dwords of the FIR pointer slot ctx+0x540668 (catches loader of ptr) + DR2 : kernel word bin43.re + DR3 : scratch word bin43 +RW=read/write (traps readers AND writers), LEN=4B.""" +import ctypes +import glob +import hashlib +import json +import os +import signal +import struct +import subprocess +import sys +import time + +DUR = float(os.environ.get('WATCH_DUR', '20')) +PTRACE_CONT = 7 +PTRACE_GETREGS = 12 +PTRACE_SETREGS = 13 +PTRACE_PEEKUSER = 3 +PTRACE_POKEUSER = 6 +PTRACE_SEIZE = 0x4206 +PTRACE_INTERRUPT = 0x4207 +PTRACE_O_TRACECLONE = 2 + +DR_BASE = 0x350 # u_debugreg[0] +DR6_OFF = DR_BASE + 48 # 0x388 - wait: idx6=+48 -> that IS DR6 slot? no: +# u_debugreg[0..7]: DR0@+0, DR1@+8, DR2@+16, DR3@+24, DR4@+32, DR5@+40, +# DR6@+48, DR7@+56 +DR7_OFF = DR_BASE + 56 +DR7_VAL = 0xFFFF000F # L0-3 enabled, all RW=11, all LEN=11 (4B) + +libc = ctypes.CDLL('libc.so.6', use_errno=True) + + +class UserRegs(ctypes.Structure): + _fields_ = [(n, ctypes.c_ulonglong) for n in ( + 'r15', 'r14', 'r13', 'r12', 'rbp', 'rbx', 'r11', 'r10', + 'r9', 'r8', 'rax', 'rcx', 'rdx', 'rsi', 'rdi', 'orig_rax', + 'rip', 'cs', 'eflags', 'rsp', 'ss', 'fs_base', 'gs_base', + 'ds', 'es', 'fs', 'gs')] + + +def pt(req, pid, addr=0, data=0): + libc.ptrace.restype = ctypes.c_long + r = libc.ptrace(req, pid, ctypes.c_void_p(addr), ctypes.c_void_p(data)) + return None if r == -1 else r + + +def find_host(): + for p in glob.glob('/proc/[0-9]*'): + pid = int(os.path.basename(p)) + try: + cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace') + maps = open(f'/proc/{pid}/maps').read() + except Exception: + continue + if 'soothe2' in maps and 'reaper' not in cmd: + return pid + return None + + +def main(): + subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; " + "rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True) + rpp = '/home/m/soothe-bt/dual_b1q_0.5.rpp' + wav = rpp.replace('.rpp', '.wav') + wt0 = os.path.getmtime(wav) if os.path.exists(wav) else 0 + proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors', + '-renderproject', rpp], + stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT) + host = None + t0 = time.time() + while time.time() - t0 < 30 and not host: + host = find_host() + time.sleep(0.002) + if not host: + print('NO HOST') + return 1 + + fd = os.open(f'/proc/{host}/mem', os.O_RDONLY) + + def rd(a, n): + try: + return os.pread(fd, n, a) + except OSError: + return None + + ctx = firptr = scrptr = None + vt = struct.pack('= 0: + cand = a + j + sb = rd(cand + 0x540870, 4) + if sb and struct.unpack(' 100: + ctx = cand + firptr = struct.unpack(' 0.03: + last_sweep = now + for tid_s in glob.glob(f'/proc/{host}/task/*'): + tid = int(os.path.basename(tid_s)) + if tid not in seized: + if pt(PTRACE_SEIZE, tid, 0, PTRACE_O_TRACECLONE) is not None: + seized.add(tid) + pt(PTRACE_INTERRUPT, tid) + for _ in range(40): + try: + wpid, _st = os.waitpid(tid, os.WUNTRACED | os.WNOHANG) + except ChildProcessError: + break + if wpid == tid: + break + time.sleep(0.001) + if arm(tid): + armed.add(tid) + try: + pt(PTRACE_CONT, tid, 0, 0) + except OSError: + pass + try: + pid, status = os.waitpid(-1, os.WSTOPPED | os.WNOHANG) + except ChildProcessError: + break + if pid == 0: + time.sleep(0.0005) + continue + sig = status >> 8 + if os.WIFEXITED(status) or os.WIFSIGNALED(status): + continue + if os.WIFSTOPPED(pid) and sig == signal.SIGTRAP: + regs = UserRegs() + if pt(PTRACE_GETREGS, pid, 0, ctypes.addressof(regs)) is None: + continue + dr6v = pt(PTRACE_PEEKUSER, pid, DR_BASE + 48) + rec = dict(rip=regs.rip, rsp=regs.rsp, tid=pid, + dr6=dr6v, + ret=struct.unpack(' wt0: + main._wv = True + print('wav fresh at %.2fs' % (time.time() - t0), flush=True) + if not hasattr(main, '_wv'): + main._wv = False + fresh = main._wv + print('hits=%d render_fresh=%s armed=%d/%d' % (len(hits), fresh, len(armed), len(seized))) + json.dump(hits[:200], open('/tmp/opencode/fnwatch/hits4.json', 'w'), + indent=1, default=str) + proc.kill() + return 0 + + +if __name__ == '__main__': + os.makedirs('/tmp/opencode/fnwatch', exist_ok=True) + sys.exit(main())