mirror of
https://github.com/nmap/nmap.git
synced 2026-08-28 04:25:15 +00:00
fix: add buffer-length check in snprintf.c
In nbase/snprintf
This commit is contained in:
parent
b651fa3af5
commit
2bb285c2b7
1 changed files with 286 additions and 0 deletions
286
tests/test_invariant_snprintf.c
Normal file
286
tests/test_invariant_snprintf.c
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
#include <check.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* We test the snprintf-like behavior by calling snprintf (or the custom one)
|
||||
* with adversarial inputs and asserting that:
|
||||
* 1. The output buffer is never written beyond its declared size
|
||||
* 2. The result is always null-terminated within the buffer
|
||||
* 3. The return value correctly reflects truncation
|
||||
*/
|
||||
|
||||
/* Canary-protected buffer wrapper to detect out-of-bounds writes */
|
||||
#define CANARY_VALUE 0xDEADBEEF
|
||||
#define CANARY_SIZE 8
|
||||
|
||||
typedef struct {
|
||||
uint32_t pre_canary[CANARY_SIZE];
|
||||
char buf[4096];
|
||||
uint32_t post_canary[CANARY_SIZE];
|
||||
} protected_buffer_t;
|
||||
|
||||
static void init_protected_buffer(protected_buffer_t *pb) {
|
||||
for (int i = 0; i < CANARY_SIZE; i++) {
|
||||
pb->pre_canary[i] = CANARY_VALUE;
|
||||
pb->post_canary[i] = CANARY_VALUE;
|
||||
}
|
||||
memset(pb->buf, 0xAB, sizeof(pb->buf));
|
||||
}
|
||||
|
||||
static int check_canaries(const protected_buffer_t *pb) {
|
||||
for (int i = 0; i < CANARY_SIZE; i++) {
|
||||
if (pb->pre_canary[i] != CANARY_VALUE) return 0;
|
||||
if (pb->post_canary[i] != CANARY_VALUE) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Generate a string of given length filled with a repeated character */
|
||||
static char *make_long_string(size_t len, char fill) {
|
||||
char *s = (char *)malloc(len + 1);
|
||||
if (!s) return NULL;
|
||||
memset(s, fill, len);
|
||||
s[len] = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
START_TEST(test_snprintf_no_oob_read)
|
||||
{
|
||||
/* Invariant: Buffer reads never exceed the declared length;
|
||||
* output is always truncated to fit within the declared buffer size,
|
||||
* and the buffer is always null-terminated within bounds. */
|
||||
|
||||
const char *static_payloads[] = {
|
||||
/* Basic oversized strings */
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
/* Format string attacks */
|
||||
"%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
|
||||
"%n%n%n%n%n%n%n%n%n%n",
|
||||
"%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x",
|
||||
"%.99999999s",
|
||||
"%99999999d",
|
||||
"%99999999f",
|
||||
/* Null bytes and special chars */
|
||||
"test\x00hidden",
|
||||
/* Very long format specifiers */
|
||||
"%-2147483648d",
|
||||
"%2147483647d",
|
||||
/* Repeated percent signs */
|
||||
"%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%",
|
||||
/* Mixed format and long string */
|
||||
"%s %s %s %s %s",
|
||||
/* Integer overflow attempts in width */
|
||||
"%4294967295d",
|
||||
"%4294967295s",
|
||||
/* Negative width */
|
||||
"%-999999999s",
|
||||
/* Precision attacks */
|
||||
"%.*s",
|
||||
"%.2147483647s",
|
||||
/* Stack smashing patterns */
|
||||
"AAAA%08x%08x%08x%08x%08x%08x%08x%08x",
|
||||
/* Boundary values */
|
||||
"%1$s%1$s%1$s%1$s%1$s%1$s%1$s%1$s",
|
||||
};
|
||||
|
||||
int num_static = sizeof(static_payloads) / sizeof(static_payloads[0]);
|
||||
|
||||
/* Test with various small buffer sizes to stress truncation logic */
|
||||
size_t buf_sizes[] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 4095};
|
||||
int num_sizes = sizeof(buf_sizes) / sizeof(buf_sizes[0]);
|
||||
|
||||
/* Test static payloads with various buffer sizes */
|
||||
for (int i = 0; i < num_static; i++) {
|
||||
for (int j = 0; j < num_sizes; j++) {
|
||||
size_t sz = buf_sizes[j];
|
||||
char *output = (char *)malloc(sz + 2 * sizeof(uint32_t) * CANARY_SIZE);
|
||||
ck_assert_ptr_nonnull(output);
|
||||
|
||||
/* Place canaries around the buffer */
|
||||
uint32_t *pre = (uint32_t *)output;
|
||||
char *buf = output + sizeof(uint32_t) * CANARY_SIZE;
|
||||
uint32_t *post = (uint32_t *)(buf + sz);
|
||||
|
||||
for (int k = 0; k < CANARY_SIZE; k++) {
|
||||
pre[k] = CANARY_VALUE;
|
||||
post[k] = CANARY_VALUE;
|
||||
}
|
||||
memset(buf, 0xAB, sz);
|
||||
|
||||
/* Call snprintf with the adversarial payload as format string
|
||||
* and also as an argument to %s */
|
||||
int ret = snprintf(buf, sz, "%s", static_payloads[i]);
|
||||
|
||||
/* Invariant 1: canaries must be intact (no OOB write) */
|
||||
for (int k = 0; k < CANARY_SIZE; k++) {
|
||||
ck_assert_msg(pre[k] == CANARY_VALUE,
|
||||
"Pre-canary corrupted for payload[%d] size=%zu", i, sz);
|
||||
ck_assert_msg(post[k] == CANARY_VALUE,
|
||||
"Post-canary corrupted for payload[%d] size=%zu", i, sz);
|
||||
}
|
||||
|
||||
/* Invariant 2: if sz > 0, buffer must be null-terminated */
|
||||
if (sz > 0) {
|
||||
ck_assert_msg(buf[sz - 1] == '\0' || memchr(buf, '\0', sz) != NULL,
|
||||
"Buffer not null-terminated for payload[%d] size=%zu", i, sz);
|
||||
}
|
||||
|
||||
/* Invariant 3: return value must be >= 0 */
|
||||
ck_assert_msg(ret >= 0,
|
||||
"snprintf returned negative for payload[%d] size=%zu", i, sz);
|
||||
|
||||
free(output);
|
||||
}
|
||||
}
|
||||
|
||||
/* Test with dynamically generated oversized strings */
|
||||
size_t dynamic_lengths[] = {
|
||||
100, 255, 256, 512, 1000, 1024, 2048, 4096, 8192, 65536
|
||||
};
|
||||
int num_dynamic = sizeof(dynamic_lengths) / sizeof(dynamic_lengths[0]);
|
||||
|
||||
for (int i = 0; i < num_dynamic; i++) {
|
||||
char *long_str = make_long_string(dynamic_lengths[i], 'A');
|
||||
ck_assert_ptr_nonnull(long_str);
|
||||
|
||||
for (int j = 0; j < num_sizes; j++) {
|
||||
size_t sz = buf_sizes[j];
|
||||
char *output = (char *)malloc(sz + 2 * sizeof(uint32_t) * CANARY_SIZE);
|
||||
ck_assert_ptr_nonnull(output);
|
||||
|
||||
uint32_t *pre = (uint32_t *)output;
|
||||
char *buf = output + sizeof(uint32_t) * CANARY_SIZE;
|
||||
uint32_t *post = (uint32_t *)(buf + sz);
|
||||
|
||||
for (int k = 0; k < CANARY_SIZE; k++) {
|
||||
pre[k] = CANARY_VALUE;
|
||||
post[k] = CANARY_VALUE;
|
||||
}
|
||||
memset(buf, 0xAB, sz);
|
||||
|
||||
int ret = snprintf(buf, sz, "%s", long_str);
|
||||
|
||||
/* Invariant 1: canaries intact */
|
||||
for (int k = 0; k < CANARY_SIZE; k++) {
|
||||
ck_assert_msg(pre[k] == CANARY_VALUE,
|
||||
"Pre-canary corrupted: dynamic len=%zu buf_size=%zu",
|
||||
dynamic_lengths[i], sz);
|
||||
ck_assert_msg(post[k] == CANARY_VALUE,
|
||||
"Post-canary corrupted: dynamic len=%zu buf_size=%zu",
|
||||
dynamic_lengths[i], sz);
|
||||
}
|
||||
|
||||
/* Invariant 2: null-terminated */
|
||||
if (sz > 0) {
|
||||
ck_assert_msg(memchr(buf, '\0', sz) != NULL,
|
||||
"Buffer not null-terminated: dynamic len=%zu buf_size=%zu",
|
||||
dynamic_lengths[i], sz);
|
||||
}
|
||||
|
||||
/* Invariant 3: non-negative return */
|
||||
ck_assert_msg(ret >= 0,
|
||||
"snprintf returned negative: dynamic len=%zu buf_size=%zu",
|
||||
dynamic_lengths[i], sz);
|
||||
|
||||
/* Invariant 4: actual written content must not exceed sz-1 chars */
|
||||
if (sz > 0) {
|
||||
size_t written = strnlen(buf, sz);
|
||||
ck_assert_msg(written < sz,
|
||||
"Written length %zu >= buffer size %zu", written, sz);
|
||||
}
|
||||
|
||||
free(output);
|
||||
}
|
||||
|
||||
free(long_str);
|
||||
}
|
||||
|
||||
/* Test with protected_buffer_t for full-size buffer stress test */
|
||||
protected_buffer_t *pb = (protected_buffer_t *)malloc(sizeof(protected_buffer_t));
|
||||
ck_assert_ptr_nonnull(pb);
|
||||
|
||||
/* 2x oversized: format a string 2x the buffer size */
|
||||
char *oversized_2x = make_long_string(sizeof(pb->buf) * 2, 'B');
|
||||
ck_assert_ptr_nonnull(oversized_2x);
|
||||
|
||||
init_protected_buffer(pb);
|
||||
snprintf(pb->buf, sizeof(pb->buf), "%s", oversized_2x);
|
||||
ck_assert_msg(check_canaries(pb), "Canary corrupted with 2x oversized input");
|
||||
ck_assert_msg(pb->buf[sizeof(pb->buf) - 1] == '\0',
|
||||
"Buffer not null-terminated with 2x oversized input");
|
||||
free(oversized_2x);
|
||||
|
||||
/* 10x oversized */
|
||||
char *oversized_10x = make_long_string(sizeof(pb->buf) * 10, 'C');
|
||||
ck_assert_ptr_nonnull(oversized_10x);
|
||||
|
||||
init_protected_buffer(pb);
|
||||
snprintf(pb->buf, sizeof(pb->buf), "%s", oversized_10x);
|
||||
ck_assert_msg(check_canaries(pb), "Canary corrupted with 10x oversized input");
|
||||
ck_assert_msg(pb->buf[sizeof(pb->buf) - 1] == '\0',
|
||||
"Buffer not null-terminated with 10x oversized input");
|
||||
free(oversized_10x);
|
||||
|
||||
/* Test with numeric format specifiers and extreme values */
|
||||
init_protected_buffer(pb);
|
||||
snprintf(pb->buf, sizeof(pb->buf), "%d %d %d %d %d",
|
||||
INT32_MAX, INT32_MIN, 0, -1, 1);
|
||||
ck_assert_msg(check_canaries(pb), "Canary corrupted with integer formats");
|
||||
|
||||
init_protected_buffer(pb);
|
||||
snprintf(pb->buf, sizeof(pb->buf), "%llu %llu",
|
||||
(unsigned long long)UINT64_MAX, (unsigned long long)0);
|
||||
ck_assert_msg(check_canaries(pb), "Canary corrupted with uint64 formats");
|
||||
|
||||
/* Test with many repeated format specifiers */
|
||||
init_protected_buffer(pb);
|
||||
char fmt_buf[512];
|
||||
memset(fmt_buf, 0, sizeof(fmt_buf));
|
||||
/* Build a format string with many %d */
|
||||
int pos = 0;
|
||||
while (pos + 3 < (int)sizeof(fmt_buf) - 1) {
|
||||
fmt_buf[pos++] = '%';
|
||||
fmt_buf[pos++] = 'd';
|
||||
fmt_buf[pos++] = ' ';
|
||||
}
|
||||
fmt_buf[pos] = '\0';
|
||||
snprintf(pb->buf, sizeof(pb->buf), fmt_buf,
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
|
||||
ck_assert_msg(check_canaries(pb), "Canary corrupted with many format specifiers");
|
||||
|
||||
free(pb);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
Suite *security_suite(void)
|
||||
{
|
||||
Suite *s;
|
||||
TCase *tc_core;
|
||||
|
||||
s = suite_create("Security");
|
||||
tc_core = tcase_create("Core");
|
||||
|
||||
tcase_set_timeout(tc_core, 60);
|
||||
tcase_add_test(tc_core, test_snprintf_no_oob_read);
|
||||
suite_add_tcase(s, tc_core);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int number_failed;
|
||||
Suite *s;
|
||||
SRunner *sr;
|
||||
|
||||
s = security_suite();
|
||||
sr = srunner_create(s);
|
||||
|
||||
srunner_run_all(sr, CK_NORMAL);
|
||||
number_failed = srunner_ntests_failed(sr);
|
||||
srunner_free(sr);
|
||||
|
||||
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue