diff --git a/tests/FUZZING.md b/tests/FUZZING.md new file mode 100644 index 0000000..b6ec564 --- /dev/null +++ b/tests/FUZZING.md @@ -0,0 +1,228 @@ +# Fuzzing the URL Parser + +This directory contains fuzzing infrastructure for the URL parser using AFL++ (American Fuzzy Lop). + +## What is Fuzzing? + +Fuzzing is an automated testing technique that feeds malformed, unexpected, or random data to a program to find bugs, crashes, and security vulnerabilities. AFL++ is a coverage-guided fuzzer that intelligently mutates inputs to maximize code coverage. + +## Prerequisites + +### Install AFL++ + +**Ubuntu/Debian:** +```bash +sudo apt-get update +sudo apt-get install afl++ +``` + +**From source:** +```bash +git clone https://github.com/AFLplusplus/AFLplusplus +cd AFLplusplus +make +sudo make install +``` + +## Quick Start + +1. **Build the fuzz target:** + ```bash + cd tests/ + ./build_afl.sh + ``` + +2. **Run fuzzing:** + ```bash + ./run_afl.sh + ``` + + Or run for a specific duration: + ```bash + ./run_afl.sh 60 # Run for 60 minutes + ``` + +3. **View results:** + - Crashes: `findings/default/crashes/` + - Hangs: `findings/default/hangs/` + - Interesting inputs: `findings/default/queue/` + +## Directory Structure + +``` +tests/ +├── fuzz_afl.c # AFL++ fuzz target +├── build_afl.sh # Build script +├── run_afl.sh # Run script +├── corpus/ # Seed inputs +│ ├── basic.txt +│ ├── complex.txt +│ ├── ipv4.txt +│ ├── ipv6.txt +│ └── ... +└── findings/ # AFL++ output (created when running) + └── default/ + ├── crashes/ # Inputs that caused crashes + ├── hangs/ # Inputs that caused timeouts + └── queue/ # All interesting inputs found +``` + +## What Gets Tested + +The fuzz target tests multiple parser functions: + +- `url_parse()` - Main URL parser (with different flag combinations) +- `url_parse_ipv4()` - IPv4 address parser +- `url_parse_ipv6()` - IPv6 address parser +- `url_serialize()` - URL serialization +- `url_percent_decode()` - Percent-decoding +- `url_remove_white_space()` - Whitespace removal + +## Analyzing Results + +### Reproducing Crashes + +If AFL++ finds a crash, you can reproduce it: + +```bash +./fuzz_afl < findings/default/crashes/id:000000,* +``` + +Or use a debugger: + +```bash +gdb ./fuzz_afl +(gdb) run < findings/default/crashes/id:000000,* +``` + +### Minimizing Crash Inputs + +AFL++ can minimize crash inputs to their smallest form: + +```bash +afl-tmin -i findings/default/crashes/id:000000,* -o minimized.txt -- ./fuzz_afl +``` + +### Viewing Statistics + +While fuzzing is running, AFL++ displays real-time statistics: + +- **Cycles done**: Number of times the fuzzer went through the queue +- **Corpus count**: Number of interesting test cases found +- **Crashes**: Number of unique crashes found +- **Exec speed**: Fuzzing throughput (executions per second) + +## Advanced Usage + +### Parallel Fuzzing + +Run multiple AFL++ instances in parallel for better coverage: + +```bash +# Terminal 1 (master) +afl-fuzz -i corpus/ -o findings/ -M fuzzer1 -- ./fuzz_afl + +# Terminal 2 (secondary) +afl-fuzz -i corpus/ -o findings/ -S fuzzer2 -- ./fuzz_afl + +# Terminal 3 (secondary) +afl-fuzz -i corpus/ -o findings/ -S fuzzer3 -- ./fuzz_afl +``` + +### Using Different Modes + +AFL++ supports various mutation modes: + +```bash +# Explore mode (default) +afl-fuzz -i corpus/ -o findings/ -- ./fuzz_afl + +# Exploitation mode (focus on found crashes) +afl-fuzz -i corpus/ -o findings/ -p exploit -- ./fuzz_afl + +# COE (Continue on Error) mode +afl-fuzz -i corpus/ -o findings/ -c -- ./fuzz_afl +``` + +### With AddressSanitizer (ASAN) + +For better bug detection, rebuild with AddressSanitizer: + +```bash +AFL_USE_ASAN=1 ./build_afl.sh +./run_afl.sh +``` + +Note: ASAN significantly slows down fuzzing but can detect memory issues that wouldn't cause crashes. + +### With UndefinedBehaviorSanitizer (UBSAN) + +To detect undefined behavior: + +```bash +AFL_USE_UBSAN=1 ./build_afl.sh +./run_afl.sh +``` + +## Performance Tips + +1. **Disable CPU frequency scaling:** + ```bash + echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor + ``` + +2. **Increase system limits:** + ```bash + sudo sysctl -w kernel.core_pattern=core + sudo sysctl -w kernel.sched_child_runs_first=1 + ``` + +3. **Use persistent mode** (requires code changes to fuzz target) + +4. **Use dictionary files** for better mutations: + ```bash + afl-fuzz -i corpus/ -o findings/ -x url.dict -- ./fuzz_afl + ``` + +## Continuous Fuzzing + +For long-term fuzzing campaigns: + +```bash +# Run in screen/tmux +screen -S fuzzing +./run_afl.sh + +# Detach: Ctrl+A, D +# Reattach: screen -r fuzzing +``` + +Or use a systemd service for continuous fuzzing. + +## Troubleshooting + +### "No instrumentation detected" + +Make sure you built with an AFL++ compiler (afl-gcc, afl-clang-fast, etc.) + +### "Suboptimal CPU scaling governor" + +Run as suggested by AFL++ or set `AFL_SKIP_CPUFREQ=1` + +### Low execution speed + +- Close other programs +- Use afl-clang-fast instead of afl-gcc +- Consider using QEMU mode for uninstrumented binaries + +### No new paths found + +- Add more diverse seed inputs to corpus/ +- Try different mutation strategies +- Consider parallel fuzzing + +## References + +- [AFL++ Documentation](https://github.com/AFLplusplus/AFLplusplus/tree/stable/docs) +- [Fuzzing Tutorial](https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md) +- [OWASP Fuzzing Guide](https://owasp.org/www-community/Fuzzing) diff --git a/tests/build_afl.sh b/tests/build_afl.sh new file mode 100755 index 0000000..a8def8b --- /dev/null +++ b/tests/build_afl.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Build script for AFL++ fuzzing +# +# Prerequisites: +# - AFL++ installed (https://github.com/AFLplusplus/AFLplusplus) +# +# Install AFL++ on Ubuntu/Debian: +# sudo apt-get install afl++ +# +# Or build from source: +# git clone https://github.com/AFLplusplus/AFLplusplus +# cd AFLplusplus +# make +# sudo make install + +set -e + +echo "Building fuzz target with AFL++..." + +# Try different AFL++ compilers in order of preference +if command -v afl-clang-fast &> /dev/null; then + echo "Using afl-clang-fast (recommended)" + AFL_CC=afl-clang-fast +elif command -v afl-gcc &> /dev/null; then + echo "Using afl-gcc" + AFL_CC=afl-gcc +elif command -v afl-clang &> /dev/null; then + echo "Using afl-clang" + AFL_CC=afl-clang +else + echo "Error: No AFL++ compiler found!" + echo "Please install AFL++ first:" + echo " sudo apt-get install afl++" + echo "Or see: https://github.com/AFLplusplus/AFLplusplus" + exit 1 +fi + +# Build the fuzz target +$AFL_CC -o fuzz_afl fuzz_afl.c ../url.c -Wall -Wextra -ggdb + +echo "Build successful! Binary: fuzz_afl" +echo "" +echo "To run fuzzing:" +echo " ./run_afl.sh" diff --git a/tests/build_fuzz.bat b/tests/build_fuzz.bat new file mode 100644 index 0000000..9f4dfe1 --- /dev/null +++ b/tests/build_fuzz.bat @@ -0,0 +1 @@ +gcc -o fuzz.exe fuzz.c ../url.c -Wall -Wextra -ggdb -O2 diff --git a/tests/build_fuzz.sh b/tests/build_fuzz.sh new file mode 100755 index 0000000..8f48826 --- /dev/null +++ b/tests/build_fuzz.sh @@ -0,0 +1,2 @@ +#!/bin/bash +gcc -o fuzz.out fuzz.c ../url.c -Wall -Wextra -ggdb -O2 diff --git a/tests/corpus/authority_only.txt b/tests/corpus/authority_only.txt new file mode 100644 index 0000000..eee666a --- /dev/null +++ b/tests/corpus/authority_only.txt @@ -0,0 +1 @@ +//example.com/path diff --git a/tests/corpus/basic.txt b/tests/corpus/basic.txt new file mode 100644 index 0000000..7bc94d0 --- /dev/null +++ b/tests/corpus/basic.txt @@ -0,0 +1 @@ +http://example.com diff --git a/tests/corpus/complex.txt b/tests/corpus/complex.txt new file mode 100644 index 0000000..95a474a --- /dev/null +++ b/tests/corpus/complex.txt @@ -0,0 +1 @@ +https://user:pass@example.com:8080/path?query=value#fragment diff --git a/tests/corpus/encoded.txt b/tests/corpus/encoded.txt new file mode 100644 index 0000000..ff53484 --- /dev/null +++ b/tests/corpus/encoded.txt @@ -0,0 +1 @@ +http://example.com/path%20with%20spaces diff --git a/tests/corpus/file.txt b/tests/corpus/file.txt new file mode 100644 index 0000000..0e13e6f --- /dev/null +++ b/tests/corpus/file.txt @@ -0,0 +1 @@ +file:///path/to/file diff --git a/tests/corpus/ipv4.txt b/tests/corpus/ipv4.txt new file mode 100644 index 0000000..637f1d1 --- /dev/null +++ b/tests/corpus/ipv4.txt @@ -0,0 +1 @@ +http://192.168.1.1/path diff --git a/tests/corpus/ipv6.txt b/tests/corpus/ipv6.txt new file mode 100644 index 0000000..95befb4 --- /dev/null +++ b/tests/corpus/ipv6.txt @@ -0,0 +1 @@ +http://[2001:db8::1]/ diff --git a/tests/corpus/localhost.txt b/tests/corpus/localhost.txt new file mode 100644 index 0000000..1f44330 --- /dev/null +++ b/tests/corpus/localhost.txt @@ -0,0 +1 @@ +http://localhost:3000/api/v1 diff --git a/tests/corpus/mailto.txt b/tests/corpus/mailto.txt new file mode 100644 index 0000000..a90d24b --- /dev/null +++ b/tests/corpus/mailto.txt @@ -0,0 +1 @@ +mailto:user@example.com diff --git a/tests/corpus/relative.txt b/tests/corpus/relative.txt new file mode 100644 index 0000000..7daeee8 --- /dev/null +++ b/tests/corpus/relative.txt @@ -0,0 +1 @@ +../relative/path diff --git a/tests/fuzz.c b/tests/fuzz.c new file mode 100644 index 0000000..e065d7e --- /dev/null +++ b/tests/fuzz.c @@ -0,0 +1,363 @@ +#include +#include +#include +#include +#include +#include + +#include "../url.h" + +// PRNG state +static uint64_t rng_state = 0; + +// Simple xorshift64 RNG +static uint64_t rand64(void) +{ + rng_state ^= rng_state << 13; + rng_state ^= rng_state >> 7; + rng_state ^= rng_state << 17; + return rng_state; +} + +// Generate random byte +static uint8_t rand_byte(void) +{ + return (uint8_t)(rand64() & 0xFF); +} + +// Generate random integer in range [0, max) +static int rand_range(int max) +{ + if (max <= 0) return 0; + return (int)(rand64() % max); +} + +// Seed URLs for mutation-based fuzzing +static const char *seed_urls[] = { + "http://example.com", + "https://user:pass@example.com:8080/path?query=value#fragment", + "ftp://192.168.1.1/file.txt", + "http://[2001:db8::1]/", + "mailto:user@example.com", + "file:///path/to/file", + "http://example.com:80/path", + "https://example.com/path%20with%20spaces", + "http://localhost:3000", + "http://127.0.0.1:8080/api/v1/resource", + "//example.com/path", + "/path/to/resource", + "../relative/path", + "?query=only", + "#fragment-only", + "http://example.com:99999", + "http://256.256.256.256", + "http://example..com", + "http://:password@example.com", + "http://user:@example.com", + "http://example.com/path//double//slash", + "http://example.com/path/../../../etc/passwd", + "http://example.com/%2e%2e%2f", + "http://example.com/\x00null", + "http://example.com/\x0a\x0d", + "http://\t\n\rexample.com", + "http://example.com:0", + "http://example.com:-1", + "http://example.com:abc", + "http://[::1]", + "http://[::ffff:192.0.2.1]", + "http://[2001:db8::1]:8080", + "http://[::1", + "http://::1]", + "data:text/plain;base64,SGVsbG8=", + "javascript:alert(1)", + "about:blank", +}; + +#define NUM_SEEDS (sizeof(seed_urls) / sizeof(seed_urls[0])) + +// Character classes for generation +static const char *char_classes[] = { + "abcdefghijklmnopqrstuvwxyz", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "0123456789", + "://", + "@.:/?#[]", + "%", + "\t\n\r ", + "\x00\x01\x02\x03\x04\x05\x06\x07\x08", +}; + +#define NUM_CHAR_CLASSES (sizeof(char_classes) / sizeof(char_classes[0])) + +// Mutation strategies +typedef enum { + MUT_BIT_FLIP, + MUT_BYTE_FLIP, + MUT_BYTE_INSERT, + MUT_BYTE_DELETE, + MUT_BYTE_REPLACE, + MUT_CHUNK_INSERT, + MUT_CHUNK_DELETE, + MUT_CHUNK_DUPLICATE, + MUT_INTERESTING_VALUE, + MUT_COUNT +} MutationType; + +// Interesting byte values +static const uint8_t interesting_bytes[] = { + 0x00, 0x01, 0xFF, 0x7F, 0x80, + '/', '?', '#', ':', '@', '[', ']', + '%', '.', '-', '_', '~', +}; + +// Mutate a buffer +static int mutate_buffer(char *dst, int dst_cap, const char *src, int src_len) +{ + if (src_len <= 0 || src_len >= dst_cap) { + // Can't mutate empty or too large buffer + return src_len; + } + + // Copy source to destination + memcpy(dst, src, src_len); + int len = src_len; + + // Apply 1-5 random mutations + int num_mutations = 1 + rand_range(5); + for (int i = 0; i < num_mutations && len > 0 && len < dst_cap - 1; i++) { + MutationType mut = rand_range(MUT_COUNT); + int pos = rand_range(len); + + switch (mut) { + case MUT_BIT_FLIP: + if (pos < len) { + dst[pos] ^= (1 << rand_range(8)); + } + break; + + case MUT_BYTE_FLIP: + if (pos < len) { + dst[pos] ^= 0xFF; + } + break; + + case MUT_BYTE_INSERT: + if (len < dst_cap - 1) { + memmove(dst + pos + 1, dst + pos, len - pos); + dst[pos] = rand_byte(); + len++; + } + break; + + case MUT_BYTE_DELETE: + if (pos < len && len > 1) { + memmove(dst + pos, dst + pos + 1, len - pos - 1); + len--; + } + break; + + case MUT_BYTE_REPLACE: + if (pos < len) { + dst[pos] = rand_byte(); + } + break; + + case MUT_CHUNK_INSERT: { + int chunk_len = 1 + rand_range(16); + if (len + chunk_len < dst_cap) { + memmove(dst + pos + chunk_len, dst + pos, len - pos); + for (int j = 0; j < chunk_len; j++) { + dst[pos + j] = rand_byte(); + } + len += chunk_len; + } + break; + } + + case MUT_CHUNK_DELETE: { + int chunk_len = 1 + rand_range(16); + if (pos + chunk_len <= len) { + memmove(dst + pos, dst + pos + chunk_len, len - pos - chunk_len); + len -= chunk_len; + } + break; + } + + case MUT_CHUNK_DUPLICATE: { + int chunk_len = 1 + rand_range(16); + if (pos + chunk_len <= len && len + chunk_len < dst_cap) { + memmove(dst + pos + chunk_len, dst + pos, len - pos); + memcpy(dst + pos + chunk_len, dst + pos, chunk_len); + len += chunk_len; + } + break; + } + + case MUT_INTERESTING_VALUE: + if (pos < len) { + dst[pos] = interesting_bytes[rand_range(sizeof(interesting_bytes))]; + } + break; + + default: + break; + } + } + + return len; +} + +// Generate a random URL-like string +static int generate_random_url(char *buf, int cap) +{ + int strategy = rand_range(3); + + if (strategy == 0) { + // Fully random generation + int len = rand_range(cap - 1); + for (int i = 0; i < len; i++) { + buf[i] = rand_byte(); + } + return len; + } else if (strategy == 1) { + // Generate from character classes + int len = rand_range(cap - 1); + for (int i = 0; i < len; i++) { + const char *class = char_classes[rand_range(NUM_CHAR_CLASSES)]; + int class_len = strlen(class); + buf[i] = class[rand_range(class_len)]; + } + return len; + } else { + // Mutation-based: pick a seed and mutate it + const char *seed = seed_urls[rand_range(NUM_SEEDS)]; + int seed_len = strlen(seed); + return mutate_buffer(buf, cap, seed, seed_len); + } +} + +// Fuzz url_parse +static void fuzz_url_parse(const char *input, int len) +{ + URL url; + int flags_options[] = { 0, URL_FLAG_ALLOWREF, URL_FLAG_RFC3986, URL_FLAG_ALLOWREF | URL_FLAG_RFC3986 }; + int flags = flags_options[rand_range(4)]; + + // Test with NULL cursor + url_parse((char *)input, len, NULL, &url, flags); + + // Test with cursor + int cursor = 0; + url_parse((char *)input, len, &cursor, &url, flags); + + // Test serialization if parse succeeded + if (url_parse((char *)input, len, NULL, &url, flags) == 0) { + char output[4096]; + url_serialize(url, NULL, output, sizeof(output)); + } + + // Test percent decode on random substrings + if (len > 0) { + int start = rand_range(len); + int end = start + rand_range(len - start); + URL_String str = { (char *)input + start, end - start }; + char decoded[4096]; + url_percent_decode(str, decoded, sizeof(decoded)); + } + + // Test whitespace removal + char tmp[4096]; + url_remove_white_space((char *)input, len, tmp, sizeof(tmp)); +} + +// Fuzz url_parse_ipv4 +static void fuzz_ipv4_parse(const char *input, int len) +{ + URL_IPv4 ipv4; + + // Test with NULL cursor + url_parse_ipv4((char *)input, len, NULL, &ipv4); + + // Test with cursor + int cursor = 0; + url_parse_ipv4((char *)input, len, &cursor, &ipv4); +} + +// Fuzz url_parse_ipv6 +static void fuzz_ipv6_parse(const char *input, int len) +{ + URL_IPv6 ipv6; + + // Test with NULL cursor + url_parse_ipv6((char *)input, len, NULL, &ipv6); + + // Test with cursor + int cursor = 0; + url_parse_ipv6((char *)input, len, &cursor, &ipv6); +} + +// Run one fuzz iteration +static void fuzz_iteration(int iteration) +{ + char input[4096]; + int len = generate_random_url(input, sizeof(input)); + + // Ensure null termination for safety (though our API doesn't require it) + if (len < (int)sizeof(input)) { + input[len] = '\0'; + } + + // Fuzz different functions + int target = rand_range(3); + switch (target) { + case 0: + fuzz_url_parse(input, len); + break; + case 1: + fuzz_ipv4_parse(input, len); + break; + case 2: + fuzz_ipv6_parse(input, len); + break; + } +} + +int main(int argc, char **argv) +{ + // Initialize RNG + if (argc > 1) { + rng_state = strtoull(argv[1], NULL, 10); + } else { + rng_state = time(NULL); + } + + printf("URL Parser Fuzzer\n"); + printf("Seed: %llu\n", (unsigned long long)rng_state); + printf("Running fuzz tests...\n\n"); + + int num_iterations = 100000; + if (argc > 2) { + num_iterations = atoi(argv[2]); + } + + clock_t start = clock(); + + for (int i = 0; i < num_iterations; i++) { + fuzz_iteration(i); + + // Progress update every 10000 iterations + if (i > 0 && i % 10000 == 0) { + double elapsed = (double)(clock() - start) / CLOCKS_PER_SEC; + double rate = i / elapsed; + printf("\rIteration %d/%d (%.0f iter/sec)", i, num_iterations, rate); + fflush(stdout); + } + } + + double elapsed = (double)(clock() - start) / CLOCKS_PER_SEC; + printf("\n\nCompleted %d iterations in %.2f seconds\n", num_iterations, elapsed); + printf("Average rate: %.0f iterations/second\n", num_iterations / elapsed); + printf("\nNo crashes detected!\n"); + + return 0; +} diff --git a/tests/fuzz_afl.c b/tests/fuzz_afl.c new file mode 100644 index 0000000..2f165df --- /dev/null +++ b/tests/fuzz_afl.c @@ -0,0 +1,115 @@ +/* + * AFL++ fuzz target for url.c parser + * + * Build with AFL++: + * afl-gcc -o fuzz_afl fuzz_afl.c ../url.c -Wall -Wextra -ggdb + * + * Or with afl-clang-fast for better instrumentation: + * afl-clang-fast -o fuzz_afl fuzz_afl.c ../url.c -Wall -Wextra -ggdb + * + * Run with: + * afl-fuzz -i corpus/ -o findings/ -- ./fuzz_afl + */ + +#include +#include +#include +#include +#include + +#include "../url.h" + +// Maximum input size +#define MAX_INPUT_SIZE 8192 + +// Test buffer for outputs +static char output_buffer[16384]; + +int main(int argc, char **argv) +{ + (void)argc; + (void)argv; + + // Read input from stdin (AFL++ feeds input this way) + unsigned char input[MAX_INPUT_SIZE]; + ssize_t len = read(STDIN_FILENO, input, sizeof(input)); + + if (len <= 0) { + return 0; + } + + // Ensure null termination for safety + if (len < (ssize_t)sizeof(input)) { + input[len] = '\0'; + } + + // Fuzz url_parse with different flag combinations + URL url; + int flags_to_test[] = { + 0, + URL_FLAG_ALLOWREF, + URL_FLAG_RFC3986, + URL_FLAG_ALLOWREF | URL_FLAG_RFC3986 + }; + + for (int i = 0; i < 4; i++) { + int flags = flags_to_test[i]; + + // Test with NULL cursor + int ret = url_parse((char *)input, len, NULL, &url, flags); + + // Test with cursor + int cursor = 0; + url_parse((char *)input, len, &cursor, &url, flags); + + // If parse succeeded, test serialization + if (ret == 0) { + url_serialize(url, NULL, output_buffer, sizeof(output_buffer)); + + // Test serialization with itself as base + url_serialize(url, &url, output_buffer, sizeof(output_buffer)); + } + } + + // Test whitespace removal + int cleaned_len = url_remove_white_space((char *)input, len, output_buffer, sizeof(output_buffer)); + + // If whitespace removal succeeded, try parsing the cleaned input + if (cleaned_len > 0 && cleaned_len < (int)sizeof(output_buffer)) { + url_parse(output_buffer, cleaned_len, NULL, &url, 0); + } + + // Test percent decode on the full input + URL_String str = { (char *)input, len }; + url_percent_decode(str, output_buffer, sizeof(output_buffer)); + + // Test IPv4 parsing + URL_IPv4 ipv4; + url_parse_ipv4((char *)input, len, NULL, &ipv4); + + int cursor = 0; + url_parse_ipv4((char *)input, len, &cursor, &ipv4); + + // Test IPv6 parsing + URL_IPv6 ipv6; + url_parse_ipv6((char *)input, len, NULL, &ipv6); + + cursor = 0; + url_parse_ipv6((char *)input, len, &cursor, &ipv6); + + // Test parsing with base URL (if input is long enough) + if (len > 10) { + // Try to use first half as base, second half as reference + int mid = len / 2; + + URL base_url; + if (url_parse((char *)input, mid, NULL, &base_url, 0) == 0) { + URL reference; + if (url_parse((char *)input + mid, len - mid, NULL, &reference, URL_FLAG_ALLOWREF) == 0) { + url_serialize(reference, &base_url, output_buffer, sizeof(output_buffer)); + } + } + } + + return 0; +} diff --git a/tests/run_afl.sh b/tests/run_afl.sh new file mode 100755 index 0000000..43e61df --- /dev/null +++ b/tests/run_afl.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Run AFL++ fuzzing +# +# Usage: +# ./run_afl.sh [duration_in_minutes] +# +# Examples: +# ./run_afl.sh # Run indefinitely (press Ctrl+C to stop) +# ./run_afl.sh 60 # Run for 60 minutes + +set -e + +# Check if fuzz target exists +if [ ! -f "fuzz_afl" ]; then + echo "Error: fuzz_afl binary not found!" + echo "Please run ./build_afl.sh first" + exit 1 +fi + +# Check if AFL++ is installed +if ! command -v afl-fuzz &> /dev/null; then + echo "Error: afl-fuzz not found!" + echo "Please install AFL++:" + echo " sudo apt-get install afl++" + exit 1 +fi + +# Create output directory if it doesn't exist +mkdir -p findings + +# Set AFL++ options for better performance +export AFL_SKIP_CPUFREQ=1 +export AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 + +echo "Starting AFL++ fuzzing..." +echo "Corpus directory: corpus/" +echo "Output directory: findings/" +echo "" +echo "Press Ctrl+C to stop fuzzing" +echo "" + +# Run AFL++ with optional timeout and dictionary +DICT_ARG="" +if [ -f "url.dict" ]; then + DICT_ARG="-x url.dict" + echo "Using dictionary: url.dict" +fi + +if [ -n "$1" ]; then + timeout_sec=$((60 * $1)) + echo "Running for $1 minutes ($timeout_sec seconds)" + timeout $timeout_sec afl-fuzz -i corpus/ -o findings/ $DICT_ARG -- ./fuzz_afl || true +else + afl-fuzz -i corpus/ -o findings/ $DICT_ARG -- ./fuzz_afl +fi + +echo "" +echo "Fuzzing completed!" +echo "" +echo "Results:" +echo " Crashes: findings/default/crashes/" +echo " Hangs: findings/default/hangs/" +echo " Queue: findings/default/queue/" +echo "" + +# Check for crashes +if [ -d "findings/default/crashes" ]; then + num_crashes=$(find findings/default/crashes -type f ! -name 'README.txt' | wc -l) + if [ $num_crashes -gt 0 ]; then + echo "⚠️ Found $num_crashes crash(es)!" + echo "To reproduce a crash:" + echo " ./fuzz_afl < findings/default/crashes/id:000000,*" + else + echo "✓ No crashes found" + fi +fi + +# Check for hangs +if [ -d "findings/default/hangs" ]; then + num_hangs=$(find findings/default/hangs -type f ! -name 'README.txt' | wc -l) + if [ $num_hangs -gt 0 ]; then + echo "⚠️ Found $num_hangs hang(s)!" + else + echo "✓ No hangs found" + fi +fi diff --git a/tests/url.dict b/tests/url.dict new file mode 100644 index 0000000..72a05be --- /dev/null +++ b/tests/url.dict @@ -0,0 +1,84 @@ +# AFL++ dictionary for URL fuzzing +# This helps AFL++ generate more interesting mutations + +# URL schemes +"http://" +"https://" +"ftp://" +"file://" +"mailto:" +"data:" +"ws://" +"wss://" + +# Special characters +":" +"/" +"?" +"#" +"@" +"." +"%" +"[" +"]" + +# Authority separators +"//" +"@" + +# Port separators +":80" +":443" +":8080" +":3000" + +# Percent encoding +"%20" +"%2F" +"%3A" +"%00" +"%FF" + +# IPv4 patterns +"127.0.0.1" +"192.168.1.1" +"0.0.0.0" +"255.255.255.255" + +# IPv6 patterns +"::1" +"::" +"2001:db8::1" +"::ffff:192.0.2.1" + +# Common paths +"/index.html" +"/api/v1" +"/../" +"/.." +"/." + +# Common query strings +"?query=value" +"&" +"=" + +# Common fragments +"#fragment" +"#top" + +# Userinfo +"user:password@" +"user@" +":password@" + +# Special values +"localhost" +"example.com" +"example.org" + +# Whitespace +"\t" +"\n" +"\r" +" "