Nmap Development mailing list archives

Re: [NSE] False timestamp in ssl-date


From: Daniel Miller <bonsaiviking () gmail com>
Date: Wed, 20 Aug 2014 17:35:19 -0500

nnposter,

Thanks for your work on this problem! I think I'm ready to commit it,
except for a couple questions:

1. How did you decide on 3 seconds for max allowed jitter? This value
should be chosen to be a small number that is larger than the variance we
would see in half of one round trip. 3 seconds sounds pretty good, given
that we set a socket timeout of 5 seconds, but maybe we should be more
lenient? Given that we allow a 3-minute window (90 seconds each way) before
we even apply this second test, we can probably allow a wider range of
values to allow for weird network conditions.

2. When we determine that the randomness does not represent time, should we
report this as a legitimate result, not as an error (with
stdnse.format_output)? I think people may be interested in that as a
datapoint, since it could be used to fingerprint SSL implementations.

For anyone following along at home, here [1] is a reference for the change
in OpenSSL that removed this "feature." The patch was written by Nick
Mathewson of the Tor Project.

Dan

[1] http://marc.info/?l=openssl-cvs&m=138232287615758


On Mon, Aug 18, 2014 at 5:06 PM, <nnposter () users sourceforge net> wrote:

Fyodor wrote:
Thanks nnposter. Personally, I like this quick-success version better.
 And I might even give more wiggle room than 5 minutes. Even if we give
+/- 90 minutes, wouldn't the odds be about one in 400,000 that a random
number would falsely match (1 / ((2 * 90 * 60) / 2^32))? If this math is
correct, the risk seems pretty immaterial to me.

The patch below hopefully represents the consensus. Compared to the
original version (prior to revision 33394) the script now detects and
ignores cases where the timestamp field is either randomized or fixed,
such as in newer versions of OpenSSL.

The tests are driven by two named constants (not parameters):

max_clock_skew = 90*60 (seconds)
Maximum acceptable difference between target and scanner clocks
to avoid additional testing. Only a single probe (TLS handshake)
is sent to the target if the clock difference falls into this
range. Othewrise one or two additional probes are sent.

max_clock_jitter = 3 (seconds)
Maximum acceptable target clock jitter. Higher variance causes the
target clock to be disqualified.


Cheers,
nnposter


Patch against revision 33580 follows:


--- scripts/ssl-date.nse.orig   2014-08-18 15:57:26.905547900 -0600
+++ scripts/ssl-date.nse        2014-08-18 16:00:12.868142500 -0600
@@ -42,6 +42,14 @@
   return shortport.ssl(host, port) or sslcert.isPortSupported(port)
 end

+-- Miscellaneous script-wide constants
+local max_clock_skew = 90*60   -- maximum acceptable difference between
target
+                               --   and scanner clocks to avoid additional
+                               --   testing
+local max_clock_jitter = 3     -- maximum acceptable target clock jitter
+local detail_debug = 2         -- debug level for printing detailed steps
+
+
 --
 -- most of the code snatched from tls-nextprotoneg until we decide if we
want a separate library
 --
@@ -126,6 +134,13 @@
   return nil
 end

+
+---
+-- Retrieve a timestamp from a TLS port and compare it to the scanner
clock
+--
+-- @param host TLS host
+-- @param port TLS port
+-- @return Timestamp sample object or nil (if the operation failed)
 local get_time_sample = function (host, port)
   -- Send crafted client hello
   local rstatus, response = client_hello(host, port)
@@ -134,39 +149,75 @@
   -- extract time from response
   local tstatus, ttm = extract_time(response)
   if not tstatus then return nil end
-  return ttm, stm
+  stdnse.debug(detail_debug, "TLS sample: %s",
stdnse.format_timestamp(ttm, 0))
+  return {target=ttm, scanner=stm, delta=os.difftime(ttm, stm)}
+end
+
+
+local result = { STAGNANT = "stagnant",
+                 ACCEPTED = "accepted",
+                 REJECTED = "rejected" }
+
+---
+-- Obtain a new timestamp sample and validate it against a reference
sample
+--
+-- @param host TLS host
+-- @param port TLS port
+-- @param reftm Reference timestamp sample
+-- @return Result code
+-- @return New timestamp sample object or nil (if the operation failed)
+local test_time_sample = function (host, port, reftm)
+  local tm = get_time_sample(host, port)
+  if not tm then return nil end
+  local tchange = os.difftime(tm.target, reftm.target)
+  local schange = os.difftime(tm.scanner, reftm.scanner)
+  local status =
+           -- clock cannot run backwards or drift rapidly
+           (tchange < 0 or math.abs(tchange - schange) > max_clock_jitter)
+             and result.REJECTED
+           -- the clock did not advance
+           or tchange == 0
+             and result.STAGNANT
+           -- plausible enough
+           or result.ACCEPTED
+  stdnse.debug(detail_debug, "TLS sample verdict: %s", status)
+  return status, tm
 end


 action = function(host, port)
-  local ttm, stm = get_time_sample(host, port)
-  if not ttm then
+  local tm = get_time_sample(host, port)
+  if not tm then
     return stdnse.format_output(false, "Unable to obtain data from the
target")
   end
-  local delta = os.difftime(ttm, stm)
-  stdnse.debug(2, "Sample #1 time difference is %d seconds", delta)
-
-  if math.abs(delta) > 15*60 then
-    -- time difference is suspect
-    -- get a second sample to determine if the clock is simply off
-    -- or if the TLS randomness does not represent the time at all
-    ttm, stm = get_time_sample(host, port)
-    if not ttm then
+  if math.abs(tm.delta) > max_clock_skew then
+    -- The target clock differs substantially from the scanner
+    -- Let's take another sample to eliminate cases where the TLS field
+    -- contains either random or fixed data instead of the timestamp
+    local reftm = tm
+    local status
+    status, tm = test_time_sample(host, port, reftm)
+    if status and status == result.STAGNANT then
+      -- The target clock did not advance between the two samples (reftm,
tm)
+      -- Let's wait long enough for the target clock to advance
+      -- and then re-take the second sample
+      stdnse.sleep(1.1)
+      status, tm = test_time_sample(host, port, reftm)
+    end
+    if not status then
       return stdnse.format_output(false, "Unable to obtain data from the
target")
     end
-    local origdelta = delta
-    delta = os.difftime(ttm, stm)
-    stdnse.debug(2, "Sample #2 time difference is %d seconds", delta)
-    if math.abs(origdelta - delta) > 5 then
+    if status ~= result.ACCEPTED then
       return stdnse.format_output(false, "TLS randomness does not
represent time")
     end
   end

   local output = {
-                 date = stdnse.format_timestamp(ttm, 0),
-                 delta = delta,
+                 date = stdnse.format_timestamp(tm.target, 0),
+                 delta = tm.delta,
                  }
   return output,
          string.format("%s; %s from scanner time.", output.date,
-                 stdnse.format_difftime(os.date("!*t",ttm),os.date("!*t",
stm)))
+                 stdnse.format_difftime(os.date("!*t", tm.target),
+                                        os.date("!*t", tm.scanner)))
 end
_______________________________________________
Sent through the dev mailing list
http://nmap.org/mailman/listinfo/dev
Archived at http://seclists.org/nmap-dev/

_______________________________________________
Sent through the dev mailing list
http://nmap.org/mailman/listinfo/dev
Archived at http://seclists.org/nmap-dev/


Current thread: