Chapter 11 What is the first TLS 1.3 client random that was used to establish a connection with

11.1 Objects in memory/packages loaded from preceding chapters

Objects available:

  • read_zeek_log() — helper function to read Zeek log files (Chapter 3)
  • packets — data frame of PCAP packet data (Chapter 4)
  • conn — Zeek conn.log data (Chapter 5)
  • hoststshark host/IP file (Chapter 8)
  • ftp — Zeek ftp.log data (Chapter 10)


  • {tidyverse}

11.2 Question Setup

We’ve mazed into encrypted, technical territory with our new quest to seek out the first TLS 1.3 client random that was used to establish a connection with TLS (transport layer security) is what your browser (and other clients) use to keep data away from prying eyes. TLS connections must be setup/established through a handshake process and we’ve been asked to pick out a certain element from the first handshake made to a connection to ProtonMail, an end-to-end encrypted email service hosted in Switzerland.

This quest expects us to know about this handshake and where to look for the associated data. In the most TLS exchange algorithm (RSA) the first message in this handshake is what is known as the “client hello” message. Your client send a “hey there” greeting to the server, telling it what TLS version and cipher suites it supports plus a string of random bytes boringly known as the “client random.” This is target of our quest.

While we’ve generated many files from the PCAP, we’re going to have to poke at it again to solve this question with the least amount of frustration. The tshark filters contain a cadre of tls filters, one of which is tls.handshake.extensions_server_name which can be used to search for server names specified in the Server Name Indication (SNI) TLS extension field. Since this name will be in the client hello, we can filter on it and then identify the first client random.

11.3 Solving the quest with tshark custom filters

We really don’t need R at all since we can create a display filter and choose what fields to output right on the tshark command line:

system("tshark -r maze/maze.pcapng -e tls.handshake.random -Tfields tls.handshake.extensions_server_name == ''", intern=TRUE)
## [1] "24e92513b97a0348f733d16996929a79be21b0b1400cd7e2862a732ce7775b70"
## [2] "32b9d36ddc0a2cc8c46811a50114ee2425c3dbf67be6b3d76f186ef25551548a"
## [3] "be82534e4aaef468ac88fe15473dd429bd7c4051c7a032d51d7979f36d76fdc7"
## [4] "492fc3d932fb6426bb9d7a087cc5e2ae8fd4a0f6826f8736fb0f1ad225b962f4"
## [5] "721f28c2a407810abfdbf7d9c8be8e3f452cc80d52e6f738fa158e521ff2f20e"
## [6] "ddfb32c96ba450dee42f208944d96bebad751298ce3471cb8e06ee112e37493c"

The first element is the target of our quest.

11.4 Solving the quest with R and tshark custom filters

TLS JSON output in tshark is ginormous, so filtering as much as possible before outputting the JSON is paramount.

We’ll take a similar strategy to the pure tshark version above and grab all the handshakes for Proton Mail. The -T json unsurprisingly generates JSON output and you are encouraged to bring that file up in vim, Sublime Text, or even pipe it through jq to see how complex the structure is. We’ll do the same in R:

system("tshark -T json -r maze/maze.pcapng tls.handshake.extensions_server_name == '' > maze/proton-q.json")

library(jsonlite, include.only = "fromJSON")

proton_hands <- fromJSON("maze/proton-q.json")

str(proton_hands, 3) # look at the structure of the object, only going 3 nested levels deep
## 'data.frame':    6 obs. of  4 variables:
##  $ _index : chr  "packets-2021-04-29" "packets-2021-04-29" "packets-2021-04-29" "packets-2021-04-29" ...
##  $ _type  : chr  "doc" "doc" "doc" "doc" ...
##  $ _score : logi  NA NA NA NA NA NA
##  $ _source:'data.frame': 6 obs. of  1 variable:
##   ..$ layers:'data.frame':   6 obs. of  5 variables:
##   .. ..$ frame:'data.frame': 6 obs. of  15 variables:
##   .. ..$ eth  :'data.frame': 6 obs. of  5 variables:
##   .. ..$ ip   :'data.frame': 6 obs. of  19 variables:
##   .. ..$ tcp  :'data.frame': 6 obs. of  24 variables:
##   .. ..$ tls  :'data.frame': 6 obs. of  1 variable:

We can see that each packet has many layers (and not all packets must or will have the same layer types in a given PCAP). This data structure contains extracted handshakes associated with the SNI, and all of this information will be in the tls structure.

str(proton_hands$`_source`$layers$tls, 2)
## 'data.frame':    6 obs. of  1 variable:
##  $ tls.record:'data.frame':  6 obs. of  4 variables:
##   ..$ tls.record.content_type: chr  "22" "22" "22" "22" ...
##   ..$ tls.record.version     : chr  "0x00000301" "0x00000301" "0x00000301" "0x00000301" ...
##   ..$ tls.record.length      : chr  "512" "512" "512" "512" ...
##   ..$ tls.handshake          :'data.frame':  6 obs. of  31 variables:

This structure include the tls.handshake, so we can wind down a twisty path to get to our quarry:

# grab the first handshake random
## [1] "24:e9:25:13:b9:7a:03:48:f7:33:d1:69:96:92:9a:79:be:21:b0:b1:40:0c:d7:e2:86:2a:73:2c:e7:77:5b:70"

There’s one “nit” left, which is that the challenge answer box expects a value with no colons. We can quickly fix that:

gsub(":", "", proton_hands$`_source`$layers$tls$tls.record$tls.handshake$tls.handshake.random[1])
## [1] "24e92513b97a0348f733d16996929a79be21b0b1400cd7e2862a732ce7775b70"