Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd0a0f0c26 |
@@ -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)
|
||||||
Executable
+44
@@ -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"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
gcc -o fuzz.exe fuzz.c ../url.c -Wall -Wextra -ggdb -O2
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
gcc -o fuzz.out fuzz.c ../url.c -Wall -Wextra -ggdb -O2
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
//example.com/path
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
http://example.com
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
https://user:pass@example.com:8080/path?query=value#fragment
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
http://example.com/path%20with%20spaces
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
file:///path/to/file
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
http://192.168.1.1/path
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
http://[2001:db8::1]/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
http://localhost:3000/api/v1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
mailto:user@example.com
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../relative/path
|
||||||
+363
@@ -0,0 +1,363 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -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 <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
Executable
+86
@@ -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
|
||||||
@@ -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"
|
||||||
|
" "
|
||||||
Reference in New Issue
Block a user