Skip to content

Commit 315a705

Browse files
parasol-aserjeffhuang
andauthored
fuzz: harden ndpi_hex_decode and add targeted parser harnesses (#3163)
Closes #3159 Co-authored-by: jeffhuang <jeff@docker.xydrsucermoubd24xgo33yhsgd.bx.internal.cloudapp.net>
1 parent 836eefc commit 315a705

12 files changed

Lines changed: 532 additions & 17 deletions

File tree

example/utests.c

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2296,6 +2296,148 @@ static void cryptoUnitTest(void) {
22962296
}
22972297

22982298

2299+
static void checkHexDecode(const u_char *encoded, size_t encoded_len,
2300+
const u_char *expected, size_t expected_len) {
2301+
size_t out_len = 0;
2302+
u_char *decoded;
2303+
2304+
decoded = ndpi_hex_decode(encoded, encoded_len, &out_len);
2305+
assert(decoded != NULL);
2306+
assert(out_len == expected_len);
2307+
assert(memcmp(decoded, expected, expected_len) == 0);
2308+
assert(decoded[out_len] == '\0');
2309+
2310+
ndpi_free(decoded);
2311+
}
2312+
2313+
static void checkTlsBlocksDecode(const u_char *encoded, size_t encoded_len,
2314+
const struct ndpi_tls_block *expected_blocks,
2315+
u_int8_t expected_num_blocks) {
2316+
u_int8_t num_blocks = 0;
2317+
u_int8_t i;
2318+
struct ndpi_tls_block *decoded;
2319+
2320+
decoded = ndpi_decode_tls_blocks(encoded, encoded_len, &num_blocks);
2321+
assert(decoded != NULL);
2322+
assert(num_blocks == expected_num_blocks);
2323+
2324+
for(i = 0; i < num_blocks; i++) {
2325+
assert(decoded[i].block_type == expected_blocks[i].block_type);
2326+
assert(decoded[i].same_pkt == expected_blocks[i].same_pkt);
2327+
assert(decoded[i].len == expected_blocks[i].len);
2328+
}
2329+
2330+
ndpi_free(decoded);
2331+
}
2332+
2333+
static void rewriteHexCase(u_char *encoded, size_t encoded_len) {
2334+
size_t i;
2335+
2336+
for(i = 0; i < encoded_len; i++) {
2337+
if(encoded[i] >= 'a' && encoded[i] <= 'f') {
2338+
if((i & 0x1) == 0)
2339+
encoded[i] -= 'a' - 'A';
2340+
} else if(encoded[i] >= 'A' && encoded[i] <= 'F') {
2341+
if((i & 0x1) == 1)
2342+
encoded[i] += 'a' - 'A';
2343+
}
2344+
}
2345+
}
2346+
2347+
static void hexDecodeUnitTest(void) {
2348+
static const u_char lower_hex[] = { 'a', 'f' };
2349+
static const u_char upper_hex[] = { 'A', 'F' };
2350+
static const u_char mixed_hex[] = { '0', '1', 'a', 'B', 'c', 'D' };
2351+
static const u_char zero_hex[] = { '0', '0', 'f', 'F' };
2352+
static const u_char invalid_hex[] = { '0', 'g' };
2353+
static const u_char odd_hex[] = { '0' };
2354+
static const u_char lower_expected[] = { 0xAF };
2355+
static const u_char upper_expected[] = { 0xAF };
2356+
static const u_char mixed_expected[] = { 0x01, 0xAB, 0xCD };
2357+
static const u_char zero_expected[] = { 0x00, 0xFF };
2358+
size_t out_len = 123;
2359+
u_char *decoded;
2360+
2361+
checkHexDecode(lower_hex, sizeof(lower_hex), lower_expected, sizeof(lower_expected));
2362+
checkHexDecode(upper_hex, sizeof(upper_hex), upper_expected, sizeof(upper_expected));
2363+
checkHexDecode(mixed_hex, sizeof(mixed_hex), mixed_expected, sizeof(mixed_expected));
2364+
checkHexDecode(zero_hex, sizeof(zero_hex), zero_expected, sizeof(zero_expected));
2365+
2366+
decoded = ndpi_hex_decode(invalid_hex, sizeof(invalid_hex), &out_len);
2367+
assert(decoded == NULL);
2368+
assert(out_len == 0);
2369+
2370+
out_len = 123;
2371+
decoded = ndpi_hex_decode(odd_hex, sizeof(odd_hex), &out_len);
2372+
assert(decoded == NULL);
2373+
assert(out_len == 0);
2374+
2375+
out_len = 123;
2376+
decoded = ndpi_hex_decode((const u_char *)"", 0, &out_len);
2377+
assert(decoded != NULL);
2378+
assert(out_len == 0);
2379+
assert(decoded[0] == '\0');
2380+
ndpi_free(decoded);
2381+
}
2382+
2383+
static void tlsBlocksUnitTest(void) {
2384+
struct ndpi_tls_block expected_blocks[] = {
2385+
{ .block_type = tls_handshake_client_hello, .len = 0x0000, .same_pkt = 1 },
2386+
{ .block_type = tls_application_data, .len = 0xFFFF, .same_pkt = 0 },
2387+
{ .block_type = tls_application_data, .len = 0x00AF, .same_pkt = 1 }
2388+
};
2389+
static const u_char truncated_tls_blocks[] = { '0', '0', '0', '1' };
2390+
static const u_char invalid_tls_blocks[] = { '0', 'g', '0', '0', '0', '0' };
2391+
static const u_char odd_tls_blocks[] = { '0', '0', '0' };
2392+
u_int8_t expected_num_blocks = sizeof(expected_blocks) / sizeof(expected_blocks[0]);
2393+
u_int8_t num_blocks = 0;
2394+
size_t encoded_len;
2395+
size_t oversized_encoded_len = (size_t)256 * 6;
2396+
u_char *encoded;
2397+
u_char *encoded_no_nul;
2398+
u_char *oversized_encoded;
2399+
2400+
encoded = ndpi_encode_tls_blocks(expected_blocks, expected_num_blocks);
2401+
assert(encoded != NULL);
2402+
2403+
encoded_len = strlen((const char *)encoded);
2404+
encoded_no_nul = ndpi_malloc(encoded_len);
2405+
assert(encoded_no_nul != NULL);
2406+
2407+
memcpy(encoded_no_nul, encoded, encoded_len);
2408+
2409+
checkTlsBlocksDecode(encoded_no_nul, encoded_len, expected_blocks, expected_num_blocks);
2410+
rewriteHexCase(encoded_no_nul, encoded_len);
2411+
checkTlsBlocksDecode(encoded_no_nul, encoded_len, expected_blocks, expected_num_blocks);
2412+
ndpi_free(encoded_no_nul);
2413+
ndpi_free(encoded);
2414+
2415+
num_blocks = expected_num_blocks;
2416+
assert(ndpi_decode_tls_blocks(truncated_tls_blocks, sizeof(truncated_tls_blocks), &num_blocks) == NULL);
2417+
assert(num_blocks == 0);
2418+
2419+
num_blocks = expected_num_blocks;
2420+
assert(ndpi_decode_tls_blocks(invalid_tls_blocks, sizeof(invalid_tls_blocks), &num_blocks) == NULL);
2421+
assert(num_blocks == 0);
2422+
2423+
num_blocks = expected_num_blocks;
2424+
assert(ndpi_decode_tls_blocks(odd_tls_blocks, sizeof(odd_tls_blocks), &num_blocks) == NULL);
2425+
assert(num_blocks == 0);
2426+
2427+
oversized_encoded = ndpi_malloc(oversized_encoded_len);
2428+
assert(oversized_encoded != NULL);
2429+
memset(oversized_encoded, '0', oversized_encoded_len);
2430+
2431+
num_blocks = expected_num_blocks;
2432+
assert(ndpi_decode_tls_blocks(oversized_encoded, oversized_encoded_len, &num_blocks) == NULL);
2433+
assert(num_blocks == 0);
2434+
ndpi_free(oversized_encoded);
2435+
2436+
num_blocks = expected_num_blocks;
2437+
assert(ndpi_decode_tls_blocks((const u_char *)"", 0, &num_blocks) == NULL);
2438+
assert(num_blocks == 0);
2439+
}
2440+
22992441
void run_unit_tests() {
23002442

23012443
checkRankingUnitTest(false);
@@ -2352,5 +2494,7 @@ void run_unit_tests() {
23522494
bitmap64FuseUnitTest();
23532495
riskUtilsUnitTest();
23542496
cryptoUnitTest();
2497+
hexDecodeUnitTest();
2498+
tlsBlocksUnitTest();
23552499

23562500
}

fuzz/Makefile.am

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
bin_PROGRAMS = fuzz_process_packet fuzz_ndpi_reader fuzz_ndpi_reader_nalloc fuzz_ndpi_reader_alloc_fail fuzz_ndpi_reader_payload_analyzer fuzz_quic_get_crypto_data fuzz_config fuzz_community_id fuzz_serialization fuzz_tls_certificate fuzz_dga fuzz_is_stun_udp fuzz_is_stun_tcp fuzz_match_custom_category
1+
bin_PROGRAMS = fuzz_process_packet fuzz_ndpi_reader fuzz_ndpi_reader_nalloc fuzz_ndpi_reader_alloc_fail fuzz_ndpi_reader_payload_analyzer fuzz_quic_get_crypto_data fuzz_config fuzz_community_id fuzz_serialization fuzz_tls_certificate fuzz_dga fuzz_is_stun_udp fuzz_is_stun_tcp fuzz_match_custom_category fuzz_tls_client_server_hello fuzz_dns_parse fuzz_http_parse fuzz_ndpi_decode_tls_blocks
22
#Alghoritms
33
bin_PROGRAMS += fuzz_alg_ranking fuzz_alg_bins fuzz_alg_hll fuzz_alg_hw_rsi_outliers_da fuzz_alg_jitter fuzz_alg_ses_des fuzz_alg_crc32_md5 fuzz_alg_bytestream fuzz_alg_shoco fuzz_alg_memmem fuzz_alg_strnstr fuzz_alg_quick_encryption
44
#Data structures
@@ -186,6 +186,21 @@ fuzz_is_stun_tcp_LINK = $(FUZZ_LINK_COMMAND)
186186
fuzz_match_custom_category_SOURCES = fuzz_match_custom_category.c fuzz_common_code.c
187187
fuzz_match_custom_category_LINK = $(FUZZ_LINK_COMMAND)
188188

189+
fuzz_tls_client_server_hello_SOURCES = fuzz_tls_client_server_hello.c fuzz_common_code.c
190+
fuzz_tls_client_server_hello_CFLAGS = $(AM_CFLAGS) -DNDPI_LIB_COMPILATION
191+
fuzz_tls_client_server_hello_LINK = $(FUZZ_LINK_COMMAND)
192+
193+
fuzz_dns_parse_SOURCES = fuzz_dns_parse.c fuzz_common_code.c
194+
fuzz_dns_parse_CFLAGS = $(AM_CFLAGS) -DNDPI_LIB_COMPILATION
195+
fuzz_dns_parse_LINK = $(FUZZ_LINK_COMMAND)
196+
197+
fuzz_http_parse_SOURCES = fuzz_http_parse.c fuzz_common_code.c
198+
fuzz_http_parse_CFLAGS = $(AM_CFLAGS) -DNDPI_LIB_COMPILATION
199+
fuzz_http_parse_LINK = $(FUZZ_LINK_COMMAND)
200+
201+
fuzz_ndpi_decode_tls_blocks_SOURCES = fuzz_ndpi_decode_tls_blocks.c
202+
fuzz_ndpi_decode_tls_blocks_LINK = $(FUZZ_LINK_COMMAND)
203+
189204
fuzz_gcrypt_light_SOURCES = fuzz_gcrypt_light.cpp fuzz_common_code.c
190205
fuzz_gcrypt_light_LINK = $(FUZZ_LINK_COMMAND)
191206

fuzz/fuzz_dns_parse.c

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* fuzz_dns_parse
3+
*
4+
* What it tests:
5+
* DNS query / answer RR walking in src/lib/protocols/dns.c. Calls
6+
* ndpi_search_dns() directly with a synthesised packet_struct so the
7+
* dissector is reached without depending on ndpi_detection_process_packet()
8+
* and the full flow state machine. Exercises attacker-controlled uint16
9+
* counts (num_queries, num_answers, authority_rrs, additional_rrs) and the
10+
* name-compression pointer loops.
11+
*
12+
* Expected input format:
13+
* Raw DNS payload (starts with the 12-byte ndpi_dns_packet_header prefix).
14+
* The last byte is consumed as a selector:
15+
* bit 0 -> TCP vs UDP framing
16+
* bit 1 -> MDNS port (5353) vs DNS port (53)
17+
*/
18+
19+
#include "ndpi_api.h"
20+
#include "ndpi_private.h"
21+
#include "fuzz_common_code.h"
22+
23+
#include <arpa/inet.h>
24+
#include <stdint.h>
25+
#include <stdio.h>
26+
#include <string.h>
27+
28+
static struct ndpi_detection_module_struct *ndpi_struct = NULL;
29+
static struct ndpi_flow_struct *ndpi_flow = NULL;
30+
static struct ndpi_iphdr iph;
31+
static struct ndpi_udphdr udph;
32+
static struct ndpi_tcphdr tcph;
33+
34+
static char *path = NULL;
35+
36+
int LLVMFuzzerInitialize(int *argc, char ***argv) {
37+
(void)argc;
38+
path = dirname(strdup(*argv[0])); /* No errors; no free! */
39+
return 0;
40+
}
41+
42+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
43+
struct ndpi_packet_struct *packet;
44+
uint8_t selector;
45+
int is_tcp, is_mdns;
46+
uint16_t port;
47+
48+
if (ndpi_struct == NULL) {
49+
fuzz_init_detection_module(&ndpi_struct, NULL, path);
50+
ndpi_flow = ndpi_calloc(1, sizeof(struct ndpi_flow_struct));
51+
52+
memset(&iph, 0, sizeof(iph));
53+
iph.version = 4;
54+
iph.ihl = 5;
55+
iph.saddr = htonl(0x0A000001);
56+
iph.daddr = htonl(0x0A000002);
57+
}
58+
59+
if (size < 1)
60+
return 0;
61+
62+
fuzz_set_alloc_callbacks_and_seed(size);
63+
64+
selector = data[size - 1];
65+
is_tcp = selector & 0x01;
66+
is_mdns = (selector & 0x02) ? 1 : 0;
67+
port = is_mdns ? 5353 : 53;
68+
69+
packet = &ndpi_struct->packet;
70+
packet->payload = data;
71+
packet->payload_packet_len = (u_int16_t)size;
72+
packet->iph = &iph;
73+
packet->iphv6 = NULL;
74+
75+
if (is_tcp) {
76+
memset(&tcph, 0, sizeof(tcph));
77+
tcph.source = htons(port);
78+
tcph.dest = htons(port);
79+
packet->tcp = &tcph;
80+
packet->udp = NULL;
81+
iph.protocol = 6;
82+
} else {
83+
memset(&udph, 0, sizeof(udph));
84+
udph.source = htons(port);
85+
udph.dest = htons(port);
86+
packet->udp = &udph;
87+
packet->tcp = NULL;
88+
iph.protocol = 17;
89+
}
90+
91+
memset(ndpi_flow, 0, sizeof(struct ndpi_flow_struct));
92+
ndpi_flow->l4_proto = is_tcp ? IPPROTO_TCP : IPPROTO_UDP;
93+
94+
ndpi_search_dns(ndpi_struct, ndpi_flow);
95+
ndpi_free_flow_data(ndpi_flow);
96+
97+
return 0;
98+
}

fuzz/fuzz_http_parse.c

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* fuzz_http_parse
3+
*
4+
* What it tests:
5+
* HTTP dissector (ndpi_search_http_tcp) in src/lib/protocols/http.c. Calls
6+
* the parser directly with a synthesised packet_struct so the header
7+
* extractors (Host, User-Agent, Content-Type, Referer), chunked/transfer
8+
* handling, URL/URI walking, WebSocket upgrade, and URL-decode helpers are
9+
* reached without relying on the full ndpi_detection_process_packet() flow
10+
* state machine.
11+
*
12+
* Expected input format:
13+
* Raw TCP payload bytes (e.g. "GET / HTTP/1.1\r\nHost: x\r\n\r\n").
14+
*/
15+
16+
#include "ndpi_api.h"
17+
#include "ndpi_private.h"
18+
#include "fuzz_common_code.h"
19+
20+
#include <arpa/inet.h>
21+
#include <stdint.h>
22+
#include <stdio.h>
23+
#include <string.h>
24+
25+
static struct ndpi_detection_module_struct *ndpi_struct = NULL;
26+
static struct ndpi_flow_struct *ndpi_flow = NULL;
27+
static struct ndpi_iphdr iph;
28+
static struct ndpi_tcphdr tcph;
29+
30+
static char *path = NULL;
31+
32+
int LLVMFuzzerInitialize(int *argc, char ***argv) {
33+
(void)argc;
34+
path = dirname(strdup(*argv[0])); /* No errors; no free! */
35+
return 0;
36+
}
37+
38+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
39+
struct ndpi_packet_struct *packet;
40+
41+
if (ndpi_struct == NULL) {
42+
fuzz_init_detection_module(&ndpi_struct, NULL, path);
43+
ndpi_flow = ndpi_calloc(1, sizeof(struct ndpi_flow_struct));
44+
45+
memset(&iph, 0, sizeof(iph));
46+
iph.version = 4;
47+
iph.ihl = 5;
48+
iph.protocol = 6; /* TCP */
49+
iph.saddr = htonl(0x0A000001);
50+
iph.daddr = htonl(0x0A000002);
51+
52+
memset(&tcph, 0, sizeof(tcph));
53+
tcph.source = htons(80);
54+
tcph.dest = htons(80);
55+
}
56+
57+
fuzz_set_alloc_callbacks_and_seed(size);
58+
59+
packet = &ndpi_struct->packet;
60+
packet->payload = data;
61+
packet->payload_packet_len = (u_int16_t)size;
62+
packet->iph = &iph;
63+
packet->iphv6 = NULL;
64+
packet->tcp = &tcph;
65+
packet->udp = NULL;
66+
67+
memset(ndpi_flow, 0, sizeof(struct ndpi_flow_struct));
68+
ndpi_flow->l4_proto = IPPROTO_TCP;
69+
70+
ndpi_search_http_tcp(ndpi_struct, ndpi_flow);
71+
ndpi_free_flow_data(ndpi_flow);
72+
73+
return 0;
74+
}

fuzz/fuzz_ndpi_decode_tls_blocks.c

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* fuzz_ndpi_decode_tls_blocks
3+
*
4+
* What it tests:
5+
* ndpi_decode_tls_blocks() in src/lib/ndpi_utils.c, which in turn invokes
6+
* ndpi_hex_decode(). The decoder consumes an attacker-controlled byte
7+
* stream, hex-decodes it, then walks it as { block_type, len_hi, len_lo }
8+
* triples. Both the hex decoder and the structural parser are exposed.
9+
*
10+
* Expected input format:
11+
* Arbitrary bytes (interpreted as hex ASCII by the target). No state, one
12+
* shot per invocation; all memory returned by the target is freed here.
13+
*/
14+
15+
#include "ndpi_api.h"
16+
17+
#include <stdint.h>
18+
#include <stdlib.h>
19+
20+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
21+
u_int8_t num_blocks = 0;
22+
struct ndpi_tls_block *blocks;
23+
24+
if (size > (u_int)UINT32_MAX)
25+
return 0;
26+
27+
blocks = ndpi_decode_tls_blocks((const u_char *)data,
28+
(u_int)size,
29+
&num_blocks);
30+
if (blocks != NULL)
31+
ndpi_free(blocks);
32+
33+
return 0;
34+
}

0 commit comments

Comments
 (0)