CheckExit.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. #!/usr/bin/env python
  2. """
  3. Collects data (mainly certificates) for documentation of TLS MitM at Tor exits.
  4. Does not by itself attempt to validate certificates or check for deviations.
  5. Dependencies: sqlite3, stem, PySocks, PyOpenSSL
  6. antsy (for ANSI on terminal)
  7. """
  8. #
  9. # Initialize:
  10. #
  11. # python CheckExit.py -3 # (creates certs.db)
  12. # sqlite3 certs.db '.dump'
  13. #
  14. # Use (for example):
  15. #
  16. # python CheckExit.py -H foobar.com
  17. # python CheckExit.py -H jabber.foobar.com -q xmpp
  18. #
  19. # or with exits' ntor keys (found in microdescriptor, ends in =)
  20. #
  21. # python CheckExit.py -H foobar.com -O <key> -O <key> ...
  22. #
  23. #
  24. # select exits.fingerprint, obs.success from exits inner join obs on (obs.exit_id = exits.id) where obs.success != 'yes' order by exits.id;
  25. #
  26. #
  27. # This is far from finished.
  28. #
  29. # FIXME: we hit some timeouts after some connections. is it Tor refusing to make too many circuits? -> we ought to start our own Tor process and configure it appropriately.
  30. # TODO: timeout earlier AND find the underlying problem
  31. # TODO: we must also record failed attempts because of failed TLS/SSL handshakes! Could be downgrade attacks?
  32. # TODO: InvalidRequest: Unknown circuit (reproduce this?)
  33. # TODO: clean up on keyboardinterrupt
  34. # TODO: investigate problem with other circuits than intended being destroyed? attachstream applies to circuits created by other applications?? -> ought to start own Tor process
  35. # FIXME: don't use uniform random selection, use the normal mechanism and then just extend the circuit
  36. import sys
  37. import getopt
  38. import os.path
  39. import traceback
  40. import logging
  41. import random
  42. import datetime
  43. import socks
  44. import socket
  45. import OpenSSL
  46. import sqlite3
  47. import stem
  48. from stem.descriptor import DocumentHandler, parse_file
  49. from stem.descriptor.router_status_entry import *
  50. from stem.descriptor.remote import DescriptorDownloader
  51. from stem.control import Controller
  52. from stem.util import conf, connection
  53. from stem import CircuitExtensionFailed
  54. import antsy
  55. RANDOM = random.SystemRandom()
  56. def usage():
  57. print "\nUsage:"
  58. print "%s [options]" % sys.argv[0]
  59. print "--host / -H <host> must be given"
  60. print "--port / -P <port> defaults to default value of protocol"
  61. print "--protocol / -q <protocol> if not given, TLS. valid options are xmpp"
  62. print "--ntor-onion-key / -O <ntor-onion-key> use this exit. may be given more than once"
  63. print "--initdb / -3 use once to init database ./certs.db"
  64. print "--controlport / -p <port> use this tor instance. defaults to 9051"
  65. print "--help / -h just show this help screen"
  66. class Protocol:
  67. def __init__(self):
  68. pass
  69. def what(self):
  70. pass
  71. class Protocol_TLS(Protocol):
  72. def __init__(self):
  73. Protocol.__init__(self)
  74. self.context = OpenSSL.SSL.Context(\
  75. OpenSSL.SSL.TLSv1_2_METHOD)
  76. self.context.set_timeout(10)
  77. def callback(connobj, x509obj, errno, errdepth, ret):
  78. return True
  79. self.context.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_CLIENT_ONCE, callback)
  80. self.result = None
  81. self.host = None
  82. self.port = 443
  83. def what(self):
  84. return "TLS"
  85. def execute(self, soxsock, pre_tls=None):
  86. print "Socket Connect ..."
  87. soxsock.connect((self.host, self.port))
  88. if not (pre_tls == None):
  89. pre_tls.execute()
  90. sslsock = OpenSSL.SSL.Connection(self.context, soxsock)
  91. sslsock.set_connect_state()
  92. print "Doing TLS Handshake ..."
  93. sslsock.do_handshake()
  94. print "Cipher list starts with %s ..." % sslsock.get_cipher_list()[:5]
  95. print "Extracting Certificate ..."
  96. peercert = sslsock.get_peer_certificate()#_cert_chain()
  97. sslsock.close()
  98. self.result = peercert
  99. return self.result
  100. class Protocol_StartTLS_XMPP(Protocol):
  101. def __init__(self):
  102. Protocol.__init__(self)
  103. self.port = 5222
  104. self.result = None
  105. self.host = None
  106. self.sslprot = Protocol_TLS()
  107. def what(self):
  108. return "XMPP/STARTTLS"
  109. def execute(self, soxsock):
  110. class starttls():
  111. def __init__(self, host, sock):
  112. self.host = host
  113. self.sock = sock
  114. def execute(self):
  115. sock = self.sock
  116. print "Greeting XMPP server ..."
  117. # the to= ... chooses a virtual host, apparently
  118. sock.sendall("<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='%s' version='1.0'>" % self.host)
  119. print "Doing STARTTLS ..."
  120. def recv_all(sock):
  121. r = ""
  122. while 1:
  123. data = sock.recv(4096)
  124. if not data: break
  125. r += data
  126. if (len(data)< 4096): break
  127. return r
  128. r = recv_all(sock)
  129. print r
  130. # sock.sendall("<starttls xmlns=\"urn:ietf:params:xml:xmpp-tls\"/>")
  131. sock.sendall("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>")
  132. r = recv_all(sock)
  133. print r
  134. self.sslprot.host = self.host
  135. self.sslprot.port = self.port
  136. self.result = self.sslprot.execute(soxsock, pre_tls=starttls(self.host, soxsock))
  137. return self.result
  138. def try_connect(controller, circstart, myexit, dbconn, protocol):
  139. # logging.debug("Attempting connection via %s " % myexit)
  140. print "Attempting connection to %s:%d via exit %s... (exit policy %s) " % (protocol.host, protocol.port, myexit.digest[:6], myexit.exit_policy)
  141. cur = dbconn.cursor()
  142. path = circstart + [myexit.ntor_onion_key]
  143. circuit_id = controller.new_circuit(path, await_build=True)
  144. def attach_stream(stream):
  145. if stream.status == 'NEW':
  146. controller.attach_stream(stream.id, circuit_id)
  147. controller.add_event_listener(attach_stream, stem.control.EventType.STREAM)
  148. try:
  149. socksport = int(controller.get_conf("SocksPort").split(" ")[0])
  150. controller.set_conf('__LeaveStreamsUnattached', '1')
  151. socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, '127.0.0.1', socksport)
  152. soxsock = socks.socksocket()
  153. # soxsock.settimeout ( 20 )
  154. try:
  155. print "Saving Connection Info ..."
  156. # can clean up later if interrupted
  157. # select exits.id from exits where not exists (select * from obs where obs.exit_id = exits.id);
  158. cur.execute("SELECT (id) FROM exits WHERE (fingerprint==?)", (myexit.ntor_onion_key,))
  159. idu = cur.fetchone()
  160. if (idu == None):
  161. cur.execute("INSERT INTO exits (fingerprint, microdescriptor) VALUES (?,?)", (myexit.ntor_onion_key,str(myexit)))
  162. cur.execute("SELECT (id) FROM exits WHERE (fingerprint==?)", (myexit.ntor_onion_key,))
  163. idu = cur.fetchone()
  164. print "Inserted new entry into EXITS table"
  165. else:
  166. print "Found existing entry in EXITS table"
  167. protocol.execute(soxsock)
  168. peercert = protocol.result
  169. pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,peercert)
  170. digest = peercert.digest("sha1")
  171. print "Saving Certificate Info ..."
  172. cur.execute("SELECT (id) FROM certs WHERE (digest==?)", (digest,))
  173. idc = cur.fetchone()
  174. if (idc == None):
  175. cur.execute("INSERT INTO certs (digest, x509) VALUES (?,?)", (digest,str(pem)))
  176. cur.execute("SELECT (id) FROM certs WHERE (digest==?)", (digest,))
  177. idc = cur.fetchone()
  178. msg = "Inserted new entry into CERTS table"
  179. if antsy.supports_color():
  180. print antsy.fg(msg, "red")
  181. else:
  182. print msg
  183. else:
  184. msg = "Found existing entry in CERTS table"
  185. if antsy.supports_color():
  186. print antsy.fg(msg, "blue")
  187. else:
  188. print msg
  189. cur.execute("INSERT INTO obs (what, host, port, cert_id, exit_id, success, recorded) VALUES (?,?,?,?,?,?,?)", (protocol.what(), protocol.host, protocol.port, idc[0], idu[0], "yes", datetime.datetime.utcnow()))
  190. dbconn.commit()
  191. except OpenSSL.SSL.Error as excuse:
  192. cur.execute("INSERT INTO obs (what, host, port, exit_id, success, recorded) VALUES (?,?,?,?,?,?)", (protocol.what(), protocol.host, protocol.port, idu[0], "no: " + str(excuse), datetime.datetime.utcnow()))
  193. dbconn.commit()
  194. print excuse
  195. # results like
  196. # (-1, "Unexpected EOF")
  197. # [('SSL routines', 'SSL3_GET_RECORD', 'wrong version number')]
  198. soxsock.close()
  199. except socks.GeneralProxyError as e: # timeout
  200. print e
  201. finally:
  202. controller.remove_event_listener(attach_stream)
  203. controller.reset_conf('__LeaveStreamsUnattached')
  204. controller.close_circuit(circuit_id)
  205. def initdb():
  206. conn = sqlite3.connect('certs.db')
  207. conn.execute('CREATE TABLE EXITS (id integer PRIMARY KEY, fingerprint TEXT NOT NULL, microdescriptor TEXTNOT NULL)')
  208. conn.execute('CREATE TABLE CERTS (id integer PRIMARY KEY, digest TEXT NOT NULL, x509 TEXT)')
  209. conn.execute('CREATE TABLE OBS (id integer PRIMARY KEY, what TEXT, host TEXT NOT NULL, port INTEGER NOT NULL, cert_id INTEGER, exit_id INTEGER NOT NULL, success TEXT, recorded DATE)')
  210. conn.commit()
  211. def main(controller, protocol, chosen_exits=None):
  212. conn = sqlite3.connect('certs.db')
  213. # descriptors = [ x for x in parse_file(consensuspath, validate=True) ]
  214. # FIXME: we should rely on the normal circuit creation mechanism instead
  215. try:
  216. print "Getting microdescriptors from Tor ..."
  217. descriptors = [ x for x in controller.get_microdescriptors() ]
  218. exits = [ x for x in descriptors
  219. if x.exit_policy.is_exiting_allowed() ]
  220. valid_exits = [ x for x in exits
  221. if x.exit_policy.can_exit_to(port = protocol.port) ]
  222. print "Found %d relays, %d seem to be exits, %d exit to port %d" % (len(descriptors), len(exits), len(valid_exits), protocol.port)
  223. if chosen_exits:
  224. sample = [ x for x in exits if x.ntor_onion_key in chosen_exits ] # check if still up?
  225. else:
  226. HOWMANY = 13
  227. sample = RANDOM.sample(valid_exits, HOWMANY)
  228. circstart = [ x.ntor_onion_key for x in RANDOM.sample(descriptors, 2) ]
  229. for it in sample:
  230. try:
  231. try_connect(controller, circstart, myexit=it, dbconn=conn, protocol=protocol)
  232. except CircuitExtensionFailed as e:
  233. print e
  234. except Exception as e:
  235. print e
  236. conn.commit()
  237. conn.close()
  238. if __name__ == '__main__':
  239. try:
  240. host = None
  241. port = None
  242. chosen_exits = []
  243. controlport = 9051
  244. protocol = Protocol_TLS()
  245. # -R number of random exits
  246. opts, args = getopt.getopt(sys.argv[1:], "3hH:P:p:q:O:", ["initdb", "help", "host=", "port=", "controlport", "protocol", "ntor-onion-key=", "exit="])
  247. for o, a in opts:
  248. if o in ("-O", "--ntor-onion-key", "--exit"):
  249. chosen_exits += [a]
  250. if o in ("-q", "--protocol"):
  251. if (a == "xmpp"):
  252. protocol = Protocol_StartTLS_XMPP()
  253. if o in ("-3", "--initdb"):
  254. initdb()
  255. if o in ("-h", "--help"):
  256. usage()
  257. sys.exit()
  258. elif o in ("-p", "--controlport"):
  259. controlport = int(a)
  260. elif o in ("-H", "--host"):
  261. if ":" in a:
  262. host = a.split(":")[0]
  263. port = int(a.split(":")[1])
  264. else:
  265. host = a
  266. elif o in ("-P", "--port"):
  267. port = int(a)
  268. if (host == None):
  269. print "No host given."
  270. usage()
  271. sys.exit(1)
  272. controller = stem.control.Controller.from_port(port = controlport)
  273. controller.authenticate()
  274. if not(port == None):
  275. protocol.port = port
  276. protocol.host = host
  277. if (chosen_exits == []):
  278. chosen_exits = None
  279. main(controller=controller, protocol=protocol, chosen_exits=chosen_exits)
  280. except getopt.GetoptError as err:
  281. print(err)
  282. usage()
  283. sys.exit(2)
  284. except SystemExit as err:
  285. pass
  286. except:
  287. msg = "failed with:\n\n%s" % traceback.format_exc()
  288. logging.error(msg)