Browse Source

reality/stcp: fix OOB read in key_share parse, padding clamp, shbuf leak

- reality_parse_client_hello: bound shares_len by extension data length
  (len-2) to prevent stack-buffer-overflow from malformed ClientHello
- stcp handshake: reject padding_size > STCP_HS_PADDING_MAX (512)
- reality_ch_body_cb: free shbuf on stcp_try_send error (leak)
- tests: fuzz key_share parse; raw-socket handshake control/clamp/garbage

Signed-off-by: uTun <dev@utun>
proxy
evgeny 2 weeks ago
parent
commit
1340ab123b
  1. 1
      src/transport_layer/reality.c
  2. 1
      src/transport_layer/stcp.h
  3. 4
      src/transport_layer/stcp_client.c
  4. 6
      src/transport_layer/stcp_server.c
  5. 70
      tests/test_reality_hello.c
  6. 141
      tests/test_stcp.c

1
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) {

1
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

4
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];

6
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;

70
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 =

141
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);

Loading…
Cancel
Save