├── .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 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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}") --------------------------------------------------------------------------------