Skip to content

Commit c81b4ef

Browse files
committed
[ext_lib]
standalone Python library. Thank you Cédric (@saidelike).
1 parent 4229fe0 commit c81b4ef

File tree

2 files changed

+245
-1
lines changed

2 files changed

+245
-1
lines changed

README.md

+48-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ that I developed and maintained during my stay at
6262
- [OllyDbg 1.10 usage](#ollydbg-110-usage)
6363
- [OllyDbg2 usage](#ollydbg2-usage)
6464
- [x64dbg usage](#x64dbg-usage)
65+
- [Python library usage](#python-library-usage)
6566
- [Extend](#extend)
6667
- [TODO](#todo)
6768
- [Known Bugs/Limitations](#known-bugslimitations)
@@ -80,13 +81,18 @@ The debugger plugins:
8081
* `ext_olly2`: OllyDbg v2 plugin
8182
* `ext_x64dbg`: x64dbg plugin
8283

83-
And the disassembler plugins:
84+
The disassembler plugins:
8485

8586
* `ext_ida/SyncPlugin.py`
8687
* `ext_ghidra/dist/ghidra_*_retsync.zip`: Ghidra plugin
8788
* `ext_bn/retsync`: Binary Ninja plugin
8889

8990

91+
And the library plugin:
92+
93+
* `ext_lib/sync.py`: standalone Python library
94+
95+
9096
# General prerequisites
9197

9298
IDA and GDB plugins require a valid Python setup. Python 2 (>=2.7) and Python
@@ -966,6 +972,47 @@ specific address (equivalent of running **disasm <rebased addr>** in x64dbg
966972
command line).
967973

968974

975+
## Python library usage
976+
977+
One may want to use **ret-sync** core features (position syncing with a
978+
disassembler, symbol resolution) even though a full debugging environment is
979+
not available or with a custom tool. To that end, a minimalist Python library
980+
has been extracted.
981+
982+
The example below illustrates the usage of the Python library with a script
983+
that walks through the output of an event logging/tracing tool.
984+
985+
986+
```python
987+
from sync import *
988+
989+
HOST = '127.0.0.1'
990+
991+
MAPPINGS = [
992+
[0x555555400000, 0x555555402000, 0x2000, " /bin/tempfile"],
993+
[0x7ffff7dd3000, 0x7ffff7dfc000, 0x29000, " /lib/x86_64-linux-gnu/ld-2.27.so"],
994+
[0x7ffff7ff7000, 0x7ffff7ffb000, 0x4000, " [vvar]"],
995+
[0x7ffff7ffb000, 0x7ffff7ffc000, 0x1000, " [vdso]"],
996+
[0x7ffffffde000, 0x7ffffffff000, 0x21000, " [stack]"],
997+
]
998+
999+
EVENTS = [
1000+
[0x0000555555400e74, "malloc"],
1001+
[0x0000555555400eb3, "open"],
1002+
[0x0000555555400ee8, "exit"]
1003+
]
1004+
1005+
synctool = Sync(HOST, MAPPINGS)
1006+
1007+
for e in EVENTS:
1008+
offset, name = e
1009+
synctool.invoke(offset)
1010+
print(" 0x%08x - %s" % (offset, name))
1011+
print("[>] press enter for next event")
1012+
input()
1013+
```
1014+
1015+
9691016
# Extend
9701017

9711018
While initially focused on dynamic analysis (debuggers), it is of-course

ext_lib/sync.py

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/python3
2+
#
3+
# Copyright (C) 2016, Alexandre Gazet.
4+
# Copyright (C) 2012-2014, Quarkslab.
5+
#
6+
# Copyright (C) 2017, Cedric Halbronn, NCC Group.
7+
#
8+
# This file is part of ret-sync.
9+
#
10+
# ret-sync is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License as published by
12+
# the Free Software Foundation, either version 3 of the License, or
13+
# (at your option) any later version.
14+
#
15+
# This program is distributed in the hope that it will be useful,
16+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
# GNU General Public License for more details.
19+
#
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
#
23+
# Random notes:
24+
# There is no concept of disabling tunnel polling for commands (happy race...).
25+
26+
import os
27+
import re
28+
import sys
29+
import time
30+
import socket
31+
import errno
32+
33+
VERBOSE = 0
34+
35+
HOST = "localhost"
36+
PORT = 9100
37+
38+
39+
# ext_python is adapted from ret-sync/ext_gdb/sync.py
40+
# TODO: factorize with the GNU GDB plugin
41+
42+
43+
def get_mod_by_addr(maps, addr):
44+
for mod in maps:
45+
if (addr > mod[0]) and (addr < mod[1]):
46+
return [mod[0], mod[3]]
47+
return None
48+
49+
50+
class Tunnel():
51+
52+
def __init__(self, host):
53+
print("[sync] Initializing tunnel to IDA using %s:%d..." % (host, PORT))
54+
try:
55+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
56+
self.sock.connect((host, PORT))
57+
except socket.error as msg:
58+
self.sock.close()
59+
self.sock = None
60+
self.sync = False
61+
print("[sync] Tunnel initialization error: %s" % msg)
62+
return None
63+
64+
self.sync = True
65+
66+
def is_up(self):
67+
return (self.sock is not None and self.sync is True)
68+
69+
def poll(self):
70+
if not self.is_up():
71+
return None
72+
73+
self.sock.setblocking(False)
74+
75+
try:
76+
msg = self.sock.recv(4096).decode()
77+
except socket.error as e:
78+
err = e.args[0]
79+
if (err == errno.EAGAIN or err == errno.EWOULDBLOCK):
80+
return '\n'
81+
else:
82+
self.close()
83+
return None
84+
85+
self.sock.setblocking(True)
86+
return msg
87+
88+
def send(self, msg):
89+
if not self.sock:
90+
print("[sync] tunnel_send: tunnel is unavailable (did you forget to sync ?)")
91+
return
92+
93+
try:
94+
self.sock.send(msg.encode())
95+
except socket.error as msg:
96+
print(msg)
97+
self.sync = False
98+
self.close()
99+
100+
print("[sync] tunnel_send error: %s" % msg)
101+
102+
def close(self):
103+
if self.is_up():
104+
self.send("[notice]{\"type\":\"dbg_quit\",\"msg\":\"dbg disconnected\"}\n")
105+
106+
if self.sock:
107+
try:
108+
self.sock.close()
109+
except socket.error as msg:
110+
print("[sync] tunnel_close error: %s" % msg)
111+
112+
self.sync = False
113+
self.sock = None
114+
115+
116+
class Rln:
117+
118+
def __init__(self, sync):
119+
self.sync = sync
120+
121+
def invoke(self, raddr):
122+
self.sync.locate(raddr)
123+
124+
if (raddr is None) or (self.sync.offset is None):
125+
return "-"
126+
127+
self.sync.tunnel.send("[sync]{\"type\":\"rln\",\"raddr\":%d" % raddr)
128+
129+
# Let time for the IDB client to reply if it exists
130+
# Need to give it more time than usual to avoid "Resource temporarily unavailable"
131+
time.sleep(0.5)
132+
133+
# Poll tunnel
134+
msg = self.sync.tunnel.poll()
135+
if msg:
136+
return msg[:-1] # strip newline
137+
else:
138+
return "-"
139+
140+
141+
class Sync:
142+
def __init__(self, host, maps):
143+
if not maps:
144+
print("[sync] the memory mappings needs to be provided")
145+
return None
146+
147+
self.maps = maps
148+
self.base = None
149+
self.offset = None
150+
self.tunnel = None
151+
self.poller = None
152+
self.host = host
153+
154+
def locate(self, offset):
155+
if not offset:
156+
print("<unknown offset>")
157+
return
158+
159+
self.offset = offset
160+
mod = get_mod_by_addr(self.maps, self.offset)
161+
if mod:
162+
if VERBOSE >= 2:
163+
print("[sync] mod found")
164+
print(mod)
165+
166+
base, sym = mod
167+
168+
if self.base != base:
169+
self.tunnel.send("[notice]{\"type\":\"module\",\"path\":\"%s\"}\n" % sym)
170+
self.base = base
171+
172+
self.tunnel.send("[sync]{\"type\":\"loc\",\"base\":%d,\"offset\":%d}\n" % (self.base, self.offset))
173+
else:
174+
self.base = None
175+
self.offset = None
176+
177+
def invoke(self, offset):
178+
if self.tunnel and not self.tunnel.is_up():
179+
self.tunnel = None
180+
181+
if not self.tunnel:
182+
self.tunnel = Tunnel(self.host)
183+
if not self.tunnel.is_up():
184+
print("[sync] sync failed")
185+
return
186+
187+
id = "ext_python"
188+
self.tunnel.send("[notice]{\"type\":\"new_dbg\",\"msg\":\"dbg connect - %s\",\"dialect\":\"gdb\"}\n" % id)
189+
print("[sync] sync is now enabled with host %s" % str(self.host))
190+
else:
191+
print('(update)')
192+
193+
self.locate(offset)
194+
195+
196+
if __name__ == "__main__":
197+
print("[sync] this module cannot be called directly and needs to be imported from an external script")

0 commit comments

Comments
 (0)