boB Rudis
3 years ago
13 changed files with 759 additions and 18 deletions
@ -1,24 +1,31 @@ |
|||||
Package: jarmed |
Package: jarmed |
||||
Type: Package |
Type: Package |
||||
Title: jarmed title goes here otherwise CRAN checks fail |
Title: Fingerprint TLS Servers with Salesforece JARM Algorithm |
||||
Version: 0.1.0 |
Version: 0.1.0 |
||||
Date: 2020-11-29 |
Date: 2020-11-29 |
||||
Authors@R: c( |
Authors@R: c( |
||||
person("Bob", "Rudis", email = "bob@rud.is", role = c("aut", "cre"), |
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; <https://github.com/salesforce/jarm>") |
||||
) |
) |
||||
Maintainer: Bob Rudis <bob@rud.is> |
Maintainer: Bob Rudis <bob@rud.is> |
||||
Description: A good description goes here otherwise CRAN checks fail. |
Description: The Salesforce JARM Tool <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. |
||||
URL: https://git.rud.is/hrbrmstr/jarmed |
URL: https://git.rud.is/hrbrmstr/jarmed |
||||
BugReports: https://git.rud.is/hrbrmstr/jarmed/issues |
BugReports: https://git.rud.is/hrbrmstr/jarmed/issues |
||||
Encoding: UTF-8 |
Encoding: UTF-8 |
||||
License: AGPL |
License: BSD_3_clause + file LICENSE |
||||
Suggests: |
Suggests: |
||||
covr, tinytest |
covr, tinytest |
||||
Depends: |
Depends: |
||||
R (>= 3.5.0) |
R (>= 3.6.0), |
||||
Imports: |
reticulate |
||||
httr, |
|
||||
jsonlite |
|
||||
Roxygen: list(markdown = TRUE) |
Roxygen: list(markdown = TRUE) |
||||
RoxygenNote: 7.1.1 |
RoxygenNote: 7.1.1 |
||||
|
@ -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. |
@ -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. |
@ -1,4 +1,5 @@ |
|||||
# Generated by roxygen2: do not edit by hand |
# Generated by roxygen2: do not edit by hand |
||||
|
|
||||
import(httr) |
S3method(print,jarm_result) |
||||
importFrom(jsonlite,fromJSON) |
export(jarm_fingerprint) |
||||
|
import(reticulate) |
||||
|
@ -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) |
||||
|
} |
||||
|
|
||||
|
|
@ -1,9 +1,17 @@ |
|||||
#' ... |
#' Fingerprint TLS Servers with Salesforece JARM Algorithm |
||||
#' |
#' |
||||
|
#' The Salesforce JARM Tool <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. |
||||
|
#' |
||||
#' @md |
#' @md |
||||
#' @name jarmed |
#' @name jarmed |
||||
#' @keywords internal |
#' @keywords internal |
||||
#' @author Bob Rudis (bob@@rud.is) |
#' @author Bob Rudis (bob@@rud.is) |
||||
#' @import httr |
#' @import reticulate |
||||
#' @importFrom jsonlite fromJSON |
|
||||
"_PACKAGE" |
"_PACKAGE" |
||||
|
@ -0,0 +1,6 @@ |
|||||
|
.jarm <- NULL |
||||
|
|
||||
|
.onLoad <- function(libname, pkgname) { |
||||
|
path <- system.file("python", "jarm.py", package = "jarmed") |
||||
|
.jarm <<- reticulate::py_run_file(path) |
||||
|
} |
@ -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 <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. |
||||
|
|
||||
|
## 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 |
||||
|
## <chr> <int> <chr> <chr> <chr> |
||||
|
## 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 <NA> 07d19d1ad21d21d07c42d43d000000ee… 0033|0303|http/1.1|ff01-0000-0001-0023-0010-0017,00c0|030… |
||||
|
## 3 rstudio.… 443 <NA> 2ad2ad16d2ad2ad00042d42d000000df… c030|0303|h2|ff01-0000-0001-000b-0023-0010-0017,c030|0303… |
||||
|
## 4 apple.com 443 <NA> 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. |
@ -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 }) |
@ -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} |
Loading…
Reference in new issue