Skip to content

Commit 22a8a28

Browse files
committed
[ext_ida] net code hardening and better error reporting
**broker**: - receive beacon from dispatcher - reply to beacon request from idb - better error reporting and logging - connect to the dispatcher in the localhost interface **dispatcher**: - check port availability - sends a beacon to the broker if run properly - support for new command dbg_err - better error reporting and logging **syncrays**: - handle decompilation error (may happen if flooding the engine) **syncplugin**: - request beacon from broker Objective is to prevent or make more explicit issues like #33 and #32
1 parent 556afb1 commit 22a8a28

File tree

5 files changed

+118
-63
lines changed

5 files changed

+118
-63
lines changed

ext_ida/SyncPlugin.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ def req_broker(self, hash):
523523
elif(subtype == 'notice'):
524524
# notice from broker
525525
self.broker_port = int(hash['port'])
526-
rs_log("<< broker << listening on port %d" % self.broker_port)
526+
rs_debug("<< broker << binding on port %d" % self.broker_port)
527527

528528
for attempt in range(rsconfig.CONNECT_BROKER_MAX_ATTEMPT):
529529
try:
@@ -543,6 +543,10 @@ def req_broker(self, hash):
543543
self.announcement("[sync] failed to connect to broker (attempt %d)" % attempt)
544544
raise RuntimeError
545545

546+
# request broker to validate its beacon
547+
time.sleep(0.4)
548+
self.beacon_notice()
549+
546550
# enable/disable idb, if disable it drops most sync requests
547551
elif(subtype == 'enable_idb'):
548552
self.is_active = True
@@ -616,6 +620,10 @@ def normalize(self, req, taglen):
616620
def kill_notice(self):
617621
self.notice_broker("kill")
618622

623+
# send a beacon notice to the broker
624+
def beacon_notice(self):
625+
self.notice_broker('beacon')
626+
619627
# send a bp command (F2) to the debugger (via the broker and dispatcher)
620628
def bp_notice(self, oneshot=False):
621629
if not self.is_active:
@@ -817,6 +825,7 @@ def __init__(self, parser):
817825
# create a request handler
818826
self.worker = RequestHandler(parser)
819827

828+
820829
# --------------------------------------------------------------------------
821830

822831

ext_ida/retsync/broker.py

+48-37
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (C) 2016-2019, Alexandre Gazet.
2+
# Copyright (C) 2016-2020, Alexandre Gazet.
33
#
44
# Copyright (C) 2012-2015, Quarkslab.
55
#
@@ -30,6 +30,7 @@
3030
import subprocess
3131
import socket
3232
import select
33+
from contextlib import contextmanager
3334

3435
try:
3536
from ConfigParser import SafeConfigParser
@@ -106,16 +107,14 @@ def run_dispatcher(self):
106107
tokenizer = shlex.shlex(cmdline)
107108
tokenizer.whitespace_split = True
108109
args = [arg.replace('\"', '') for arg in list(tokenizer)]
109-
110110
try:
111111
proc = subprocess.Popen(args, shell=False,
112112
stdout=subprocess.PIPE,
113113
stderr=subprocess.PIPE)
114114
pid = proc.pid
115115
except (OSError, ValueError):
116116
pid = None
117-
self.announcement('failed to run dispatcher')
118-
err_log('failed to run dispatcher')
117+
self.err_log('failed to run dispatcher')
119118

120119
time.sleep(0.2)
121120
return pid
@@ -125,7 +124,7 @@ def notify(self):
125124
try:
126125
self.notify_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
127126
self.notify_socket.settimeout(2)
128-
self.notify_socket.connect((HOST, PORT))
127+
self.notify_socket.connect(('127.0.0.1', PORT))
129128
break
130129
except socket.error:
131130
self.notify_socket.close()
@@ -163,16 +162,18 @@ def recvall(self, client):
163162
if data == '':
164163
raise Exception('rabbit eating the cable')
165164
except socket.error:
166-
self.announcement('dispatcher connection error, quitting')
167-
sys.exit()
165+
self.err_log('dispatcher connection error, quitting')
168166

169167
return client.feed(data)
170168

171169
def req_dispatcher(self, s, hash):
172170
subtype = hash['subtype']
173-
if (subtype == 'msg'):
171+
if subtype == 'msg':
174172
msg = hash['msg']
175173
self.announcement("dispatcher msg: %s" % msg)
174+
elif subtype == 'beacon':
175+
# dispatcher sends a beacon at startup
176+
self.beaconed = True
176177

177178
def req_cmd(self, s, hash):
178179
cmd = hash['cmd']
@@ -185,6 +186,12 @@ def req_kill(self, s, hash):
185186
s.close()
186187
sys.exit()
187188

