TOR is a well known “software” able to protect communications dispatching packets between different relays spread over the world run by a network of volunteers. Because the high rate of anonymity TOR has been used over the past years to cover malicious actions by physical and cyber attackers. TOR, especially through its browser implementation (the TOR Browser), is also know as one of the main (by meaning of the most used) way to get access to the Dark WEB  in where “malicious” people buy and sell illegal stuff through dark markets. Each relay belonging to the network is able to decide if being an ExitPoint (in the following picture represented by the last machine contacting “Bob”) or just a middle relay (in the following picture: a TOR node highlighted by “green cross”) depending on its own configuration status. If the relay decides to be an ExitNode it will expose its own IP address to the public world; it’s usually a good idea alert local police and used ISP about that in order to avoid penalties.
From TheTorProject.org

During the past year mass-media such as: television shows, radio stations, youtube channels, Facebook groups, etc. disclosed many dark markets address swelling up flows of curios people to the DarkWeb and consequently exposing them to numerous new attack scenarios. Indeed new attackers set up Exit Nodes or Relay Nodes in order to spy and/or compromise communication flows passing through them. The attack could happen in many single ways but the most used ones (as today writing) are mainly three:
  1. DNS Poisoning: This technique consists in redirecting DNS calls related to well-known web sites to creative fake pages containing exploit kits able to eventually compromise  user browsers.
  2. File Patching: This technique consists in altering the requested file during its way back to destination by adding malicious content to it: this happens directly on ExitPoint/Relay before being issued to original requester.
  3. Certification Substitution (SSL – MITM). This techniques consists in substitute the real web-site certificate with a fake one in order to be able to decrypt the communication flow intercepting credentials and parameters.
Working on CyberSecurity means being aware of such attacks and being able to decide whenever passing through TOR relays or not. Please be aware that TOR is not the only anonymous networks in the DarkWeb !  
 My goal was to figure out when my TOR flow was passing through malicious relays. For such a reason I decided to write a little python script able to make some quick and dirty checks such as: DNS Poison, File patching and SSL-MITM which are significant checks on the status of the used TOR circuit. The script has 2 years old and it was undisclosed until now. I decided to public it since scientific researches have been implemented an advanced version of my FindMalExit.py. Please have a read here for the full paper on that topic.
The IDEA.
Well, actually it is a pretty simple idea: “let’s grab certificates, IP addresses and files without passing through TOR network (or passing through trusted circuits) and then replicate  the process passing through all available relays. Compare the results and check if somebody is chaining the “ground”.
The IMPLEMENTATION:
Following please find my poor code. Please remember it is a non production code so do not use on production envronments! (This code is just a first release of a bigger project now maintained by Yoroi ). I decided to publish the code in HTML format (so I can easily comment it), if you need it in a more common way check my github repo (here
td.linenos { background-color: #f0f0f0; padding-right: 10px; } span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } pre { line-height: 125%; } body .hll { background-color: #ffffcc } body { background: #f8f8f8; } body .c { color: #408080; font-style: italic } /* Comment */ body .err { border: 1px solid #FF0000 } /* Error */ body .k { color: #008000; font-weight: bold } /* Keyword */ body .o { color: #666666 } /* Operator */ body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ body .cp { color: #BC7A00 } /* Comment.Preproc */ body .c1 { color: #408080; font-style: italic } /* Comment.Single */ body .cs { color: #408080; font-style: italic } /* Comment.Special */ body .gd { color: #A00000 } /* Generic.Deleted */ body .ge { font-style: italic } /* Generic.Emph */ body .gr { color: #FF0000 } /* Generic.Error */ body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ body .gi { color: #00A000 } /* Generic.Inserted */ body .go { color: #888888 } /* Generic.Output */ body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ body .gs { font-weight: bold } /* Generic.Strong */ body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ body .gt { color: #0044DD } /* Generic.Traceback */ body .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ body .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ body .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ body .kp { color: #008000 } /* Keyword.Pseudo */ body .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ body .kt { color: #B00040 } /* Keyword.Type */ body .m { color: #666666 } /* Literal.Number */ body .s { color: #BA2121 } /* Literal.String */ body .na { color: #7D9029 } /* Name.Attribute */ body .nb { color: #008000 } /* Name.Builtin */ body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ body .no { color: #880000 } /* Name.Constant */ body .nd { color: #AA22FF } /* Name.Decorator */ body .ni { color: #999999; font-weight: bold } /* Name.Entity */ body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ body .nf { color: #0000FF } /* Name.Function */ body .nl { color: #A0A000 } /* Name.Label */ body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ body .nt { color: #008000; font-weight: bold } /* Name.Tag */ body .nv { color: #19177C } /* Name.Variable */ body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ body .w { color: #bbbbbb } /* Text.Whitespace */ body .mb { color: #666666 } /* Literal.Number.Bin */ body .mf { color: #666666 } /* Literal.Number.Float */ body .mh { color: #666666 } /* Literal.Number.Hex */ body .mi { color: #666666 } /* Literal.Number.Integer */ body .mo { color: #666666 } /* Literal.Number.Oct */ body .sb { color: #BA2121 } /* Literal.String.Backtick */ body .sc { color: #BA2121 } /* Literal.String.Char */ body .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ body .s2 { color: #BA2121 } /* Literal.String.Double */ body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ body .sh { color: #BA2121 } /* Literal.String.Heredoc */ body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ body .sx { color: #008000 } /* Literal.String.Other */ body .sr { color: #BB6688 } /* Literal.String.Regex */ body .s1 { color: #BA2121 } /* Literal.String.Single */ body .ss { color: #19177C } /* Literal.String.Symbol */ body .bp { color: #008000 } /* Name.Builtin.Pseudo */ body .vc { color: #19177C } /* Name.Variable.Class */ body .vg { color: #19177C } /* Name.Variable.Global */ body .vi { color: #19177C } /* Name.Variable.Instance */ body .il { color: #666666 } /* Literal.Number.Integer.Long */
#!/usr/bin/env python2

#========================================================================#
# THIS IS NOT A PRODUCTION RELEASED SOFTWARE #
#========================================================================#
# Purpose of finMaliciousRelayPoints is to proof the way it's possible to#
# discover TOR malicious Relays Points. Please do not use it in  #
# any production  environment                                            #
# Author: Marco Ramilli                                                  #
# eMail: XXXXXXXX #
# WebSite: marcoramilli.blogspot.com #
# Use it at your own #
#========================================================================#

#==============================Disclaimer: ==============================#
#THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR #
#IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED #
#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE #
#DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, #
#INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES #
#(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR #
#SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) #
#HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, #
#STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING #
#IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE #
#POSSIBILITY OF SUCH DAMAGE. #
#========================================================================#




#-------------------------------------------------------------------------
#------------------- GENERAL SECTION -------------------------------------
#-------------------------------------------------------------------------
import StringIO
import tempfile
import time
import hashlib
import traceback
from geoip import geolite2
import stem.control

TRUSTED_HOP_FINGERPRINT = '379FB450010D17078B3766C2273303C358C3A442' 
#trusted hop
SOCKS_PORT = 9050
CONNECTION_TIMEOUT = 30 # timeout before we give up on a circuit

#-------------------------------------------------------------------------
#---------------- File Patching Section ----------------------------------
#-------------------------------------------------------------------------
import pycurl

check_files = {
"http://live.sysinternals.com/psexec.exe",
"http://live.sysinternals.com/psexec.exe",
"http://live.sysinternals.com/psping.exe", }
check_files_patch_results = []

class File_Check_Results:
"""
Analysis Results against File Patching
"""
def __init__(self, url, filen, filepath, exitnode, found_hash):
self.url = url
self.filename = filen
self.filepath = filepath
self.exitnode = exitnode
self.filehash = found_hash


#------------------------------------------------------------------------
#------------------- DNS Poison Section ---------------------------------
#------------------------------------------------------------------------
import dns.resolver
import socks
import socket

check_domain_poison_results = []
domains = {
"www.youporn.com",
"youporn.com",
"www.torproject.org",
"www.wikileaks.org",
"www.i2p2.de",
"torrentfreak.com",
"blockchain.info",
}

class Domain_Poison_Check:
"""
Analysis Results against Domain Poison
"""
def __init__(self, domain):
self.domain = domain
self.address = []
self.path = []

def pushAddr(self, add):
self.address.append(add)

def pushPath(self, path):
self.path = path

#-----------------------------------------------------------------------
#------------------- SSL Sltrip Section --------------------------------
#-----------------------------------------------------------------------
import OpenSSL
import ssl

check_ssl_strip_results = []
ssl_strip_monitored_urls = {
"www.google.com",
"www.microsoft.com",
"www.apple.com",
"www.bbc.com",
}

class SSL_Strip_Check:
"""
Analysis Result against SSL Strip
"""
def __init__(self, domain, public_key, serial_number):
self.domain = domain
self.public_key = public_key
self.serial_number = serial_number


#----------------------------------------------------------------------
#---------------- Starting Coding -------------------------------
#----------------------------------------------------------------------


def sslCheckOriginal():
"""
Download the original Certificate without TOR connection
"""
print('[+] Populating SSL for later check')
for url in ssl_strip_monitored_urls:
try:
cert = ssl.get_server_certificate((str(url), 443))
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
p_k = x509.get_pubkey()
s_n = x509.get_serial_number()

print('[+] Acquired Certificate: %s' % url)
print(' |_________> serial_number %s' % s_n)
print(' |_________> public_key %s' % p_k)

check_ssl_strip_results.append(SSL_Strip_Check(url, p_k, s_n))

except Exception as err:
print('[-] Error While Acquiring certificats on setup phase !')
traceback.print_exc()
return time.time()


def fileCheckOriginal():
"""
Downloading file ORIGINAL without TOR
"""

print('[+] Populating File Hasing for later check')
for url in check_files:
try:
data = query(url)
file_name = url.split("/")[-1]
_,tmp_file = tempfile.mkstemp(prefix="exitmap_%s_" % file_name)

with open(tmp_file, "wb") as fd:
fd.write(data)
print('[+] Saving File \"%s\".' % tmp_file)
check_files_patch_results.append( File_Check_Results(url, file_name, tmp_file, "NO", sha512_file(tmp_file)) )
print('[+] First Time we see the file..')
print(' |_________> exitnode : None' )
print(' |_________> :url: %s' % str(url) )
print(' |_________> :filePath: %s' % str(tmp_file))
print(' |_________> :file Hash: %s' % str(sha512_file(tmp_file)))
except Exception as err:
print('[-] Error ! %s' % err)
traceback.print_exc()
pass
return time.time()


def resolveOriginalDomains():
"""
Resolving DNS For original purposes
"""
print('[+] Populating Domain Name Resolution for later check ')

try:
for domain in domains:
response = dns.resolver.query(domain)
d = Domain_Poison_Check(domain)
print('[+] Domain: %s' % domain)
for record in response:
print(' |____> maps to %s.' % (record.address))
d.pushAddr(record)
check_domain_poison_results.append(d)
return time.time()
except Exception as err:
print('[+] Exception: %s' % err)
traceback.print_exc()
return time.time()


def query(url):
"""
Uses pycurl to fetch a site using the proxy on the SOCKS_PORT.
"""
output = StringIO.StringIO()
query = pycurl.Curl()
query.setopt(pycurl.URL, url)
query.setopt(pycurl.PROXY, 'localhost')
query.setopt(pycurl.PROXYPORT, SOCKS_PORT)
query.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5_HOSTNAME)
query.setopt(pycurl.CONNECTTIMEOUT, CONNECTION_TIMEOUT)
query.setopt(pycurl.WRITEFUNCTION, output.write)

try:
query.perform()
return output.getvalue()
except pycurl.error as exc:
raise ValueError("Unable to reach %s (%s)" % (url, exc))



def scan(controller, path):
"""
Scan Tor Relays Point to find File Patching
"""

def attach_stream(stream):
if stream.status == 'NEW':
try:
controller.attach_stream(stream.id, circuit_id)
#print('[+] New Circuit id (%s) attached and ready to be used!' % circuit_id)
except Exception as err:
controller.remove_event_listener(attach_stream)
controller.reset_conf('__LeaveStreamsUnattached')

try:

print('[+] Creating a New TOR circuit based on path: %s' % path)
circuit_id = controller.new_circuit(path, await_build = True)
controller.add_event_listener(attach_stream, stem.control.EventType.STREAM)
controller.set_conf('__LeaveStreamsUnattached', '1') # leave stream management to us
start_time = time.time()

socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", 9050)
socket.socket = socks.socket

ip = query('http://ip.42.pl/raw')
if ip is not None:
country = geolite2.lookup( str(ip) ).country
print('\n \n')
print('[+] Performing FilePatch, DNS Spoofing and Certificate Checking\
passing through --> %s (%s) \n \n' % (str(ip), str(country)) )

time_FileCheck = fileCheck(path)
print('[+] FileCheck took: %0.2f seconds' % ( time_FileCheck - start_time))

#time_CertsCheck = certsCheck(path)
#print('[+] CertsCheck took: %0.2f seconds' % ( time_DNSCheck - start_time))

time_DNSCheck = dnsCheck(path)
print('[+] DNSCheck took: %0.2f seconds' % ( time_DNSCheck - start_time))

except Exception as err:
print('[-] Circuit creation error: %s' % path)

return time.time() - start_time

def certsCheck(path):
"""
SSL Strip detection
TODO: It's still a weak control. Need to collect and to compare public_key()
"""
print('[+] Checking Certificates')
try:
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", 9050)
socket.socket = socks.socket

for url in ssl_strip_monitored_urls:
cert = ssl.get_server_certificate((str(url), 443))
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
p_k = x509.get_pubkey()
s_n = x509.get_serial_number()
for stored_cert in check_ssl_strip_results:
if str(url) == str(stored_cert.domain):
if str(stored_cert.serial_number) != str(s_n):
print('[+] ALERT Found SSL Strip on uri (%s) through path %s ' % (url, path))
break
else:
print('[+] Certificate Check seems to be OK for %s' % url)

except Exception as err:
print('[-] Error: %s' % err)
traceback.print_exc()

socket.close()
return time.time()

def dnsCheck(path):
"""
DNS Poisoning Check
"""
try:
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", 9050)
socket.socket = socks.socket

print('[+] Checking DNS ')
for domain in domains:
ipv4 = socket.gethostbyname(domain)
for p_d in check_domain_poison_results:
if str(p_d.domain) == str(domain):
found = False
for d_ip in p_d.address:
if str(ipv4) == str(d_ip):
found = True
break
if found == False:
print('[+] ALERT:DNS SPOOFING FOUND: name: %s ip: %s (path: %s )' % (domain, ipv4, path) )
else:
print('[+] Check DNS (%s) seems to be OK' % domain)
except Exception as err:
print('[-] Error: %s' % err)
traceback.print_exc()

socket.close()
return time.time()


def fileCheck(path):
"""
Downloading file through TOR circuits doing the hashing
"""
print('[+] Checking For File patching ')
for url in check_files:
try:
#File Rereive
data = query(url)
file_name = url.split("/")[-1]
_,tmp_file = tempfile.mkstemp(prefix="exitmap_%s_" % file_name)
with open(tmp_file, "wb") as fd:
fd.write(data)
for i in check_files_patch_results:
if str(i.url) == str(url):
if str(i.filehash) != str(sha512_file(tmp_file)):
print('[+] ALERT File Patch FOUND !')
print(' | exitnode : %s' % str(i.exitnode) )
print(' |_________> url: %s' % str(i.url) )
print(' |_________> filePath: %s' % str(i.filepath) )
print(' |_________> fileHash: %s' % str(i.filehash) )
#check_files_patch_results.append( File_Check_Results(url, file_name, tmp_file, path, sha512_file(tmp_file)) )
else :
print('[+] File (%s) seems to be ok' % i.url)
break

except Exception as err:
print('[-] Error ! %s' % err)
traceback.print_exc()
pass
return time.time()


def sha512_file(file_name):
"""
Calculate SHA512 over the given file.
"""

hash_func = hashlib.sha256()

with open(file_name, "rb") as fd:
hash_func.update(fd.read())

return hash_func.hexdigest()


if __name__ == '__main__':

start_analysis = time.time()
print("""

|=====================================================================|
| Find Malicious Relay Nodes is a python script made for checking 3 |
| unique kind of frauds such as: |
| (1) File Patching |
| (2) DNS Poison |
| (3) SSL Stripping (MITM SSL) |
|=====================================================================|
""")

print("""
|=====================================================================|
| Initialization Phase |
|=====================================================================|
""")
dns_setup_time = resolveOriginalDomains()
print('[+] DNS Setup Finished: %0.2f' % (dns_setup_time - start_analysis))
file_check_original_time = fileCheckOriginal()
print('[+] File Setup Finished: %0.2f' % (file_check_original_time - start_analysis))
ssl_checking_original_time = sslCheckOriginal()
print('[+] Acquiring Certificates Setup Finished: %0.2f' % (ssl_checking_original_time - start_analysis))

print("""
|=====================================================================|
| Analysis Phase |
|=====================================================================|
""")

print('[+] Connecting and Fetching possible Relays ...')
with stem.control.Controller.from_port() as controller:
controller.authenticate()

net_status = controller.get_network_statuses()


for descriptor in net_status:
try:
fingerprint = descriptor.fingerprint

print('[+] Selecting a New Exit Point:')
print('[+] |_________> FingerPrint: %s ' % fingerprint)
print('[+] |_________> Flags: %s ' % descriptor.flags)
print('[+] |_________> Exit_Policies: %s ' % descriptor.exit_policy)

if 'EXIT' in (flag.upper() for flag in descriptor.flags):
print('[+] Found Exit Point. Performing Scan through EXIT: %s' % fingerprint)
if None == descriptor.exit_policy:
print('[+] No Exit Policies found ... no certs checking')
time_taken = scan(controller, [TRUSTED_HOP_FINGERPRINT, fingerprint])
else:
#print('[+] Not Exit Point found. Using it as Relay passing to TRUST Exit Point')
pass
#time_taken = scan(controller, [fingerprint, TRUSTED_HOP_FINGERPRINT])
#print('[+] Finished Analysis for %s finished => %0.2f seconds' % (fingerprint, time_taken))

except Exception as exc:
print('[-] Exception on FingerPrint: %s => %s' % (fingerprint, exc))
traceback.print_exc()
pass

The RESULTS

I am not going to publish my results since Tor Relays change over time and what I found using this script might be inaccurate and imprecise: more check must be done. Moreover it could be unpleasant charge specific relays (ergo IP, ergo owners) to be “malicious”. But I am going to indorse part of results described by Philipp Winte and Stefan Lindskog published on their paper (here).

From Spoiled Onions: Exposing Malicious Tor Exit Relays (Philipp Winter and Stefan Lindskog)
Many of the found Malicious relays have been found on Russia, Turky and Hong Honk. Not every malicious relay used the both techniques to compromise flow but almost one was found. The definitive more used technique is the SSL-Strip MITM mainly used to spy over channels. Few file patching techniques were identified. This kind of attack is useful to spread Malware over the networks and together with DNS poisoning is more used to “attack” rather then to “spy”.

Hope you might enjoy the script, which is quite old and will need a code refactoring session but still interesting (at least on my personal point of view).