Skip to content

Commit

Permalink
tls - compute JA4 fingerprint if set
Browse files Browse the repository at this point in the history
- setting is content_profile attribute (default: off)
  • Loading branch information
astibal committed Nov 7, 2024
1 parent 8a5bc09 commit 9b85b14
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 24 deletions.
1 change: 1 addition & 0 deletions src/policy/profiles.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ class ProfileContent : public socle::sobject, public CfgElement {
// content webhook options
bool webhook_enable = false;
bool webhook_lock_traffic = false;
bool ja4_tls_ch = false;

std::string rules_session_filter;
std::optional<std::regex> rules_session_filter_rx;
Expand Down
44 changes: 41 additions & 3 deletions src/proxy/mitmproxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
#include <socle/common/base64.hpp>
#include <socle/timed_guard.hpp>

#include <inspect/fp/ja4.hpp>

using namespace socle;

MitmProxy::MitmProxy(baseCom* c): baseProxy(c), sobject() {
Expand Down Expand Up @@ -304,6 +306,7 @@ void MitmProxy::webhook_session_stop() const {
{"policy", matched_policy() },
{ "bytes_up", uB },
{ "bytes_down", dB },
{ "ja4_ch", ja4.ClientHello },
};

if(tls.has_value()) j["info"]["tls"] = tls.value();
Expand Down Expand Up @@ -791,14 +794,49 @@ bool MitmProxy::is_white_listed(MitmHostCX const* mh, SSLCom* peercom) {

bool MitmProxy::handle_com_response_ssl(MitmHostCX* mh)
{
// cast only once: in ja4 section, or later in code
SSLCom* scom = nullptr;

// check TLS ClientHello and calculate JA4, if allowed by options
if(acct_opts.ja4_clienthello and ja4.ClientHello.empty() and ja4.clienthello_counter < ja4.max_reads) {

// be ready to read empty CH buffer and retry with max attemtps set
ja4.clienthello_counter++;
scom = dynamic_cast<SSLCom *>(mh->peercom());

// we are always left context
if (scom && !scom->client_hello_buffer().empty()) {
sx::ja4::TLSClientHello ch;
auto const &ch_buf = scom->client_hello_buffer();

// yes, some copying :( - in c++20 is span, but we are still at c++17
auto bufvec = std::vector(ch_buf.data() + 5, ch_buf.data() + ch_buf.size());
ch.from_buffer(bufvec);
ja4.ClientHello = ch.ja4();
_dia("JA4: %s (attempt %d)", ja4.ClientHello.c_str(), ja4.clienthello_counter);
}
else {
// dont try next time
acct_opts.ja4_clienthello = false;
ssl_handled = true;
}
}

if(ssl_handled) {
return false;
}

bool redirected = false;

auto* scom = dynamic_cast<SSLCom*>(mh->peercom());
// set scom if not set earlier
if(! scom) {
scom = dynamic_cast<SSLCom *>(mh->peercom());
if(! scom) {
// spare some cycles and avoid futile next calls
// ssl_handled can then be reset manually if needed
ssl_handled = true;
}
}

bool redirected = false;

if(scom && scom->is_verify_status_opt_allowed()) {

Expand Down
9 changes: 9 additions & 0 deletions src/proxy/mitmproxy.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,17 @@ class MitmProxy : public baseProxy, public socle::sobject, public IOController {

struct Opts_Accounting {
bool details = true;
bool ja4_clienthello = false;
} acct_opts;

struct JA4 {
size_t max_reads = 10;

std::string ClientHello;
size_t clienthello_counter = 0;

} ja4;

// Remote filters - use other proxy to filter content of this proxy.
// Elements are pair of "name" and pointer to the filter proxy
std::vector<std::pair<std::string, std::unique_ptr<FilterProxy>>> filters_;
Expand Down
32 changes: 18 additions & 14 deletions src/service/cfgapi/cfgapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,10 @@ bool CfgFactory::upgrade_schema(int upgrade_to_num) {
log.event(INF, "added content_profile.[x].rules_session_filter");
return true;
}
else if(upgrade_to_num == 1035) {
log.event(INF, "added content_profile.[x].ja4_tls_ch");
return true;
}


return false;
Expand Down Expand Up @@ -2126,6 +2130,7 @@ int CfgFactory::load_db_prof_content () {

load_if_exists(cur_object, "webhook_enable", new_profile->webhook_enable);
load_if_exists(cur_object, "webhook_lock_traffic", new_profile->webhook_lock_traffic);
load_if_exists(cur_object, "ja4_tls_ch", new_profile->ja4_tls_ch);

if(load_if_exists(cur_object, "rules_session_filter", new_profile->rules_session_filter)) {
if(not new_profile->rules_session_filter.empty()) {
Expand Down Expand Up @@ -2603,12 +2608,10 @@ size_t CfgFactory::cleanup_db_prof_auth () {
}


bool CfgFactory::prof_content_apply (baseHostCX *originator, baseProxy *new_proxy, const std::shared_ptr<ProfileContent> &pc) {
bool CfgFactory::prof_content_apply (baseHostCX *originator, MitmProxy *mitm_proxy, const std::shared_ptr<ProfileContent> &pc) {

auto const& log = log::policy();

auto* mitm_proxy = dynamic_cast<MitmProxy*>(new_proxy);

bool ret = true;
bool cfg_wrt;

Expand All @@ -2621,6 +2624,8 @@ bool CfgFactory::prof_content_apply (baseHostCX *originator, baseProxy *new_prox
mitm_proxy->writer_opts()->webhook_enable = pc->webhook_enable;
mitm_proxy->writer_opts()->webhook_lock_traffic = pc->webhook_lock_traffic;

mitm_proxy->acct_opts.ja4_clienthello = pc->ja4_tls_ch;

bool filter_ok = true;
if(pc->rules_session_filter_rx.has_value()) {
try {
Expand Down Expand Up @@ -2661,7 +2666,7 @@ bool CfgFactory::prof_content_apply (baseHostCX *originator, baseProxy *new_prox
}


bool CfgFactory::prof_detect_apply (baseHostCX *originator, baseProxy *new_proxy, const std::shared_ptr<ProfileDetection> &pd) {
bool CfgFactory::prof_detect_apply (baseHostCX *originator, MitmProxy *mitm_proxy, const std::shared_ptr<ProfileDetection> &pd) {

auto* mitm_originator = dynamic_cast<MitmHostCX*>(originator);
auto const& log = log::policy();
Expand Down Expand Up @@ -2721,7 +2726,7 @@ std::optional<std::vector<std::string>> CfgFactory::find_bypass_domain_hosts(std
return to_match.empty() ? std::nullopt : std::make_optional(to_match);
};

bool CfgFactory::prof_tls_apply (baseHostCX *originator, baseProxy *new_proxy, const std::shared_ptr<ProfileTls> &ps) {
bool CfgFactory::prof_tls_apply (baseHostCX *originator, MitmProxy *new_proxy, const std::shared_ptr<ProfileTls> &ps) {

auto const& log = log::policy();

Expand Down Expand Up @@ -2813,12 +2818,11 @@ bool CfgFactory::prof_tls_apply (baseHostCX *originator, baseProxy *new_proxy, c
return tls_applied;
}

bool CfgFactory::prof_alg_dns_apply (baseHostCX *originator, baseProxy *new_proxy, const std::shared_ptr<ProfileAlgDns> &p_alg_dns) {
bool CfgFactory::prof_alg_dns_apply (baseHostCX *originator, MitmProxy *new_proxy, const std::shared_ptr<ProfileAlgDns> &p_alg_dns) {

auto const& log = log::policy();

auto* mitm_originator = dynamic_cast<AppHostCX*>(originator);
auto* mh = dynamic_cast<MitmHostCX*>(mitm_originator);
auto* mh = dynamic_cast<MitmHostCX*>(originator);

bool ret = false;

Expand All @@ -2828,7 +2832,7 @@ bool CfgFactory::prof_alg_dns_apply (baseHostCX *originator, baseProxy *new_prox
if(DNS_Inspector::dns_prefilter(mh)) {
auto* n = new DNS_Inspector();

_dia("policy_apply: policy dns profile[%s] for %s", p_alg_dns->element_name().c_str(), mitm_originator->full_name('L').c_str());
_dia("policy_apply: policy dns profile[%s] for %s", p_alg_dns->element_name().c_str(), mh->full_name('L').c_str());
n->opt_match_id = p_alg_dns->match_request_id;
n->opt_randomize_id = p_alg_dns->randomize_id;
n->opt_cached_responses = p_alg_dns->cached_responses;
Expand All @@ -2845,20 +2849,19 @@ bool CfgFactory::prof_alg_dns_apply (baseHostCX *originator, baseProxy *new_prox
}


bool CfgFactory::prof_script_apply (baseHostCX *originator, baseProxy *new_proxy, std::shared_ptr<ProfileScript> const& p_script) {
bool CfgFactory::prof_script_apply (baseHostCX *originator, MitmProxy *new_proxy, std::shared_ptr<ProfileScript> const& p_script) {

auto const& log = log::policy();

auto* mitm_originator = dynamic_cast<AppHostCX*>(originator);
auto* mh = dynamic_cast<MitmHostCX*>(mitm_originator);
auto* mh = dynamic_cast<MitmHostCX*>(originator);

bool ret = false;

if(mh != nullptr) {

if(p_script) {

_dia("policy_apply: policy script profile[%s] for %s", p_script->element_name().c_str(), mitm_originator->full_name('L').c_str());
_dia("policy_apply: policy script profile[%s] for %s", p_script->element_name().c_str(), mh->full_name('L').c_str());

if(p_script->script_type == ProfileScript::ST_PYTHON) {
#ifdef USE_PYTHON
Expand Down Expand Up @@ -2933,7 +2936,7 @@ void CfgFactory::policy_apply_features(std::shared_ptr<PolicyRule> const & polic

}

int CfgFactory::policy_apply (baseHostCX *originator, baseProxy *proxy, int matched_policy) {
int CfgFactory::policy_apply (baseHostCX *originator, MitmProxy *proxy, int matched_policy) {

auto const& log = log::policy();

Expand Down Expand Up @@ -3898,6 +3901,7 @@ int CfgFactory::save_content_profiles(Config& ex) const {

item.add("webhook_enable", Setting::TypeBoolean) = obj->webhook_enable;
item.add("webhook_lock_traffic", Setting::TypeBoolean) = obj->webhook_lock_traffic;
item.add("ja4_tls_ch", Setting::TypeBoolean) = obj->ja4_tls_ch;
item.add("rules_session_filter", Setting::TypeString) = obj->rules_session_filter;

if(! obj->content_rules.empty() ) {
Expand Down
14 changes: 7 additions & 7 deletions src/service/cfgapi/cfgapi.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class CfgFactory : public CfgFactoryBase {
public:
// Each version bump implies a config upgrade - we start on 1000
// see upgrade_schema() - control config upgrade
constexpr static inline const int SCHEMA_VERSION = 1034;
constexpr static inline const int SCHEMA_VERSION = 1035;

static inline std::atomic_bool LOAD_ERRORS = false;

Expand Down Expand Up @@ -522,21 +522,21 @@ class CfgFactory : public CfgFactoryBase {


bool apply_config_change(std::string_view section);
int policy_apply (baseHostCX *originator, baseProxy *proxy, int matched_policy=-1);
int policy_apply (baseHostCX *originator, MitmProxy *proxy, int matched_policy=-1);
void policy_apply_features(std::shared_ptr<PolicyRule> const& policy_rule, MitmProxy *mitm_proxy);
std::shared_ptr<PolicyRule> lookup_policy(std::size_t i) { if(i < db_policy_list.size()) return db_policy_list.at(i); else return nullptr; }

bool policy_apply_tls (int policy_num, baseCom *xcom);
bool policy_apply_tls (const std::shared_ptr<ProfileTls> &pt, baseCom *xcom);

bool prof_content_apply (baseHostCX *originator, baseProxy *new_proxy, const std::shared_ptr<ProfileContent> &pc);
bool prof_detect_apply (baseHostCX *originator, baseProxy *new_proxy, const std::shared_ptr<ProfileDetection> &pd);
bool prof_content_apply (baseHostCX *originator, MitmProxy *new_proxy, const std::shared_ptr<ProfileContent> &pc);
bool prof_detect_apply (baseHostCX *originator, MitmProxy *new_proxy, const std::shared_ptr<ProfileDetection> &pd);

std::optional<std::vector<std::string>> find_bypass_domain_hosts(std::string const& filter_element, bool wildcards_only);
bool prof_tls_apply (baseHostCX *originator, baseProxy *new_proxy, const std::shared_ptr<ProfileTls> &ps);
bool prof_alg_dns_apply (baseHostCX *originator, baseProxy *new_proxy, const std::shared_ptr<ProfileAlgDns>& p_alg_dns);
bool prof_tls_apply (baseHostCX *originator, MitmProxy *new_proxy, const std::shared_ptr<ProfileTls> &ps);
bool prof_alg_dns_apply (baseHostCX *originator, MitmProxy *new_proxy, const std::shared_ptr<ProfileAlgDns>& p_alg_dns);
[[maybe_unused]]
bool prof_script_apply (baseHostCX *originator, baseProxy *new_proxy, const std::shared_ptr<ProfileScript>& p_script);
bool prof_script_apply (baseHostCX *originator, MitmProxy *new_proxy, const std::shared_ptr<ProfileScript>& p_script);

static void gre_export_apply(traflog::PcapLog* pcaplog);

Expand Down
3 changes: 3 additions & 0 deletions src/service/cmd/diag/diag_cmds.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1854,6 +1854,9 @@ auto get_more_info(sobject_info const* so_info, MitmProxy const* curr_proxy, Mit
info_ss << "\n L7_params: none\n";
}

if(curr_proxy and ! curr_proxy->ja4.ClientHello.empty()) {
info_ss << "\n JA4_ch: " << curr_proxy->ja4.ClientHello << "\n";
}

if (verbosity > INF) {
long expiry = -1;
Expand Down

0 comments on commit 9b85b14

Please sign in to comment.