189+
# idb is checking if broker has received beacon from dispatcher
190+
def req_beacon(self, s, hash):
191+
if not self.beaconed:
192+
self.announcement('beacon not received (possible dispatcher error)')
193+
self.req_kill(s, hash)
194+
188195
def parse_exec(self, s, req):
189196
if not (req[0:8] == '[notice]'):
190197
self.puts(req)
@@ -195,7 +202,7 @@ def parse_exec(self, s, req):
195202
try:
196203
hash = json.loads(req)
197204
except ValueError:
198-
print("[-] broker failed to parse json\n %s" % repr(req))
205+
self.announcement("[-] broker failed to parse json\n %s" % repr(req))
199206
return
200207

201208
type = hash['type']
@@ -239,38 +246,55 @@ def loop(self):
239246
else:
240247
self.handle(s)
241248

242-
def __init__(self, name):
243-
self.name = name
249+
# use logging facility to record the exception and exit
250+
def err_log(self, msg):
251+
rs_log.exception(msg, exc_info=True)
252+
try:
253+
# inform idb and dispatcher
254+
self.announcement(msg)
255+
self.notice_dispatcher('kill')
256+
except Exception as e:
257+
pass
258+
finally:
259+
sys.exit()
260+
261+
def __init__(self):
262+
self.name = None
263+
self.beaconed = False
244264
self.opened_sockets = []
245265
self.clients_list = []
246266
self.pat = re.compile('dbg disconnected')
247267
self.req_handlers = {
248268
'dispatcher': self.req_dispatcher,
249269
'cmd': self.req_cmd,
250-
'kill': self.req_kill
270+
'kill': self.req_kill,
271+
'beacon': self.req_beacon
251272
}
252273

253274

254-
def err_log(msg=''):
255-
rs_log.debug(msg, exc_info=True)
256-
sys.exit()
275+
@contextmanager
276+
def error_reporting(stage, info=None):
277+
try:
278+
yield
279+
except Exception as e:
280+
server.err_log(' error - '.join(filter(None, (stage, info))))
257281

258282

259283
if __name__ == "__main__":
260284

261-
try:
285+
server = BrokerSrv()
286+
287+
with error_reporting('server.env', 'PYTHON_PATH not found'):
262288
PYTHON_PATH = os.environ['PYTHON_PATH']
263-
except Exception as e:
264-
err_log('broker failed to retrieve PYTHON_PATH value from env')
265289

266290
parser = argparse.ArgumentParser()
267291
parser.add_argument('--idb', nargs=1, action='store')
268292
args = parser.parse_args()
269293

270-
if not args.idb:
271-
err_log('[sync] no idb argument')
294+
with error_reporting('server.arg', 'missing idb argument'):
295+
server.name = args.idb[0]
272296

273-
try:
297+
with error_reporting('server.config'):
274298
for loc in ('IDB_PATH', 'USERPROFILE', 'HOME'):
275299
if loc in os.environ:
276300
confpath = os.path.join(os.path.realpath(os.environ[loc]), '.sync')
@@ -281,25 +305,12 @@ def err_log(msg=''):
281305
PORT = config.getint('INTERFACE', 'port')
282306
HOST = config.get('INTERFACE', 'host')
283307
break
284-
except Exception as e:
285-
err_log('failed to load configuration file')
286308

287-
server = BrokerSrv(args.idb[0])
288-
289-
try:
309+
with error_reporting('server.bind'):
290310
server.bind()
291-
except socket.error as e:
292-
server.announcement('failed to bind')
293-
err_log('server.bind error')
294311

295-
try:
312+
with error_reporting('server.notify'):
296313
server.notify()
297-
except Exception as e:
298-
server.announcement('failed to notify dispatcher')
299-
err_log('server.notify error')
300314

301-
try:
315+
with error_reporting('server.loop'):
302316
server.loop()
303-
except Exception as e:
304-
server.announcement('broker stop')
305-
err_log('server.loop error')

ext_ida/retsync/dispatcher.py

+56-24
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (C) 2016-2019, Alexandre Gazet.
2+
# Copyright (C) 2016-2020, Alexandre Gazet.
33
#
44
# Copyright (C) 2012-2015, Quarkslab.
55
#
@@ -29,6 +29,7 @@
2929
import select
3030
import re
3131
import traceback
32+
from contextlib import contextmanager
3233
try:
3334
from ConfigParser import SafeConfigParser
3435
except ImportError:
@@ -100,23 +101,35 @@ def __init__(self):
100101
'idb_n': self.req_idb_n,
101102
'idb_list': self.req_idb_list,
102103
'module': self.req_module,
104+
'dbg_err': self.req_dbg_err,
103105
'sync_mode': self.req_sync_mode,
104106
'cmd': self.req_cmd,
105107
'bc': self.req_bc,
106108
'kill': self.req_kill
107109
}
108110

111+
def is_port_available(self, host, port):
112+
try:
113+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
114+
if sys.platform == 'win32':
115+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
116+
sock.bind((host, port))
117+
finally:
118+
sock.close()
119+
120+
def bind_sock(self, host, port):
121+
self.is_port_available(host, port)
122+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
123+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
124+
sock.bind((host, port))
125+
self.srv_socks.append(sock)
126+
return sock
127+
109128
def bind(self, host, port):
110-
self.dbg_srv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
111-
self.dbg_srv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
112-
self.dbg_srv_sock.bind((host, port))
113-
self.srv_socks.append(self.dbg_srv_sock)
129+
self.dbg_srv_sock = self.bind_sock(host, port)
114130

115131
if not (socket.gethostbyname(host) == '127.0.0.1'):
116-
self.localhost_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
117-
self.localhost_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
118-
self.localhost_sock.bind(('localhost', port))
119-
self.srv_socks.append(self.localhost_sock)
132+
self.localhost_sock = self.bind_sock('127.0.0.1', port)
120133

121134
def accept(self, s):
122135
new_socket, addr = s.accept()
@@ -179,7 +192,7 @@ def recvall(self, client):
179192
self.dbg_quit()
180193
else:
181194
self.client_quit(client.srv_sock)
182-
self.broadcast("a client quit, nb client(s) left: %d" % len(self.idb_clients))
195+
self.broadcast("a client quit, %d client(s) left" % len(self.idb_clients))
183196

184197
return []
185198

@@ -250,6 +263,10 @@ def forward_all(self, msg, s=None):
250263
for idbc in self.idb_clients:
251264
self.forward(msg, idbc.client_sock)
252265

266+
# send a beacon to the broker
267+
def send_beacon(self, s):
268+
s.sendall(rs_encode("[notice]{\"type\":\"dispatcher\",\"subtype\":\"beacon\"}\n"))
269+
253270
# disable current idb and enable new idb matched from current module name
254271
def switch_idb(self, new_idb):
255272
msg = "[sync]{\"type\":\"broker\",\"subtype\":\"%s\"}\n"
@@ -277,6 +294,9 @@ def req_new_client(self, srv_sock, hash):
277294
srv_sock.close()
278295
return
279296

297+
# send beacon to acknowledge dispatcher presence
298+
self.send_beacon(client_sock)
299+
280300
# check if an idb client is already registered with the same name
281301
conflicting = [client for client in self.idb_clients if (client.name == name)]
282302

@@ -429,11 +449,16 @@ def req_module(self, s, hash):
429449
if self.current_idb and self.current_idb.enabled:
430450
self.switch_idb(None)
431451

452+
# dbg notice of error, e.g. current module resolution failed
453+
def req_dbg_err(self, s, hash):
454+
if self.sync_mode_auto:
455+
self.switch_idb(None)
456+
432457
# sync mode tells if idb switch is automatic or manual
433458
def req_sync_mode(self, s, hash):
434459
mode = hash['auto']
435460
self.broadcast("sync mode auto set to %s" % mode)
436-
self.sync_mode_auto = (mode == "on")
461+
self.sync_mode_auto = (mode == 'on')
437462

438463
# bc request should be forwarded to all idbs
439464
def req_bc(self, s, hash):
@@ -444,17 +469,31 @@ def req_cmd(self, s, hash):
444469
cmd = "%s\n" % hash['cmd']
445470
self.current_dbg.client_sock.sendall(rs_encode(cmd))
446471

472+
# use logging facility to record the exception and exit
473+
def err_log(self, msg):
474+
rs_log.exception(msg, exc_info=True)
475+
try:
476+
self.announcement('dispatcher stopped')
477+
[sckt.close() for sckt in self.srv_socks]
478+
except Exception:
479+
pass
480+
finally:
481+
sys.exit()
447482

448-
def err_log(msg):
449-
rs_log.debug(msg, exc_info=True)
450-
sys.exit()
483+
484+
@contextmanager
485+
def error_reporting(stage, info=None):
486+
try:
487+
yield
488+
except Exception as e:
489+
server.err_log(' error - '.join(filter(None, (stage, info))))
451490

452491

453492
if __name__ == "__main__":
454493

455494
server = DispatcherSrv()
456495

457-
try:
496+
with error_reporting('server.config'):
458497
for loc in ('IDB_PATH', 'USERPROFILE', 'HOME'):
459498
if loc in os.environ:
460499
confpath = os.path.join(os.path.realpath(os.environ[loc]), '.sync')
@@ -466,16 +505,9 @@ def err_log(msg):
466505
PORT = config.getint('INTERFACE', 'port')
467506
server.announcement('configuration file loaded')
468507
break
469-
except Exception as e:
470-
err_log('failed to load configuration file')
471508

472-
try:
509+
with error_reporting('server.bind', '(%s:%s)' % (HOST, PORT)):
473510
server.bind(HOST, PORT)
474-
except Exception as e:
475-
err_log("server.bind error %s:%s" % (HOST, PORT))
476511

477-
try:
512+
with error_reporting('server.loop'):
478513
server.loop()
479-
except Exception as e:
480-
server.announcement('dispatcher stopped')
481-
err_log('server.loop error')

0 commit comments

Comments
 (0)