├── .gitignore
├── .idea
├── .gitignore
├── discord.xml
├── inspectionProfiles
│ ├── Project_Default.xml
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
├── requests-ja3.iml
└── vcs.xml
├── README.md
├── gui
├── __init__.py
├── fetch.py
└── imitate.py
├── requests_ja3
├── __init__.py
├── decoder.py
├── imitate
│ ├── .gitignore
│ ├── __init__.py
│ ├── fakessl
│ │ ├── .idea
│ │ │ ├── .gitignore
│ │ │ ├── fakessl.iml
│ │ │ ├── modules.xml
│ │ │ └── vcs.xml
│ │ ├── SSLContext.cpp
│ │ ├── SSLContext.hpp
│ │ ├── SSLSocket.cpp
│ │ ├── SSLSocket.hpp
│ │ ├── fakessl.cpp
│ │ ├── test_no_ssl.cpp
│ │ └── test_no_ssl.hpp
│ ├── fakessl_py
│ │ ├── Options.py
│ │ ├── SSLContext.py
│ │ ├── SSLSocket.py
│ │ ├── __init__.py
│ │ ├── libssl_binder.py
│ │ ├── libssl_library_loader.py
│ │ ├── libssl_macros_mixin.py
│ │ ├── libssl_type_bindings.py
│ │ ├── libssl_utils_mixin.py
│ │ ├── protocol_constants.py
│ │ └── shims_and_mixins.py
│ ├── imitate.py
│ ├── server_cert
│ │ ├── gen.sh
│ │ ├── localhost.crt
│ │ ├── localhost.key
│ │ ├── localhost.p12
│ │ └── thanks_to.txt
│ ├── test.py
│ ├── test_app.c
│ ├── test_server.py
│ └── verify.py
├── monitor
│ ├── __init__.py
│ ├── monitor.py
│ └── utils.py
├── patcher.py
├── patcher_utils.py
└── ssl_utils.py
├── test.py
├── test_imitate.py
├── test_monitor.py
├── test_patcher.py
└── test_server.py
/.gitignore:
--------------------------------------------------------------------------------
1 | /requests_ja3/imitate/fakessl/fakessl.cpython-39-x86_64-linux-gnu.so
2 | /requests_ja3/imitate/fakessl/build/
3 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/discord.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/requests-ja3.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # requests-ja3
2 |
3 | Modifying Python's `requests` module to spoof a ja3 fingerprint
4 |
5 | (Since the repo seems to be getting some external attention, I want to clarify that this is experimental and not currently in a working state. Thank you!)
6 |
7 | ### TO-DO LIST
8 | - [x] `decoder`: parse string version of ja3
9 | - [x] `patcher`: find and replace usages of ssl module in an imported `requests` module
10 | - [x] `monitor`: dump ja3 off network using scapy
11 | - some inconsistency here? some fields don't seem to match ja3 reported by ja3er
12 | - not sure how to further debug this, client hello packet data from wireshark matches scapy
13 | - [x] `server`: dump ja3 server-side with customized openssl
14 | - the cause for the ^ inconsistency with ja3er is because the last two fields (extension info) aren't used by tls 1.3
15 | - this server dumps the field values despite the extensions going unused
16 | - [ ] `imitate`: fake ssl module with customizable ja3
17 | - [x] ~~dummy C extension with setup.py and portable compiler setup~~
18 | - [x] ~~dummy pybind11 + cppimport extension~~ **(moved to ctypes)**
19 | - [ ] ~~ability to verify fakessl against real ssl module~~ can't inspect pybind11 method signatures yet
20 | - [x] dummy SSLSocket class
21 | - [x] customized openssl compile options
22 | - [x] linkage against C extension
23 | - [x] usage of compiled openssl in SSLSocket class
24 | - [x] `ssl.wrap_socket` at minimum
25 | - [ ] other `ssl` functions used by requests/urllib3
26 | - [ ] tests using ja3s from common browsers
27 | - [x] brave browser (works!)
28 | - [ ] firefox (segfaults)
29 | - [ ] `patcher`: patch fakessl into a requests module
30 | - [ ] patch the module
31 | - [ ] automatically test the patched module against a local server
32 |
33 | **progress (5/13/22):**
34 | - all fields are customized
35 | - I implemented the compressed certificate extension in my fork of OpenSSL, so that works
36 | - Most other extensions are enabled automatically
37 | - server (for dumping ja3) works, with solutions for ja3er's flaws
38 | - can imitate brave browser's fingerprint successfully!!!
39 | - need to sort segfault when imitating firefox, and finish code for patching a requests module
40 |
41 | ### REFERENCES
42 | JA3 specification: [https://github.com/salesforce/ja3/blob/master/README.md](https://github.com/salesforce/ja3/blob/master/README.md)
--------------------------------------------------------------------------------
/gui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/an0ndev/requests-ja3/606fe61aea9e2c68e8c36b3e1654d0d69707a8be/gui/__init__.py
--------------------------------------------------------------------------------
/gui/fetch.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import functools
3 | import threading
4 | import tkinter
5 | import datetime
6 | import typing
7 |
8 | from requests_ja3.decoder import JA3
9 | from requests_ja3.imitate.imitate import generate_imitation_libssl
10 | from requests_ja3.imitate.test_server import JA3Fetcher
11 |
12 | class JA3FetcherGUI:
13 | def __init__(self):
14 | self.BG = "#000000"
15 | self.FG = "#00FF00"
16 | self.COMMON = {"font": "TkFixedFont", "bg": self.BG, "fg": self.FG}
17 |
18 | self.root = tkinter.Tk()
19 | self.root.title("JA3 Fetcher")
20 | self.root.wait_visibility()
21 |
22 | self.startup_msg = tkinter.Label(self.root, text = "Preparing fakessl...", **self.COMMON)
23 | self.startup_msg.grid()
24 | self.root.update_idletasks ()
25 | self.fakessl = generate_imitation_libssl (
26 | None,
27 | use_in_tree_libssl = True
28 | )
29 | self.startup_msg.grid_forget()
30 | self.startup_msg.destroy()
31 |
32 | self.container = tkinter.Frame(self.root, bg = self.BG)
33 | self.container.grid()
34 |
35 | self._fetch_thread = threading.Thread (target = self._fetch_func)
36 | self.output_header = tkinter.Label (self.container, text = "--- Received JA3s: ---", **self.COMMON)
37 | self.output_header.grid(row = 0)
38 | self.output_frame = tkinter.Frame(self.container, bg = self.BG)
39 | self.output_frame.grid(row = 1)
40 | self.current_output_frame_row = 0
41 |
42 | self.data_widgets = []
43 | self.first = True
44 |
45 | self.cancelled = False
46 | self.shut_down = False
47 | self.cancel_button = tkinter.Button (self.container, command = self._cancel, text = "Close", **self.COMMON, relief = tkinter.SOLID, borderwidth = 0)
48 | self.cancel_button.grid(row = 3)
49 | self.clear_button = tkinter.Button (self.container, command = self._clear, text = "Clear", **self.COMMON,
50 | relief = tkinter.SOLID, borderwidth = 0)
51 | self.clear_button.grid (row = 2)
52 | self.last_ja3: typing.Optional[JA3] = None
53 | self._fetch_thread.start ()
54 | def run (self):
55 | try:
56 | self.root.mainloop ()
57 | finally:
58 | self._shutdown_internal()
59 | def _cancel (self):
60 | self._shutdown_internal()
61 | self.root.destroy ()
62 | def _shutdown_internal(self):
63 | if self.shut_down: return
64 | self.fetcher.cancel ()
65 | self._fetch_thread.join ()
66 | self.shut_down = True
67 | def _copy (self, ja3: JA3):
68 | self.root.clipboard_clear()
69 | self.root.clipboard_append(ja3.to_string())
70 | def _clear(self):
71 | for data_widget in self.data_widgets:
72 | data_widget.grid_forget()
73 | data_widget.destroy()
74 | self.data_widgets = []
75 | self.current_output_frame_row = 0 if self.first else 1
76 | def _fetch_func (self):
77 | while True:
78 | self.fetcher = JA3Fetcher (self.fakessl)
79 | resp = self.fetcher.fetch ()
80 | if self.fetcher.cancelled: break
81 | ja3, user_agent = resp
82 | self.last_ja3 = ja3
83 |
84 | now = datetime.datetime.now ().strftime ("%X")
85 |
86 | if self.first:
87 | tkinter.Label (self.output_frame, text = "Time", **self.COMMON).grid (row = 0, column = 0)
88 | tkinter.Label (self.output_frame, text = "Reports As", **self.COMMON).grid (row = 0, column = 1)
89 | tkinter.Label (self.output_frame, text = "JA3 Hash", **self.COMMON).grid (row = 0, column = 2)
90 | tkinter.Label (self.output_frame, text = "Copy", **self.COMMON).grid (row = 0, column = 3)
91 | self.current_output_frame_row += 1
92 | self.first = False
93 |
94 | new_time = tkinter.Label (self.output_frame, text = now, **self.COMMON)
95 | new_time.grid (row = self.current_output_frame_row, column = 0)
96 | new_user_agent = tkinter.Label (self.output_frame, text = user_agent, **self.COMMON)
97 | new_user_agent.grid (row = self.current_output_frame_row, column = 1)
98 | new_hash = tkinter.Label (self.output_frame, text = ja3.to_hash(), **self.COMMON)
99 | new_hash.grid (row = self.current_output_frame_row, column = 2)
100 | new_copy_btn = tkinter.Button (self.output_frame, command = functools.partial (self._copy, ja3), text = "Copy", **self.COMMON,
101 | relief = tkinter.SOLID, borderwidth = 0)
102 | new_copy_btn.grid(row = self.current_output_frame_row, column = 3)
103 |
104 | self.data_widgets.append(new_time)
105 | self.data_widgets.append(new_user_agent)
106 | self.data_widgets.append(new_hash)
107 | self.data_widgets.append(new_copy_btn)
108 |
109 | self.current_output_frame_row += 1
110 |
111 | if __name__ == "__main__":
112 | JA3FetcherGUI ().run ()
113 |
--------------------------------------------------------------------------------
/gui/imitate.py:
--------------------------------------------------------------------------------
1 | import tkinter
2 | import typing
3 |
4 | from requests_ja3.decoder import JA3
5 | from requests_ja3.imitate.imitate import generate_imitation_libssl
6 | from requests_ja3.imitate.test import ja3_from_any_ssl
7 | import ssl as system_ssl
8 |
9 | class JA3ImitatorGUI:
10 | def __init__(self):
11 | self.BG = "#000000"
12 | self.FG = "#00FF00"
13 | self.COMMON = {"font": "TkFixedFont", "bg": self.BG, "fg": self.FG}
14 |
15 | self.root = tkinter.Tk()
16 | self.root.wm_title("JA3 Imitator")
17 |
18 | self.container = tkinter.Frame(self.root, bg = self.BG)
19 | self.container.grid()
20 |
21 | self.target_fingerprint_label = tkinter.Label(self.container, text = "Target JA3: ", **self.COMMON)
22 | self.target_fingerprint_label.grid(row = 1, column = 0)
23 | self.target_fingerprint_var = tkinter.StringVar(self.container)
24 | self.target_fingerprint_field = tkinter.Entry(self.container, textvariable = self.target_fingerprint_var, **self.COMMON)
25 | self.target_fingerprint_field.grid(row = 1, column = 1)
26 | self.regenerate_button = tkinter.Button(self.container, text = "Regenerate fakessl", command = self._regenerate_fakessl, **self.COMMON)
27 | self.regenerate_button.grid(row = 2, columnspan = 2)
28 | self.reset_button = tkinter.Button(self.container, text = "Reset to System", command = self._reset, **self.COMMON)
29 | self.reset_button.grid(row = 3, columnspan = 2)
30 |
31 | self.ssl_type: str = "System SSL"
32 | self.current_ja3: typing.Optional[JA3] = None
33 | self.fakessl_in_use_var = tkinter.StringVar(self.root)
34 | self._update_fakessl_in_use_var ()
35 |
36 | self.fakessl_in_use_label = tkinter.Label(self.container, textvariable = self.fakessl_in_use_var, **self.COMMON)
37 | self.fakessl_in_use_label.grid(row = 0, columnspan = 2)
38 | self.test_button = tkinter.Button(self.container, text = "Test Current SSL", command = self._test_fakessl, **self.COMMON)
39 | self.test_button.grid(row = 4, columnspan = 2)
40 | self.test_result_var = tkinter.StringVar(self.container)
41 | self.test_result_var.set("JA3 from Test: (n/a)")
42 | self.test_result = tkinter.Label(self.container, textvariable = self.test_result_var, **self.COMMON)
43 | self.test_result.grid(row = 5, columnspan = 2)
44 |
45 | self.fakessl = system_ssl
46 | def _update_fakessl_in_use_var(self):
47 | new_value = f"Current SSL: {self.ssl_type}"
48 | if self.current_ja3 is not None:
49 | new_value += f" --> {self.current_ja3.to_hash()}"
50 | self.fakessl_in_use_var.set(new_value)
51 | def run(self):
52 | self.root.mainloop()
53 | def _regenerate_fakessl (self):
54 | self.test_result_var.set ("JA3 from Test: (n/a)")
55 | target_fingerprint_str = self.target_fingerprint_var.get()
56 | self.current_ja3 = JA3.from_string(target_fingerprint_str)
57 | self.fakessl = generate_imitation_libssl(self.current_ja3, True)
58 | self.ssl_type = "fakessl"
59 | self._update_fakessl_in_use_var()
60 | def _reset(self):
61 | self.test_result_var.set ("JA3 from Test: (n/a)")
62 | self.fakessl = system_ssl
63 | self.current_ja3 = None
64 | self.ssl_type = "System SSL"
65 | self._update_fakessl_in_use_var()
66 | def _test_fakessl (self):
67 | received_ja3, received_user_agent = ja3_from_any_ssl(self.fakessl, start_server = False)
68 | result_text = f"JA3 from Test: {received_ja3.to_hash()}"
69 | if self.ssl_type == "fakessl":
70 | try:
71 | received_ja3.print_comparison_with(self.current_ja3)
72 | result_text += " (MATCHES)"
73 | except AssertionError as e:
74 | print(e)
75 | result_text += " (DOESN'T MATCH)"
76 | self.test_result_var.set(result_text)
77 |
78 | if __name__ == "__main__":
79 | JA3ImitatorGUI().run()
80 |
--------------------------------------------------------------------------------
/requests_ja3/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/an0ndev/requests-ja3/606fe61aea9e2c68e8c36b3e1654d0d69707a8be/requests_ja3/__init__.py
--------------------------------------------------------------------------------
/requests_ja3/decoder.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Union, List
2 | import hashlib
3 | import copy
4 | import dataclasses
5 |
6 | Value = int
7 | default_field_delimiter = ','
8 | default_value_delimiter = '-'
9 |
10 | @dataclasses.dataclass
11 | class JA3:
12 | ssl_version: Value
13 | accepted_ciphers: list [Value]
14 | list_of_extensions: list [Value]
15 | elliptic_curve: Optional [list [Value]]
16 | elliptic_curve_point_format: Optional [list [Value]]
17 | @staticmethod
18 | def from_string (source_string: str, field_delimiter = default_field_delimiter, value_delimiter = default_value_delimiter) -> "JA3":
19 | def parse_list (_list: str) -> list [Value]: return list (map (int, _list.split (value_delimiter)))
20 | raw_fields = source_string.split (field_delimiter)
21 | raw_ssl_version, raw_accepted_ciphers, raw_list_of_extensions, raw_elliptic_curve, raw_elliptic_curve_point_format = tuple (raw_fields)
22 |
23 | list_of_extensions = parse_list (raw_list_of_extensions)
24 |
25 | return JA3 (
26 | ssl_version = int (raw_ssl_version),
27 | accepted_ciphers = parse_list (raw_accepted_ciphers),
28 | list_of_extensions = list_of_extensions,
29 | elliptic_curve = parse_list (raw_elliptic_curve) if 10 in list_of_extensions else None,
30 | elliptic_curve_point_format = parse_list (raw_elliptic_curve_point_format) if 11 in list_of_extensions else None
31 | )
32 | def to_string (self, field_delimiter = default_field_delimiter, value_delimiter = default_value_delimiter) -> str:
33 | def stringify_list (_list: list [Value]) -> str: return value_delimiter.join (map (str, _list))
34 | return field_delimiter.join ((
35 | str (self.ssl_version),
36 | stringify_list (self.accepted_ciphers),
37 | stringify_list (self.list_of_extensions),
38 | stringify_list (self.elliptic_curve) if self.elliptic_curve is not None else "",
39 | stringify_list (self.elliptic_curve_point_format) if self.elliptic_curve_point_format is not None else ""
40 | ))
41 | def to_hash (self) -> str:
42 | return hashlib.md5 (self.to_string ().encode ()).hexdigest ()
43 | def compare_to (self: "JA3", reference: "JA3", lenient: bool) -> bool:
44 | try:
45 | assert self == reference
46 | return True
47 | except AssertionError:
48 | if not lenient: return False
49 | self_lenient = copy.copy (self)
50 | self_lenient.elliptic_curve = None
51 | self_lenient.elliptic_curve_point_format = None
52 | reference_lenient = copy.copy (reference)
53 | reference_lenient.elliptic_curve = None
54 | reference_lenient.elliptic_curve_point_format = None
55 | assert self_lenient == reference_lenient
56 | return True
57 | def print_comparison_with (self: "JA3", reference: "JA3") -> bool:
58 | for _field_name in (
59 | "ssl_version", "accepted_ciphers", "list_of_extensions", "elliptic_curve", "elliptic_curve_point_format"
60 | ):
61 | if getattr (self, _field_name) == getattr (reference, _field_name):
62 | print (f"{_field_name} matches")
63 | else:
64 | print (f"{_field_name} doesn't match ({getattr (self, _field_name)} vs {getattr (reference, _field_name)})")
65 | if _field_name in ("list_of_extensions", "elliptic_curve, elliptic_curve_point_format"):
66 | self_list = getattr (self, _field_name)
67 | ref_list = getattr (reference, _field_name)
68 | missing = set (ref_list) - set (self_list)
69 | if len (missing) > 0: print (f"--> missing: {missing}")
70 | extra = set (self_list) - set (ref_list)
71 | if len (extra) > 0: print (f"--> extra: {extra}")
72 | if self.compare_to (reference, lenient = False):
73 | print ("MATCHES")
74 | return True
75 | elif self.compare_to (reference, lenient = True):
76 | print ("MATCHES (lenient)")
77 | return True
78 | else:
79 | print ("DOESN'T MATCH")
80 | return False
81 | def __repr__ (self) -> str:
82 | return f""
83 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/.gitignore:
--------------------------------------------------------------------------------
1 | openssl
2 | openssl/*
--------------------------------------------------------------------------------
/requests_ja3/imitate/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/an0ndev/requests-ja3/606fe61aea9e2c68e8c36b3e1654d0d69707a8be/requests_ja3/imitate/__init__.py
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl/.idea/fakessl.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl/SSLContext.cpp:
--------------------------------------------------------------------------------
1 | #include "SSLContext.hpp"
2 | #include "SSLSocket.hpp"
3 |
4 | SSLContext::SSLContext () {
5 | context = SSL_CTX_new (TLS_client_method ());
6 | if (context == NULL) throw std::runtime_error ("failed to create SSL context");
7 | };
8 |
9 | SSLSocket SSLContext::wrap_socket (py::object sock) {
10 | return SSLSocket (sock, *this);
11 | };
12 |
13 | SSLContext::~SSLContext () {
14 | SSL_CTX_free (context);
15 | };
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl/SSLContext.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "SSLSocket.hpp"
4 |
5 | #include
6 | namespace py = pybind11;
7 |
8 | #include
9 | #include
10 |
11 | class SSLContext {
12 | SSL_CTX* context;
13 |
14 | friend SSLSocket;
15 | public:
16 | SSLContext ();
17 | SSLSocket wrap_socket (py::object sock);
18 | ~SSLContext ();
19 | };
20 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl/SSLSocket.cpp:
--------------------------------------------------------------------------------
1 | #include "SSLSocket.hpp"
2 | #include "SSLContext.hpp"
3 | #include "test_no_ssl.hpp"
4 |
5 | SSLSocket::SSLSocket (py::object sock_, SSLContext & context_) : sock (sock_), context (context_) {
6 | if (!py::isinstance (sock, py::module_::import ("socket").attr ("socket"))) {
7 | throw std::invalid_argument ("A socket or socket-like object is required");
8 | }
9 |
10 | fd = sock.attr ("fileno") ().cast ();
11 |
12 | ssl = SSL_new (context.context);
13 | if (ssl == NULL) throw std::runtime_error ("failed to create SSL object");
14 |
15 | printf ("socket file descriptor: %d\n", fd);
16 | if (SSL_set_fd (ssl, fd) < 1) {
17 | SSL_free (ssl);
18 | throw std::runtime_error ("failed to set SSL file descriptor");
19 | }
20 | };
21 |
22 | void SSLSocket::connect (py::tuple address) {
23 | printf ("C++ calling connect\n");
24 | sock.attr ("connect") (address);
25 | printf ("C++ called connect\n");
26 | printf ("calling test_post_connect\n");
27 | test_post_connect (fd, context.context, ssl);
28 | printf ("called test_post_connect\n");
29 | if (ssl == nullptr) throw std::runtime_error ("SSL is nullptr?");
30 | printf ("ssl: %lu\n", (unsigned long) ssl);
31 | if (SSL_connect (ssl) < 1) {
32 | printf ("SSL_connect failed\n");
33 | throw std::runtime_error ("SSL_connect failed");
34 | }
35 | printf ("done with connect\n");
36 | };
37 |
38 | SSLSocket::~SSLSocket () {
39 | SSL_free (ssl);
40 | };
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl/SSLSocket.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | namespace py = pybind11;
5 |
6 | #include
7 | #include
8 |
9 | class SSLContext;
10 |
11 | class SSLSocket {
12 | py::object sock;
13 | SSLContext & context;
14 | int fd;
15 | SSL* ssl;
16 | public:
17 | SSLSocket (py::object sock_, SSLContext & context_);
18 | void connect (py::tuple address);
19 | ~SSLSocket ();
20 | };
21 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl/fakessl.cpp:
--------------------------------------------------------------------------------
1 | #include "test_no_ssl.hpp"
2 | #include "SSLSocket.hpp"
3 | #include "SSLContext.hpp"
4 |
5 | namespace py = pybind11;
6 |
7 | PYBIND11_MODULE (fakessl, module) {
8 | module.def ("test_no_ssl", &test_no_ssl);
9 | module.def ("test_ssl", &test_ssl);
10 | py::class_ (module, "SSLSocket")
11 | .def ("connect", &SSLSocket::connect);
12 | py::class_ (module, "SSLContext")
13 | .def (py::init <> ())
14 | .def ("wrap_socket", &SSLContext::wrap_socket);
15 | }
16 |
17 | /*
18 | <%
19 | cfg ["parallel"] = True
20 | cfg ["sources"] = ["test_no_ssl.cpp", "SSLSocket.cpp", "SSLContext.cpp"]
21 | setup_pybind11 (cfg)
22 | %>
23 | */
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl/test_no_ssl.cpp:
--------------------------------------------------------------------------------
1 | #include "test_no_ssl.hpp"
2 |
3 | #include
4 |
5 | #include
6 | #include
7 | #include
8 |
9 | sockaddr_in get_google_com_addr () {
10 | char domain_name [] = "google.com";
11 |
12 | addrinfo hints = {
13 | .ai_flags = 0,
14 | .ai_family = AF_INET,
15 | .ai_socktype = SOCK_STREAM,
16 | .ai_protocol = 0
17 | };
18 | addrinfo* addressInfo;
19 | int ret = getaddrinfo (domain_name, NULL, &hints, &addressInfo);
20 | if (ret < 0) {
21 | throw std::runtime_error ("failed to lookup domain name");
22 | }
23 |
24 | sockaddr_in address = *((sockaddr_in*) addressInfo->ai_addr);
25 | freeaddrinfo (addressInfo);
26 |
27 | return address;
28 | };
29 |
30 | void write_http_request (int connection) {
31 | #define CRLF "\r\n"
32 | static char http_request [] = "GET / HTTP/1.1" CRLF "Host: google.com" CRLF "Connection: close" CRLF CRLF;
33 |
34 | int writeReturnValue = write (connection, http_request, (sizeof http_request) - 1);
35 | if (writeReturnValue < (int) ((sizeof http_request) - 1)) {
36 | if (writeReturnValue < 1) {
37 | throw std::runtime_error ("couldn't write to connection");
38 | } else {
39 | throw std::runtime_error ("couldn't write entire payload to connection");
40 | }
41 | }
42 | };
43 |
44 | void read_http_response (int connection) {
45 | const unsigned int bufferSize = 256;
46 | char buffer [bufferSize];
47 |
48 | while (true) {
49 | int readReturnValue = read (connection, buffer, bufferSize);
50 | if (readReturnValue > 0) {
51 | fwrite (buffer, readReturnValue, 1, stdout);
52 | } else {
53 | if (readReturnValue == 0) {
54 | break;
55 | } else {
56 | throw std::runtime_error ("couldn't read from connection");
57 | }
58 | }
59 | }
60 | };
61 |
62 | void test_no_ssl (py::object sock) {
63 | printf ("aye\n");
64 |
65 | if (!py::isinstance (sock, py::module_::import ("socket").attr ("socket"))) {
66 | throw std::invalid_argument ("A socket or socket-like object is required");
67 | }
68 |
69 | int rawSock = sock.attr ("fileno") ().cast ();
70 | printf ("raw socket file descriptor: %d\n", rawSock);
71 |
72 | sockaddr_in google_com_addr = get_google_com_addr ();
73 | google_com_addr.sin_port = htons (80);
74 |
75 | if (connect (rawSock, (sockaddr*) &google_com_addr, sizeof (google_com_addr)) < 0) {
76 | throw std::runtime_error ("couldn't connect");
77 | }
78 |
79 | write_http_request (rawSock);
80 |
81 | read_http_response (rawSock);
82 |
83 | shutdown (rawSock, SHUT_RDWR);
84 | close (rawSock);
85 | };
86 |
87 | int test_ssl (py::object sock) {
88 | int clientSocket = sock.attr ("fileno") ().cast ();
89 |
90 | char domain_name [] = "google.com";
91 |
92 | struct addrinfo hints = {
93 | .ai_flags = 0,
94 | .ai_family = AF_INET,
95 | .ai_socktype = SOCK_STREAM,
96 | .ai_protocol = 0,
97 | };
98 | struct addrinfo* addressInfo;
99 | printf ("looking up domain name\n");
100 | int ret = getaddrinfo (domain_name, NULL, &hints, &addressInfo);
101 | if (ret < 0) {
102 | perror ("failed to lookup domain name");
103 | return EXIT_FAILURE;
104 | }
105 |
106 | struct sockaddr_in address = *((struct sockaddr_in*) addressInfo->ai_addr);
107 | freeaddrinfo (addressInfo);
108 |
109 | char inet_ntop_result [INET_ADDRSTRLEN];
110 | if (inet_ntop (AF_INET, &(address.sin_addr), (char*) &inet_ntop_result, INET_ADDRSTRLEN) == NULL) {
111 | perror ("failed to convert address to text form");
112 | return EXIT_FAILURE;
113 | }
114 | printf ("address for domain name %s: %s\n", domain_name, inet_ntop_result);
115 |
116 | address.sin_port = htons (443);
117 | if (connect (clientSocket, (struct sockaddr*) &address, sizeof (address)) < 0) {
118 | perror ("couldn't connect");
119 | return EXIT_FAILURE;
120 | }
121 |
122 | SSL_CTX* context = SSL_CTX_new (TLS_client_method ());
123 | #define ssl_perror(inner) printf (inner ": %s\n", ERR_error_string (ERR_get_error (), NULL))
124 | if (context == NULL) {
125 | ssl_perror ("couldn't create context");
126 | return EXIT_FAILURE;
127 | }
128 |
129 | SSL* connection = SSL_new (context);
130 | if (connection == NULL) {
131 | ssl_perror ("couldn't create connection");
132 | SSL_CTX_free (context);
133 | return EXIT_FAILURE;
134 | }
135 |
136 | if (SSL_set_fd (connection, clientSocket) < 1) {
137 | ssl_perror ("couldn't set file descriptor");
138 | SSL_free (connection);
139 | SSL_CTX_free (context);
140 | return EXIT_FAILURE;
141 | }
142 |
143 | if (SSL_connect (connection) < 1) {
144 | ssl_perror ("couldn't prepare connection");
145 | SSL_free (connection);
146 | SSL_CTX_free (context);
147 | return EXIT_FAILURE;
148 | }
149 |
150 | #define CRLF "\r\n"
151 | char http_request [] = "GET / HTTP/1.1" CRLF "Host: google.com" CRLF "Connection: close" CRLF CRLF;
152 |
153 | int writeReturnValue = SSL_write (connection, http_request, (sizeof http_request) - 1);
154 | if (writeReturnValue < (int) ((sizeof http_request) - 1)) {
155 | if (writeReturnValue < 1) {
156 | ssl_perror ("couldn't write to connection");
157 | } else {
158 | ssl_perror ("couldn't write entire payload to connection");
159 | }
160 | SSL_free (connection);
161 | SSL_CTX_free (context);
162 | return EXIT_FAILURE;
163 | }
164 |
165 | const unsigned int bufferSize = 256;
166 | char buffer [bufferSize];
167 |
168 | while (1) {
169 | int readReturnValue = SSL_read (connection, buffer, bufferSize);
170 | if (readReturnValue > 0) {
171 | fwrite (buffer, readReturnValue, 1, stdout);
172 | } else {
173 | if (readReturnValue == 0) {
174 | break;
175 | } else {
176 | ssl_perror ("couldn't read from connection");
177 | SSL_free (connection);
178 | SSL_CTX_free (context);
179 | return EXIT_FAILURE;
180 | }
181 | }
182 | }
183 |
184 | if (SSL_shutdown (connection) < 0) {
185 | ssl_perror ("couldn't shut down connection");
186 | SSL_free (connection);
187 | SSL_CTX_free (context);
188 | return EXIT_FAILURE;
189 | }
190 |
191 | SSL_free (connection);
192 | SSL_CTX_free (context);
193 |
194 | shutdown (clientSocket, SHUT_RDWR);
195 | close (clientSocket);
196 |
197 | return EXIT_SUCCESS;
198 | }
199 |
200 | int test_post_connect (int fd, SSL_CTX* context, SSL* connection) {
201 | if (SSL_connect (connection) < 1) {
202 | ssl_perror ("couldn't prepare connection");
203 | SSL_free (connection);
204 | SSL_CTX_free (context);
205 | return EXIT_FAILURE;
206 | }
207 |
208 | return EXIT_SUCCESS;
209 | }
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl/test_no_ssl.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 | namespace py = pybind11;
7 |
8 | void test_no_ssl (py::object sock);
9 |
10 | int test_ssl (py::object sock);
11 |
12 | int test_post_connect (int fd, SSL_CTX* context, SSL* connection);
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl_py/Options.py:
--------------------------------------------------------------------------------
1 | class Options (int):
2 | OP_NO_COMPRESSION = 1 << 0
3 | OP_NO_TICKET = 1 << 1
4 | OP_NO_SSLv2 = 1 << 2
5 | OP_NO_SSLv3 = 1 << 3
6 | OP_NO_ENCRYPT_THEN_MAC = 0x80000
7 | OP_SAFARI_ECDHE_ECDSA_BUG = 0x40
8 | OP_DONT_INSERT_EMPTY_FRAGMENTS = 0x800
9 | OP_TLSEXT_PADDING = 0x10
10 | OP_LEGACY_SERVER_CONNECT = 0x40
11 | OP_ALL = OP_SAFARI_ECDHE_ECDSA_BUG | OP_DONT_INSERT_EMPTY_FRAGMENTS | OP_TLSEXT_PADDING | OP_LEGACY_SERVER_CONNECT
12 |
13 | class VerifyMode (int):
14 | CERT_NONE = 0
15 | CERT_OPTIONAL = 1
16 | CERT_REQUIRED = 2
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl_py/SSLContext.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | import typing
3 | import pathlib
4 |
5 | from . import libssl_type_bindings as types
6 |
7 | from requests_ja3.decoder import JA3
8 |
9 | libssl_handle: ctypes.CDLL = None
10 | target_ja3: JA3 = None
11 |
12 | def initialize (_libssl_handle, _target_ja3):
13 | global libssl_handle, target_ja3
14 | libssl_handle = _libssl_handle
15 | SSLSocket_module.initialize (libssl_handle, _target_ja3)
16 | target_ja3 = _target_ja3
17 |
18 | from . import SSLSocket as SSLSocket_module
19 | SSLSocket = SSLSocket_module.SSLSocket
20 | socket_exceptions = SSLSocket_module.socket_exceptions
21 |
22 | from . import protocol_constants
23 |
24 | from .Options import Options, VerifyMode
25 |
26 | def stub (name): return lambda *args, **kwargs: print (f"{name} called with args {args} and kwargs {kwargs}")
27 |
28 | class SSLContext:
29 | def __init__ (self, protocol = None):
30 | if protocol is None: raise Exception ("protocol must be PROTOCOL_TLS_CLIENT or PROTOCOL_TLS_SERVER")
31 | if protocol not in (protocol_constants.PROTOCOL_TLS_CLIENT, protocol_constants.PROTOCOL_TLS_SERVER): raise Exception ("alternate protocols not supported")
32 | self._is_client = protocol == protocol_constants.PROTOCOL_TLS_CLIENT
33 |
34 | method: types.SSL_METHOD_ptr = libssl_handle.TLS_client_method () if self._is_client else libssl_handle.TLS_server_method ()
35 | self.context: types.SSL_CTX_ptr = libssl_handle.SSL_CTX_new (method)
36 | if not self.context: raise Exception ("failed to create ssl context")
37 | self.options: Options = Options.OP_ALL
38 |
39 | if self._is_client:
40 | self.check_hostname = True
41 | self.verify_mode = VerifyMode.CERT_REQUIRED
42 | else:
43 | self.check_hostname = False
44 | self.verify_mode = VerifyMode.CERT_NONE
45 |
46 | if self.verify_mode == VerifyMode.CERT_REQUIRED:
47 | libssl_handle.SSL_CTX_set_verify (self.context, types.SSL_VERIFY_PEER, None)
48 |
49 | if self._is_client:
50 | self._do_client_setup ()
51 |
52 | self._keylog_filename = None
53 | self._keylog_callback = None
54 | def _do_client_setup (self):
55 | if 5 in target_ja3.list_of_extensions:
56 | set_tlsext_status_type_ret = libssl_handle.SSL_CTX_set_tlsext_status_type (self.context, types.TLSEXT_STATUSTYPE_ocsp)
57 | if set_tlsext_status_type_ret == 0: raise Exception ("failed to enable extension #5")
58 |
59 | if 16 in target_ja3.list_of_extensions:
60 | proto_name = "http/1.1".encode ()
61 | proto_name_bytes = bytes ([len (proto_name)]) + proto_name
62 | set_alpn_protos_ret = libssl_handle.SSL_CTX_set_alpn_protos (self.context, proto_name_bytes, len (proto_name_bytes))
63 | if set_alpn_protos_ret != 0: raise Exception ("failed to enable extension #16 (ALPN)")
64 |
65 | if 18 in target_ja3.list_of_extensions:
66 | enable_ct_ret = libssl_handle.SSL_CTX_enable_ct (self.context, types.SSL_CT_VALIDATION_PERMISSIVE)
67 | if enable_ct_ret != 1: raise Exception ("failed to enable extension #18 (certificate transparency)")
68 |
69 | if 21 not in target_ja3.list_of_extensions:
70 | self.options ^= Options.OP_TLSEXT_PADDING
71 |
72 | if 22 not in target_ja3.list_of_extensions:
73 | self.options |= Options.OP_NO_ENCRYPT_THEN_MAC
74 |
75 | libssl_handle.SSL_CTX_set_options (self.context, self.options)
76 | def set_ciphers (self, cipher_list: str):
77 | set_cipher_list_ret = libssl_handle.SSL_CTX_set_cipher_list (self.context, cipher_list.encode ())
78 | if set_cipher_list_ret == 0: raise Exception ("failed to set cipher list on ssl context")
79 | def load_verify_locations (self, cafile: typing.Optional [str] = None, capath: typing.Optional [str] = None, cadata: typing.Optional [typing.Union [str, bytes]] = None):
80 | if cafile is not None or capath is not None:
81 | load_verify_locations_ret = libssl_handle.SSL_CTX_load_verify_locations (self.context, cafile.encode () if cafile is not None else None, capath.encode () if capath is not None else None)
82 | if load_verify_locations_ret < 1: raise Exception ("failed to load verify locations")
83 | if cadata is not None:
84 | if isinstance (cadata, str): cadata = cadata.encode ()
85 | use_certificate_chain_file_ret = libssl_handle.SSL_CTX_use_certificate_chain_file (self.context, cadata)
86 | if use_certificate_chain_file_ret < 1: raise Exception ("failed to use certificate file")
87 | def load_default_certs (self, purpose: protocol_constants.Purpose):
88 | self.set_default_verify_paths ()
89 | def load_cert_chain (self, certfile: str, keyfile: str = None, password: typing.Optional [typing.Union [typing.Callable [[], typing.Union [str, bytes, bytearray]], str, bytes, bytearray]] = None):
90 | if password is not None: raise Exception ("encrypted cert chain not implemented")
91 |
92 |
93 | use_certificate_chain_file_ret = libssl_handle.SSL_CTX_use_certificate_chain_file (self.context, certfile.encode ())
94 | if use_certificate_chain_file_ret < 1: raise Exception ("failed to use certificate file")
95 |
96 | if keyfile is not None:
97 | use_private_key_file_ret = libssl_handle.SSL_CTX_use_PrivateKey_file (self.context, keyfile.encode (), types.X509_FILETYPE_PEM)
98 | if use_private_key_file_ret < 1: raise Exception ("failed to use private key")
99 | def set_default_verify_paths (self):
100 | self.load_verify_locations (cafile = "/etc/ssl/certs/ca-certificates.crt", capath = "/etc/ssl/certs")
101 | def wrap_socket (self, socket, server_side = False, do_handshake_on_connect = True, server_hostname: typing.Optional [str] = None, session = None):
102 | return SSLSocket (socket, self, server_side = server_side, do_handshake_on_connect = do_handshake_on_connect, server_hostname = server_hostname, session = session)
103 | def get_keylog_filename (self) -> typing.Optional [str]: return self._keylog_filename
104 | def set_keylog_filename (self, keylog_filename: typing.Optional [str]):
105 | should_enable = self._keylog_filename is None
106 | self._keylog_filename = keylog_filename
107 | if self._keylog_filename is None:
108 | del self._keylog_callback
109 | libssl_handle.SSL_CTX_set_keylog_callback (self.context, None)
110 | return
111 | if should_enable:
112 | self._keylog_callback = _gen_keylog_callback (self)
113 | libssl_handle.SSL_CTX_set_keylog_callback (self.context, self._keylog_callback)
114 | keylog_filename = property (get_keylog_filename, set_keylog_filename)
115 | def __del__ (self):
116 | libssl_handle.SSL_CTX_free (self.context)
117 |
118 | def _gen_keylog_callback (self):
119 | @types.SSL_CTX_keylog_cb_func
120 | def _keylog_callback (ssl, line):
121 | with open (self._keylog_filename, "a+") as keylog_file:
122 | keylog_file.write (line.decode () + "\n")
123 | return _keylog_callback
124 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl_py/SSLSocket.py:
--------------------------------------------------------------------------------
1 | import typing
2 | import socket
3 |
4 | import ctypes
5 |
6 | from requests_ja3.decoder import JA3
7 |
8 | libssl_handle: ctypes.CDLL = None
9 | target_ja3: JA3 = None
10 |
11 | def initialize (_libssl_handle, _target_ja3: JA3):
12 | global libssl_handle, target_ja3
13 | libssl_handle = _libssl_handle
14 | target_ja3 = _target_ja3
15 |
16 | from . import libssl_type_bindings as types
17 |
18 | import socket as _socket_module
19 |
20 | import ssl as clean_ssl
21 |
22 | from .Options import VerifyMode
23 |
24 | class LibSSLError (Exception):
25 | def __init__ (self, thread_id: int, info: str, file_name: str, file_line: str, extra_data: str):
26 | super ().__init__ (f"Thread ID: {thread_id}, info: {info}, file name: {file_name}, file line: {file_line}, extra data: {extra_data}")
27 | self.thread_id = thread_id
28 | self.info = info
29 | self.file_name = file_name
30 | self.file_line = file_line
31 | self.extra_data = extra_data
32 |
33 | class SpecificLibSSLError (Exception):
34 | def __init__ (self, text, libssl_error):
35 | super ().__init__ (f"{text}: {libssl_error}")
36 | self.text = text
37 | self.libssl_error = libssl_error
38 | class socket_exceptions:
39 | class FailedAccept (SpecificLibSSLError): pass
40 |
41 | def _get_libssl_errors () -> list [LibSSLError]:
42 | errors: list [LibSSLError] = []
43 | @types.ERR_print_errors_cb_callback
44 | def callback (_str: ctypes.c_char_p, _len: int, user_data: ctypes.c_void_p) -> int:
45 | error_details = ctypes.string_at (_str, _len).decode () [:-len ("\n")].split (":")
46 | libssl_error = LibSSLError (int (error_details [0]), ':'.join (error_details [1:-3]), error_details [-3], error_details [-2], error_details [-1])
47 | errors.append (libssl_error)
48 | return 1 # continue outputting the error report
49 | libssl_handle.ERR_print_errors_cb (callback, 0)
50 | return errors
51 |
52 | def _get_libssl_error_str () -> str:
53 | return ", ".join (map (str, _get_libssl_errors ()))
54 |
55 | class SSLSocket:
56 | def __init__ (self, socket: _socket_module.socket, context, server_side = False, do_handshake_on_connect = True, server_hostname: typing.Optional [str] = None, session = None, _client_from_server: bool = False):
57 | self.socket = socket
58 | self.context = context
59 | self.fd = ctypes.c_int (socket.fileno ())
60 |
61 | self.ssl = libssl_handle.SSL_new (self.context.context)
62 | if not self.ssl: raise Exception ("failed to create ssl object")
63 |
64 | self.server_side = server_side
65 | self.do_handshake_on_connect = do_handshake_on_connect
66 | self.handshake_complete = False
67 | self._client_from_server = _client_from_server
68 |
69 | self.server_hostname = server_hostname
70 |
71 | if session is not None: raise NotImplementedError ("SSLSession not yet supported")
72 |
73 | if not self.server_side and not self._client_from_server: self._do_client_setup ()
74 | def _do_client_setup (self):
75 | supported_ciphers: types.STACK_OF_SSL_CIPHER_ptr = libssl_handle.SSL_get_ciphers (self.ssl)
76 |
77 | # Make sure all requested ciphers are available
78 | # (the list of supported ciphers starts as the list of all available)
79 | supported_ciphers_ids = libssl_handle.cipher_ids_from_stack (supported_ciphers)
80 | for required_cipher in target_ja3.accepted_ciphers:
81 | if required_cipher == 255: continue # renegotiation info as cipher
82 | if required_cipher not in supported_ciphers_ids:
83 | raise Exception (f"Your build of OpenSSL does not support cipher {required_cipher}")
84 |
85 | ### START MODIFICATION OF ACCEPTED CIPHERS FIELD
86 |
87 | target_supported_ciphers_array = (ctypes.c_uint16 * len (target_ja3.accepted_ciphers)) (*target_ja3.accepted_ciphers)
88 | libssl_handle.FAKESSL_SSL_set_cipher_list (
89 | self.ssl,
90 | ctypes.cast (target_supported_ciphers_array, ctypes.POINTER (ctypes.c_uint16)),
91 | len (target_ja3.accepted_ciphers)
92 | )
93 |
94 | ### END MODIFICATION OF ACCEPTED CIPHERS FIELD
95 |
96 | ciphers_to_use: types.STACK_OF_SSL_CIPHER_ptr = libssl_handle.SSL_get1_supported_ciphers (self.ssl)
97 | try:
98 | ciphers_to_use_ids = libssl_handle.cipher_ids_from_stack (ciphers_to_use)
99 | if 255 in target_ja3.accepted_ciphers: ciphers_to_use_ids.append(255) # enabled by imitate.py with FAKESSL_RFC5746_AS_CIPHER define
100 | print (f"CIPHERS TO USE: {ciphers_to_use_ids}")
101 | assert target_ja3.accepted_ciphers == ciphers_to_use_ids
102 | finally:
103 | libssl_handle.OPENSSL_sk_free (ctypes.cast (ciphers_to_use, types.OPENSSL_STACK_ptr))
104 |
105 | if 10 in target_ja3.list_of_extensions: # supported_groups
106 | target_supported_groups_array = (ctypes.c_uint16 * len (target_ja3.elliptic_curve)) (*target_ja3.elliptic_curve)
107 | libssl_handle.FAKESSL_SSL_set_groups_list (
108 | self.ssl,
109 | ctypes.cast (target_supported_groups_array, ctypes.POINTER (ctypes.c_uint16)),
110 | len (target_ja3.elliptic_curve)
111 | )
112 | if 11 in target_ja3.list_of_extensions: # EC point formats
113 | target_point_formats_array = (ctypes.c_uint8 * len (target_ja3.elliptic_curve_point_format)) (*target_ja3.elliptic_curve_point_format)
114 | libssl_handle.FAKESSL_SSL_set_format_list (
115 | self.ssl,
116 | ctypes.cast (target_point_formats_array, ctypes.POINTER (ctypes.c_uint8)),
117 | len (target_ja3.elliptic_curve_point_format)
118 | )
119 |
120 | if 17513 in target_ja3.list_of_extensions: # Application Settings
121 | # see https://boringssl.googlesource.com/boringssl/+/refs/heads/master/include/openssl/tls1.h#247
122 | # also https://www.ietf.org/archive/id/draft-vvv-tls-alps-01.txt
123 | proto_name = "http/1.1".encode ()
124 | proto_name_bytes = bytes ([len (proto_name)]) + proto_name
125 | libssl_handle.FAKESSL_SSL_set_alps_protos (self.ssl, proto_name_bytes, len (proto_name_bytes))
126 |
127 | if 27 in target_ja3.list_of_extensions:
128 | libssl_handle.FAKESSL_SSL_set_compress_certificates (self.ssl, True)
129 |
130 | extensions_array = (ctypes.c_uint16 * len (target_ja3.list_of_extensions)) (*target_ja3.list_of_extensions)
131 | libssl_handle.FAKESSL_SSL_set_ext_order (
132 | self.ssl,
133 | ctypes.cast (extensions_array, ctypes.POINTER (ctypes.c_uint16)),
134 | len (target_ja3.list_of_extensions)
135 | )
136 | def connect (self, address: tuple):
137 | self.socket.connect (address)
138 |
139 | set_fd_ret = libssl_handle.SSL_set_fd (self.ssl, self.fd)
140 | if set_fd_ret == 0: raise Exception ("failed to set ssl file descriptor")
141 |
142 | if self.do_handshake_on_connect:
143 | self.do_handshake ()
144 | def do_handshake (self):
145 | if self.server_hostname is not None:
146 | assert not self.server_side
147 | set_host_name_ret = libssl_handle.SSL_set_tlsext_host_name (self.ssl, self.server_hostname)
148 | if set_host_name_ret != 1:
149 | raise Exception (f"failed to set TLS host name: {self._get_error (set_host_name_ret)}")
150 |
151 | if not self._client_from_server:
152 | connect_ret = libssl_handle.SSL_connect (self.ssl)
153 | if connect_ret < 1:
154 | raise Exception (f"failed to connect using ssl object: {self._get_error (connect_ret)}")
155 | else:
156 | accept_ret = libssl_handle.SSL_accept (self.ssl)
157 | if accept_ret < 1:
158 | raise socket_exceptions.FailedAccept ("failed to accept using ssl object", self._get_error (accept_ret))
159 |
160 | if self.context.verify_mode == VerifyMode.CERT_REQUIRED:
161 | get_verify_result = libssl_handle.SSL_get_verify_result (self.ssl)
162 | if get_verify_result != types.X509_V_OK:
163 | raise Exception ("failed to verify certificate")
164 |
165 | if self.context.check_hostname:
166 | certificate = self.getpeercert ()
167 | clean_ssl.match_hostname (certificate, self.server_hostname)
168 |
169 | self.handshake_complete = True
170 | def accept (self) -> ("SSLSocket", tuple [str, int]):
171 | client_socket, address = self.socket.accept ()
172 |
173 | wrapped_socket = SSLSocket (
174 | socket = client_socket,
175 | context = self.context,
176 | server_side = False,
177 | _client_from_server = True
178 | )
179 | set_fd_ret = libssl_handle.SSL_set_fd (wrapped_socket.ssl, wrapped_socket.fd)
180 | if set_fd_ret == 0: raise Exception ("failed to set ssl file descriptor")
181 |
182 | wrapped_socket.do_handshake ()
183 |
184 | return wrapped_socket, address
185 | def getpeercert (self, binary_form = False) -> typing.Optional [typing.Union [dict, bytes]]:
186 | certificate = libssl_handle.SSL_get_peer_certificate (self.ssl)
187 | if certificate.value is None: return None
188 |
189 | try:
190 | if binary_form:
191 | certificate_bytes_ptr = ctypes.POINTER (ctypes.c_ubyte) ()
192 | certificate_bytes_ptr.value = 0
193 | certificate_encode_ret = libssl_handle.i2d_X509 (certificate, ctypes.byref (certificate_bytes_ptr))
194 | if certificate_encode_ret < 0: raise Exception ("encoding x509 certificate failed")
195 | return ctypes.string_at (certificate_bytes_ptr, certificate_encode_ret)
196 | else:
197 | if libssl_handle.SSL_get_verify_result (self.ssl) != types.X509_V_OK:
198 | return {}
199 | else:
200 | # example: https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.getpeercert
201 |
202 | def decode_X509_NAME (name: types.X509_NAME_ptr):
203 | rdns = []
204 | for entry_index in range (libssl_handle.X509_NAME_entry_count (name)):
205 | entry = libssl_handle.X509_NAME_get_entry (name, entry_index)
206 |
207 | asn1_obj = libssl_handle.X509_NAME_ENTRY_get_object (entry)
208 | asn1_obj_nid = libssl_handle.OBJ_obj2nid (asn1_obj)
209 | asn1_key = ctypes.string_at (libssl_handle.OBJ_nid2ln (asn1_obj_nid)).decode ()
210 |
211 | asn1_str = libssl_handle.X509_NAME_ENTRY_get_data (entry)
212 | asn1_val = ctypes.string_at (libssl_handle.ASN1_STRING_data (asn1_str), libssl_handle.ASN1_STRING_length (asn1_str)).decode ()
213 |
214 | rdns.append (((asn1_key, asn1_val),))
215 | return tuple (rdns)
216 |
217 | decoded = {}
218 |
219 | decoded ["subject"] = decode_X509_NAME (libssl_handle.X509_get_subject_name (certificate))
220 | decoded ["issuer"] = decode_X509_NAME (libssl_handle.X509_get_issuer_name (certificate))
221 |
222 | decoded ["version"] = libssl_handle.X509_get_version (certificate) + 1
223 |
224 | def print_ASN1_TIME (time: types.ASN1_TIME_ptr):
225 | time_bio = libssl_handle.MemoryBIO ()
226 | try:
227 | if libssl_handle.ASN1_TIME_print (time_bio.bio, time) == 0:
228 | raise Exception (f"failed to print ASN1 time: {_get_libssl_error_str ()}")
229 |
230 | return time_bio.get_mem_data ().decode ()
231 | finally:
232 | del time_bio
233 |
234 | decoded ["notBefore"] = print_ASN1_TIME (libssl_handle.X509_get0_notBefore (certificate))
235 | decoded ["notAfter"] = print_ASN1_TIME (libssl_handle.X509_get0_notAfter (certificate))
236 |
237 | serial_number_bio = libssl_handle.MemoryBIO ()
238 | try:
239 | libssl_handle.i2a_ASN1_INTEGER (serial_number_bio.bio, libssl_handle.X509_get_serialNumber (certificate))
240 | decoded ["serialNumber"] = serial_number_bio.get_mem_data ().decode ()
241 | finally:
242 | del serial_number_bio
243 |
244 | return decoded
245 | finally:
246 | libssl_handle.X509_free (certificate)
247 | def write (self, data: bytes):
248 | write_ret = libssl_handle.SSL_write (self.ssl, data, len (data))
249 | if write_ret <= 0: raise Exception ("failed to write to ssl object")
250 | if write_ret < len (data): raise Exception (f"only wrote {write_ret}/{len (data)} bytes to ssl object")
251 | def read (self, count: int) -> bytes:
252 | out = (ctypes.c_ubyte * count) ()
253 | read_ret = libssl_handle.SSL_read (self.ssl, ctypes.cast (out, ctypes.c_void_p), count)
254 | if read_ret < 0: raise Exception ("failed to read from ssl object")
255 | return bytes (out) [:read_ret]
256 | def close (self):
257 | if not self.server_side:
258 | shutdown_ret = libssl_handle.SSL_shutdown (self.ssl)
259 | if shutdown_ret < 0: raise Exception ("failed to shutdown ssl object")
260 | try:
261 | self.socket.shutdown (socket.SHUT_RDWR)
262 | except OSError as os_error:
263 | if os_error.errno != 107: # Transport endpoint is not connected
264 | raise
265 | self.socket.close ()
266 | def get_ja3_str (self, remove_grease: bool) -> str:
267 | assert self._client_from_server
268 | assert self.handshake_complete
269 | raw_ja3_str = libssl_handle.FAKESSL_SSL_get_ja3 (self.ssl, remove_grease)
270 | try:
271 | ja3_str = ctypes.string_at (ctypes.cast (raw_ja3_str, ctypes.c_char_p)).decode ()
272 | finally:
273 | libssl_handle.OPENSSL_free (raw_ja3_str)
274 | return ja3_str
275 | def __del__ (self):
276 | libssl_handle.SSL_free (self.ssl)
277 | def _get_error (self, source_error_code: int) -> str:
278 | error_code = libssl_handle.SSL_get_error (self.ssl, source_error_code)
279 | if error_code in [types.SSL_ERROR_SSL, types.SSL_ERROR_SYSCALL]:
280 | return _get_libssl_error_str ()
281 | else:
282 | return types.ssl_error_to_str (error_code)
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl_py/__init__.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 | import tempfile
3 | import os
4 |
5 | from . import libssl_binder
6 | from . import SSLContext as SSLContext_module
7 |
8 | from .shims_and_mixins import shim_module
9 | from .libssl_macros_mixin import MacrosMixin
10 | from .libssl_utils_mixin import UtilsMixin
11 |
12 | from requests_ja3.decoder import JA3
13 |
14 | libssl_handle = None
15 | SSLContext = None
16 | _openssl_temp_dir = None
17 | target_ja3 = None
18 | socket_exceptions = None
19 |
20 | def initialize (libssl_path: pathlib.Path, openssl_temp_dir: tempfile.TemporaryDirectory, _target_ja3: JA3):
21 | global libssl_handle, SSLContext, socket_exceptions, _openssl_temp_dir, target_ja3
22 |
23 | unshimmed_libssl_handle, binder_time_mixin_methods = libssl_binder.get_bound_libssl (libssl_path)
24 | libssl_handle = shim_module (unshimmed_libssl_handle)
25 | libssl_handle.shim_apply_mixin (MacrosMixin)
26 | libssl_handle.shim_apply_mixin (UtilsMixin)
27 | libssl_handle.shim_apply_list_mixin (binder_time_mixin_methods)
28 |
29 | SSLContext_module.initialize (libssl_handle, _target_ja3)
30 | SSLContext = SSLContext_module.SSLContext
31 |
32 | socket_exceptions = SSLContext_module.socket_exceptions
33 |
34 | _openssl_temp_dir = openssl_temp_dir
35 |
36 | target_ja3 = _target_ja3
37 |
38 | from .Options import Options, VerifyMode
39 | for option_name in ["OP_NO_COMPRESSION", "OP_NO_TICKET", "OP_NO_SSLv2", "OP_NO_SSLv3", "OP_ALL"]:
40 | globals () [option_name] = getattr (Options, option_name)
41 | for verify_mode_name in ["CERT_NONE", "CERT_OPTIONAL", "CERT_REQUIRED"]:
42 | globals () [verify_mode_name] = getattr (VerifyMode, verify_mode_name)
43 |
44 | from .protocol_constants import *
45 |
46 | import ssl as clean_ssl
47 |
48 | for constant_name in [
49 | "HAS_NEVER_CHECK_COMMON_NAME",
50 | "HAS_SNI",
51 | "OPENSSL_VERSION",
52 | "OPENSSL_VERSION_NUMBER",
53 | "TLSVersion",
54 | ]:
55 | globals () [constant_name] = getattr (clean_ssl, constant_name)
56 |
57 | # noinspection PyUnresolvedReferences
58 | def create_default_context (purpose: Purpose = Purpose.SERVER_AUTH, cafile = None, capath = None, cadata = None) -> SSLContext:
59 | assert purpose in (Purpose.SERVER_AUTH, Purpose.CLIENT_AUTH)
60 | # noinspection PyCallingNonCallable
61 | context = SSLContext (PROTOCOL_TLS_CLIENT if purpose == Purpose.SERVER_AUTH else PROTOCOL_TLS_SERVER)
62 | if cafile is None and capath is None and cadata is None:
63 | context.load_default_certs (purpose = purpose)
64 | else:
65 | context.load_verify_locations (cafile, capath, cadata)
66 | if "SSLKEYLOGFILE" in os.environ:
67 | context.keylog_filename = os.environ ["SSLKEYLOGFILE"]
68 | return context
69 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl_py/libssl_binder.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | import typing
3 | import pathlib
4 |
5 | from . import libssl_type_bindings as types
6 |
7 | def get_bound_libssl (libssl_path: pathlib.Path) -> (ctypes.CDLL, list [typing.Callable]):
8 | libssl_handle = ctypes.cdll.LoadLibrary (str (libssl_path))
9 | binder_time_mixin_methods = []
10 |
11 | def _setup_method (method_name: str, argtypes: list, restype: typing.Any):
12 | method = getattr (libssl_handle, method_name)
13 | method.argtypes = argtypes
14 | method.restype = restype
15 | _s_m = _setup_method
16 |
17 | _s_m ("BIO_s_mem", [], types.BIO_method_ptr)
18 | _s_m ("BIO_new", [types.BIO_method_ptr], types.BIO_ptr)
19 | # _s_m ("BIO_get_mem_data", [types.BIO_ptr, ctypes.POINTER (ctypes.c_char_p)], ctypes.c_long)
20 | _s_m ("BIO_ctrl", [types.BIO_ptr, ctypes.c_int, ctypes.c_long, ctypes.c_void_p], ctypes.c_long)
21 | _s_m ("BIO_free", [types.BIO_ptr], None)
22 |
23 | _s_m ("TLS_client_method", [], types.SSL_METHOD_ptr)
24 | _s_m ("TLS_server_method", [], types.SSL_METHOD_ptr)
25 |
26 | _s_m ("SSL_CTX_new", [], types.SSL_CTX_ptr)
27 | _s_m ("SSL_CTX_set_cipher_list", [types.SSL_CTX_ptr, ctypes.c_char_p], ctypes.c_int)
28 | _s_m ("SSL_CTX_load_verify_locations", [types.SSL_CTX_ptr, ctypes.c_char_p, ctypes.c_char_p], ctypes.c_int)
29 | _s_m ("SSL_CTX_use_certificate_chain_file", [types.SSL_CTX_ptr, ctypes.c_char_p], ctypes.c_int)
30 | _s_m ("SSL_CTX_use_PrivateKey_file", [types.SSL_CTX_ptr, ctypes.c_char_p, ctypes.c_int], ctypes.c_int)
31 | _s_m ("SSL_CTX_ctrl", [types.SSL_CTX_ptr, ctypes.c_int, ctypes.c_long, ctypes.c_void_p], ctypes.c_long)
32 | _s_m ("SSL_CTX_set_alpn_protos", [types.SSL_CTX_ptr, ctypes.c_char_p, ctypes.c_uint], ctypes.c_int)
33 | _s_m ("SSL_CTX_enable_ct", [types.SSL_CTX_ptr, ctypes.c_int], ctypes.c_int)
34 | _s_m ("SSL_CTX_set_options", [types.SSL_CTX_ptr, ctypes.c_ulong], ctypes.c_ulong)
35 | _s_m ("SSL_CTX_free", [types.SSL_CTX_ptr], None)
36 |
37 | _s_m ("SSL_CIPHER_find", [types.SSL_ptr, ctypes.POINTER (ctypes.c_ubyte)], types.SSL_CIPHER_ptr)
38 | _s_m ("SSL_CIPHER_get_protocol_id", [types.SSL_CIPHER_ptr], ctypes.c_int32)
39 |
40 | def define_cast_mixin_method (target_name: str, method_name: str, dest_arg_types, dest_ret_type):
41 | def cast_mixin_method (_libssl_handle, *src_args):
42 | assert len (src_args) == len (dest_arg_types)
43 | dest_args = []
44 | for src_arg, dest_arg_type in zip (src_args, dest_arg_types):
45 | if type (src_arg) == dest_arg_type:
46 | dest_args.append (src_arg)
47 | else:
48 | dest_args.append (
49 | ctypes.cast (src_arg, dest_arg_type)
50 | )
51 | src_ret = getattr (_libssl_handle, target_name) (*dest_args)
52 | if type (src_ret) == dest_ret_type:
53 | return src_ret
54 | else:
55 | return ctypes.cast (src_ret, dest_ret_type)
56 | cast_mixin_method.__name__ = method_name
57 | binder_time_mixin_methods.append (cast_mixin_method)
58 | d_c_m_m = define_cast_mixin_method
59 |
60 | generic_stack_class = types.OPENSSL_STACK_ptr
61 | generic_inner_class = ctypes.c_void_p
62 | def define_stack_funcs (specific_stack_class: type (ctypes.c_void_p), specific_inner_class: type (ctypes.c_void_p), class_name: str):
63 | compare_function_class = getattr (types, f"sk_{class_name}_compfunc")
64 | copy_function_class = getattr (types, f"sk_{class_name}_copyfunc")
65 | free_function_class = getattr (types, f"sk_{class_name}_freefunc")
66 |
67 | d_c_m_m ("OPENSSL_sk_num", f"sk_{class_name}_num", [generic_stack_class], int)
68 | d_c_m_m ("OPENSSL_sk_value", f"sk_{class_name}_value", [generic_stack_class, int], specific_inner_class)
69 | d_c_m_m ("OPENSSL_sk_delete_ptr", f"sk_{class_name}_delete_ptr", [generic_stack_class, generic_inner_class], specific_inner_class)
70 | return
71 | _s_m (f"sk_{class_name}_new", [compare_function_class], generic_stack_class)
72 | _s_m (f"sk_{class_name}_new_null", [], generic_stack_class)
73 | _s_m (f"sk_{class_name}_reserve", [generic_stack_class, ctypes.c_int], ctypes.c_int)
74 | _s_m (f"sk_{class_name}_free", [generic_stack_class], None)
75 | _s_m (f"sk_{class_name}_zero", [generic_stack_class], None)
76 | _s_m (f"sk_{class_name}_delete", [generic_stack_class, ctypes.c_int], generic_inner_class)
77 | _s_m (f"sk_{class_name}_push", [generic_stack_class, generic_inner_class], ctypes.c_int)
78 | _s_m (f"sk_{class_name}_unshift", [generic_stack_class, generic_inner_class], ctypes.c_int)
79 | _s_m (f"sk_{class_name}_pop", [generic_stack_class], generic_inner_class)
80 | _s_m (f"sk_{class_name}_shift", [generic_stack_class], generic_inner_class)
81 | _s_m (f"sk_{class_name}_pop_free", [generic_stack_class, free_function_class], None)
82 | _s_m (f"sk_{class_name}_insert", [generic_stack_class, generic_inner_class, ctypes.c_int], ctypes.c_int)
83 | _s_m (f"sk_{class_name}_set", [generic_stack_class, ctypes.c_int, generic_inner_class], generic_inner_class)
84 | _s_m (f"sk_{class_name}_find", [generic_stack_class, generic_inner_class], ctypes.c_int)
85 | _s_m (f"sk_{class_name}_find_ex", [generic_stack_class, generic_inner_class], ctypes.c_int)
86 | _s_m (f"sk_{class_name}_sort", [generic_stack_class], None)
87 | _s_m (f"sk_{class_name}_is_sorted", [generic_stack_class], ctypes.c_int)
88 | _s_m (f"sk_{class_name}_dup", [generic_stack_class], generic_stack_class)
89 | _s_m (f"sk_{class_name}_deep_copy", [generic_stack_class, copy_function_class, free_function_class], generic_stack_class)
90 | _s_m (f"sk_{class_name}_set_cmp_func", [generic_stack_class, compare_function_class], compare_function_class)
91 | _s_m (f"sk_{class_name}_new_reserve", [compare_function_class, ctypes.c_int], generic_stack_class)
92 | define_stack_funcs (types.STACK_OF_SSL_CIPHER_ptr, types.SSL_CIPHER_ptr, "SSL_CIPHER")
93 | _s_m (f"OPENSSL_sk_num", [generic_stack_class], ctypes.c_int)
94 | _s_m (f"OPENSSL_sk_value", [generic_stack_class, ctypes.c_int], generic_inner_class)
95 | _s_m (f"OPENSSL_sk_new", [types.OPENSSL_sk_compfunc], generic_stack_class)
96 | _s_m (f"OPENSSL_sk_new_null", [], generic_stack_class)
97 | _s_m (f"OPENSSL_sk_reserve", [generic_stack_class, ctypes.c_int], ctypes.c_int)
98 | _s_m (f"OPENSSL_sk_free", [generic_stack_class], None)
99 | _s_m (f"OPENSSL_sk_zero", [generic_stack_class], None)
100 | _s_m (f"OPENSSL_sk_delete", [generic_stack_class, ctypes.c_int], generic_inner_class)
101 | _s_m (f"OPENSSL_sk_delete_ptr", [generic_stack_class, generic_inner_class], generic_inner_class)
102 | _s_m (f"OPENSSL_sk_push", [generic_stack_class, generic_inner_class], ctypes.c_int)
103 | _s_m (f"OPENSSL_sk_unshift", [generic_stack_class, generic_inner_class], ctypes.c_int)
104 | _s_m (f"OPENSSL_sk_pop", [generic_stack_class], generic_inner_class)
105 | _s_m (f"OPENSSL_sk_shift", [generic_stack_class], generic_inner_class)
106 | _s_m (f"OPENSSL_sk_pop_free", [generic_stack_class, types.OPENSSL_sk_freefunc], None)
107 | _s_m (f"OPENSSL_sk_insert", [generic_stack_class, generic_inner_class, ctypes.c_int], ctypes.c_int)
108 | _s_m (f"OPENSSL_sk_set", [generic_stack_class, ctypes.c_int, generic_inner_class], generic_inner_class)
109 | _s_m (f"OPENSSL_sk_find", [generic_stack_class, generic_inner_class], ctypes.c_int)
110 | _s_m (f"OPENSSL_sk_find_ex", [generic_stack_class, generic_inner_class], ctypes.c_int)
111 | _s_m (f"OPENSSL_sk_sort", [generic_stack_class], None)
112 | _s_m (f"OPENSSL_sk_is_sorted", [generic_stack_class], ctypes.c_int)
113 | _s_m (f"OPENSSL_sk_dup", [generic_stack_class], generic_stack_class)
114 | _s_m (f"OPENSSL_sk_deep_copy", [generic_stack_class, types.OPENSSL_sk_copyfunc, types.OPENSSL_sk_freefunc], generic_stack_class)
115 | _s_m (f"OPENSSL_sk_set_cmp_func", [generic_stack_class, types.OPENSSL_sk_compfunc], types.OPENSSL_sk_compfunc)
116 | _s_m (f"OPENSSL_sk_new_reserve", [types.OPENSSL_sk_compfunc, ctypes.c_int], generic_stack_class)
117 |
118 | _s_m ("SSL_new", [types.SSL_CTX_ptr], types.SSL_ptr)
119 | _s_m ("SSL_get_error", [types.SSL_ptr, ctypes.c_int], ctypes.c_int)
120 | _s_m ("SSL_set_fd", [types.SSL_ptr, ctypes.c_int], ctypes.c_int)
121 | # _s_m ("SSL_set_tlsext_host_name", [types.SSL_ptr, ctypes.c_char_p], ctypes.c_int)
122 | _s_m ("SSL_ctrl", [types.SSL_ptr, ctypes.c_int, ctypes.c_long, ctypes.c_void_p], ctypes.c_long)
123 | _s_m ("SSL_connect", [types.SSL_ptr], ctypes.c_int)
124 | _s_m ("SSL_get_ciphers", [types.SSL_ptr], types.STACK_OF_SSL_CIPHER_ptr)
125 | _s_m ("SSL_get1_supported_ciphers", [types.SSL_ptr], types.STACK_OF_SSL_CIPHER_ptr)
126 | _s_m ("SSL_get_verify_result", [types.SSL_ptr], ctypes.c_long)
127 | _s_m ("SSL_get_peer_certificate", [types.SSL_ptr], types.X509_ptr)
128 | _s_m ("SSL_write", [types.SSL_ptr, ctypes.c_void_p, ctypes.c_int], ctypes.c_int)
129 | _s_m ("SSL_read", [types.SSL_ptr, ctypes.c_void_p, ctypes.c_int], ctypes.c_int)
130 | _s_m ("SSL_shutdown", [types.SSL_ptr], ctypes.c_int)
131 | # Client
132 | _s_m ("FAKESSL_SSL_set_cipher_list", [types.SSL_ptr, ctypes.POINTER (ctypes.c_uint16), ctypes.c_int], None)
133 | _s_m ("FAKESSL_SSL_set_groups_list", [types.SSL_ptr, ctypes.POINTER (ctypes.c_uint16), ctypes.c_int], None)
134 | _s_m ("FAKESSL_SSL_set_format_list", [types.SSL_ptr, ctypes.POINTER (ctypes.c_uint8), ctypes.c_int], None)
135 | _s_m ("FAKESSL_SSL_set_alps_protos", [types.SSL_ptr, ctypes.c_char_p, ctypes.c_uint], None)
136 | _s_m ("FAKESSL_SSL_set_compress_certificates", [types.SSL_ptr, ctypes.c_bool], None)
137 | _s_m ("FAKESSL_SSL_set_ext_order", [types.SSL_ptr, ctypes.POINTER (ctypes.c_uint16), ctypes.c_int], None)
138 | # Server
139 | _s_m ("FAKESSL_SSL_get_ja3", [types.SSL_ptr, ctypes.c_bool], ctypes.c_void_p)
140 | _s_m ("SSL_free", [types.SSL_ptr], None)
141 |
142 | _s_m ("i2d_X509", [types.X509_ptr, ctypes.POINTER (ctypes.POINTER (ctypes.c_ubyte))], ctypes.c_int)
143 | _s_m ("X509_get_subject_name", [types.X509_ptr], types.X509_NAME_ptr)
144 | _s_m ("X509_get_issuer_name", [types.X509_ptr], types.X509_NAME_ptr)
145 | _s_m ("X509_get_version", [types.X509_ptr], ctypes.c_int)
146 | _s_m ("X509_get0_notBefore", [types.X509_ptr], types.ASN1_TIME_ptr)
147 | _s_m ("X509_get0_notAfter", [types.X509_ptr], types.ASN1_TIME_ptr)
148 | _s_m ("X509_get_serialNumber", [types.X509_ptr], types.ASN1_INTEGER_ptr)
149 | _s_m ("X509_free", [types.X509_ptr], None)
150 |
151 | _s_m ("X509_NAME_entry_count", [types.X509_NAME_ptr], ctypes.c_int)
152 | _s_m ("X509_NAME_get_entry", [types.X509_NAME_ptr, ctypes.c_int], types.X509_NAME_ENTRY_ptr)
153 |
154 | _s_m ("X509_NAME_ENTRY_get_object", [types.X509_NAME_ENTRY_ptr], types.ASN1_OBJECT_ptr)
155 | _s_m ("X509_NAME_ENTRY_get_data", [types.X509_NAME_ENTRY_ptr], types.ASN1_STRING_ptr)
156 |
157 | _s_m ("OBJ_obj2txt", [ctypes.c_char_p, ctypes.c_int, types.ASN1_OBJECT_ptr, ctypes.c_int], ctypes.c_int)
158 | _s_m ("OBJ_obj2nid", [types.ASN1_OBJECT_ptr], ctypes.c_int)
159 | _s_m ("OBJ_nid2ln", [ctypes.c_int], ctypes.c_char_p)
160 |
161 | _s_m ("ASN1_STRING_length", [types.ASN1_STRING_ptr], ctypes.c_int)
162 | _s_m ("ASN1_STRING_data", [types.ASN1_STRING_ptr], ctypes.c_char_p)
163 |
164 | _s_m ("ASN1_TIME_print", [types.BIO_ptr, types.ASN1_TIME_ptr], ctypes.c_int)
165 |
166 | _s_m ("i2a_ASN1_INTEGER", [types.BIO_ptr, types.ASN1_INTEGER_ptr], ctypes.c_int)
167 |
168 | _s_m ("ERR_print_errors_cb", [types.ERR_print_errors_cb_callback, ctypes.c_void_p], None)
169 |
170 | _s_m ("CRYPTO_free", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int], None)
171 |
172 | _s_m ("SSL_CTX_set_keylog_callback", [types.SSL_CTX_ptr, types.SSL_CTX_keylog_cb_func], None)
173 |
174 | return libssl_handle, binder_time_mixin_methods
175 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl_py/libssl_library_loader.py:
--------------------------------------------------------------------------------
1 | import sys, pickle, ctypes
2 |
3 | shitter = sys.argv [1]
4 | library = ctypes.cdll.LoadLibrary (shitter)
5 |
6 | pickle.dump (library, sys.stdout.buffer)
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl_py/libssl_macros_mixin.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | from . import libssl_type_bindings as types
3 |
4 | class MacrosMixin:
5 | @staticmethod
6 | def SSL_set_tlsext_host_name (libssl_handle, s, name):
7 | return libssl_handle.SSL_ctrl (s, types.SSL_CTRL_SET_TLSEXT_HOSTNAME, types.TLSEXT_NAMETYPE_host_name, name)
8 | @staticmethod
9 | def BIO_get_mem_data (libssl_handle, b, pp):
10 | return libssl_handle.BIO_ctrl (b, types.BIO_CTRL_INFO, 0, pp)
11 | @staticmethod
12 | def SSL_CTX_set_tlsext_status_type (libssl_handle, ssl, _type):
13 | return libssl_handle.SSL_CTX_ctrl (ssl, types.SSL_CTRL_SET_TLSEXT_STATUS_REQ_TYPE, _type, 0)
14 | @staticmethod
15 | def SSL_CTX_set1_groups_list (libssl_handle, ctx, s):
16 | return libssl_handle.SSL_CTX_ctrl (ctx, types.SSL_CTRL_SET_GROUPS_LIST, 0, ctypes.cast (s, ctypes.c_char_p))
17 | @staticmethod
18 | def OPENSSL_free (libssl_handle, addr):
19 | string_buf = ctypes.create_string_buffer (1)
20 | return libssl_handle.CRYPTO_free (addr, string_buf, 0)
21 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl_py/libssl_type_bindings.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 |
3 | class BIO_method_ptr (ctypes.c_void_p): pass
4 | class BIO_ptr (ctypes.c_void_p): pass
5 | class SSL_METHOD_ptr (ctypes.c_void_p): pass
6 | class SSL_CTX_ptr (ctypes.c_void_p): pass
7 | class SSL_CIPHER_ptr (ctypes.c_void_p): pass
8 | class OPENSSL_STACK_ptr (ctypes.c_void_p): pass
9 | OPENSSL_sk_compfunc = ctypes.CFUNCTYPE (ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
10 | OPENSSL_sk_copyfunc = ctypes.CFUNCTYPE (ctypes.c_void_p, ctypes.c_void_p)
11 | OPENSSL_sk_freefunc = ctypes.CFUNCTYPE (None, ctypes.c_void_p)
12 | def define_types_for_stack_of (inner_class: type (ctypes.c_void_p), class_name: str):
13 | stack_class_name = f"STACK_OF_{class_name}_ptr"
14 | class STACK_OF_cls_ptr (ctypes.c_void_p):
15 | inner = inner_class
16 | inner_name = class_name
17 | STACK_OF_cls_ptr.__name__ = stack_class_name
18 | globals () [stack_class_name] = STACK_OF_cls_ptr
19 | globals () [f"sk_{class_name}_compfunc"] = ctypes.CFUNCTYPE (ctypes.c_int, inner_class, inner_class)
20 | globals () [f"sk_{class_name}_copyfunc"] = ctypes.CFUNCTYPE (inner_class, inner_class)
21 | globals () [f"sk_{class_name}_freefunc"] = ctypes.CFUNCTYPE (None, inner_class)
22 |
23 | define_types_for_stack_of (SSL_CIPHER_ptr, "SSL_CIPHER")
24 | class SSL_ptr (ctypes.c_void_p): pass
25 | class X509_ptr (ctypes.c_void_p): pass
26 | class X509_NAME_ptr (ctypes.c_void_p): pass
27 | class X509_NAME_ENTRY_ptr (ctypes.c_void_p): pass
28 | class ASN1_OBJECT_ptr (ctypes.c_void_p): pass
29 | class ASN1_STRING_ptr (ctypes.c_void_p): pass
30 | class ASN1_TIME_ptr (ctypes.c_void_p): pass
31 | class ASN1_INTEGER_ptr (ctypes.c_void_p): pass
32 | ERR_print_errors_cb_callback = ctypes.CFUNCTYPE (ctypes.c_int, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_void_p)
33 | SSL_CTX_keylog_cb_func = ctypes.CFUNCTYPE (None, SSL_ptr, ctypes.c_char_p)
34 |
35 | # /include/openssl/x509.h
36 | X509_FILETYPE_PEM = 1
37 | X509_FILETYPE_ASN1 = 2
38 |
39 | # defined in /include/openssl/x509_vfy.h.in
40 | X509_V_OK = 0
41 |
42 | # defined in /include/openssl/ssl.h
43 | SSL_CT_VALIDATION_PERMISSIVE = 0
44 | SSL_FILETYPE_ASN1 = X509_FILETYPE_ASN1
45 | SSL_FILETYPE_PEM = X509_FILETYPE_PEM
46 |
47 | # defined in /include/openssl/ssl.h.in
48 | SSL_VERIFY_NONE = 0
49 | SSL_VERIFY_PEER = 1 << 0
50 | # SSL_VERIFY_
51 | SSL_ERROR_NONE = 0
52 | SSL_ERROR_SSL = 1
53 | SSL_ERROR_WANT_READ = 2
54 | SSL_ERROR_WANT_WRITE = 3
55 | SSL_ERROR_WANT_X509_LOOKUP = 4
56 | SSL_ERROR_SYSCALL = 5
57 | SSL_ERROR_ZERO_RETURN = 6
58 | SSL_ERROR_WANT_CONNECT = 7
59 | SSL_ERROR_WANT_ACCEPT = 8
60 | SSL_ERROR_WANT_ASYNC = 9
61 | SSL_ERROR_WANT_ASYNC_JOB = 10
62 | SSL_ERROR_WANT_CLIENT_HELLO_CB = 11
63 | SSL_ERROR_WANT_RETRY_VERIFY = 12
64 | ssl_error_to_str = lambda value: {_value: name for name, _value in locals ().items () if name.startswith ("SSL_ERROR_")} [value]
65 |
66 | # SSL_CTRL_
67 | SSL_CTRL_SET_TLSEXT_HOSTNAME = 55
68 | SSL_CTRL_SET_TLSEXT_STATUS_REQ_TYPE = 65
69 | SSL_CTRL_SET_GROUPS_LIST = 92
70 |
71 | # /include/openssl/bio.h.in
72 | # BIO_CTRL_
73 | BIO_CTRL_INFO = 3
74 |
75 | # defined in /include/openssl/tls1.h
76 | TLSEXT_NAMETYPE_host_name = 0
77 | TLSEXT_STATUSTYPE_ocsp = 1
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl_py/libssl_utils_mixin.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | from . import libssl_type_bindings as types
3 |
4 | class MemoryBIO:
5 | def __init__ (self, libssl_handle):
6 | self.libssl_handle = libssl_handle
7 | self.bio = self.libssl_handle.BIO_new (self.libssl_handle.BIO_s_mem ())
8 | assert self.bio is not None
9 | def get_mem_data (self) -> bytes:
10 | data_pointer = ctypes.POINTER (ctypes.c_ubyte) ()
11 | data_len = self.libssl_handle.BIO_get_mem_data (self.bio, ctypes.byref (data_pointer))
12 | return ctypes.string_at (data_pointer, data_len)
13 | def __del__ (self):
14 | self.libssl_handle.BIO_free (self.bio)
15 |
16 | class UtilsMixin:
17 | @staticmethod
18 | def MemoryBIO (libssl_handle) -> MemoryBIO: return MemoryBIO (libssl_handle)
19 | @staticmethod
20 | def stack_iterator (libssl_handle, stack_ptr):
21 | inner_ptr_type = type (stack_ptr).inner
22 | inner_ptr_type_name = type (stack_ptr).inner_name
23 | for item_index in range (
24 | getattr (libssl_handle, f"sk_{inner_ptr_type_name}_num") (stack_ptr)
25 | ):
26 | yield getattr (libssl_handle, f"sk_{inner_ptr_type_name}_value") (stack_ptr, item_index)
27 | @staticmethod
28 | def cipher_ids_from_stack (libssl_handle, cipher_stack: types.STACK_OF_SSL_CIPHER_ptr) -> list [int]:
29 | cipher_ids = [
30 | libssl_handle.SSL_CIPHER_get_protocol_id (cipher)
31 | for cipher in libssl_handle.stack_iterator (cipher_stack)
32 | ]
33 | return cipher_ids
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl_py/protocol_constants.py:
--------------------------------------------------------------------------------
1 | import ssl as clean_ssl
2 |
3 | # PROTOCOL_TLS = PROTOCOL_SSLv23 = 0 # deprecated
4 | PROTOCOL_TLS_CLIENT = 1
5 | PROTOCOL_TLS_SERVER = 2
6 |
7 | Purpose = clean_ssl.Purpose
--------------------------------------------------------------------------------
/requests_ja3/imitate/fakessl_py/shims_and_mixins.py:
--------------------------------------------------------------------------------
1 | import typing
2 |
3 | class ListWrapperMixin:
4 | def __init__ (self, mixin_methods: list [typing.Callable]):
5 | self.mixin_methods = mixin_methods
6 | def __getattr__ (self, item):
7 | for mixin_method in self.mixin_methods:
8 | if mixin_method.__name__ == item: return mixin_method
9 | raise AttributeError (item)
10 |
11 | class ShimmedModule:
12 | def __init__ (self, src_module):
13 | self.src_module = src_module
14 | self.mixin_classes = []
15 | def __getattr__ (self, item):
16 | should_pass_src_module = False
17 | for mixin_class in self.mixin_classes:
18 | try:
19 | mixin_item = getattr (mixin_class, item)
20 | except AttributeError:
21 | continue
22 | should_pass_src_module = True
23 | src_method = mixin_item
24 | break
25 | else:
26 | src_method = getattr (self.src_module, item)
27 |
28 | assert isinstance (src_method, typing.Callable)
29 | def wrapper_method (*args, **kwargs):
30 | if should_pass_src_module: args = [self, *args]
31 | pretty_args = ", ".join (map (str, args [1:] if should_pass_src_module else args))
32 | pretty_kwargs = ", ".join (f"{kwarg_name} = {str (kwarg_value)}" for kwarg_name, kwarg_value in kwargs.items ())
33 | print (f"{item} ({pretty_args}{f', {pretty_kwargs}' if pretty_kwargs != '' else ''}) -> ", end = "")
34 |
35 | ret = src_method (*args, **kwargs)
36 |
37 | print (ret)
38 | return ret
39 | return wrapper_method
40 | def shim_apply_mixin (self, mixin_class):
41 | self.mixin_classes.append (mixin_class)
42 | def shim_apply_list_mixin (self, mixin_list):
43 | self.mixin_classes.append (ListWrapperMixin (mixin_list))
44 |
45 | def shim_module (src_module):
46 | return ShimmedModule (src_module)
47 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/imitate.py:
--------------------------------------------------------------------------------
1 | import importlib, importlib.util, importlib.machinery
2 | import os
3 | import shutil
4 | import tempfile
5 | import types
6 | import zipfile
7 | import subprocess
8 | import typing
9 |
10 | import requests
11 |
12 | # import cppimport, cppimport.importer, cppimport.templating, cppimport.build_module
13 |
14 | import requests_ja3.decoder as decoder
15 | from requests_ja3.imitate.verify import verify_fakessl
16 |
17 | import pathlib
18 |
19 | def generate_imitation_libssl (target_ja3: typing.Optional [decoder.JA3], use_in_tree_libssl: bool = False, verify_against_real_ssl: bool = False) -> types.ModuleType:
20 | libssl_path, openssl_temp_dir = _compile_libssl (target_ja3, use_in_tree_libssl = use_in_tree_libssl)
21 |
22 | import requests_ja3.imitate.fakessl_py as fakessl
23 | fakessl.initialize (libssl_path, openssl_temp_dir, target_ja3)
24 |
25 | # local_1 = "bruh lol"
26 | # ssl_socket = fakessl.SSLContext.wrap_socket (local_1)
27 | # print (ssl_socket)
28 |
29 | if verify_against_real_ssl: verify_fakessl (fakessl)
30 |
31 | return fakessl
32 |
33 | def _compile_libssl (target_ja3: typing.Optional [decoder.JA3], use_in_tree_libssl: bool) -> (pathlib.Path, tempfile.TemporaryDirectory):
34 | working_dir = tempfile.TemporaryDirectory ()
35 |
36 | try:
37 | working_dir_path = pathlib.Path (working_dir.name)
38 | if use_in_tree_libssl:
39 | openssl_src_latest_base_name = "openssl"
40 | openssl_src_path = pathlib.Path (__file__).parent / openssl_src_latest_base_name
41 | else:
42 | openssl_src_branch_name = "OpenSSL_1_1_1-stable"
43 | openssl_src_latest_base_name = f"openssl-{openssl_src_branch_name}"
44 | openssl_src_zip_name = f"{openssl_src_latest_base_name}.zip"
45 | openssl_src_zip_path = working_dir_path / openssl_src_zip_name
46 | openssl_src_path = working_dir_path / openssl_src_latest_base_name
47 |
48 | openssl_src_url = f"https://github.com/an0ndev/openssl/archive/refs/heads/{openssl_src_branch_name}.zip"
49 | openssl_src_resp = requests.get (openssl_src_url)
50 | openssl_src_resp.raise_for_status ()
51 | with open (openssl_src_zip_path, "wb+") as openssl_src_zip_file:
52 | openssl_src_zip_file.write (openssl_src_resp.content)
53 |
54 | openssl_src_tar = zipfile.ZipFile (openssl_src_zip_path)
55 | openssl_src_tar.extractall (working_dir_path)
56 |
57 | def quiet_exec_in_src (*args):
58 | popen = subprocess.Popen (args, cwd = openssl_src_path, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
59 | stdout_data, stderr_data = popen.communicate ()
60 | stderr_str = stderr_data.decode ()
61 | if "warning" in stderr_str:
62 | print (f"--- COMPILER WARNINGS ---\n{stderr_str}--- END COMPILER WARNINGS ---")
63 | if popen.returncode != 0:
64 | raise Exception (stderr_str)
65 |
66 | quiet_exec_in_src ("/usr/bin/make", "clean")
67 |
68 | quiet_exec_in_src ("/usr/bin/chmod", "+x", "config")
69 | quiet_exec_in_src ("/usr/bin/chmod", "+x", "Configure")
70 | config_options = ["no-ssl2", "no-ssl3", "zlib"]
71 | if target_ja3 is not None:
72 | if 0xFF in target_ja3.accepted_ciphers:
73 | config_options.append ("-DFAKESSL_RFC5746_AS_CIPHER")
74 | if 65281 in target_ja3.list_of_extensions:
75 | config_options.append ("-DFAKESSL_RFC5746_AS_EXTENSION")
76 | if target_ja3.elliptic_curve is None:
77 | config_options.append ("-DFAKESSL_DISABLE_ECC")
78 | quiet_exec_in_src ("/usr/bin/bash", "config", *config_options)
79 | quiet_exec_in_src ("/usr/bin/make", f"-j{os.cpu_count ()}")
80 |
81 | libssl_archive_path = openssl_src_path / "libssl.a"
82 | libcrypto_archive_path = openssl_src_path / "libcrypto.a"
83 | libcrypto_and_ssl_path = openssl_src_path / "libcrypto_and_ssl.so"
84 |
85 | # ugly hack to prioritize usage of symbols from compiled libcrypto:
86 | # combine symbols from compiled libcrypto and libssl into one single shared library
87 | quiet_exec_in_src ("/usr/bin/gcc", "-shared", "-o", str (libcrypto_and_ssl_path), "-Wl,--whole-archive", str (libcrypto_archive_path), str (libssl_archive_path), "-Wl,--no-whole-archive")
88 |
89 | return libcrypto_and_ssl_path, working_dir
90 | except:
91 | working_dir.cleanup ()
92 | raise
93 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/server_cert/gen.sh:
--------------------------------------------------------------------------------
1 | openssl req -x509 -out localhost.crt -keyout localhost.key \
2 | -newkey rsa:2048 -nodes -sha256 \
3 | -subj '/CN=localhost' -extensions EXT -config <( \
4 | printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
5 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/server_cert/localhost.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIC8DCCAdigAwIBAgIUNdEftLYns8g4PDx1+FkSeNGTgRYwDQYJKoZIhvcNAQEL
3 | BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDUwNTIxMjUwMloXDTIyMDYw
4 | NDIxMjUwMlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
5 | AAOCAQ8AMIIBCgKCAQEAt6pZwkLnEa0BclWww9AeadUVSsCgbWKElmPr5pb8c/Sg
6 | t+KylpxfHpYGBQezDSztn1JcTooL1xxNi6757dY2eer+KtItIWoXWa3cEVz0DMn9
7 | EDTadF8Tq6EtehUTupzc3j5fRsCOa5OJzzbVf4i3Q/Mn4nzw1DpAc9X35mssrj8T
8 | VzJNCRDF0WKEWQV4108JHpu9flnjwt+J+dNNDf0HDHiTo+V5uMQVd/pT7fBtSaCD
9 | /w+y01xDvmDOwOFQvJf60BGi93vka3BWVkb0lw0I3nVD6pFAXbEuTv0ivVwjisgP
10 | 0i6zSIjyW8bUseFRQBVqZ/QF+oKw/TYg8wX6L1F4XwIDAQABozowODAUBgNVHREE
11 | DTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB
12 | MA0GCSqGSIb3DQEBCwUAA4IBAQBpXlO/X2/KbdhcB8byNpd8ULW7m+WHvLKxLFqw
13 | uZfUhV34JVCSMCTjPNaTmdpZ+r2o8s28owtHwAbb+n1Whm/P/s3BmzM+pM0Mb0sL
14 | A5361bCKmNBmOEJtq4YTzq3q/1AHipm3OYNPoWv/12Spw+miAdfY/AF4/RYjAhZQ
15 | P2l5G+3LqZfch3i09o9Zvd2n2nJcK42Na+aAT5J2RYvU2Gh9jPQOoi8CD/9sn35H
16 | SO7rB+XwVYCof8DRWZnXt3q5ZQBhUeIwNTcF7SWvPWAAysi7s+lMRjR8Uf669Fn0
17 | k692lo9IAkm/Mr5v3jCl03m0Nx9W0aTMlC7WvyYl6L+qNqk6
18 | -----END CERTIFICATE-----
19 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/server_cert/localhost.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3qlnCQucRrQFy
3 | VbDD0B5p1RVKwKBtYoSWY+vmlvxz9KC34rKWnF8elgYFB7MNLO2fUlxOigvXHE2L
4 | rvnt1jZ56v4q0i0hahdZrdwRXPQMyf0QNNp0XxOroS16FRO6nNzePl9GwI5rk4nP
5 | NtV/iLdD8yfifPDUOkBz1ffmayyuPxNXMk0JEMXRYoRZBXjXTwkem71+WePC34n5
6 | 000N/QcMeJOj5Xm4xBV3+lPt8G1JoIP/D7LTXEO+YM7A4VC8l/rQEaL3e+RrcFZW
7 | RvSXDQjedUPqkUBdsS5O/SK9XCOKyA/SLrNIiPJbxtSx4VFAFWpn9AX6grD9NiDz
8 | BfovUXhfAgMBAAECggEAPeXsgDrZ8ywSS70zaJrQVSyaE2pd5t+H/cNJMpp6FH5C
9 | WBbPx1CGAiHff3x7zJnyr3orX9DIcsO4IrZixtQl4erfpADXcuZ1XiTpAPUJzvGY
10 | FwkpAoU0dOhKElXeI0tQaJoutAKbPQgpLqu1QlkKIp1lOMCQzYWgSSm4dXBzS3mm
11 | KjUZA6FleRR0xM8H6dUWfge+L2lrzc7cIY/f+vzzWFhv5sIYOJjYDpdMdZGjUlu2
12 | KEvjWqs1lN88DtJUK+pOdCZGWlpfyceJITNbFEVy0YM0oQBAdRvwjpY8CA7xST2V
13 | ut0QE0T2EEeIE+tACt4mOsYYLah7pF05dHT1HYtQMQKBgQDkSQ5R9qY50k9KwjJs
14 | q2Q2O9lkMG7QfMM436q79XT8H2DryYMm9Ta/ZZECS6qGWX1Ipbzfzvn60qW9tv5Y
15 | htCVxKITh7lIEs/71AuDhT9MBldMsOI+RV9lGFMPAgcwpZlBY7PywHDLLtvrZr0o
16 | ZX6l5GyDw7Yv43aGnNNfaHO0eQKBgQDN9ooIZIHUvp75RS7b/cZ+y41dtPxdqMLx
17 | ZPDDgS7fdU6LqaJlXsiFfs1JXxHucuDevZHJv/VgmW2zWgrOaWu1l/Nud0A3NJlp
18 | cLyIoy1zguowbww8sUljAIz0WlHE3fySyBCqKHXhu9tRYgFsWBm9Tiyjs7PKMTWP
19 | 0PK78W7tlwKBgQDV6AKsGKLfcUptMZSQMPUAwInOEHf1kcJX63UPFEve5wQpTDRB
20 | b+ox49jBvub1Zqs3RF27lTS1q2Y0Y2Dm0MRoYczA4h9iAoayYJm9TkDmWta4fNIy
21 | KUze7LD/UhYspi75j5QRNfM64Bif9i/ux1Op3GU1/lAbhVcNgauqLbSleQKBgEkc
22 | myMfinbas9QImvm8vBmGaEg7VtpobcsC6fR8hwcLvTYWoW8allPND1JcTSE35lsr
23 | L/VODuybijWAYRWklnd/2Qn9iu4N3edv/X0Db77xWfCXeuCQjlx2dZLH/P7FTkNM
24 | gZ3lfvMCnHfnMloja/+nIHF7+PZtQXYr1f6hVZ9BAoGBALLIGEd1YKdtPrbepbHl
25 | 7De4Yko3bpdAsJtH1QjSU2ZLCBX+qG7dzQn61P+MfhaT66IwocXSOS+4aY4fK07n
26 | +NL2i7jFqU77YcVhBPwF1IN468vYKyME6agcDyZl1esxsVijxtrjHjlZ7tn1uU05
27 | /aNplXUiWTzjsIIiwZ9rojGp
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/server_cert/localhost.p12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/an0ndev/requests-ja3/606fe61aea9e2c68e8c36b3e1654d0d69707a8be/requests_ja3/imitate/server_cert/localhost.p12
--------------------------------------------------------------------------------
/requests_ja3/imitate/server_cert/thanks_to.txt:
--------------------------------------------------------------------------------
1 | https://letsencrypt.org/docs/certificates-for-localhost/#making-and-trusting-your-own-certificates
2 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/test.py:
--------------------------------------------------------------------------------
1 | import json
2 | import socket
3 | import re
4 | import pathlib
5 |
6 | import ssl as system_ssl
7 |
8 | import requests_ja3.decoder as decoder
9 | from requests_ja3.imitate.test_server import AsyncJA3Fetcher, UserAgent
10 |
11 | def ja3_from_any_ssl (ssl_module: type (system_ssl), start_server: bool = True) -> (decoder.JA3, UserAgent):
12 | # ssl_module.test_no_ssl (client)
13 | # ssl_module.test_ssl (client)
14 | # return
15 |
16 | if not start_server:
17 | return make_req_to_local_server(ssl_module)
18 |
19 | ja3_fetcher = AsyncJA3Fetcher (ssl_module)
20 | ja3_fetcher.start ()
21 |
22 | try:
23 | make_req_to_local_server (ssl_module)
24 | except:
25 | ja3_fetcher.cancel ()
26 | raise
27 |
28 | ja3_fetcher.join ()
29 |
30 | return ja3_fetcher.fetch ()
31 |
32 |
33 | def make_req_to_local_server (ssl_module: type (system_ssl)) -> (decoder.JA3, UserAgent):
34 | context = ssl_module.create_default_context ()
35 | server_cert_dir = pathlib.Path (__file__).parent / "server_cert"
36 | context.load_verify_locations (cafile = str (server_cert_dir / "localhost.crt"))
37 | # context.keylog_filename = str (pathlib.Path.home () / "ssl-key.log")
38 |
39 | client = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
40 |
41 | wrapped_client = context.wrap_socket (client, server_hostname = "localhost")
42 | wrapped_client.connect (("localhost", 8443))
43 | def make_req (meth: str, url: str, headers: dict):
44 | crlf = "\r\n"
45 | return f"{meth} {url} HTTP/1.1{crlf}" \
46 | f"{''.join (f'{header_name}: {header_value}{crlf}' for header_name, header_value in headers.items ())}" \
47 | f"{crlf}".encode ()
48 | def parse_resp (resp: str):
49 | regex = r"HTTP/1.1 (?P[0-9]+) (?P[A-Za-z0-9 ]+)\r\n" \
50 | r"([^\r\n]+: [^\r\n]+\r\n)+\r\n" \
51 | r"(?P[\s\S]*)"
52 | match = re.fullmatch (regex, resp)
53 | assert int (match.group ("status_code")) == 200
54 | return match.group ("response_data")
55 | wrapped_client.write (make_req ("GET", "/json", {
56 | "Host": "localhost",
57 | "Connection": "close",
58 | "Accept": "*/*",
59 | "User-Agent": "requests-ja3/HEAD"
60 | }))
61 | in_buffer: bytearray = b""
62 | while True:
63 | in_bytes = wrapped_client.read (1024)
64 | if len (in_bytes) == 0: break
65 | in_buffer += in_bytes
66 | in_resp = in_buffer.decode ()
67 | in_resp_body = parse_resp (in_resp)
68 | in_json = json.loads (in_resp_body)
69 | wrapped_client.close ()
70 | del wrapped_client
71 | del context
72 | return decoder.JA3.from_string(in_json["ja3"]), in_json["User-Agent"]
73 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/test_app.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #include
6 | #include
7 |
8 | #include
9 | #include
10 |
11 | int main () {
12 | printf ("start of main\n");
13 | char domain_name [] = "google.com";
14 |
15 | struct addrinfo hints = {
16 | .ai_flags = 0,
17 | .ai_family = AF_INET,
18 | .ai_socktype = SOCK_STREAM,
19 | .ai_protocol = 0,
20 | .ai_flags = 0
21 | };
22 | struct addrinfo* addressInfo;
23 | printf ("looking up domain name\n");
24 | int ret = getaddrinfo (domain_name, NULL, &hints, &addressInfo);
25 | if (ret < 0) {
26 | perror ("failed to lookup domain name");
27 | return EXIT_FAILURE;
28 | }
29 |
30 | struct sockaddr_in address = *((struct sockaddr_in*) addressInfo->ai_addr);
31 | freeaddrinfo (addressInfo);
32 |
33 | char inet_ntop_result [INET_ADDRSTRLEN];
34 | if (inet_ntop (AF_INET, &(address.sin_addr), (void*) &inet_ntop_result, INET_ADDRSTRLEN) == NULL) {
35 | perror ("failed to convert address to text form");
36 | return EXIT_FAILURE;
37 | }
38 | printf ("address for domain name %s: %s\n", domain_name, inet_ntop_result);
39 |
40 | address.sin_port = htons (443);
41 | int clientSocket = socket (AF_INET, SOCK_STREAM, 0);
42 | if (connect (clientSocket, (struct sockaddr*) &address, sizeof (address)) < 0) {
43 | perror ("couldn't connect");
44 | return EXIT_FAILURE;
45 | }
46 |
47 | SSL_CTX* context = SSL_CTX_new (TLS_client_method ());
48 | #define ssl_perror(inner) printf (inner ": %s\n", ERR_error_string (ERR_get_error (), NULL))
49 | if (context == NULL) {
50 | ssl_perror ("couldn't create context");
51 | return EXIT_FAILURE;
52 | }
53 |
54 | SSL* connection = SSL_new (context);
55 | if (connection == NULL) {
56 | ssl_perror ("couldn't create connection");
57 | SSL_CTX_free (context);
58 | return EXIT_FAILURE;
59 | }
60 |
61 | if (SSL_set_fd (connection, clientSocket) < 1) {
62 | ssl_perror ("couldn't set file descriptor");
63 | SSL_free (connection);
64 | SSL_CTX_free (context);
65 | return EXIT_FAILURE;
66 | }
67 |
68 | if (SSL_connect (connection) < 1) {
69 | ssl_perror ("couldn't prepare connection");
70 | SSL_free (connection);
71 | SSL_CTX_free (context);
72 | return EXIT_FAILURE;
73 | }
74 |
75 | #define CRLF "\r\n"
76 | char http_request [] = "GET / HTTP/1.1" CRLF "Host: google.com" CRLF "Connection: close" CRLF CRLF;
77 |
78 | int writeReturnValue = SSL_write (connection, http_request, (sizeof http_request) - 1);
79 | if (writeReturnValue < ((sizeof http_request) - 1)) {
80 | if (writeReturnValue < 1) {
81 | ssl_perror ("couldn't write to connection");
82 | } else {
83 | ssl_perror ("couldn't write entire payload to connection");
84 | }
85 | SSL_free (connection);
86 | SSL_CTX_free (context);
87 | return EXIT_FAILURE;
88 | }
89 |
90 | const unsigned int bufferSize = 256;
91 | char buffer [bufferSize];
92 |
93 | while (1) {
94 | int readReturnValue = SSL_read (connection, buffer, bufferSize);
95 | if (readReturnValue > 0) {
96 | fwrite (buffer, readReturnValue, 1, stdout);
97 | } else {
98 | if (readReturnValue == 0) {
99 | break;
100 | } else {
101 | ssl_perror ("couldn't read from connection");
102 | SSL_free (connection);
103 | SSL_CTX_free (context);
104 | return EXIT_FAILURE;
105 | }
106 | }
107 | }
108 |
109 | if (SSL_shutdown (connection) < 0) {
110 | ssl_perror ("couldn't shut down connection");
111 | SSL_free (connection);
112 | SSL_CTX_free (context);
113 | return EXIT_FAILURE;
114 | }
115 |
116 | SSL_free (connection);
117 | SSL_CTX_free (context);
118 |
119 | shutdown (clientSocket, SHUT_RDWR);
120 | close (clientSocket);
121 |
122 | return EXIT_SUCCESS;
123 | }
124 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/test_server.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 | import re
3 | import socket
4 | import typing
5 | import json
6 | import threading
7 |
8 | import ssl as system_ssl
9 |
10 | import requests_ja3.decoder as decoder
11 |
12 | class FailedToReadResponseError (Exception): pass
13 |
14 | UserAgent = str
15 | class JA3Fetcher:
16 | def __init__ (self, ssl_module: type (system_ssl)):
17 | self.ssl_module = ssl_module
18 |
19 | context = ssl_module.SSLContext (self.ssl_module.PROTOCOL_TLS_SERVER)
20 | server_cert_dir = pathlib.Path (__file__).parent / "server_cert"
21 | context.load_cert_chain (str (server_cert_dir / "localhost.crt"), str (server_cert_dir / "localhost.key"))
22 |
23 | self.server = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
24 | self.server.setsockopt (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
25 | self.server.bind (("localhost", 8443))
26 | self.server.listen (5)
27 | self.wrapped_server = context.wrap_socket (self.server, server_side = True)
28 |
29 | self.cancelled_lock = threading.Lock ()
30 | self.cancelled = False
31 | def fetch (self) -> (decoder.JA3, UserAgent):
32 | while True:
33 | try:
34 | try:
35 | wrapped_client, client_address = self.wrapped_server.accept ()
36 | except OSError:
37 | with self.cancelled_lock:
38 | if self.cancelled: return
39 |
40 | crlf = "\r\n"
41 | def parse_req (req: str) -> typing.Optional [dict]:
42 | regex = fr"GET /[^{crlf} ]* HTTP/1.1{crlf}" \
43 | fr"(?P([^{crlf}]+: [^{crlf}]+{crlf})+)" \
44 | fr"{crlf}"
45 | match = re.fullmatch (regex, req)
46 | if match is None: return None
47 | raw_headers = match.group ("headers")
48 | headers = {key: value for key, value in map (lambda raw_header: raw_header.split (": "), raw_headers.split (crlf) [:-1])}
49 | return headers
50 | def make_resp (status_code: int, status_text: str, headers: dict, response_data: bytes) -> bytes:
51 | headers.setdefault ("Content-Length", str (len (response_data)))
52 | headers_str = crlf.join (name + ': ' + value for name, value in headers.items ()) + crlf
53 | return (f"HTTP/1.1 {status_code} {status_text}{crlf}"
54 | f"{headers_str}{crlf}").encode () + response_data
55 |
56 | in_buffer: bytearray = b""
57 | while True:
58 | in_bytes = wrapped_client.read (1024)
59 | if len (in_bytes) == 0:
60 | wrapped_client.close ()
61 | raise FailedToReadResponseError ("failed to read resp")
62 | in_buffer += in_bytes
63 | try:
64 | req_str = in_bytes.decode ()
65 | except UnicodeDecodeError:
66 | continue
67 | parsed_headers = parse_req (req_str)
68 | if parsed_headers is not None: break
69 | user_agent = parsed_headers ["User-Agent"]
70 | resp_ja3_str = wrapped_client.get_ja3_str (remove_grease = True)
71 | resp_ja3 = decoder.JA3.from_string (resp_ja3_str)
72 | if 41 in resp_ja3.list_of_extensions: resp_ja3.list_of_extensions.remove(41) # pre-shared key -- session resumption
73 | resp_bytes = json.dumps ({
74 | "ja3_hash": resp_ja3.to_hash (),
75 | "ja3": resp_ja3_str,
76 | "User-Agent": user_agent
77 | }).encode ()
78 | response = make_resp (200, "OK", {"Content-Type": "application/json"}, resp_bytes)
79 |
80 | wrapped_client.write (response)
81 | wrapped_client.close ()
82 |
83 | ret = (resp_ja3, user_agent)
84 | break
85 | except FailedToReadResponseError as failed_to_read_resp_error:
86 | print (failed_to_read_resp_error)
87 | except self.ssl_module.socket_exceptions.FailedAccept as failed_accept:
88 | print (failed_accept)
89 |
90 | self.wrapped_server.close ()
91 | return ret
92 | def cancel (self):
93 | with self.cancelled_lock:
94 | self.cancelled = True
95 | self.wrapped_server.close ()
96 |
97 | class AsyncJA3Fetcher:
98 | def __init__ (self, fakessl):
99 | self.resp_ja3 = None
100 | self.user_agent = None
101 | self.fetcher = JA3Fetcher (fakessl)
102 | self.fetch_thread = threading.Thread (target = self._fetch)
103 | def start (self):
104 | self.fetch_thread.start ()
105 | def fetch (self) -> (decoder.JA3, UserAgent):
106 | self.fetch_thread.join ()
107 | return self.resp_ja3, self.user_agent
108 | def cancel (self):
109 | self.fetcher.cancel ()
110 | def join (self):
111 | self.fetch_thread.join ()
112 | def _fetch (self):
113 | self.resp_ja3, self.user_agent = self.fetcher.fetch ()
114 |
--------------------------------------------------------------------------------
/requests_ja3/imitate/verify.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import ssl
3 |
4 | import types
5 | import typing
6 | from typing import Optional
7 |
8 | def verify_fakessl (fakessl_module: types.ModuleType) -> None:
9 | return _visit_node (node = fakessl_module, reference_root = ssl, node_name = "fakessl", is_root = True)
10 |
11 | def _visit_node (node: object, reference_root: type (ssl), node_name: str, is_root: bool = False):
12 | # raise NotImplementedError ("broken until pybind11 adds support for inspect.signature from methods")
13 |
14 | def simple_visit_children ():
15 | for item_name in dir (node):
16 | if _name_is_builtin (item_name): continue
17 | print (item_name)
18 | _visit_node (getattr (node, item_name), reference_root, node_name = f"{node_name}.{item_name}", is_root = False)
19 |
20 | def find_reference_match () -> object:
21 | name_levels_with_module = node_name.split (".")
22 | assert name_levels_with_module [0] == "fakessl"
23 | sub_name_levels = name_levels_with_module [1:]
24 | topmost_item: object = reference_root
25 | for sub_name in sub_name_levels:
26 | topmost_item = getattr (topmost_item, sub_name)
27 | return topmost_item
28 |
29 | def type_name_is (_object: object, *reference_names: str) -> bool: return any (str (type (_object)) == f"" for reference_name in reference_names)
30 |
31 | reference_match = find_reference_match ()
32 | print (f"REFERENCE MATCH TYPE {type (reference_match)}")
33 | if node == reference_match: return
34 |
35 | function_or_method_type_names = ["builtin_function_or_method", "function", "method"]
36 |
37 | if isinstance (node, types.ModuleType):
38 | if not is_root: return
39 | print (f"{node} is module")
40 | assert type_name_is (reference_match, "module")
41 | simple_visit_children ()
42 | elif type_name_is (node, "pybind11_builtins.pybind11_type"):
43 | print (f"{node} is pybind11 type")
44 | simple_visit_children ()
45 | elif type_name_is (node, "type"):
46 | print (f"{node} is python builtin type")
47 | simple_visit_children ()
48 | elif type_name_is (node, *function_or_method_type_names):
49 | print (f"{node} is builtin function or method")
50 | assert type_name_is (reference_match, *function_or_method_type_names)
51 | reference_method: typing.Callable = reference_match
52 | print (f"found ref method {reference_method}! comparing signature")
53 | reference_signature = inspect.signature (reference_method)
54 | print (f"our doc {node.__doc__}")
55 | print (f"our method dict {node.__dict__}")
56 | signature = inspect.signature (node)
57 | assert reference_signature == signature, f"ref sig {reference_signature} != {signature}"
58 | else:
59 | raise Exception (f"dont know what {node} (name {node_name}, type {type (node)}) is")
60 |
61 | def _name_is_builtin (name: str) -> bool: return name.startswith ("__") and name.endswith ("__")
--------------------------------------------------------------------------------
/requests_ja3/monitor/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/an0ndev/requests-ja3/606fe61aea9e2c68e8c36b3e1654d0d69707a8be/requests_ja3/monitor/__init__.py
--------------------------------------------------------------------------------
/requests_ja3/monitor/monitor.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | import hashlib
3 | import threading
4 | from typing import Optional, List
5 |
6 | from .utils import ensure_root
7 | ensure_root ()
8 |
9 | from scapy.layers.l2 import Ether
10 | from scapy.layers.tls.all import *
11 | from scapy.sendrecv import AsyncSniffer
12 |
13 | @dataclasses.dataclass
14 | class JA3Result:
15 | server_names: Optional [List [str]]
16 | string_ja3: str
17 | md5_ja3: str
18 |
19 | class SingleJA3Yoinker:
20 | def __init__ (self, interface: str, server_name: Optional [str] = None):
21 | self.server_name = server_name
22 |
23 | self.ja3_result: Optional [JA3Result] = None
24 | self.ja3_ready_event = threading.Event ()
25 | self.sniffer = AsyncSniffer (prn = self._packet_handler, store = False, iface = interface)
26 | self.sniffer.start ()
27 | def yoink (self) -> JA3Result:
28 | self.ja3_ready_event.wait ()
29 | return self.ja3_result
30 | def _packet_handler (self, packet: Ether):
31 | if self.ja3_ready_event.is_set (): return
32 |
33 | summary = packet.summary ()
34 | if "TLS Handshake - Client Hello" not in summary: return
35 | tls_layer: TLS = packet [TLS]
36 | self.ja3_result = self._ja3_from_tls_client_hello (tls_layer.msg [0])
37 | if self.ja3_result is None: return
38 | self.sniffer.stop (join = False)
39 | self.ja3_ready_event.set ()
40 | def _ja3_from_tls_client_hello (self, tls_client_hello: TLSClientHello) -> Optional [JA3Result]:
41 | field_delimiter = ','
42 | value_delimiter = '-'
43 | out_fields = []
44 |
45 | try:
46 | tls_client_hello.version
47 | except KeyError:
48 | print (f"aye wtf, you sure this is a client hello? {type (tls_client_hello)}")
49 | raise
50 |
51 | # def print_field (field_name): print (f"{field_name} {tls_client_hello.fields [field_name]}")
52 | # for field in ("version", "ciphers", "ext"): print_field (field)
53 | grease_values = [(base << 8) | base for base in [0xA | (upper_bit << 4) for upper_bit in range (16)]]
54 |
55 | # SSL Version
56 | out_fields.append (str (tls_client_hello.version))
57 |
58 | # Cipher
59 | out_ciphers = []
60 | for cipher in tls_client_hello.ciphers:
61 | if cipher in grease_values: continue
62 | out_ciphers.append (str (cipher))
63 | out_fields.append (value_delimiter.join (out_ciphers))
64 |
65 | # SSL Extension
66 | out_extensions = []
67 | ec_extension = None
68 | ec_formats_extension = None
69 |
70 | server_names = None
71 |
72 | for extension in tls_client_hello.ext:
73 | if extension.name == "TLS Extension - Server Name":
74 | server_names = list (server_name.servername.decode () for server_name in extension.fields ['servernames'])
75 | if self.server_name not in server_names: return
76 | extension_type = extension.type
77 | if extension_type in grease_values: continue
78 | # if extension_type == 0x15: continue # "Padding"
79 | if extension.name == "TLS Extension - Scapy Unknown":
80 | print (f"WARNING: unknown extension {extension.type}, not adding to signature")
81 | continue
82 | out_extensions.append (str (extension.type))
83 | if extension.type == 10: # "Supported Groups"
84 | ec_extension = extension
85 | elif extension.type == 11: # "EC Point Formats"
86 | ec_formats_extension = extension
87 | out_fields.append (value_delimiter.join (out_extensions))
88 |
89 | if server_names is None and self.server_name is not None: return
90 |
91 | # Elliptic Curve
92 | if ec_extension is not None:
93 | out_groups = []
94 | for group in ec_extension.fields ["groups"]:
95 | if group in grease_values: continue
96 | out_groups.append (str (group))
97 | out_fields.append (value_delimiter.join (out_groups))
98 | else: out_fields.append ("")
99 |
100 | # Elliptic Curve Point Format
101 | if ec_formats_extension is not None:
102 | out_fields.append (value_delimiter.join (str (point_format) for point_format in ec_formats_extension.fields ["ecpl"]))
103 | else: out_fields.append ("")
104 |
105 | string_ja3 = field_delimiter.join (out_fields)
106 | md5_ja3 = hashlib.md5 (string_ja3.encode ()).hexdigest ()
107 | return JA3Result (server_names, string_ja3, md5_ja3)
108 |
--------------------------------------------------------------------------------
/requests_ja3/monitor/utils.py:
--------------------------------------------------------------------------------
1 | def ensure_root ():
2 | import os
3 |
4 | euid = os.geteuid ()
5 | if euid != 0: raise Exception ("run as root")
--------------------------------------------------------------------------------
/requests_ja3/patcher.py:
--------------------------------------------------------------------------------
1 | import ssl
2 | from typing import Callable
3 |
4 | # for type annotations
5 | import requests as _clean_requests
6 | import requests.sessions as _clean_requests_sessions
7 | import requests.adapters as _clean_requests_adapters
8 | import urllib3.poolmanager as _clean_urllib3_poolmanager
9 | import urllib3.connectionpool as _clean_urllib3_connectionpool
10 | import urllib3.connection as _clean_urllib3_connection
11 | import urllib3.util.ssl_ as _clean_urllib3_util_ssl
12 | import urllib3.util.ssltransport as _clean_urllib3_util_ssltransport
13 |
14 | from requests_ja3.decoder import JA3
15 | from requests_ja3.imitate.test_server import AsyncJA3Fetcher
16 | from requests_ja3.patcher_utils import _module_from_class, _wrap
17 | from requests_ja3.ssl_utils import SSLUtils
18 | from requests_ja3.imitate.imitate import generate_imitation_libssl
19 |
20 | class Patcher:
21 | @staticmethod
22 | def patch (src_requests_module: type (_clean_requests), target_ja3: JA3):
23 | # def ssl_wrap_socket_hook (*args, **kwargs):
24 | # print (f"ssl_wrap_socket called with args {args} kwargs {kwargs}")
25 | # return args, kwargs
26 | # Patcher._inner_patch (src_requests_module, ssl_wrap_socket_hook)
27 | fakessl = generate_imitation_libssl (target_ja3)
28 | Patcher._inner_patch (src_requests_module, fakessl)
29 | @staticmethod
30 | def check (requests_module: type (_clean_requests), target_ja3: JA3):
31 | fetcher_ja3 = generate_imitation_libssl(None)
32 | ja3_fetcher = AsyncJA3Fetcher (fetcher_ja3)
33 | ja3_fetcher.start ()
34 |
35 | try:
36 | real_ja3 = JA3.from_string (requests_module.get ("https://localhost:8443/json").json () ["ja3"])
37 | except:
38 | ja3_fetcher.cancel ()
39 | raise
40 |
41 | ja3_fetcher.join ()
42 | ja3_fetcher.fetch ()
43 |
44 | assert real_ja3.print_comparison_with(target_ja3)
45 | @staticmethod
46 | def _inner_patch (src_requests_module: type (_clean_requests), fakessl: type (ssl)):
47 | src_session_class = src_requests_module.Session
48 | # src_session_class.request = _wrap (src_session_class.request, "Session.request")
49 | src_sessions_module: type (_clean_requests_sessions) = _module_from_class (src_session_class)
50 | src_adapters_module: type (_clean_requests_adapters) = _module_from_class (src_sessions_module.HTTPAdapter)
51 | src_poolmanager_module: type (_clean_urllib3_poolmanager) = _module_from_class (src_adapters_module.PoolManager)
52 | src_connectionpool_module: type (_clean_urllib3_connectionpool) = _module_from_class (src_poolmanager_module.HTTPSConnectionPool)
53 | src_connection_module: type (_clean_urllib3_connection) = _module_from_class (src_connectionpool_module.HTTPSConnection)
54 | src_util_ssl_module: type (_clean_urllib3_util_ssl) = _module_from_class (src_connection_module.create_urllib3_context)
55 | print (src_util_ssl_module)
56 |
57 | src_util_ssl_module.ssl = fakessl
58 |
59 | # https://github.com/urllib3/urllib3/blob/main/src/urllib3/util/ssl_.py#L99
60 | direct_import_object_names_from_native_ssl = [
61 | "CERT_REQUIRED",
62 | "HAS_NEVER_CHECK_COMMON_NAME",
63 | "HAS_SNI",
64 | "OP_NO_COMPRESSION",
65 | "OP_NO_TICKET",
66 | "OPENSSL_VERSION",
67 | "OPENSSL_VERSION_NUMBER",
68 | "PROTOCOL_TLS",
69 | "PROTOCOL_TLS_CLIENT",
70 | "OP_NO_SSLv2",
71 | "OP_NO_SSLv3",
72 | "SSLContext",
73 | "TLSVersion",
74 | ]
75 | for object_name in direct_import_object_names_from_native_ssl: setattr (
76 | src_util_ssl_module,
77 | object_name,
78 | getattr (fakessl, object_name)
79 | )
80 |
81 | if src_util_ssl_module.SSLTransport is not None:
82 | src_util_ssltransport_module: type (_clean_urllib3_util_ssltransport) = _module_from_class (src_util_ssl_module.SSLTransport)
83 | src_util_ssltransport_module.ssl = fakessl
84 |
85 | """
86 | src_httpadapter_class: _clean_requests_sessions.HTTPAdapter = src_sessions_module.HTTPAdapter
87 | def get_connection_hook (connection_pool: _clean_HTTPSConnectionPool):
88 | def _make_request_hook (connection: _clean_VerifiedHTTPSConnection, *args, **kwargs):
89 | src_connection_module: type (_clean_urllib3_connection) = _module_from_class (connection.__class__)
90 | src_connection_module.ssl_wrap_socket = _wrap (src_connection_module.ssl_wrap_socket, "urllib3.util.ssl_.ssl_wrap_socket", pre_hook = ssl_wrap_socket_hook)
91 | return (connection, *args), kwargs
92 | connection_pool._make_request = _wrap (connection_pool._make_request, "HTTPSConnectionPool._make_request", pre_hook = _make_request_hook)
93 | return connection_pool
94 | src_httpadapter_class.get_connection = _wrap (src_httpadapter_class.get_connection, "HTTPAdapter.get_connection", post_hook = get_connection_hook)
95 | """
--------------------------------------------------------------------------------
/requests_ja3/patcher_utils.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import sys
3 | from typing import Callable, Optional
4 |
5 | def _wrap (wrapped: Callable, name: str, pre_hook: Optional [Callable] = None, post_hook: Optional [Callable] = None):
6 | @functools.wraps (wrapped)
7 | def wrapper (*args, **kwargs):
8 | print (f"{name} called with args {args} and kwargs {kwargs}")
9 | args_after_hook, kwargs_after_hook = pre_hook (*args, **kwargs) if pre_hook is not None else (args, kwargs)
10 | wrapped_return_value = wrapped (*args_after_hook, **kwargs_after_hook)
11 | return post_hook (wrapped_return_value) if post_hook is not None else wrapped_return_value
12 | return wrapper
13 | def _module_from_class (_class): return sys.modules [_class.__module__]
14 |
--------------------------------------------------------------------------------
/requests_ja3/ssl_utils.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | from typing import Dict, List
3 |
4 | SSLCipherNumber = int
5 | SSLCipherName = str
6 |
7 | class SSLUtils:
8 | @staticmethod
9 | def get_cipher_names () -> Dict [SSLCipherNumber, SSLCipherName]:
10 | out = {}
11 |
12 | cipher_list_str = subprocess.run ("openssl ciphers -V ALL", shell = True, stdout = subprocess.PIPE).stdout.decode ().replace ("\r", "")
13 | if cipher_list_str.endswith ("\n"):
14 | cipher_list_str = cipher_list_str [:-(len ("\n"))]
15 | cipher_lines = map (lambda _str: _str.strip (), cipher_list_str.split ('\n'))
16 | for cipher_line in cipher_lines:
17 | cipher_line = cipher_line.replace (" - ", " ")
18 | while " " in cipher_line:
19 | cipher_line = cipher_line.replace (" ", " ")
20 | number_as_str, name, protocol_version, key_exchange, authentication, encryption, mac_algorithms = tuple (cipher_line.split (' '))
21 | number_first_byte_with_hex_prefix, number_second_byte_with_hex_prefix = number_as_str.split (',')
22 | assert all (map (lambda byte: byte.startswith ("0x"), (number_first_byte_with_hex_prefix, number_second_byte_with_hex_prefix)))
23 | number_first_byte_str, number_second_byte_str = tuple (map (lambda byte: byte [len ("0x"):], (number_first_byte_with_hex_prefix, number_second_byte_with_hex_prefix)))
24 | number_first_byte, number_second_byte = tuple (map (lambda byte: int (byte, 16), (number_first_byte_str, number_second_byte_str)))
25 | number_bytes = bytes ([number_first_byte, number_second_byte])
26 | number = int.from_bytes (number_bytes, byteorder = "big", signed = False)
27 | out [number] = name
28 |
29 | out_sorted = {number: out [number] for number in sorted (list (out.keys ()))}
30 | for number, name in out_sorted.items (): print (f"{number}: {name}")
31 | return out_sorted
32 | @staticmethod
33 | def cipher_numbers_to_string (cipher_numbers: List [SSLCipherNumber]) -> str:
34 | cipher_names = SSLUtils.get_cipher_names ().copy ()
35 | selected = ':'.join (cipher_names [cipher_number] for cipher_number in cipher_numbers)
36 | for cipher_number in cipher_numbers: del cipher_names [cipher_number]
37 | rejected = ':'.join (f"!{cipher_names [cipher_number]}" for cipher_number in cipher_names.keys ())
38 | print (f"selected {selected}")
39 | print (f"rejected {rejected}")
40 | return f"{selected}:{rejected}"
41 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from requests_ja3.patcher import Patcher
4 |
5 | target_ja3_str = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27,,"
6 | Patcher.patch (requests, target_ja3_str)
7 | Patcher.check (requests, target_ja3_str)
8 |
--------------------------------------------------------------------------------
/test_imitate.py:
--------------------------------------------------------------------------------
1 | from requests_ja3.imitate.imitate import generate_imitation_libssl
2 | import requests_ja3.decoder as decoder
3 | from requests_ja3.imitate.test import ja3_from_any_ssl
4 |
5 | target_ja3 = decoder.JA3.from_string ("771,4866-4867-4865-49196-49200-159-52393-52392-52394-49195-49199-158-49188-49192-107-49187-49191-103-49162-49172-57-49161-49171-51-157-156-61-60-53-47-255,0-11-10-35-22-23-13-43-45-51-21,29-23-30-25-24,0-1-2") # e1d8b
6 | print (target_ja3)
7 |
8 | fakessl = generate_imitation_libssl (
9 | target_ja3,
10 | use_in_tree_libssl = True
11 | )
12 |
13 | ja3_from_test, user_agent_from_test = ja3_from_any_ssl (fakessl)
14 |
15 | assert ja3_from_test.print_comparison_with (target_ja3)
16 |
17 | print ("PASSES IMITATION TEST")
--------------------------------------------------------------------------------
/test_monitor.py:
--------------------------------------------------------------------------------
1 | from requests_ja3.monitor.monitor import SingleJA3Yoinker
2 | import requests_ja3.decoder as decoder
3 |
4 | import requests
5 |
6 | yoinker = SingleJA3Yoinker (interface = "br0", server_name = "ja3er.com")
7 |
8 | api_response = requests.get ("https://ja3er.com/json")
9 | yoinked_response = yoinker.yoink ()
10 |
11 | def pprint_ja3 (ja3: dict):
12 | for field_name, field_value in ja3.items ():
13 | print (f"{field_name}: {field_value}")
14 | sep = '-' * 5
15 |
16 | print (f"{sep} API JA3 {sep}")
17 | print (api_response.json ())
18 | api_ja3 = decoder.Decoder.decode (api_response.json () ["ja3"])
19 | pprint_ja3 (api_ja3)
20 | print ("")
21 | print (f"{sep} YOINKED JA3 {sep}")
22 | yoinked_ja3 = decoder.Decoder.decode (yoinked_response.string_ja3)
23 | pprint_ja3 (yoinked_ja3)
24 | print ("")
25 |
26 | decoder.print_comparison (api_ja3, yoinked_ja3)
--------------------------------------------------------------------------------
/test_patcher.py:
--------------------------------------------------------------------------------
1 | from requests_ja3 import decoder
2 | from requests_ja3.patcher import Patcher
3 | import requests
4 |
5 | target_ja3 = decoder.JA3.from_string ("771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-21,29-23-24,0") # cd08e31494f9531f560d64c695473da9
6 | print (target_ja3)
7 | Patcher.patch(requests, target_ja3)
8 | Patcher.check(requests, target_ja3)
--------------------------------------------------------------------------------
/test_server.py:
--------------------------------------------------------------------------------
1 | from requests_ja3.imitate.imitate import generate_imitation_libssl
2 | from requests_ja3.imitate.test_server import JA3Fetcher
3 |
4 | fakessl = generate_imitation_libssl (
5 | None,
6 | use_in_tree_libssl = True
7 | )
8 |
9 | ja3, user_agent = JA3Fetcher (fakessl).fetch ()
10 |
11 | print (f"JA3: {ja3}")
12 | print (f"Reported user agent: {user_agent}")
--------------------------------------------------------------------------------