Skip to content

Commit 379d502

Browse files
author
Kevin Z. Snow
committed
WIP: Add remote debug server zdbserver
1 parent 4b980a8 commit 379d502

File tree

10 files changed

+851
-2
lines changed

10 files changed

+851
-2
lines changed

conftest.py

+21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import contextlib
2+
import os
3+
4+
import filelock
5+
import pytest
6+
17
from hypothesis import HealthCheck, settings
28

39

@@ -7,3 +13,18 @@ def pytest_configure(config):
713
"patience", settings(suppress_health_check=[HealthCheck.too_slow])
814
)
915
settings.load_profile("patience")
16+
17+
18+
@pytest.fixture(scope="session")
19+
def lock(tmp_path_factory):
20+
base_temp = tmp_path_factory.getbasetemp()
21+
lock_file = base_temp.parent / "serial.lock"
22+
yield filelock.FileLock(lock_file=str(lock_file))
23+
with contextlib.suppress(OSError):
24+
os.remove(path=lock_file)
25+
26+
27+
@pytest.fixture()
28+
def serial(lock):
29+
with lock.acquire(poll_intervall=0.1):
30+
yield

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ not_skip="__init__.py"
1818
use_parentheses=true
1919

2020
known_first_party="zelos"
21-
known_third_party=["capstone", "colorama", "configargparse", "dnslib", "hypothesis", "lief", "pypacker", "recommonmark", "setuptools", "sortedcontainers", "sphinx_rtd_theme", "termcolor", "unicorn", "verboselogs", "zelos"]
21+
known_third_party=["capstone", "colorama", "configargparse", "dnslib", "filelock", "hypothesis", "lief", "pypacker", "pytest", "recommonmark", "setuptools", "sortedcontainers", "sphinx_rtd_theme", "termcolor", "unicorn", "verboselogs", "zelos"]
2222

2323
[tool.pytest]
2424
junit_family = "xunit2"

src/zelos/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
import colorama
3535

36-
from .api.zelos_api import Zelos
36+
from .api.zelos_api import Zelos, ZelosCmdline
3737
from .engine import Engine
3838
from .exceptions import (
3939
InvalidHookTypeException,
@@ -51,6 +51,7 @@
5151

5252
__all__ = [
5353
"Zelos",
54+
"ZelosCmdline",
5455
"Engine",
5556
"ZelosException",
5657
"ZelosLoadException",

src/zelos/api/regs_api.py

+6
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ def __getattr__(self, attr):
5454
def __setattr__(self, attr, value):
5555
self._current_thread.set_reg(attr, value)
5656

57+
def __getitem__(self, key):
58+
return self._current_thread.get_reg(key)
59+
60+
def __setitem__(self, key, value):
61+
self._current_thread.set_reg(key, value)
62+
5763
def getIP(self) -> int:
5864
"""
5965
Returns the platform-agnostic instruction pointer. On x86, this

src/zelos/tools/zdbserver/README.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Zelos Remote Debug Server (zdb)
2+
3+
The `zdbserver` enables remote debugging with `zelos` over an HTTP/XML-based RPC protocol, i.e. the python `xmlrpc` protocol.
4+
5+
## Basic Usage
6+
7+
To remotely debug a binary with default options:
8+
9+
```console
10+
$ python -m zelos.tools.zdbserver my_binary
11+
```
12+
13+
All the standard `zelos` flags can be used here as well. By default, the debug server is hosted on http://localhost:62433. The port can be changed:
14+
15+
```console
16+
$ python -m zelos.tools.zdbserver --debug_port 555 my_binary
17+
```
18+
19+
Currently, the only `zdb` client is an `angr` [tool](https://github.com/zeropointdynamics/angr-zelos-target) that integrates symbolic execution with `zelos`.

src/zelos/tools/zdbserver/__init__.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright (C) 2020 Zeropoint Dynamics
2+
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU Affero General Public License as
5+
# published by the Free Software Foundation, either version 3 of the
6+
# License, or (at your option) any later version.
7+
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU Affero General Public License for more details.
12+
13+
# You should have received a copy of the GNU Affero General Public
14+
# License along with this program. If not, see
15+
# <http://www.gnu.org/licenses/>.
16+
# ======================================================================
17+
18+
from .zdbserver import (
19+
DEFAULT_INTERFACE,
20+
DEFAULT_PORT,
21+
ZdbServer,
22+
create_server,
23+
)
24+
25+
26+
__all__ = ["ZdbServer", "create_server", "DEFAULT_INTERFACE", "DEFAULT_PORT"]

src/zelos/tools/zdbserver/__main__.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright (C) 2020 Zeropoint Dynamics
2+
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU Affero General Public License as
5+
# published by the Free Software Foundation, either version 3 of the
6+
# License, or (at your option) any later version.
7+
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU Affero General Public License for more details.
12+
13+
# You should have received a copy of the GNU Affero General Public
14+
# License along with this program. If not, see
15+
# <http://www.gnu.org/licenses/>.
16+
# ======================================================================
17+
18+
if __name__ == "__main__":
19+
import sys
20+
21+
from .zdbserver import create_server
22+
23+
server = create_server(sys.argv[1:])
24+
server.serve_forever()
+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Copyright (C) 2020 Zeropoint Dynamics
2+
3+
# This program is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU Affero General Public License as
5+
# published by the Free Software Foundation, either version 3 of the
6+
# License, or (at your option) any later version.
7+
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU Affero General Public License for more details.
12+
13+
14+
# You should have received a copy of the GNU Affero General Public
15+
# License along with this program. If not, see
16+
# <http://www.gnu.org/licenses/>.
17+
# ======================================================================
18+
19+
import multiprocessing
20+
import unittest
21+
import xmlrpc
22+
23+
from os import path
24+
25+
import pytest
26+
27+
from zelos.tools.zdbserver import DEFAULT_PORT, create_server
28+
29+
30+
DATA_DIR = path.join(
31+
path.dirname(path.abspath(__file__)), "../../../../tests/data"
32+
)
33+
34+
35+
class Transport(xmlrpc.client.Transport):
36+
def __init__(self, use_datetime=False, use_builtin_types=False, timeout=1):
37+
super().__init__(
38+
use_datetime=use_datetime, use_builtin_types=use_builtin_types
39+
)
40+
self.timeout = timeout
41+
42+
def make_connection(self, host):
43+
conn = super(Transport, self).make_connection(host)
44+
conn.timeout = self.timeout
45+
return conn
46+
47+
48+
class TestZdbServer(unittest.TestCase):
49+
def start_server(
50+
self,
51+
server_url=f"http://localhost:{DEFAULT_PORT}",
52+
target=path.join(DATA_DIR, "static_elf_helloworld"),
53+
) -> (multiprocessing.Process, xmlrpc.client.ServerProxy):
54+
55+
srv_ready = multiprocessing.Event()
56+
srv_error = multiprocessing.Event()
57+
58+
def server_process():
59+
try:
60+
rpc_server = create_server(target)
61+
srv_ready.set()
62+
rpc_server.serve_forever()
63+
except Exception as e:
64+
print("start_server Exception:", e)
65+
srv_error.set()
66+
srv_ready.set()
67+
68+
srv = multiprocessing.Process(target=server_process)
69+
srv.start()
70+
srv_ready.wait(10)
71+
self.assertTrue(srv_ready.is_set())
72+
self.assertFalse(srv_error.is_set())
73+
74+
rpc = xmlrpc.client.ServerProxy(
75+
server_url, transport=Transport(timeout=10)
76+
)
77+
78+
return srv, rpc
79+
80+
def stop_server(
81+
self, srv: multiprocessing.Process, rpc: xmlrpc.client.ServerProxy
82+
) -> None:
83+
rpc.server_shutdown()
84+
srv.join(5)
85+
self.assertFalse(srv.is_alive())
86+
if srv.is_alive():
87+
srv.terminate()
88+
89+
# @pytest.mark.usefixtures("serial")
90+
# def test_server_connect(self):
91+
# proc, rpc = self.start_server()
92+
# self.stop_server(proc, rpc)
93+
94+
@pytest.mark.usefixtures("serial")
95+
def test_server_api(self):
96+
# Do this all in one test so we're not creating many
97+
# sub-processes.
98+
proc, zdb = self.start_server()
99+
100+
zdb_exception = None
101+
102+
try:
103+
self.assertEqual(
104+
zdb.get_filepath(),
105+
path.join(DATA_DIR, "static_elf_helloworld"),
106+
)
107+
108+
# Test syscall breaks
109+
zdb.set_syscall_breakpoint("brk")
110+
break_state = zdb.run()
111+
del break_state["syscall"]["retval"] # address may change
112+
self.assertEqual(
113+
str(break_state),
114+
"{'pc': '0x815b577', 'syscall': {'name': 'brk', 'args': "
115+
"[{'type': 'void*', 'name': 'addr', 'value': '0x0'}], "
116+
"'retval_register': 'eax'}, 'bits': 32}",
117+
)
118+
119+
# Test syscall break argument values
120+
break_state = zdb.run()
121+
brk_target_addr = int(
122+
break_state["syscall"]["args"][0]["value"], 16
123+
)
124+
# Address may change, so just check it is non-zero
125+
self.assertNotEqual(0, brk_target_addr)
126+
127+
zdb.remove_syscall_breakpoint("brk")
128+
129+
# Test address breaks
130+
zdb.set_breakpoint("0x080eccf7", False)
131+
break_state = zdb.run()
132+
self.assertEqual(
133+
str(break_state),
134+
"{'pc': '0x80eccf7', 'syscall': {}, 'bits': 32}",
135+
)
136+
zdb.remove_breakpoint("0x080eccf7")
137+
138+
# Test read/write register
139+
pc = int(zdb.read_register("pc"), 16)
140+
self.assertEqual(pc, 0x80ECCF7)
141+
zdb.write_register("pc", "0xdeadbeef")
142+
pc = int(zdb.read_register("eip"), 16)
143+
self.assertEqual(pc, 0xDEADBEEF)
144+
zdb.write_register("eip", "0x80eccf7")
145+
pc = int(zdb.read_register("pc"), 16)
146+
self.assertEqual(pc, 0x80ECCF7)
147+
148+
ecx = int(zdb.read_register("ecx"), 16)
149+
self.assertEqual(ecx, 0x81E9CA0)
150+
151+
# Test `zdbserver` exceptions
152+
# ...
153+
154+
# Test memory read after break
155+
stack_var = int(zdb.read_register("esp"), 16) + 0x1C
156+
data = zdb.read_memory(f"0x{stack_var:x}", 4)
157+
print(data.data)
158+
# self.assertEqual(data.data, b"\xc0\x00\x00\x90") # after inst
159+
self.assertEqual(data.data, b"\x80\x00\x00\x90") # before inst
160+
161+
# Test memory map list
162+
mappings = zdb.get_mappings()
163+
self.assertEqual(len(mappings), 21)
164+
mr = mappings[0]
165+
self.assertTrue("start_address" in mr)
166+
self.assertTrue("end_address" in mr)
167+
168+
# Test memory write by modifying a `write` buffer just
169+
# before the syscall.
170+
zdb.set_breakpoint("0x08106b04", True)
171+
break_state = zdb.run()
172+
buf = zdb.read_register("edi")
173+
zdb.write_memory(buf, b"Hello World! I'm a Zelos Test!")
174+
175+
# Test watchpoint
176+
zdb.set_watchpoint(buf, True, True, False)
177+
# break_state = zdb.run()
178+
# self.assertEqual(break_state, "asdf")
179+
zdb.remove_watchpoint(buf)
180+
181+
# Test memory write, continued
182+
zdb.set_syscall_breakpoint("write")
183+
break_state = zdb.run()
184+
buf = break_state["syscall"]["args"][1]["value"]
185+
count = int(break_state["syscall"]["args"][2]["value"], 16)
186+
stdout = zdb.read_memory(buf, count)
187+
self.assertEqual(stdout, b"Hello World! I'm a Zelos Test!")
188+
189+
# Run until program end
190+
break_state = zdb.run()
191+
self.assertEqual(break_state, {})
192+
193+
zdb.stop()
194+
195+
except Exception as e:
196+
zdb_exception = e
197+
198+
finally:
199+
self.stop_server(proc, zdb)
200+
201+
if zdb_exception is not None:
202+
self.fail("test_server_api error: " + str(zdb_exception))
203+
204+
205+
if __name__ == "__main__":
206+
unittest.main()

0 commit comments

Comments
 (0)