/* * StealthTlsHello.cpp * Ported from telemt/tdlib-obf (MIT License, Copyright 2026 telemt community) * Adapted for tgnet — C++17, OpenSSL only. */ #include "StealthTlsHello.h" #include #include #include #include #include #include #include #include namespace stealth { // --------------------------------------------------------------------------- // Per-run salt — initialised once, survives for the app lifetime. // Ensures same destination picks different profiles across restarts. // --------------------------------------------------------------------------- static std::atomic g_run_salt{0}; void initStealthSalt() { uint32_t s = 0; RAND_bytes(reinterpret_cast(&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(n)); } static uint32_t rand_u32() { uint32_t v; rand_bytes(reinterpret_cast(&v), 4); return v; } static uint32_t bounded(uint32_t n) { if (n <= 1) return 0; uint32_t t = static_cast(-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(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(((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 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(unix_time / 86400); uint32_t h = per_run_salt; h ^= daily * 2654435761u; for (char c : destination) h = h * 31 + static_cast(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(&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