feat: add watchpoint slot/DR7 bookkeeping to MCP protocol module
This commit is contained in:
parent
4a218cdb05
commit
3256f4b607
2 changed files with 94 additions and 0 deletions
|
|
@ -41,3 +41,50 @@ def test_comparison_operators():
|
||||||
def test_unknown_register_raises():
|
def test_unknown_register_raises():
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
evaluate_condition({"register": "bogus", "op": "==", "value": "0x0"}, {}, None)
|
evaluate_condition({"register": "bogus", "op": "==", "value": "0x0"}, {}, None)
|
||||||
|
|
||||||
|
from tools.havoc_debug_protocol import WatchpointSlots, WatchpointLimitError
|
||||||
|
|
||||||
|
def test_allocate_returns_free_slots_in_order():
|
||||||
|
slots = WatchpointSlots()
|
||||||
|
assert slots.allocate(0x47c48c, 4, "write") == 0
|
||||||
|
assert slots.allocate(0x47da88, 4, "readwrite") == 1
|
||||||
|
|
||||||
|
def test_allocate_raises_past_four():
|
||||||
|
slots = WatchpointSlots()
|
||||||
|
slots.allocate(0x1000, 4, "write")
|
||||||
|
slots.allocate(0x2000, 4, "write")
|
||||||
|
slots.allocate(0x3000, 4, "write")
|
||||||
|
slots.allocate(0x4000, 4, "write")
|
||||||
|
with pytest.raises(WatchpointLimitError):
|
||||||
|
slots.allocate(0x5000, 4, "write")
|
||||||
|
|
||||||
|
def test_release_frees_slot_for_reuse():
|
||||||
|
slots = WatchpointSlots()
|
||||||
|
slot0 = slots.allocate(0x1000, 4, "write")
|
||||||
|
slots.release(slot0)
|
||||||
|
assert slots.allocate(0x2000, 4, "write") == 0
|
||||||
|
|
||||||
|
def test_compute_dr7_enables_local_bit_per_active_slot():
|
||||||
|
slots = WatchpointSlots()
|
||||||
|
slots.allocate(0x1000, 4, "write") # slot 0 -> L0 = bit 0
|
||||||
|
dr7 = slots.compute_dr7()
|
||||||
|
assert dr7 & 0b1 == 0b1 # L0 enabled
|
||||||
|
assert dr7 & 0b100 == 0 # L1 not enabled
|
||||||
|
|
||||||
|
def test_compute_dr7_encodes_write_mode_and_4byte_len():
|
||||||
|
slots = WatchpointSlots()
|
||||||
|
slots.allocate(0x1000, 4, "write")
|
||||||
|
dr7 = slots.compute_dr7()
|
||||||
|
rw0 = (dr7 >> 16) & 0b11
|
||||||
|
len0 = (dr7 >> 18) & 0b11
|
||||||
|
assert rw0 == 0b01 # write-only
|
||||||
|
assert len0 == 0b11 # 4 bytes
|
||||||
|
|
||||||
|
def test_compute_dr7_encodes_readwrite_and_1byte_len():
|
||||||
|
slots = WatchpointSlots()
|
||||||
|
slots.allocate(0x2000, 1, "readwrite")
|
||||||
|
dr7 = slots.compute_dr7()
|
||||||
|
rw0 = (dr7 >> 16) & 0b11
|
||||||
|
len0 = (dr7 >> 18) & 0b11
|
||||||
|
assert rw0 == 0b11 # read/write
|
||||||
|
assert len0 == 0b00 # 1 byte
|
||||||
|
|
|
||||||
|
|
@ -57,3 +57,50 @@ def encode_message(op: str, params: dict) -> bytes:
|
||||||
def decode_message(line: bytes) -> dict:
|
def decode_message(line: bytes) -> dict:
|
||||||
"""Decode a single NDJSON line (request or response) back into a dict."""
|
"""Decode a single NDJSON line (request or response) back into a dict."""
|
||||||
return json.loads(line.decode("utf-8"))
|
return json.loads(line.decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
class WatchpointLimitError(Exception):
|
||||||
|
"""Raised when all 4 hardware debug address registers are already in use."""
|
||||||
|
|
||||||
|
|
||||||
|
_MODE_RW_BITS = {"write": 0b01, "readwrite": 0b11}
|
||||||
|
_SIZE_LEN_BITS = {1: 0b00, 2: 0b01, 4: 0b11}
|
||||||
|
|
||||||
|
|
||||||
|
class WatchpointSlots:
|
||||||
|
"""Tracks which of the 4 x86 hardware debug address registers (DR0-DR3) are in use
|
||||||
|
and computes the DR7 control register value. Pure bookkeeping -- does not touch
|
||||||
|
ctypes or an actual thread context; the debug agent applies compute_dr7()'s result
|
||||||
|
via Wow64SetThreadContext."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._slots: dict[int, tuple[int, int, str]] = {} # slot -> (addr, size, mode)
|
||||||
|
|
||||||
|
def allocate(self, addr: int, size: int, mode: str) -> int:
|
||||||
|
if mode not in _MODE_RW_BITS:
|
||||||
|
raise ValueError(f"mode must be 'write' or 'readwrite', got {mode!r}")
|
||||||
|
if size not in _SIZE_LEN_BITS:
|
||||||
|
raise ValueError(f"size must be 1, 2, or 4 bytes, got {size}")
|
||||||
|
for slot in range(4):
|
||||||
|
if slot not in self._slots:
|
||||||
|
self._slots[slot] = (addr, size, mode)
|
||||||
|
return slot
|
||||||
|
raise WatchpointLimitError("all 4 hardware debug registers (DR0-DR3) are in use")
|
||||||
|
|
||||||
|
def release(self, slot: int) -> None:
|
||||||
|
self._slots.pop(slot, None)
|
||||||
|
|
||||||
|
def addresses(self) -> dict[int, int]:
|
||||||
|
"""slot -> address, for setting DR0-DR3 themselves."""
|
||||||
|
return {slot: addr for slot, (addr, _size, _mode) in self._slots.items()}
|
||||||
|
|
||||||
|
def compute_dr7(self) -> int:
|
||||||
|
dr7 = 0
|
||||||
|
for slot, (_addr, size, mode) in self._slots.items():
|
||||||
|
dr7 |= 1 << (slot * 2) # local enable bit (L0=bit0, L1=bit2, L2=bit4, L3=bit6)
|
||||||
|
rw_bits = _MODE_RW_BITS[mode]
|
||||||
|
len_bits = _SIZE_LEN_BITS[size]
|
||||||
|
shift = 16 + slot * 4
|
||||||
|
dr7 |= rw_bits << shift
|
||||||
|
dr7 |= len_bits << (shift + 2)
|
||||||
|
return dr7
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue