feat: add condition evaluator for runtime-observation MCP protocol
This commit is contained in:
parent
37571e8b8a
commit
4a218cdb05
2 changed files with 102 additions and 0 deletions
43
tests/test_havoc_debug_protocol.py
Normal file
43
tests/test_havoc_debug_protocol.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import pytest
|
||||
from tools.havoc_debug_protocol import evaluate_condition
|
||||
|
||||
def test_register_condition_true():
|
||||
registers = {"eax": 0x50}
|
||||
condition = {"register": "eax", "op": "!=", "value": "0x47"}
|
||||
assert evaluate_condition(condition, registers, read_memory=None) is True
|
||||
|
||||
def test_register_condition_false():
|
||||
registers = {"eax": 0x47}
|
||||
condition = {"register": "eax", "op": "!=", "value": "0x47"}
|
||||
assert evaluate_condition(condition, registers, read_memory=None) is False
|
||||
|
||||
def test_register_condition_equals():
|
||||
registers = {"eip": 0x436070}
|
||||
condition = {"register": "eip", "op": "==", "value": "0x436070"}
|
||||
assert evaluate_condition(condition, registers, read_memory=None) is True
|
||||
|
||||
def test_memory_condition_with_offset():
|
||||
def fake_read_memory(addr, size):
|
||||
assert addr == 0x47eb48 + 0x18
|
||||
assert size == 4
|
||||
return (0x47).to_bytes(4, "little")
|
||||
condition = {"memory": "0x47eb48+0x18", "size": 4, "op": "==", "value": "0x47"}
|
||||
assert evaluate_condition(condition, {}, fake_read_memory) is True
|
||||
|
||||
def test_memory_condition_no_offset():
|
||||
def fake_read_memory(addr, size):
|
||||
assert addr == 0x47da88
|
||||
return (0).to_bytes(size, "little")
|
||||
condition = {"memory": "0x47da88", "size": 4, "op": "==", "value": "0x0"}
|
||||
assert evaluate_condition(condition, {}, fake_read_memory) is True
|
||||
|
||||
def test_comparison_operators():
|
||||
registers = {"ecx": 10}
|
||||
assert evaluate_condition({"register": "ecx", "op": "<", "value": "0xb"}, registers, None)
|
||||
assert evaluate_condition({"register": "ecx", "op": ">", "value": "0x9"}, registers, None)
|
||||
assert evaluate_condition({"register": "ecx", "op": "<=", "value": "0xa"}, registers, None)
|
||||
assert evaluate_condition({"register": "ecx", "op": ">=", "value": "0xa"}, registers, None)
|
||||
|
||||
def test_unknown_register_raises():
|
||||
with pytest.raises(KeyError):
|
||||
evaluate_condition({"register": "bogus", "op": "==", "value": "0x0"}, {}, None)
|
||||
59
tools/havoc_debug_protocol.py
Normal file
59
tools/havoc_debug_protocol.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"""Wire protocol and pure-logic helpers shared between the debug agent and MCP server.
|
||||
No ctypes, no Windows dependency -- runs and tests on any platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import operator
|
||||
from typing import Callable, Optional
|
||||
|
||||
_OPS = {
|
||||
"==": operator.eq,
|
||||
"!=": operator.ne,
|
||||
"<": operator.lt,
|
||||
">": operator.gt,
|
||||
"<=": operator.le,
|
||||
">=": operator.ge,
|
||||
}
|
||||
|
||||
|
||||
def _parse_memory_expr(expr: str) -> int:
|
||||
"""Parse "0x47eb48+0x18" or "0x47eb48" into an absolute address."""
|
||||
if "+" in expr:
|
||||
base_str, offset_str = expr.split("+", 1)
|
||||
return int(base_str.strip(), 16) + int(offset_str.strip(), 16)
|
||||
return int(expr.strip(), 16)
|
||||
|
||||
|
||||
def evaluate_condition(
|
||||
condition: dict,
|
||||
registers: dict,
|
||||
read_memory: Optional[Callable[[int, int], bytes]],
|
||||
) -> bool:
|
||||
"""Evaluate a conditional-breakpoint expression against a register snapshot and/or
|
||||
memory reader. `condition` is either {"register": name, "op": ..., "value": hexstr}
|
||||
or {"memory": expr, "size": n, "op": ..., "value": hexstr}."""
|
||||
op_fn = _OPS[condition["op"]]
|
||||
expected = int(condition["value"], 16)
|
||||
|
||||
if "register" in condition:
|
||||
actual = registers[condition["register"]]
|
||||
elif "memory" in condition:
|
||||
addr = _parse_memory_expr(condition["memory"])
|
||||
size = condition["size"]
|
||||
raw = read_memory(addr, size)
|
||||
actual = int.from_bytes(raw, "little")
|
||||
else:
|
||||
raise ValueError(f"condition must have 'register' or 'memory': {condition}")
|
||||
|
||||
return op_fn(actual, expected)
|
||||
|
||||
|
||||
def encode_message(op: str, params: dict) -> bytes:
|
||||
"""Encode a request as a single NDJSON line."""
|
||||
return (json.dumps({"op": op, "params": params}) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def decode_message(line: bytes) -> dict:
|
||||
"""Decode a single NDJSON line (request or response) back into a dict."""
|
||||
return json.loads(line.decode("utf-8"))
|
||||
Loading…
Reference in a new issue