From 8c7aa28e62ff20ee8797d250adeb7daf7d17dfed Mon Sep 17 00:00:00 2001 From: hrbrmstr Date: Sun, 29 Nov 2020 11:40:18 -0500 Subject: [PATCH] initial commit --- .Rbuildignore | 1 + DESCRIPTION | 23 ++- LICENSE | 29 +++ LICENSE.md | 21 +++ NAMESPACE | 5 +- R/jarm.R | 41 +++++ R/jarmed-package.R | 16 +- R/zzz.R | 6 + README.Rmd | 23 ++- README.md | 113 ++++++++++++ inst/python/jarm.py | 456 ++++++++++++++++++++++++++++++++++++++++++++++++ man/jarm_fingerprint.Rd | 32 ++++ man/jarmed.Rd | 11 +- 13 files changed, 759 insertions(+), 18 deletions(-) create mode 100644 LICENSE create mode 100644 LICENSE.md create mode 100644 R/jarm.R create mode 100644 R/zzz.R create mode 100644 README.md create mode 100644 inst/python/jarm.py create mode 100644 man/jarm_fingerprint.Rd diff --git a/.Rbuildignore b/.Rbuildignore index c9a5c92..19cdbd8 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -19,3 +19,4 @@ ^CRAN-RELEASE$ ^appveyor\.yml$ ^tools$ +^LICENSE\.md$ diff --git a/DESCRIPTION b/DESCRIPTION index d81474e..88f4003 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,24 +1,31 @@ Package: jarmed Type: Package -Title: jarmed title goes here otherwise CRAN checks fail +Title: Fingerprint TLS Servers with Salesforece JARM Algorithm Version: 0.1.0 Date: 2020-11-29 Authors@R: c( person("Bob", "Rudis", email = "bob@rud.is", role = c("aut", "cre"), - comment = c(ORCID = "0000-0001-5670-2640")) + comment = c(ORCID = "0000-0001-5670-2640")), + person("Salesforce", role = "aut", + comment = "JARM Python library; BSD-3; ") ) Maintainer: Bob Rudis -Description: A good description goes here otherwise CRAN checks fail. +Description: The Salesforce JARM Tool is an + active Transport Layer Security (TLS) server fingerprinting tool. JARM + fingerprints can be used to quickly verify that all servers in a group have + the same TLS configuration; group disparate servers on the internet by configuration, + identifying that a server may belong to Google vs. Salesforce vs. Apple, for example; + Identify default applications or infrastructure; and/or Identify malware command and + control infrastructure and other malicious servers on the Internet. Tools + are provided to generate JARM fingerprints. URL: https://git.rud.is/hrbrmstr/jarmed BugReports: https://git.rud.is/hrbrmstr/jarmed/issues Encoding: UTF-8 -License: AGPL +License: BSD_3_clause + file LICENSE Suggests: covr, tinytest Depends: - R (>= 3.5.0) -Imports: - httr, - jsonlite + R (>= 3.6.0), + reticulate Roxygen: list(markdown = TRUE) RoxygenNote: 7.1.1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..07e5697 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2020, Bob Rudis + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + Neither the name of — Bob Rudis — nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"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 COPYRIGHT +HOLDER OR CONTRIBUTORS 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. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..524f6aa --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# MIT License + +Copyright (c) 2020 Bob Rudis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NAMESPACE b/NAMESPACE index 5b4b9ae..2d9db67 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,4 +1,5 @@ # Generated by roxygen2: do not edit by hand -import(httr) -importFrom(jsonlite,fromJSON) +S3method(print,jarm_result) +export(jarm_fingerprint) +import(reticulate) diff --git a/R/jarm.R b/R/jarm.R new file mode 100644 index 0000000..428e7fa --- /dev/null +++ b/R/jarm.R @@ -0,0 +1,41 @@ +#' Fingerprint a TLS server +#' +#' @param host host or IP to fingerprint +#' @param port port to connect on (defaults to `443L`) +#' @return `list` with class `jarm_result` +#' @export +#' @examples +#' jarm_fingerprint("rud.is") +jarm_fingerprint <- function(host, port = 443L) { + + host <- host[1] + port <- as.integer(port[1]) + + res <- .jarm$jarm_query(host, port) + + if (is.null(res$ip)) res$ip <- NA_character_ + + class(res) <- c("jarm_result", "list") + + res + +} + +#' @keywords internal +#' @rdname jarm_fingerprint +#' @param x `jarm_result` object +#' @param ... unused +#' @return x (invisibly) +#' @export +print.jarm_result <- function(x, ...) { + cat( +" Host: ", x$host[1], "\n", +" Port: ", x$port[1], "\n", +" IP: ", x$ip[1], "\n", +"Result: ", x$result[1], "\n", +" JARM: ", x$jarm[1], "\n", sep="" + ) + invisible(x) +} + + diff --git a/R/jarmed-package.R b/R/jarmed-package.R index 0778502..3cddf94 100644 --- a/R/jarmed-package.R +++ b/R/jarmed-package.R @@ -1,9 +1,17 @@ -#' ... -#' +#' Fingerprint TLS Servers with Salesforece JARM Algorithm +#' +#' The Salesforce JARM Tool is an +#' active Transport Layer Security (TLS) server fingerprinting tool. JARM +#' fingerprints can be used to quickly verify that all servers in a group have +#' the same TLS configuration; group disparate servers on the internet by configuration, +#' identifying that a server may belong to Google vs. Salesforce vs. Apple, for example; +#' Identify default applications or infrastructure; and/or Identify malware command and +#' control infrastructure and other malicious servers on the Internet. Tools +#' are provided to generate JARM fingerprints. +#' #' @md #' @name jarmed #' @keywords internal #' @author Bob Rudis (bob@@rud.is) -#' @import httr -#' @importFrom jsonlite fromJSON +#' @import reticulate "_PACKAGE" diff --git a/R/zzz.R b/R/zzz.R new file mode 100644 index 0000000..9d2a124 --- /dev/null +++ b/R/zzz.R @@ -0,0 +1,6 @@ +.jarm <- NULL + +.onLoad <- function(libname, pkgname) { + path <- system.file("python", "jarm.py", package = "jarmed") + .jarm <<- reticulate::py_run_file(path) +} \ No newline at end of file diff --git a/README.Rmd b/README.Rmd index ca29102..cafadd9 100644 --- a/README.Rmd +++ b/README.Rmd @@ -39,6 +39,26 @@ packageVersion("jarmed") ``` +```{r ex-01} + +(res <- jarm_fingerprint("rud.is")) + +str(res, 1) +``` + +```{r ex-02} +library(tidyverse) + +c( + "rud.is", + "r-project.org", + "rstudio.com", + "apple.com" +) -> sites + +sites %>% + map_df(jarm_fingerprint) +``` ## jarmed Metrics ```{r cloc, echo=FALSE} @@ -47,5 +67,4 @@ cloc::cloc_pkg_md() ## Code of Conduct -Please note that this project is released with a Contributor Code of Conduct. -By participating in this project you agree to abide by its terms. +Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4460d86 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ + +[![Project Status: Active – The project has reached a stable, usable +state and is being actively +developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) +[![Signed +by](https://img.shields.io/badge/Keybase-Verified-brightgreen.svg)](https://keybase.io/hrbrmstr) +![Signed commit +%](https://img.shields.io/badge/Signed_Commits-100%25-lightgrey.svg) +[![Linux build +Status](https://travis-ci.org/hrbrmstr/jarmed.svg?branch=master)](https://travis-ci.org/hrbrmstr/jarmed) +![Minimal R +Version](https://img.shields.io/badge/R%3E%3D-3.6.0-blue.svg) +![License](https://img.shields.io/badge/License-BSD_3_clause%20+%20file%20LICENSE-blue.svg) + +# jarmed + +Fingerprint TLS Servers with Salesforece JARM Algorithm + +## Description + +The Salesforce JARM Tool is an +active Transport Layer Security (TLS) server fingerprinting tool. JARM +fingerprints can be used to quickly verify that all servers in a group +have the same TLS configuration; group disparate servers on the internet +by configuration, identifying that a server may belong to Google +vs. Salesforce vs. Apple, for example; Identify default applications or +infrastructure; and/or Identify malware command and control +infrastructure and other malicious servers on the Internet. Tools are +provided to generate JARM fingerprints. + +## What’s Inside The Tin + +The following functions are implemented: + +- `jarm_fingerprint`: Fingerprint a TLS server + +## Installation + +``` r +remotes::install_git("https://git.rud.is/hrbrmstr/jarmed.git") +# or +remotes::install_gitlab("hrbrmstr/jarmed") +# or +remotes::install_bitbucket("hrbrmstr/jarmed") +``` + +NOTE: To use the ‘remotes’ install options you will need to have the +[{remotes} package](https://github.com/r-lib/remotes) installed. + +## Usage + +``` r +library(jarmed) + +# current version +packageVersion("jarmed") +## [1] '0.1.0' +``` + +``` r +(res <- jarm_fingerprint("rud.is")) +## Host: rud.is +## Port: 443 +## IP: 172.93.49.183 +## Result: 15d2ad16d29d29d00015d2ad15d29dd1c3ca624d74ad1df5cec63008795502 +## JARM: 009e|0303|h2|0000-ff01-0010,c030|0303|h2|0000-ff01-000b-0010,009f|0303|h2|0000-ff01-0010,c02f|0303||0000-ff01-000b,c02f|0303||0000-ff01-000b,|||,009e|0303|h2|0000-ff01-0010,c030|0303|h2|0000-ff01-000b-0010,009e|0303|h2|0000-ff01-0010,c02f|0303|h2|0000-ff01-000b-0010 + +str(res, 1) +## List of 5 +## $ host : chr "rud.is" +## $ port : int 443 +## $ ip : chr "172.93.49.183" +## $ result: chr "15d2ad16d29d29d00015d2ad15d29dd1c3ca624d74ad1df5cec63008795502" +## $ jarm : chr "009e|0303|h2|0000-ff01-0010,c030|0303|h2|0000-ff01-000b-0010,009f|0303|h2|0000-ff01-0010,c02f|0303||0000-ff01-0"| __truncated__ +## - attr(*, "class")= chr [1:2] "jarm_result" "list" +``` + +``` r +library(tidyverse) + +c( + "rud.is", + "r-project.org", + "rstudio.com", + "apple.com" +) -> sites + +sites %>% + map_df(jarm_fingerprint) +## # A tibble: 4 x 5 +## host port ip result jarm +## +## 1 rud.is 443 172.93.4… 15d2ad16d29d29d00015d2ad15d29dd1… 009e|0303|h2|0000-ff01-0010,c030|0303|h2|0000-ff01-000b-0… +## 2 r-projec… 443 07d19d1ad21d21d07c42d43d000000ee… 0033|0303|http/1.1|ff01-0000-0001-0023-0010-0017,00c0|030… +## 3 rstudio.… 443 2ad2ad16d2ad2ad00042d42d000000df… c030|0303|h2|ff01-0000-0001-000b-0023-0010-0017,c030|0303… +## 4 apple.com 443 29d29d15d29d29d00041d41d000000a5… c02f|0303||ff01-0000-0001-000b-0023-0017,c02f|0303||ff01-… +``` + +## jarmed Metrics + +| Lang | \# Files | (%) | LoC | (%) | Blank lines | (%) | \# Lines | (%) | +|:-----|---------:|----:|----:|-----:|------------:|-----:|---------:|-----:| +| R | 4 | 0.4 | 27 | 0.29 | 12 | 0.19 | 30 | 0.25 | +| Rmd | 1 | 0.1 | 19 | 0.21 | 20 | 0.31 | 31 | 0.25 | +| SUM | 5 | 0.5 | 46 | 0.50 | 32 | 0.50 | 61 | 0.50 | + +clock Package Metrics for jarmed + +## Code of Conduct + +Please note that this project is released with a Contributor Code of +Conduct. By participating in this project you agree to abide by its +terms. diff --git a/inst/python/jarm.py b/inst/python/jarm.py new file mode 100644 index 0000000..878d17a --- /dev/null +++ b/inst/python/jarm.py @@ -0,0 +1,456 @@ +import socket +import struct +import os +import sys +import random +import hashlib +import ipaddress + +#Randomly choose a grease value +def choose_grease(): + grease_list = [b"\x0a\x0a", b"\x1a\x1a", b"\x2a\x2a", b"\x3a\x3a", b"\x4a\x4a", b"\x5a\x5a", b"\x6a\x6a", b"\x7a\x7a", b"\x8a\x8a", b"\x9a\x9a", b"\xaa\xaa", b"\xba\xba", b"\xca\xca", b"\xda\xda", b"\xea\xea", b"\xfa\xfa"] + return random.choice(grease_list) + +def packet_building(jarm_details): + payload = b"\x16" + #Version Check + if jarm_details[2] == "TLS_1.3": + payload += b"\x03\x01" + client_hello = b"\x03\x03" + elif jarm_details[2] == "SSLv3": + payload += b"\x03\x00" + client_hello = b"\x03\x00" + elif jarm_details[2] == "TLS_1": + payload += b"\x03\x01" + client_hello = b"\x03\x01" + elif jarm_details[2] == "TLS_1.1": + payload += b"\x03\x02" + client_hello = b"\x03\x02" + elif jarm_details[2] == "TLS_1.2": + payload += b"\x03\x03" + client_hello = b"\x03\x03" + #Random values in client hello + client_hello += os.urandom(32) + session_id = os.urandom(32) + session_id_length = struct.pack(">B", len(session_id)) + client_hello += session_id_length + client_hello += session_id + #Get ciphers + cipher_choice = get_ciphers(jarm_details) + client_suites_length = struct.pack(">H", len(cipher_choice)) + client_hello += client_suites_length + client_hello += cipher_choice + client_hello += b"\x01" #cipher methods + client_hello += b"\x00" #compression_methods + #Add extensions to client hello + extensions = get_extensions(jarm_details) + client_hello += extensions + #Finish packet assembly + inner_length = b"\x00" + inner_length += struct.pack(">H", len(client_hello)) + handshake_protocol = b"\x01" + handshake_protocol += inner_length + handshake_protocol += client_hello + outer_length = struct.pack(">H", len(handshake_protocol)) + payload += outer_length + payload += handshake_protocol + return payload + +def get_ciphers(jarm_details): + selected_ciphers = b"" + #Two cipher lists: NO1.3 and ALL + if jarm_details[3] == "ALL": + list = [b"\x00\x16", b"\x00\x33", b"\x00\x67", b"\xc0\x9e", b"\xc0\xa2", b"\x00\x9e", b"\x00\x39", b"\x00\x6b", b"\xc0\x9f", b"\xc0\xa3", b"\x00\x9f", b"\x00\x45", b"\x00\xbe", b"\x00\x88", b"\x00\xc4", b"\x00\x9a", b"\xc0\x08", b"\xc0\x09", b"\xc0\x23", b"\xc0\xac", b"\xc0\xae", b"\xc0\x2b", b"\xc0\x0a", b"\xc0\x24", b"\xc0\xad", b"\xc0\xaf", b"\xc0\x2c", b"\xc0\x72", b"\xc0\x73", b"\xcc\xa9", b"\x13\x02", b"\x13\x01", b"\xcc\x14", b"\xc0\x07", b"\xc0\x12", b"\xc0\x13", b"\xc0\x27", b"\xc0\x2f", b"\xc0\x14", b"\xc0\x28", b"\xc0\x30", b"\xc0\x60", b"\xc0\x61", b"\xc0\x76", b"\xc0\x77", b"\xcc\xa8", b"\x13\x05", b"\x13\x04", b"\x13\x03", b"\xcc\x13", b"\xc0\x11", b"\x00\x0a", b"\x00\x2f", b"\x00\x3c", b"\xc0\x9c", b"\xc0\xa0", b"\x00\x9c", b"\x00\x35", b"\x00\x3d", b"\xc0\x9d", b"\xc0\xa1", b"\x00\x9d", b"\x00\x41", b"\x00\xba", b"\x00\x84", b"\x00\xc0", b"\x00\x07", b"\x00\x04", b"\x00\x05"] + elif jarm_details[3] == "NO1.3": + list = [b"\x00\x16", b"\x00\x33", b"\x00\x67", b"\xc0\x9e", b"\xc0\xa2", b"\x00\x9e", b"\x00\x39", b"\x00\x6b", b"\xc0\x9f", b"\xc0\xa3", b"\x00\x9f", b"\x00\x45", b"\x00\xbe", b"\x00\x88", b"\x00\xc4", b"\x00\x9a", b"\xc0\x08", b"\xc0\x09", b"\xc0\x23", b"\xc0\xac", b"\xc0\xae", b"\xc0\x2b", b"\xc0\x0a", b"\xc0\x24", b"\xc0\xad", b"\xc0\xaf", b"\xc0\x2c", b"\xc0\x72", b"\xc0\x73", b"\xcc\xa9", b"\xcc\x14", b"\xc0\x07", b"\xc0\x12", b"\xc0\x13", b"\xc0\x27", b"\xc0\x2f", b"\xc0\x14", b"\xc0\x28", b"\xc0\x30", b"\xc0\x60", b"\xc0\x61", b"\xc0\x76", b"\xc0\x77", b"\xcc\xa8", b"\xcc\x13", b"\xc0\x11", b"\x00\x0a", b"\x00\x2f", b"\x00\x3c", b"\xc0\x9c", b"\xc0\xa0", b"\x00\x9c", b"\x00\x35", b"\x00\x3d", b"\xc0\x9d", b"\xc0\xa1", b"\x00\x9d", b"\x00\x41", b"\x00\xba", b"\x00\x84", b"\x00\xc0", b"\x00\x07", b"\x00\x04", b"\x00\x05"] + #Change cipher order + if jarm_details[4] != "FORWARD": + list = cipher_mung(list, jarm_details[4]) + #Add GREASE to beginning of cipher list (if applicable) + if jarm_details[5] == "GREASE": + list.insert(0,choose_grease()) + #Generate cipher list + for cipher in list: + selected_ciphers += cipher + return selected_ciphers + +def cipher_mung(ciphers, request): + output = [] + cipher_len = len(ciphers) + #Ciphers backward + if (request == "REVERSE"): + output = ciphers[::-1] + #Bottom half of ciphers + elif (request == "BOTTOM_HALF"): + if (cipher_len % 2 == 1): + output = ciphers[int(cipher_len/2)+1:] + else: + output = ciphers[int(cipher_len/2):] + #Top half of ciphers in reverse order + elif (request == "TOP_HALF"): + if (cipher_len % 2 == 1): + output.append(ciphers[int(cipher_len/2)]) + #Top half gets the middle cipher + output += cipher_mung(cipher_mung(ciphers, "REVERSE"),"BOTTOM_HALF") + #Middle-out cipher order + elif (request == "MIDDLE_OUT"): + middle = int(cipher_len/2) + # if ciphers are uneven, start with the center. Second half before first half + if (cipher_len % 2 == 1): + output.append(ciphers[middle]) + for i in range(1, middle+1): + output.append(ciphers[middle + i]) + output.append(ciphers[middle - i]) + else: + for i in range(1, middle+1): + output.append(ciphers[middle-1 + i]) + output.append(ciphers[middle - i]) + return output + +def get_extensions(jarm_details): + extension_bytes = b"" + all_extensions = b"" + grease = False + #GREASE + if jarm_details[5] == "GREASE": + all_extensions += choose_grease() + all_extensions += b"\x00\x00" + grease = True + #Server name + all_extensions += extension_server_name(jarm_details[0]) + #Other extensions + extended_master_secret = b"\x00\x17\x00\x00" + all_extensions += extended_master_secret + max_fragment_length = b"\x00\x01\x00\x01\x01" + all_extensions += max_fragment_length + renegotiation_info = b"\xff\x01\x00\x01\x00" + all_extensions += renegotiation_info + supported_groups = b"\x00\x0a\x00\x0a\x00\x08\x00\x1d\x00\x17\x00\x18\x00\x19" + all_extensions += supported_groups + ec_point_formats = b"\x00\x0b\x00\x02\x01\x00" + all_extensions += ec_point_formats + session_ticket = b"\x00\x23\x00\x00" + all_extensions += session_ticket + #Application Layer Protocol Negotiation extension + all_extensions += app_layer_proto_negotiation(jarm_details) + signature_algorithms = b"\x00\x0d\x00\x14\x00\x12\x04\x03\x08\x04\x04\x01\x05\x03\x08\x05\x05\x01\x08\x06\x06\x01\x02\x01" + all_extensions += signature_algorithms + #Key share extension + all_extensions += key_share(grease) + psk_key_exchange_modes = b"\x00\x2d\x00\x02\x01\x01" + all_extensions += psk_key_exchange_modes + #Supported versions extension + if (jarm_details[2] == "TLS_1.3") or (jarm_details[7] == "1.2_SUPPORT"): + all_extensions += supported_versions(jarm_details, grease) + #Finish assembling extensions + extension_length = len(all_extensions) + extension_bytes += struct.pack(">H", extension_length) + extension_bytes += all_extensions + return extension_bytes + +#Client hello server name extension +def extension_server_name(host): + ext_sni = b"\x00\x00" + ext_sni_length = len(host)+5 + ext_sni += struct.pack(">H", ext_sni_length) + ext_sni_length2 = len(host)+3 + ext_sni += struct.pack(">H", ext_sni_length2) + ext_sni += b"\x00" + ext_sni_length3 = len(host) + ext_sni += struct.pack(">H", ext_sni_length3) + ext_sni += host.encode() + return ext_sni + +#Client hello apln extension +def app_layer_proto_negotiation(jarm_details): + ext = b"\x00\x10" + if (jarm_details[6] == "RARE_APLN"): + #Removes h2 and http/1.1 + alpns = [b"\x08\x68\x74\x74\x70\x2f\x30\x2e\x39", b"\x08\x68\x74\x74\x70\x2f\x31\x2e\x30", b"\x06\x73\x70\x64\x79\x2f\x31", b"\x06\x73\x70\x64\x79\x2f\x32", b"\x06\x73\x70\x64\x79\x2f\x33", b"\x03\x68\x32\x63", b"\x02\x68\x71"] + else: + #All apln extensions in order from weakest to strongest + alpns = [b"\x08\x68\x74\x74\x70\x2f\x30\x2e\x39", b"\x08\x68\x74\x74\x70\x2f\x31\x2e\x30", b"\x08\x68\x74\x74\x70\x2f\x31\x2e\x31", b"\x06\x73\x70\x64\x79\x2f\x31", b"\x06\x73\x70\x64\x79\x2f\x32", b"\x06\x73\x70\x64\x79\x2f\x33" b"\x02\x68\x32", b"\x03\x68\x32\x63", b"\x02\x68\x71"] + #apln extensions can be reordered + if jarm_details[8] != "FORWARD": + alpns = cipher_mung(alpns, jarm_details[8]) + all_alpns = b"" + for alpn in alpns: + all_alpns += alpn + second_length = len(all_alpns) + first_length = second_length+2 + ext += struct.pack(">H", first_length) + ext += struct.pack(">H", second_length) + ext += all_alpns + return ext + +#Generate key share extension for client hello +def key_share(grease): + ext = b"\x00\x33" + #Add grease value if necessary + if grease == True: + share_ext = choose_grease() + share_ext += b"\x00\x01\x00" + else: + share_ext = b"" + group = b"\x00\x1d" + share_ext += group + key_exchange_length = b"\x00\x20" + share_ext += key_exchange_length + share_ext += os.urandom(32) + second_length = len(share_ext) + first_length = second_length+2 + ext += struct.pack(">H", first_length) + ext += struct.pack(">H", second_length) + ext += share_ext + return ext + +#Supported version extension for client hello +def supported_versions(jarm_details, grease): + if (jarm_details[7] == "1.2_SUPPORT"): + #TLS 1.3 is not supported + tls = [b"\x03\x01", b"\x03\x02", b"\x03\x03"] + else: + #TLS 1.3 is supported + tls = [b"\x03\x01", b"\x03\x02", b"\x03\x03", b"\x03\x04"] + #Change supported version order, by default, the versions are from oldest to newest + if jarm_details[8] != "FORWARD": + tls = cipher_mung(tls, jarm_details[8]) + #Assemble the extension + ext = b"\x00\x2b" + #Add GREASE if applicable + if grease == True: + versions = choose_grease() + else: + versions = b"" + for version in tls: + versions += version + second_length = len(versions) + first_length = second_length+1 + ext += struct.pack(">H", first_length) + ext += struct.pack(">B", second_length) + ext += versions + return ext + +#Send the assembled client hello using a socket +def send_packet(destination_host, destination_port, packet): + try: + #Determine if the input is an IP or domain name + try: + if (type(ipaddress.ip_address(destination_host)) == ipaddress.IPv4Address) or (type(ipaddress.ip_address(destination_host)) == ipaddress.IPv6Address): + raw_ip = True + ip = (destination_host, destination_port) + except ValueError as e: + ip = (None, None) + raw_ip = False + #Connect the socket + if ":" in destination_host: + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + #Timeout of 20 seconds + sock.settimeout(20) + sock.connect((destination_host, destination_port, 0, 0)) + else: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + #Timeout of 20 seconds + sock.settimeout(20) + sock.connect((destination_host, destination_port)) + #Resolve IP if given a domain name + if raw_ip == False: + ip = sock.getpeername() + sock.sendall(packet) + #Receive server hello + data = sock.recv(1484) + #Close socket + sock.shutdown(socket.SHUT_RDWR) + sock.close() + return data, ip[0] + #Timeout errors result in an empty hash + except (TimeoutError,socket.timeout) as e: + if sock is not None: + sock.close() + return "TIMEOUT", ip[0] + except Exception as e: + return None, None + # if sock is not None: + # sock.close() + # return None, ip[0] + +#If a packet is received, decipher the details +def read_packet(data, jarm_details): + try: + if data == None: + return "|||" + jarm = "" + #Server hello error + if data[0] == 21: + selected_cipher = b"" + return "|||" + #Check for server hello + elif (data[0] == 22) and (data[5] == 2): + counter = data[43] + #Find server's selected cipher + selected_cipher = data[counter+44:counter+46] + #Find server's selected version + version = data[9:11] + #Format + jarm += str(selected_cipher.hex()) + jarm += "|" + jarm += str(version.hex()) + jarm += "|" + #Extract extensions + extensions = (extract_extension_info(data, counter)) + jarm += extensions + return jarm + else: + return "|||" + + except Exception as e: + return "|||" + +#Deciphering the extensions in the server hello +def extract_extension_info(data, counter): + try: + #Error handling + if (data[counter+47] == 11): + return "|||" + elif (data[counter+50:counter+53] == b"\x0e\xac\x0b") or (data[82:85] == b"\x0f\xf0\x0b"): + return "|||" + count = 49+counter + length = int.from_bytes(data[counter+47:counter+49], byteorder='big') + maximum = length+(count-1) + types = [] + values = [] + #Collect all extension types and values for later reference + while count < maximum: + types.append(data[count:count+2]) + ext_length = int.from_bytes(data[count+2:count+4], byteorder='big') + if ext_length == 0: + count += 4 + values.append("") + else: + values.append(data[count+4:count+4+ext_length]) + count += ext_length+4 + result = "" + #Read application_layer_protocol_negotiation + alpn = find_extension(b"\x00\x10", types, values) + result += str(alpn) + result += "|" + #Add formating hyphens + add_hyphen = 0 + while add_hyphen < len(types): + result += types[add_hyphen].hex() + add_hyphen += 1 + if add_hyphen == len(types): + break + else: + result += "-" + return result + #Error handling + except IndexError as e: + result = "|||" + return result + +#Matching cipher extensions to values +def find_extension(ext_type, types, values): + iter = 0 + #For the APLN extension, grab the value in ASCII + if ext_type == b"\x00\x10": + while iter < len(types): + if types[iter] == ext_type: + return ((values[iter][3:]).decode()) + iter += 1 + else: + while iter < len(types): + if types[iter] == ext_type: + return values[iter].hex() + iter += 1 + return "" + +#Custom fuzzy hash +def jarm_hash(jarm_raw): + #If jarm is empty, 62 zeros for the hash + if jarm_raw == "|||,|||,|||,|||,|||,|||,|||,|||,|||,|||": + return "0"*62 + fuzzy_hash = "" + handshakes = jarm_raw.split(",") + alpns_and_ext = "" + for handshake in handshakes: + components = handshake.split("|") + #Custom jarm hash includes a fuzzy hash of the ciphers and versions + fuzzy_hash += cipher_bytes(components[0]) + fuzzy_hash += version_byte(components[1]) + alpns_and_ext += components[2] + alpns_and_ext += components[3] + #Custom jarm hash has the sha256 of alpns and extensions added to the end + sha256 = (hashlib.sha256(alpns_and_ext.encode())).hexdigest() + fuzzy_hash += sha256[0:32] + return fuzzy_hash + +#Fuzzy hash for ciphers is the index number (in hex) of the cipher in the list +def cipher_bytes(cipher): + if cipher == "": + return "00" + list = [b"\x00\x04", b"\x00\x05", b"\x00\x07", b"\x00\x0a", b"\x00\x16", b"\x00\x2f", b"\x00\x33", b"\x00\x35", b"\x00\x39", b"\x00\x3c", b"\x00\x3d", b"\x00\x41", b"\x00\x45", b"\x00\x67", b"\x00\x6b", b"\x00\x84", b"\x00\x88", b"\x00\x9a", b"\x00\x9c", b"\x00\x9d", b"\x00\x9e", b"\x00\x9f", b"\x00\xba", b"\x00\xbe", b"\x00\xc0", b"\x00\xc4", b"\xc0\x07", b"\xc0\x08", b"\xc0\x09", b"\xc0\x0a", b"\xc0\x11", b"\xc0\x12", b"\xc0\x13", b"\xc0\x14", b"\xc0\x23", b"\xc0\x24", b"\xc0\x27", b"\xc0\x28", b"\xc0\x2b", b"\xc0\x2c", b"\xc0\x2f", b"\xc0\x30", b"\xc0\x60", b"\xc0\x61", b"\xc0\x72", b"\xc0\x73", b"\xc0\x76", b"\xc0\x77", b"\xc0\x9c", b"\xc0\x9d", b"\xc0\x9e", b"\xc0\x9f", b"\xc0\xa0", b"\xc0\xa1", b"\xc0\xa2", b"\xc0\xa3", b"\xc0\xac", b"\xc0\xad", b"\xc0\xae", b"\xc0\xaf", b'\xcc\x13', b'\xcc\x14', b'\xcc\xa8', b'\xcc\xa9', b'\x13\x01', b'\x13\x02', b'\x13\x03', b'\x13\x04', b'\x13\x05'] + count = 1 + for bytes in list: + strtype_bytes = str(bytes.hex()) + if cipher == strtype_bytes: + break + count += 1 + hexvalue = str(hex(count))[2:] + #This part must always be two bytes + if len(hexvalue) < 2: + return_bytes = "0" + hexvalue + else: + return_bytes = hexvalue + return return_bytes + +#This captures a single version byte based on version +def version_byte(version): + if version == "": + return "0" + options = "abcdef" + count = int(version[3:4]) + byte = options[count] + return byte + +def jarm_query(destination_host, destination_port): + #Select the packets and formats to send + #Array format = [destination_host,destination_port,version,cipher_list,cipher_order,GREASE,RARE_APLN,1.3_SUPPORT,extension_orders] + tls1_2_forward = [destination_host, destination_port, "TLS_1.2", "ALL", "FORWARD", "NO_GREASE", "APLN", "1.2_SUPPORT", "REVERSE"] + tls1_2_reverse = [destination_host, destination_port, "TLS_1.2", "ALL", "REVERSE", "NO_GREASE", "APLN", "1.2_SUPPORT", "FORWARD"] + tls1_2_top_half = [destination_host, destination_port, "TLS_1.2", "ALL", "TOP_HALF", "NO_GREASE", "APLN", "NO_SUPPORT", "FORWARD"] + tls1_2_bottom_half = [destination_host, destination_port, "TLS_1.2", "ALL", "BOTTOM_HALF", "NO_GREASE", "RARE_APLN", "NO_SUPPORT", "FORWARD"] + tls1_2_middle_out = [destination_host, destination_port, "TLS_1.2", "ALL", "MIDDLE_OUT", "GREASE", "RARE_APLN", "NO_SUPPORT", "REVERSE"] + tls1_1_middle_out = [destination_host, destination_port, "TLS_1.1", "ALL", "FORWARD", "NO_GREASE", "APLN", "NO_SUPPORT", "FORWARD"] + tls1_3_forward = [destination_host, destination_port, "TLS_1.3", "ALL", "FORWARD", "NO_GREASE", "APLN", "1.3_SUPPORT", "REVERSE"] + tls1_3_reverse = [destination_host, destination_port, "TLS_1.3", "ALL", "REVERSE", "NO_GREASE", "APLN", "1.3_SUPPORT", "FORWARD"] + tls1_3_invalid = [destination_host, destination_port, "TLS_1.3", "NO1.3", "FORWARD", "NO_GREASE", "APLN", "1.3_SUPPORT", "FORWARD"] + tls1_3_middle_out = [destination_host, destination_port, "TLS_1.3", "ALL", "MIDDLE_OUT", "GREASE", "APLN", "1.3_SUPPORT", "REVERSE"] + #Possible versions: SSLv3, TLS_1, TLS_1.1, TLS_1.2, TLS_1.3 + #Possible cipher lists: ALL, NO1.3 + #GREASE: either NO_GREASE or GREASE + #APLN: either APLN or RARE_APLN + #Supported Verisons extension: 1.2_SUPPPORT, NO_SUPPORT, or 1.3_SUPPORT + #Possible Extension order: FORWARD, REVERSE + queue = [tls1_2_forward, tls1_2_reverse, tls1_2_top_half, tls1_2_bottom_half, tls1_2_middle_out, tls1_1_middle_out, tls1_3_forward, tls1_3_reverse, tls1_3_invalid, tls1_3_middle_out] + jarm = "" + #Assemble, send, and decipher each packet + iterate = 0 + while iterate < len(queue): + payload = packet_building(queue[iterate]) + server_hello, ip = send_packet(destination_host, destination_port, payload) + #Deal with timeout error + if server_hello == "TIMEOUT": + jarm = "|||,|||,|||,|||,|||,|||,|||,|||,|||,|||" + break + ans = read_packet(server_hello, queue[iterate]) + jarm += ans + iterate += 1 + if iterate == len(queue): + break + else: + jarm += "," + #Fuzzy hash + result = jarm_hash(jarm) + + return({ "host" : destination_host, "port" : destination_port, "ip": ip, "result" : result, "jarm" : jarm }) diff --git a/man/jarm_fingerprint.Rd b/man/jarm_fingerprint.Rd new file mode 100644 index 0000000..057c104 --- /dev/null +++ b/man/jarm_fingerprint.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/jarm.R +\name{jarm_fingerprint} +\alias{jarm_fingerprint} +\alias{print.jarm_result} +\title{Fingerprint a TLS server} +\usage{ +jarm_fingerprint(host, port = 443L) + +\method{print}{jarm_result}(x, ...) +} +\arguments{ +\item{host}{host or IP to fingerprint} + +\item{port}{port to connect on (defaults to \code{443L})} + +\item{x}{\code{jarm_result} object} + +\item{...}{unused} +} +\value{ +\code{list} with class \code{jarm_result} + +x (invisibly) +} +\description{ +Fingerprint a TLS server +} +\examples{ +jarm_fingerprint("rud.is") +} +\keyword{internal} diff --git a/man/jarmed.Rd b/man/jarmed.Rd index 78eac22..6a5124f 100644 --- a/man/jarmed.Rd +++ b/man/jarmed.Rd @@ -4,9 +4,16 @@ \name{jarmed} \alias{jarmed} \alias{jarmed-package} -\title{...} +\title{Fingerprint TLS Servers with Salesforece JARM Algorithm} \description{ -A good description goes here otherwise CRAN checks fail. +The Salesforce JARM Tool \url{https://github.com/salesforce/jarm} is an +active Transport Layer Security (TLS) server fingerprinting tool. JARM +fingerprints can be used to quickly verify that all servers in a group have +the same TLS configuration; group disparate servers on the internet by configuration, +identifying that a server may belong to Google vs. Salesforce vs. Apple, for example; +Identify default applications or infrastructure; and/or Identify malware command and +control infrastructure and other malicious servers on the Internet. Tools +are provided to generate JARM fingerprints. } \seealso{ Useful links: