diff --git a/src/transport_layer/reality.c b/src/transport_layer/reality.c index d2431fde..3764eff8 100644 --- a/src/transport_layer/reality.c +++ b/src/transport_layer/reality.c @@ -465,6 +465,7 @@ static int reality_parse_client_hello(const uint8_t *ch, size_t ch_len, if (remaining < 4 + len) return REALITY_ERR_FORMAT; if (type == TLS_EXT_KEY_SHARE && len >= 4) { uint16_t shares_len = (uint16_t)((ext[4] << 8) | ext[5]); + if ((size_t)shares_len > (size_t)(len - 2)) return REALITY_ERR_FORMAT; const uint8_t *s = ext + 6; size_t sl = shares_len; while (sl >= 4) { diff --git a/src/transport_layer/stcp.h b/src/transport_layer/stcp.h index 862127c7..8fc91b4e 100644 --- a/src/transport_layer/stcp.h +++ b/src/transport_layer/stcp.h @@ -42,6 +42,7 @@ typedef void (*stcp_ping_cb)(int success, uint16_t rtt, void *arg); // [44..45] keepalive_interval // [46] flags #define STCP_HS_PLAIN_SIZE 47 // без CRC32 +#define STCP_HS_PADDING_MAX 512 // верхняя граница padding_size в рукопожатии (легитимно 8) #define STCP_HANDSHAKE_FLAG_PING 0x01 diff --git a/src/transport_layer/stcp_client.c b/src/transport_layer/stcp_client.c index b61a7835..0771431c 100644 --- a/src/transport_layer/stcp_client.c +++ b/src/transport_layer/stcp_client.c @@ -113,6 +113,10 @@ static void client_hs_cb(struct stcp_conn *c, uint8_t *data, size_t len) { c->peer_got_initial_pkt = enc_hs[32]; { uint64_t rid_be; memcpy(&rid_be, enc_hs + 33, 8); c->peer_reset_id = be64toh(rid_be); } uint16_t padding_size = (uint16_t)enc_hs[41] | ((uint16_t)enc_hs[42] << 8); + if (padding_size > STCP_HS_PADDING_MAX) { + DEBUG_WARN(DEBUG_CATEGORY_ETCP, "stcp_client: server handshake padding too large: %u", padding_size); + stcp_conn_do_close(c, 4); return; + } c->peer_device_type = enc_hs[43]; c->peer_keepalive_interval = ((uint16_t)enc_hs[44] << 8) | enc_hs[45]; c->peer_flags = enc_hs[46]; diff --git a/src/transport_layer/stcp_server.c b/src/transport_layer/stcp_server.c index c13e4efb..a90c7ddc 100644 --- a/src/transport_layer/stcp_server.c +++ b/src/transport_layer/stcp_server.c @@ -97,6 +97,10 @@ static void server_hs_phase1_cb(struct stcp_conn *c, uint8_t *data, size_t len) c->peer_got_initial_pkt = enc_hs[32]; { uint64_t rid_be; memcpy(&rid_be, enc_hs + 33, 8); c->peer_reset_id = be64toh(rid_be); } uint16_t padding_size = (uint16_t)enc_hs[41] | ((uint16_t)enc_hs[42] << 8); + if (padding_size > STCP_HS_PADDING_MAX) { + DEBUG_WARN(DEBUG_CATEGORY_ETCP, "stcp_server: handshake padding too large: %u", padding_size); + stcp_conn_do_close(c, 2); return; + } c->peer_device_type = enc_hs[43]; c->peer_keepalive_interval = ((uint16_t)enc_hs[44] << 8) | enc_hs[45]; c->peer_flags = enc_hs[46]; @@ -239,7 +243,7 @@ static void reality_ch_body_cb(struct stcp_conn *c, uint8_t *data, size_t len) { uint8_t *shbuf = u_malloc(sh_len); if (!shbuf) { stcp_conn_do_close(c, ENOMEM); return; } memcpy(shbuf, sh, sh_len); - if (stcp_try_send(c, shbuf, sh_len) < 0) { stcp_conn_do_close(c, 1); return; } + if (stcp_try_send(c, shbuf, sh_len) < 0) { u_free(shbuf); stcp_conn_do_close(c, 1); return; } DEBUG_INFO(DEBUG_CATEGORY_REALITY, "stcp_server: reality auth OK, continue STCP handshake"); stcp_recv_set(c, SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT, 0, server_hs_phase1_cb); return; diff --git a/tests/test_reality_hello.c b/tests/test_reality_hello.c index b9be94b8..690f0c21 100644 --- a/tests/test_reality_hello.c +++ b/tests/test_reality_hello.c @@ -60,6 +60,37 @@ static void init_server_cfg(struct reality_server_config *sc, sc->fingerprint = REALITY_FP_CHROME; } +// Собирает структурно-валидный ClientHello (TLS record + handshake), тело которого +// содержит ровно одну extension-запись ext_block/ext_block_len. Для fuzz-проверок +// парсера (malformed key_share и т.п.). +static size_t build_ch_with_ext(uint8_t *out, size_t out_cap, + const uint8_t *ext_block, size_t ext_block_len) { + (void)out_cap; + uint8_t body[REALITY_MAX_CH_SIZE]; + size_t b = 0; + body[b++] = 0x03; body[b++] = 0x03; // legacy_version + memset(body + b, 0x11, 32); b += 32; // random + body[b++] = 32; // session_id len + memset(body + b, 0x00, 32); b += 32; // session_id (нули) + body[b++] = 0x00; body[b++] = 0x02; // cipher_suites: 1 суит + body[b++] = 0x13; body[b++] = 0x01; // TLS_AES_128_GCM_SHA256 + body[b++] = 0x01; body[b++] = 0x00; // compression (null) + body[b++] = (uint8_t)(ext_block_len >> 8); // extensions len + body[b++] = (uint8_t)(ext_block_len & 0xff); + memcpy(body + b, ext_block, ext_block_len); b += ext_block_len; + + size_t body_len = b; + size_t hs_len = 4 + body_len; + out[0] = 0x16; out[1] = 0x03; out[2] = 0x01; // TLS record header + out[3] = (uint8_t)(hs_len >> 8); out[4] = (uint8_t)(hs_len & 0xff); + out[5] = 0x01; // handshake type = ClientHello + out[6] = (uint8_t)(body_len >> 16); + out[7] = (uint8_t)(body_len >> 8); + out[8] = (uint8_t)(body_len & 0xff); + memcpy(out + 9, body, body_len); + return 5 + hs_len; +} + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); @@ -175,6 +206,45 @@ int main(void) { CHECK(reality_client_hello_build(&cc, ch, 16, &ch_len) == REALITY_ERR_ARG, "client build small buffer rejected"); CHECK(reality_server_hello_build(&sc, NULL, 0, sh, sizeof(sh), &sh_len) == REALITY_ERR_ARG, "server build NULL input rejected"); + // ── Сценарий 9b: malformed key_share (fuzz парсера, без OOB-read) ── + init_server_cfg(&sc, srv_priv, g_short_id, g_version, 60); + { + uint8_t ch2[REALITY_MAX_CH_SIZE], sh2[REALITY_MAX_SH_SIZE]; + size_t ch2_len = 0, sh2_len = 0; + uint8_t ks[10]; + + // shares_len (0xFFFF) не влезает в extension-данные (len=4) → FORMAT, без OOB + memset(ks, 0, sizeof(ks)); + ks[0] = 0x00; ks[1] = 0x33; // type = key_share + ks[2] = 0x00; ks[3] = 0x04; // len = 4 + ks[4] = 0xFF; ks[5] = 0xFF; // shares_len = 0xFFFF (завышен) + ch2_len = build_ch_with_ext(ch2, sizeof(ch2), ks, 8); + CHECK(reality_server_hello_build(&sc, ch2, ch2_len, sh2, sizeof(sh2), &sh2_len) == REALITY_ERR_FORMAT, + "key_share shares_len > ext len rejected"); + + // запись X25519 с klen=32, но без тела ключа → AUTH, без OOB + memset(ks, 0, sizeof(ks)); + ks[0] = 0x00; ks[1] = 0x33; // type = key_share + ks[2] = 0x00; ks[3] = 0x06; // len = 6 + ks[4] = 0x00; ks[5] = 0x04; // shares_len = 4 (= len-2) + ks[6] = 0x00; ks[7] = 0x1d; // group = X25519 + ks[8] = 0x00; ks[9] = 0x20; // klen = 32 (тела нет) + ch2_len = build_ch_with_ext(ch2, sizeof(ch2), ks, sizeof(ks)); + CHECK(reality_server_hello_build(&sc, ch2, ch2_len, sh2, sizeof(sh2), &sh2_len) == REALITY_ERR_AUTH, + "key_share truncated X25519 entry → AUTH"); + + // цепочка записей не-X25519 с klen=0 (в границах) → AUTH, без OOB + memset(ks, 0, sizeof(ks)); + ks[0] = 0x00; ks[1] = 0x33; // type = key_share + ks[2] = 0x00; ks[3] = 0x06; // len = 6 + ks[4] = 0x00; ks[5] = 0x04; // shares_len = 4 + ks[6] = 0x00; ks[7] = 0x17; // group = secp256r1 + ks[8] = 0x00; ks[9] = 0x00; // klen = 0 + ch2_len = build_ch_with_ext(ch2, sizeof(ch2), ks, sizeof(ks)); + CHECK(reality_server_hello_build(&sc, ch2, ch2_len, sh2, sizeof(sh2), &sh2_len) == REALITY_ERR_AUTH, + "key_share without X25519 → AUTH"); + } + // ── Сценарий 10: парсинг секции [reality] из конфига ── { const char *cfg_text = diff --git a/tests/test_stcp.c b/tests/test_stcp.c index 29ee71ed..4e88e371 100644 --- a/tests/test_stcp.c +++ b/tests/test_stcp.c @@ -404,6 +404,144 @@ static int test8_srv_recv_close(void) { return 0; } +// ======================= raw TCP client helpers ======================= + +// Подключается к 127.0.0.1:port, отправляет len байт, оставляет сокет открытым. +// Возвращает открытый socket (SOCKET_INVALID при ошибке). +static socket_t raw_connect_send(uint16_t port, const uint8_t *data, size_t len) { + socket_t s = socket(AF_INET, SOCK_STREAM, 0); + if (s == SOCKET_INVALID) return SOCKET_INVALID; + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) != 1) { socket_close_wrapper(s); return SOCKET_INVALID; } + if (connect(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) { socket_close_wrapper(s); return SOCKET_INVALID; } + size_t off = 0; + while (off < len) { + int n = (int)send(s, data + off, len - off, 0); + if (n <= 0) { socket_close_wrapper(s); return SOCKET_INVALID; } + off += (size_t)n; + } + return s; +} + +// Собирает криптографически валидный client handshake (91 байт): +// salt(8) + obfuscated_client_pubkey(32) + зашифрованный hs(51) с заданным padding_size. +// Клиентская сторона: X25519(c_keys.priv, s_keys.pub), stream STCP_STREAM_CLIENT_SEND. +static int build_malicious_handshake(uint16_t padding_size, uint8_t *out) { + uint8_t salt[SC_PUBKEY_ENC_SALT_SIZE]; + memset(salt, 0x5A, sizeof(salt)); + memcpy(out, salt, SC_PUBKEY_ENC_SALT_SIZE); + if (sc_obfuscate_pubkey(salt, s_keys.public_key, c_keys.public_key, + out + SC_PUBKEY_ENC_SALT_SIZE) != SC_OK) return -1; + + uint8_t plain[STCP_HS_PLAIN_SIZE]; + memset(plain, 0, sizeof(plain)); + plain[41] = (uint8_t)(padding_size & 0xff); + plain[42] = (uint8_t)(padding_size >> 8); + + uint8_t enc[STCP_HS_ENC_CLIENT]; + memcpy(enc, plain, STCP_HS_PLAIN_SIZE); + uint32_t crc = crc32_calc(plain, STCP_HS_PLAIN_SIZE); + enc[47] = (uint8_t)(crc >> 0); enc[48] = (uint8_t)(crc >> 8); + enc[49] = (uint8_t)(crc >> 16); enc[50] = (uint8_t)(crc >> 24); + + struct secure_channel sc; + sc_init_ctx(&sc, &c_keys); + if (sc_set_peer_public_key(&sc, s_keys.public_key, SC_PEER_PUBKEY_BIN) != SC_OK) return -1; + struct sc_stream_state ss; + if (sc_stream_init(&sc, &ss, STCP_STREAM_CLIENT_SEND) != SC_OK) return -1; + int rc = sc_stream_xor(&ss, enc, STCP_HS_ENC_CLIENT); + sc_stream_cleanup(&ss); + if (rc != SC_OK) return -1; + + memcpy(out + SC_PUBKEY_ENC_SIZE, enc, STCP_HS_ENC_CLIENT); + return 0; +} + +// ======================= test 9: контроль — raw handshake с padding=8 принимается ======================= + +static int test9_raw_handshake_ok(void) { + struct UASYNC *ua = uasync_create(); TASSERT(ua); + struct test_peer srv = {0}; + uint16_t port = BASE_PORT + 9; + + struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss); + + uint8_t buf[SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT + 8]; + TASSERT(build_malicious_handshake(8, buf) == 0); + memset(buf + SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT, 0, 8); // 8 байт паддинга (как легитимный клиент) + socket_t s = raw_connect_send(port, buf, sizeof(buf)); + TASSERT(s != SOCKET_INVALID); + + int ticks = 0; + while (!srv.ready && !srv.closed && ticks < 200) { uasync_poll(ua, 10); ticks++; } + TASSERT(srv.ready); + TASSERT(!srv.closed); + + socket_close_wrapper(s); + peer_cleanup(&srv); + if (srv.conn) stcp_conn_free(srv.conn); + stcp_server_destroy(ss); + uasync_destroy(ua, 1); + return 0; +} + +// ======================= test 10: oversized padding отклоняется (clamp) ======================= + +static int test10_oversized_padding(void) { + struct UASYNC *ua = uasync_create(); TASSERT(ua); + struct test_peer srv = {0}; + uint16_t port = BASE_PORT + 10; + + struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss); + + uint8_t buf[SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT]; + TASSERT(build_malicious_handshake(0xFFFF, buf) == 0); + socket_t s = raw_connect_send(port, buf, sizeof(buf)); + TASSERT(s != SOCKET_INVALID); + + int ticks = 0; + while (!srv.closed && ticks < 200) { uasync_poll(ua, 10); ticks++; } + TASSERT(srv.closed); + TASSERT(!srv.ready); + + socket_close_wrapper(s); + peer_cleanup(&srv); + srv.conn = NULL; // уже освобождён deferred-close (allocated=1) + stcp_server_destroy(ss); + uasync_destroy(ua, 1); + return 0; +} + +// ======================= test 11: мусор в handshake не валит сервер ======================= + +static int test11_garbage_handshake(void) { + struct UASYNC *ua = uasync_create(); TASSERT(ua); + struct test_peer srv = {0}; + uint16_t port = BASE_PORT + 11; + + struct stcp_server *ss = stcp_server_create(ua, port, &s_keys, NULL, NULL, server_connect_cb, &srv, peer_close_cb, &srv, AF_INET); TASSERT(ss); + + uint8_t buf[SC_PUBKEY_ENC_SIZE + STCP_HS_ENC_CLIENT]; + for (int i = 0; i < (int)sizeof(buf); i++) buf[i] = (uint8_t)(i * 31 + 7); + socket_t s = raw_connect_send(port, buf, sizeof(buf)); + TASSERT(s != SOCKET_INVALID); + + int ticks = 0; + while (!srv.closed && ticks < 200) { uasync_poll(ua, 10); ticks++; } + TASSERT(srv.closed); + TASSERT(!srv.ready); + + socket_close_wrapper(s); + peer_cleanup(&srv); + srv.conn = NULL; + stcp_server_destroy(ss); + uasync_destroy(ua, 1); + return 0; +} + // ======================= main ======================= int main(void) { @@ -425,6 +563,9 @@ int main(void) { TRUN(test6_interleaved); TRUN(test7_bulk_4mb); TRUN(test8_srv_recv_close); + TRUN(test9_raw_handshake_ok); + TRUN(test10_oversized_padding); + TRUN(test11_garbage_handshake); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "============================================"); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Results: %d/%d passed", tests_passed, tests_total);