Based on Nekogram. Key additions: - Rebrand to FoxiGram (app name, APK name, applicationId com.foxigram.app) - Embedded Xray (VLESS+Reality) proxy client via JNI libxray.so - Bundled hidden one-tap proxies (LTE + WiFi), read-only in UI - Auto-restore proxy on restart, rebind to active network (LTE/WiFi) - Server credentials externalized to git-ignored XrayServers.java (+ template) - libxray Go source included; compiled .so, keystore, google-services.json ignored
633 lines
21 KiB
C++
633 lines
21 KiB
C++
/*
|
|
* StealthTlsHello.cpp
|
|
* Ported from telemt/tdlib-obf (MIT License, Copyright 2026 telemt community)
|
|
* Adapted for tgnet — C++17, OpenSSL only.
|
|
*/
|
|
|
|
#include "StealthTlsHello.h"
|
|
|
|
#include <openssl/rand.h>
|
|
#include <openssl/hmac.h>
|
|
#include <openssl/evp.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
namespace stealth {
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Per-run salt — initialised once, survives for the app lifetime.
|
|
// Ensures same destination picks different profiles across restarts.
|
|
// ---------------------------------------------------------------------------
|
|
static std::atomic<uint32_t> g_run_salt{0};
|
|
|
|
void initStealthSalt() {
|
|
uint32_t s = 0;
|
|
RAND_bytes(reinterpret_cast<uint8_t *>(&s), 4);
|
|
g_run_salt.store(s, std::memory_order_relaxed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static void rand_bytes(uint8_t *buf, size_t n) {
|
|
RAND_bytes(buf, static_cast<int>(n));
|
|
}
|
|
|
|
static uint32_t rand_u32() {
|
|
uint32_t v;
|
|
rand_bytes(reinterpret_cast<uint8_t *>(&v), 4);
|
|
return v;
|
|
}
|
|
|
|
static uint32_t bounded(uint32_t n) {
|
|
if (n <= 1) return 0;
|
|
uint32_t t = static_cast<uint32_t>(-n) % n;
|
|
for (;;) { uint32_t v = rand_u32(); if (v >= t) return v % n; }
|
|
}
|
|
|
|
static void write_u16be(uint8_t *p, uint16_t v) {
|
|
p[0] = v >> 8; p[1] = v & 0xff;
|
|
}
|
|
|
|
// Simple append-only buffer over a caller-supplied array
|
|
struct Buf {
|
|
uint8_t *base;
|
|
uint32_t pos{0};
|
|
|
|
void u8(uint8_t v) { base[pos++] = v; }
|
|
void u16(uint16_t v) { base[pos++]=v>>8; base[pos++]=v&0xff; }
|
|
void u24(uint32_t v) { base[pos++]=(v>>16)&0xff; base[pos++]=(v>>8)&0xff; base[pos++]=v&0xff; }
|
|
void bytes(const void *p, size_t n) { memcpy(base+pos, p, n); pos+=n; }
|
|
void rnd(size_t n) { rand_bytes(base+pos, n); pos+=n; }
|
|
void zero(size_t n) { memset(base+pos, 0, n); pos+=n; }
|
|
uint32_t mark() { return pos; }
|
|
// patch big-endian u16 length at offset m (not counting the 2-byte field itself)
|
|
void patch16(uint32_t m) { write_u16be(base+m, static_cast<uint16_t>(pos-m-2)); }
|
|
// patch big-endian u24 length at offset m
|
|
void patch24(uint32_t m) {
|
|
uint32_t len = pos - m - 3;
|
|
base[m]=(len>>16)&0xff; base[m+1]=(len>>8)&0xff; base[m+2]=len&0xff;
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GREASE (RFC 8701) — 7 slots, Chrome-style
|
|
// ---------------------------------------------------------------------------
|
|
struct Grease {
|
|
uint16_t v[7];
|
|
Grease() {
|
|
uint8_t raw[7];
|
|
rand_bytes(raw, 7);
|
|
for (int i = 0; i < 7; i++)
|
|
v[i] = static_cast<uint16_t>(((raw[i] & 0xF0) | 0x0A) * 0x0101);
|
|
for (int i = 1; i < 7; i += 2)
|
|
if (v[i] == v[i-1]) v[i] ^= 0x1010;
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared cipher lists
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Chrome 131/133/147 cipher suite list
|
|
static const uint16_t CHROME_CIPHERS[] = {
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02b, 0xc02f, 0xc02c, 0xc030,
|
|
0xcca9, 0xcca8,
|
|
0xc013, 0xc014,
|
|
0x009c, 0x009d,
|
|
0x002f, 0x0035,
|
|
};
|
|
static const int CHROME_CIPHERS_N = sizeof(CHROME_CIPHERS)/sizeof(CHROME_CIPHERS[0]);
|
|
|
|
// Firefox 148/149 cipher suite list
|
|
static const uint16_t FIREFOX_CIPHERS[] = {
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02b, 0xc02f, 0xc02c, 0xc030,
|
|
0xcca9, 0xcca8,
|
|
0xc013, 0xc014,
|
|
0x009c, 0x009d,
|
|
0x002f, 0x0035, 0x000a,
|
|
};
|
|
static const int FIREFOX_CIPHERS_N = sizeof(FIREFOX_CIPHERS)/sizeof(FIREFOX_CIPHERS[0]);
|
|
|
|
// Safari / iOS cipher suite list
|
|
static const uint16_t SAFARI_CIPHERS[] = {
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02c, 0xc02b, 0xc030, 0xc02f,
|
|
0xcca9, 0xcca8,
|
|
0xc00a, 0xc009, 0xc014, 0xc013,
|
|
0x009d, 0x009c, 0x0035, 0x002f, 0x000a,
|
|
};
|
|
static const int SAFARI_CIPHERS_N = sizeof(SAFARI_CIPHERS)/sizeof(SAFARI_CIPHERS[0]);
|
|
|
|
// Android OkHttp cipher suite list (conservative)
|
|
static const uint16_t OKHTTP_CIPHERS[] = {
|
|
0x1301, 0x1302, 0x1303,
|
|
0xc02b, 0xc02f, 0xcca9, 0xcca8,
|
|
0xc02c, 0xc030, 0xc013, 0xc014,
|
|
0x009c, 0x009d, 0x002f, 0x0035,
|
|
};
|
|
static const int OKHTTP_CIPHERS_N = sizeof(OKHTTP_CIPHERS)/sizeof(OKHTTP_CIPHERS[0]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Key material: HMAC-SHA256(secret, domain || unix_time_be) → 32 bytes
|
|
// Used in the key_share "K" slot of the ClientHello, matching tdlib-obf.
|
|
// ---------------------------------------------------------------------------
|
|
static void compute_key_material(
|
|
const uint8_t *secret, size_t secret_len,
|
|
const std::string &domain, int32_t unix_time,
|
|
uint8_t out[32]) {
|
|
uint8_t tb[4] = {
|
|
uint8_t(unix_time >> 24), uint8_t(unix_time >> 16),
|
|
uint8_t(unix_time >> 8), uint8_t(unix_time),
|
|
};
|
|
HMAC_CTX *ctx = HMAC_CTX_new();
|
|
HMAC_Init_ex(ctx, secret, (int)secret_len, EVP_sha256(), nullptr);
|
|
HMAC_Update(ctx, (const uint8_t*)domain.data(), domain.size());
|
|
HMAC_Update(ctx, tb, 4);
|
|
unsigned int len = 32;
|
|
HMAC_Final(ctx, out, &len);
|
|
HMAC_CTX_free(ctx);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Profile builders
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Chrome 133 / 131 / 147 (Chromium family)
|
|
// has_pq=true (X25519MLKEM768, group 0x11EC), ALPS 0x44CD (133/147) or 0x4469 (131)
|
|
static uint32_t build_chrome(Buf &b, const std::string &domain,
|
|
const uint8_t *secret, size_t slen,
|
|
int32_t unix_time, uint16_t alps_type, bool has_pq) {
|
|
Grease g;
|
|
uint8_t km[32]; // key material (X25519 public key slot)
|
|
compute_key_material(secret, slen, domain, unix_time, km);
|
|
uint8_t ml_kem[1216]; // ML-KEM-768 public key placeholder (random)
|
|
rand_bytes(ml_kem, sizeof(ml_kem));
|
|
|
|
// TLS record header: \x16\x03\x01 <length u16>
|
|
b.u8(0x16); b.u8(0x03); b.u8(0x01);
|
|
uint32_t rec_len = b.mark(); b.u16(0); // patched later
|
|
|
|
// Handshake: ClientHello type=1, length u24
|
|
b.u8(0x01);
|
|
uint32_t hs_len = b.mark(); b.u24(0);
|
|
|
|
// client_version
|
|
b.u8(0x03); b.u8(0x03);
|
|
// client_random: 32 zeros (HMAC written into bytes[11..42] later by caller)
|
|
b.zero(32);
|
|
// session_id: 32 random bytes
|
|
b.u8(0x20); b.rnd(32);
|
|
|
|
// cipher_suites: GREASE + chrome list
|
|
uint32_t cs_len = b.mark(); b.u16(0);
|
|
b.u16(g.v[0]); // GREASE
|
|
for (int i = 0; i < CHROME_CIPHERS_N; i++) b.u16(CHROME_CIPHERS[i]);
|
|
b.patch16(cs_len);
|
|
|
|
// compression: null only
|
|
b.u8(0x01); b.u8(0x00);
|
|
|
|
// extensions
|
|
uint32_t ext_len = b.mark(); b.u16(0);
|
|
|
|
// GREASE extension
|
|
b.u16(g.v[2]); b.u16(0x00);
|
|
|
|
// SNI (0x0000)
|
|
b.u16(0x0000);
|
|
uint32_t sni_ext = b.mark(); b.u16(0);
|
|
uint32_t sni_list = b.mark(); b.u16(0);
|
|
b.u8(0x00); // host_name type
|
|
uint32_t sni_name = b.mark(); b.u16(0);
|
|
b.bytes(domain.data(), domain.size());
|
|
b.patch16(sni_name); b.patch16(sni_list); b.patch16(sni_ext);
|
|
|
|
// extended_master_secret (0x0017)
|
|
b.u16(0x0017); b.u16(0x0000);
|
|
|
|
// renegotiation_info (0xff01)
|
|
b.u16(0xff01); b.u16(0x0001); b.u8(0x00);
|
|
|
|
// supported_groups (0x000a): GREASE + X25519MLKEM768 + x25519 + secp256r1 + secp384r1
|
|
b.u16(0x000a);
|
|
uint32_t sg_ext = b.mark(); b.u16(0);
|
|
uint32_t sg_list = b.mark(); b.u16(0);
|
|
b.u16(g.v[4]); // GREASE
|
|
if (has_pq) b.u16(0x11EC); // X25519MLKEM768
|
|
b.u16(0x001d); b.u16(0x0017); b.u16(0x0018);
|
|
b.patch16(sg_list); b.patch16(sg_ext);
|
|
|
|
// ec_point_formats (0x000b)
|
|
b.u16(0x000b); b.u16(0x0002); b.u8(0x01); b.u8(0x00);
|
|
|
|
// session_ticket (0x0023)
|
|
b.u16(0x0023); b.u16(0x0000);
|
|
|
|
// ALPN (0x0010): h2 + http/1.1
|
|
b.u16(0x0010);
|
|
uint32_t alpn_ext = b.mark(); b.u16(0);
|
|
uint32_t alpn_list = b.mark(); b.u16(0);
|
|
b.u8(0x02); b.bytes("h2", 2);
|
|
b.u8(0x08); b.bytes("http/1.1", 8);
|
|
b.patch16(alpn_list); b.patch16(alpn_ext);
|
|
|
|
// status_request (0x0005)
|
|
b.u16(0x0005); b.u16(0x0005);
|
|
b.u8(0x01); b.u16(0x0000); b.u16(0x0000);
|
|
|
|
// signature_algorithms (0x000d)
|
|
b.u16(0x000d); b.u16(0x0012); b.u16(0x0010);
|
|
b.u16(0x0403); b.u16(0x0804); b.u16(0x0401); b.u16(0x0503);
|
|
b.u16(0x0805); b.u16(0x0501); b.u16(0x0806); b.u16(0x0601);
|
|
|
|
// signed_cert_timestamp (0x0012)
|
|
b.u16(0x0012); b.u16(0x0000);
|
|
|
|
// key_share (0x0033)
|
|
b.u16(0x0033);
|
|
uint32_t ks_ext = b.mark(); b.u16(0);
|
|
uint32_t ks_list = b.mark(); b.u16(0);
|
|
if (has_pq) {
|
|
b.u16(0x11EC); // X25519MLKEM768
|
|
b.u16(0x04C0); // key length 1216
|
|
b.bytes(ml_kem, 1216);
|
|
}
|
|
b.u16(0x001d); // x25519
|
|
b.u16(0x0020); // key length 32
|
|
b.bytes(km, 32);
|
|
b.patch16(ks_list); b.patch16(ks_ext);
|
|
|
|
// psk_key_exchange_modes (0x002d)
|
|
b.u16(0x002d); b.u16(0x0002); b.u8(0x01); b.u8(0x01);
|
|
|
|
// supported_versions (0x002b): GREASE + TLS1.3 + TLS1.2
|
|
b.u16(0x002b); b.u16(0x0007); b.u8(0x06);
|
|
b.u16(g.v[6]); b.u16(0x0304); b.u16(0x0303);
|
|
|
|
// compress_certificate (0x001b)
|
|
b.u16(0x001b); b.u16(0x0003); b.u8(0x02); b.u16(0x0002);
|
|
|
|
// GREASE extension (trailing)
|
|
b.u16(g.v[3]); b.u16(0x0001); b.u8(0x00);
|
|
|
|
// ALPS (application_settings) — type varies by Chrome version
|
|
if (alps_type != 0) {
|
|
b.u16(alps_type); b.u16(0x0005); b.u8(0x00); b.u8(0x03); b.u8(0x02);
|
|
b.bytes("h2", 2);
|
|
}
|
|
|
|
// padding (0x0015) — vary per build to defeat static record-size hashing
|
|
{
|
|
// target ~517 bytes for ClientHello body; add random entropy
|
|
uint32_t pad = (uint32_t)bounded(128);
|
|
b.u16(0x0015);
|
|
uint32_t pad_len = b.mark(); b.u16(0);
|
|
b.zero(pad);
|
|
b.patch16(pad_len);
|
|
}
|
|
|
|
b.patch16(ext_len);
|
|
b.patch24(hs_len);
|
|
b.patch16(rec_len);
|
|
return b.pos;
|
|
}
|
|
|
|
// Firefox 148 / 149
|
|
static uint32_t build_firefox(Buf &b, const std::string &domain,
|
|
const uint8_t *secret, size_t slen,
|
|
int32_t unix_time) {
|
|
Grease g;
|
|
uint8_t km[32];
|
|
compute_key_material(secret, slen, domain, unix_time, km);
|
|
uint8_t ml_kem[1216];
|
|
rand_bytes(ml_kem, sizeof(ml_kem));
|
|
|
|
b.u8(0x16); b.u8(0x03); b.u8(0x01);
|
|
uint32_t rec_len = b.mark(); b.u16(0);
|
|
b.u8(0x01);
|
|
uint32_t hs_len = b.mark(); b.u24(0);
|
|
|
|
b.u8(0x03); b.u8(0x03);
|
|
b.zero(32);
|
|
b.u8(0x20); b.rnd(32);
|
|
|
|
// ciphers (no leading GREASE for Firefox)
|
|
uint32_t cs_len = b.mark(); b.u16(0);
|
|
for (int i = 0; i < FIREFOX_CIPHERS_N; i++) b.u16(FIREFOX_CIPHERS[i]);
|
|
b.patch16(cs_len);
|
|
|
|
b.u8(0x01); b.u8(0x00);
|
|
|
|
uint32_t ext_len = b.mark(); b.u16(0);
|
|
|
|
// SNI
|
|
b.u16(0x0000);
|
|
uint32_t sni_ext = b.mark(); b.u16(0);
|
|
uint32_t sni_list = b.mark(); b.u16(0);
|
|
b.u8(0x00);
|
|
uint32_t sni_name = b.mark(); b.u16(0);
|
|
b.bytes(domain.data(), domain.size());
|
|
b.patch16(sni_name); b.patch16(sni_list); b.patch16(sni_ext);
|
|
|
|
// extended_master_secret
|
|
b.u16(0x0017); b.u16(0x0000);
|
|
|
|
// renegotiation_info
|
|
b.u16(0xff01); b.u16(0x0001); b.u8(0x00);
|
|
|
|
// supported_groups: X25519MLKEM768 + x25519 + secp256r1 + secp384r1 + secp521r1
|
|
b.u16(0x000a);
|
|
uint32_t sg_ext = b.mark(); b.u16(0);
|
|
uint32_t sg_list = b.mark(); b.u16(0);
|
|
b.u16(0x11EC); b.u16(0x001d); b.u16(0x0017); b.u16(0x0018); b.u16(0x0019);
|
|
b.patch16(sg_list); b.patch16(sg_ext);
|
|
|
|
// ec_point_formats
|
|
b.u16(0x000b); b.u16(0x0002); b.u8(0x01); b.u8(0x00);
|
|
|
|
// session_ticket
|
|
b.u16(0x0023); b.u16(0x0000);
|
|
|
|
// ALPN: http/1.1 only (Firefox proxy mode)
|
|
b.u16(0x0010);
|
|
uint32_t alpn_ext = b.mark(); b.u16(0);
|
|
uint32_t alpn_list = b.mark(); b.u16(0);
|
|
b.u8(0x08); b.bytes("http/1.1", 8);
|
|
b.patch16(alpn_list); b.patch16(alpn_ext);
|
|
|
|
// status_request
|
|
b.u16(0x0005); b.u16(0x0005);
|
|
b.u8(0x01); b.u16(0x0000); b.u16(0x0000);
|
|
|
|
// signature_algorithms
|
|
b.u16(0x000d); b.u16(0x0014); b.u16(0x0012);
|
|
b.u16(0x0403); b.u16(0x0804); b.u16(0x0401);
|
|
b.u16(0x0503); b.u16(0x0805); b.u16(0x0501);
|
|
b.u16(0x0806); b.u16(0x0601); b.u16(0x0201);
|
|
|
|
// key_share: X25519MLKEM768 + x25519
|
|
b.u16(0x0033);
|
|
uint32_t ks_ext = b.mark(); b.u16(0);
|
|
uint32_t ks_list = b.mark(); b.u16(0);
|
|
b.u16(0x11EC); b.u16(0x04C0); b.bytes(ml_kem, 1216);
|
|
b.u16(0x001d); b.u16(0x0020); b.bytes(km, 32);
|
|
b.patch16(ks_list); b.patch16(ks_ext);
|
|
|
|
// psk_key_exchange_modes
|
|
b.u16(0x002d); b.u16(0x0002); b.u8(0x01); b.u8(0x01);
|
|
|
|
// supported_versions: TLS 1.3 + 1.2
|
|
b.u16(0x002b); b.u16(0x0005); b.u8(0x04);
|
|
b.u16(0x0304); b.u16(0x0303);
|
|
|
|
// compress_certificate
|
|
b.u16(0x001b); b.u16(0x0003); b.u8(0x02); b.u16(0x0002);
|
|
|
|
// delegated_credentials (Firefox-specific, 0x0022)
|
|
b.u16(0x0022); b.u16(0x0012); b.u16(0x0010);
|
|
b.u16(0x0403); b.u16(0x0804); b.u16(0x0401);
|
|
b.u16(0x0503); b.u16(0x0805); b.u16(0x0501);
|
|
b.u16(0x0806); b.u16(0x0601); b.u16(0x0201); // 9 pairs = 18 bytes = 0x0012... correct
|
|
|
|
// record_size_limit (0x001c) — Firefox advertises 0x4001
|
|
b.u16(0x001c); b.u16(0x0002); b.u16(0x4001);
|
|
|
|
// padding
|
|
uint32_t pad = (uint32_t)bounded(96);
|
|
b.u16(0x0015);
|
|
uint32_t pad_len = b.mark(); b.u16(0);
|
|
b.zero(pad);
|
|
b.patch16(pad_len);
|
|
|
|
b.patch16(ext_len);
|
|
b.patch24(hs_len);
|
|
b.patch16(rec_len);
|
|
return b.pos;
|
|
}
|
|
|
|
// Safari 26 / iOS 14 (Apple TLS stack)
|
|
static uint32_t build_safari(Buf &b, const std::string &domain,
|
|
const uint8_t *secret, size_t slen,
|
|
int32_t unix_time) {
|
|
uint8_t km[32];
|
|
compute_key_material(secret, slen, domain, unix_time, km);
|
|
uint8_t ml_kem[1216];
|
|
rand_bytes(ml_kem, sizeof(ml_kem));
|
|
|
|
b.u8(0x16); b.u8(0x03); b.u8(0x01);
|
|
uint32_t rec_len = b.mark(); b.u16(0);
|
|
b.u8(0x01);
|
|
uint32_t hs_len = b.mark(); b.u24(0);
|
|
b.u8(0x03); b.u8(0x03);
|
|
b.zero(32);
|
|
b.u8(0x20); b.rnd(32);
|
|
|
|
uint32_t cs_len = b.mark(); b.u16(0);
|
|
for (int i = 0; i < SAFARI_CIPHERS_N; i++) b.u16(SAFARI_CIPHERS[i]);
|
|
b.patch16(cs_len);
|
|
b.u8(0x01); b.u8(0x00);
|
|
|
|
uint32_t ext_len = b.mark(); b.u16(0);
|
|
|
|
b.u16(0x0000);
|
|
uint32_t sni_ext = b.mark(); b.u16(0);
|
|
uint32_t sni_list = b.mark(); b.u16(0);
|
|
b.u8(0x00);
|
|
uint32_t sni_name = b.mark(); b.u16(0);
|
|
b.bytes(domain.data(), domain.size());
|
|
b.patch16(sni_name); b.patch16(sni_list); b.patch16(sni_ext);
|
|
|
|
b.u16(0x0017); b.u16(0x0000);
|
|
|
|
b.u16(0x000a);
|
|
uint32_t sg_ext = b.mark(); b.u16(0);
|
|
uint32_t sg_list = b.mark(); b.u16(0);
|
|
b.u16(0x11EC); b.u16(0x001d); b.u16(0x0017); b.u16(0x0018); b.u16(0x0019);
|
|
b.patch16(sg_list); b.patch16(sg_ext);
|
|
|
|
b.u16(0x000b); b.u16(0x0002); b.u8(0x01); b.u8(0x00);
|
|
b.u16(0x0023); b.u16(0x0000);
|
|
|
|
b.u16(0x0010);
|
|
uint32_t alpn_ext = b.mark(); b.u16(0);
|
|
uint32_t alpn_list = b.mark(); b.u16(0);
|
|
b.u8(0x08); b.bytes("http/1.1", 8);
|
|
b.patch16(alpn_list); b.patch16(alpn_ext);
|
|
|
|
b.u16(0x0005); b.u16(0x0005); b.u8(0x01); b.u16(0x0000); b.u16(0x0000);
|
|
|
|
b.u16(0x000d); b.u16(0x0012); b.u16(0x0010);
|
|
b.u16(0x0403); b.u16(0x0804); b.u16(0x0401);
|
|
b.u16(0x0503); b.u16(0x0805); b.u16(0x0501);
|
|
b.u16(0x0806); b.u16(0x0601);
|
|
|
|
b.u16(0x0033);
|
|
uint32_t ks_ext = b.mark(); b.u16(0);
|
|
uint32_t ks_list = b.mark(); b.u16(0);
|
|
b.u16(0x11EC); b.u16(0x04C0); b.bytes(ml_kem, 1216);
|
|
b.u16(0x001d); b.u16(0x0020); b.bytes(km, 32);
|
|
b.patch16(ks_list); b.patch16(ks_ext);
|
|
|
|
b.u16(0x002d); b.u16(0x0002); b.u8(0x01); b.u8(0x01);
|
|
b.u16(0x002b); b.u16(0x0005); b.u8(0x04); b.u16(0x0304); b.u16(0x0303);
|
|
b.u16(0xff01); b.u16(0x0001); b.u8(0x00);
|
|
|
|
uint32_t pad = (uint32_t)bounded(64);
|
|
b.u16(0x0015);
|
|
uint32_t pad_len = b.mark(); b.u16(0);
|
|
b.zero(pad);
|
|
b.patch16(pad_len);
|
|
|
|
b.patch16(ext_len); b.patch24(hs_len); b.patch16(rec_len);
|
|
return b.pos;
|
|
}
|
|
|
|
// Android OkHttp (no ECH, no PQ)
|
|
static uint32_t build_okhttp(Buf &b, const std::string &domain,
|
|
const uint8_t *secret, size_t slen,
|
|
int32_t unix_time) {
|
|
uint8_t km[32];
|
|
compute_key_material(secret, slen, domain, unix_time, km);
|
|
|
|
b.u8(0x16); b.u8(0x03); b.u8(0x01);
|
|
uint32_t rec_len = b.mark(); b.u16(0);
|
|
b.u8(0x01);
|
|
uint32_t hs_len = b.mark(); b.u24(0);
|
|
b.u8(0x03); b.u8(0x03);
|
|
b.zero(32);
|
|
b.u8(0x00); // no session_id
|
|
|
|
uint32_t cs_len = b.mark(); b.u16(0);
|
|
for (int i = 0; i < OKHTTP_CIPHERS_N; i++) b.u16(OKHTTP_CIPHERS[i]);
|
|
b.patch16(cs_len);
|
|
b.u8(0x01); b.u8(0x00);
|
|
|
|
uint32_t ext_len = b.mark(); b.u16(0);
|
|
|
|
b.u16(0x0000);
|
|
uint32_t sni_ext = b.mark(); b.u16(0);
|
|
uint32_t sni_list = b.mark(); b.u16(0);
|
|
b.u8(0x00);
|
|
uint32_t sni_name = b.mark(); b.u16(0);
|
|
b.bytes(domain.data(), domain.size());
|
|
b.patch16(sni_name); b.patch16(sni_list); b.patch16(sni_ext);
|
|
|
|
b.u16(0x000a);
|
|
uint32_t sg_ext = b.mark(); b.u16(0);
|
|
uint32_t sg_list = b.mark(); b.u16(0);
|
|
b.u16(0x001d); b.u16(0x0017); b.u16(0x0018);
|
|
b.patch16(sg_list); b.patch16(sg_ext);
|
|
|
|
b.u16(0x000b); b.u16(0x0002); b.u8(0x01); b.u8(0x00);
|
|
b.u16(0x0023); b.u16(0x0000);
|
|
|
|
b.u16(0x0010);
|
|
uint32_t alpn_ext = b.mark(); b.u16(0);
|
|
uint32_t alpn_list = b.mark(); b.u16(0);
|
|
b.u8(0x08); b.bytes("http/1.1", 8);
|
|
b.patch16(alpn_list); b.patch16(alpn_ext);
|
|
|
|
b.u16(0x000d); b.u16(0x000c); b.u16(0x000a);
|
|
b.u16(0x0403); b.u16(0x0401); b.u16(0x0503);
|
|
b.u16(0x0501); b.u16(0x0603); b.u16(0x0601);
|
|
|
|
b.u16(0x0033);
|
|
uint32_t ks_ext = b.mark(); b.u16(0);
|
|
uint32_t ks_list = b.mark(); b.u16(0);
|
|
b.u16(0x001d); b.u16(0x0020); b.bytes(km, 32);
|
|
b.patch16(ks_list); b.patch16(ks_ext);
|
|
|
|
b.u16(0x002d); b.u16(0x0002); b.u8(0x01); b.u8(0x01);
|
|
b.u16(0x002b); b.u16(0x0005); b.u8(0x04); b.u16(0x0304); b.u16(0x0303);
|
|
|
|
b.patch16(ext_len); b.patch24(hs_len); b.patch16(rec_len);
|
|
return b.pos;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public API
|
|
// ---------------------------------------------------------------------------
|
|
|
|
uint32_t buildTlsHelloForProfile(
|
|
const std::string &domain,
|
|
const uint8_t *secret, size_t secret_len,
|
|
int32_t unix_time,
|
|
BrowserProfile profile,
|
|
uint8_t *out) {
|
|
Buf b{out, 0};
|
|
switch (profile) {
|
|
case BrowserProfile::Chrome133:
|
|
return build_chrome(b, domain, secret, secret_len, unix_time, 0x44CD, true);
|
|
case BrowserProfile::Chrome131:
|
|
return build_chrome(b, domain, secret, secret_len, unix_time, 0x4469, true);
|
|
case BrowserProfile::Chrome120:
|
|
return build_chrome(b, domain, secret, secret_len, unix_time, 0x4469, false);
|
|
case BrowserProfile::Chrome147_Windows:
|
|
case BrowserProfile::Chrome147_IOSChromium:
|
|
return build_chrome(b, domain, secret, secret_len, unix_time, 0x44CD, true);
|
|
case BrowserProfile::Firefox148:
|
|
case BrowserProfile::Firefox149_Windows:
|
|
return build_firefox(b, domain, secret, secret_len, unix_time);
|
|
case BrowserProfile::Safari26_3:
|
|
case BrowserProfile::IOS14:
|
|
return build_safari(b, domain, secret, secret_len, unix_time);
|
|
case BrowserProfile::Android11_OkHttp:
|
|
return build_okhttp(b, domain, secret, secret_len, unix_time);
|
|
default:
|
|
return build_chrome(b, domain, secret, secret_len, unix_time, 0x44CD, true);
|
|
}
|
|
}
|
|
|
|
BrowserProfile pickProfile(const std::string &destination, int32_t unix_time,
|
|
uint32_t per_run_salt) {
|
|
static const struct { BrowserProfile p; uint32_t w; } TABLE[] = {
|
|
{ BrowserProfile::Chrome133, 50 },
|
|
{ BrowserProfile::Chrome131, 20 },
|
|
{ BrowserProfile::Chrome120, 15 },
|
|
{ BrowserProfile::Chrome147_Windows, 10 },
|
|
{ BrowserProfile::Chrome147_IOSChromium, 30 },
|
|
{ BrowserProfile::Firefox148, 15 },
|
|
{ BrowserProfile::Firefox149_Windows, 10 },
|
|
{ BrowserProfile::Safari26_3, 5 },
|
|
{ BrowserProfile::IOS14, 5 },
|
|
{ BrowserProfile::Android11_OkHttp, 30 },
|
|
};
|
|
static const uint32_t TOTAL = 190;
|
|
|
|
uint32_t daily = static_cast<uint32_t>(unix_time / 86400);
|
|
uint32_t h = per_run_salt;
|
|
h ^= daily * 2654435761u;
|
|
for (char c : destination) h = h * 31 + static_cast<uint8_t>(c);
|
|
h ^= h >> 16; h *= 0x45d9f3b; h ^= h >> 16;
|
|
|
|
uint32_t slot = h % TOTAL, acc = 0;
|
|
for (auto &e : TABLE) {
|
|
acc += e.w;
|
|
if (slot < acc) return e.p;
|
|
}
|
|
return BrowserProfile::Chrome133;
|
|
}
|
|
|
|
uint32_t buildStealthTlsHello(
|
|
const std::string &domain,
|
|
const uint8_t *secret, size_t secret_len,
|
|
int32_t unix_time,
|
|
uint8_t *out) {
|
|
uint32_t salt = g_run_salt.load(std::memory_order_relaxed);
|
|
if (salt == 0) {
|
|
RAND_bytes(reinterpret_cast<uint8_t *>(&salt), 4);
|
|
g_run_salt.store(salt, std::memory_order_relaxed);
|
|
}
|
|
BrowserProfile profile = pickProfile(domain, unix_time, salt);
|
|
return buildTlsHelloForProfile(domain, secret, secret_len, unix_time, profile, out);
|
|
}
|
|
|
|
} // namespace stealth
|