PeAR: A Static Binary Rewriting Framework for Binary-Only Fuzzing Alvin Charles
Adrian Herrera
School of Computing The Australian National University Canberra, ACT, Australia
School of Computing The Australian National University Canberra, ACT, Australia
arXiv:2606.02126v1 [cs.CR] 1 Jun 2026
Peter Oslington
Alwen Tiu
School of Computing The Australian National University Canberra, ACT, Australia
School of Computing The Australian National University Canberra, ACT, Australia typically injected at compile time. However, there are often cases where source code is unavailable (e.g., when testing close-source software), requiring alternative techniques for tracking coverage. Binary-only fuzzing—fuzzing when source code is unavailable— relies on either static binary instrumentation (SBI) or dynamic binary instrumentation (DBI) to track code coverage. SBI frameworks apply instrumentation to target binaries by generating new binaries (e.g., via rewriting) containing the desired instrumentation. SBI has a relatively low runtime performance cost (compared to DBI). However, reliably and correctly applying instrumentation statically is challenging, with many SBI frameworks requiring strong assumptions about the target binary [1]. In contrast, DBI applies instrumentation to the target at runtime. DBI techniques include: (i) just-in-time (JIT) recompilation (e.g., Intel Pin [15], Dyninst [18]); (ii) emulation frameworks (e.g., QEMU [2], Icicle [3]); and (iii) probe-based instrumentation (e.g., kprobes in the Linux kernel). While applying DBI is simpler (compared to SBI), it comes with significant performance costs (e.g., 10–100× when fuzzing the LAVA-M benchmark suite with AFL [5]). Thus, improving the accuracy and generalizability of SBI has seen a renewed focus. Despite advances in SBI fuzzing frameworks [4, 9, 17, 23], we found that many still lack the features and extensibility of modern compiler- and DBI-based systems. Key performance techniques— such as deferred initialization, persistent mode, and shared memory fuzzing—remain unsupported. Moreover, we found that many of these frameworks failed to generalize and accurately instrument common fuzz targets, despite recent works [21, 23] showing that accurate static binary rewriting is now practical. This raises the question: can these advances be used to implement advanced fuzzing techniques in binary-only fuzzers? We answer this question by developing PeAR, an efficient binaryonly fuzzing framework that implements advanced fuzzing techniques. PeAR uses an off-the-shelf static binary rewriter based on the GrammaTech Intermediate Representation for Binaries [21] (GTIRB) to inject fuzzing instrumentation with features and performance comparable to state-of-the-art compiler-based solutions. In summary, we contribute:
Abstract Binary-only fuzzing is a key technique for finding bugs in closesource software. Without access to source code, the fuzzer must rely on static or dynamic binary instrumentation for coverage guidance. In practice, most fuzzers favor dynamic binary instrumentation (DBI), accepting runtime overhead to avoid the perceived accuracy and soundness challenges associated with static binary instrumentation (SBI). We show that these concerns are unwarranted, and that accurate, scalable SBI is achievable using offthe-shelf frameworks. Building on these frameworks, we develop PeAR, an extensible binary-only fuzzing framework. We demonstrate PeAR’s versatility by implementing several modern fuzzer features—including, deferred initialization, persistent mode, and shared-memory fuzzing. We evaluate PeAR over 4.25 CPU-yrs of fuzzing on the FuzzBench benchmark and find that PeAR: (i) successfully instruments 88 % of FuzzBench targets, comparable to the best SBI-based fuzzers; (ii) achieves a median throughput improvement of 4× when using persistent mode and shared memory fuzzing; and (iii) attains coverage comparable to compiler-based instrumentation. Our results show that SBI is a practical and effective technique for binary-only fuzzing, and that modern binary rewriting frameworks can apply complex instrumentation with high granularity and negligible performance compromise.
CCS Concepts • Security and privacy → Software and application security.
Keywords Fuzzing, Binary rewriting
1
Introduction
Fuzzing is a popular and effective bug-finding technique. It relies on the ability to quickly generate a large number of inputs and test those inputs against a target program. These inputs are generated to explore corner cases in the target’s behavior, which existing tests may not cover and where bugs may lurk. Modern greybox fuzzers rely on a feedback loop to determine whether these corner cases are being reached. Feedback is derived from program instrumentation that measures and tracks code coverage, guiding the fuzzer to uncover and explore new target behaviors. This instrumentation is
• PeAR, an SBI fuzzing framework for injecting modern fuzzing instrumentation across a wide range of targets (Section 3); • A comprehensive evaluation (over 4.25 CPU-yrs) of PeAR and four other state-of-the-art SBI fuzzing frameworks: 1
Alvin Charles, Adrian Herrera, Peter Oslington, and Alwen Tiu
ZAFL [17], StochFuzz [23], afl-dyninst [12], and E9AFL [9], on the FuzzBench benchmark suite (Section 4).
allows the target to read inputs directly from memory, avoiding interaction with the filesystem.
Our results show that PeAR achieves the highest code coverage among SBI-based fuzzers while being the only SBI framework to support advanced fuzzing techniques. With persistent mode and shared memory fuzzing enabled, PeAR achieves a median throughput improvement of 4× over its base configuration, all while achieving comparable levels of code coverage to compiler-based instrumentation. PeAR is available as an open-source project.1
2.2
Static Binary Instrumentation
Without access to source code, a fuzzer must rely on static or dynamic binary instrumentation for guidance. While emulation frameworks (e.g., QEMU [2], Icicle [3]) are commonly used to fuzz targets compiled for different architectures, they suffer from significant reductions in fuzzer throughput. In contrast, static binary instrumentation (SBI) does not suffer from the same throughput reductions. However, SBI is less accurate and reliable, because recovering arbitrary control flow from a binary is undecidable. Unfortunately, this impacts the usability of SBI: Schulte et al. [20] compared ten SBI frameworks and found that—despite broad support for AFL++ instrumentation—only four successfully instrumented any of the test binaries. Kim et al. [14] undertook a similar study with comparable results. These studies highlight the ongoing challenges of SBI. We summarize and compare five popular SBI fuzzing frameworks in Table 1. E9AFL, afl-dyninst, and StochFuzz rewrite target instructions with a trampoline, a small snippet of code that redirects control flow to the instrumentation (and ensures control flow returns to the original code). While trampolines are performant, they may not always be possible; e.g., if the target instruction(s) is smaller than the trampoline. RetroWrite uses reassembleable disassembly, exploiting position independent code (PIC) and relocation data to generate an assembly file that can be instrumented with fuzzer instrumentation. However, not all targets are compiled with PIC. Finally, ZAFL lifts the binary into an intermediate representation (IR), rewrites this IR, and then lowers it back down to native code. While performant, the rewriter may fail if the lifter encounters unsupported instructions. Table 1 also shows the fuzzing features supported by these five frameworks. While four frameworks support the forkserver model, none of them support the other features described in Section 2.1. In contrast, AFL++’s QEMU mode supports all of these features (albeit with larger performance overheads). This lack of modern fuzzer features—combined with the unreliability of existing SBI frameworks—motivates us to develop PeAR, which we describe in the following section.
2 Background and Related Work 2.1 Greybox Fuzzing Fuzzing is an important technique for automated bug discovery. Fuzzers typically require a target program and a set of valid inputs— known as “seeds”—to bootstrap the fuzzer. In a fuzzing campaign, new inputs are generated from these seeds (typically via random mutation) and are tested against the target. If the target crashes, the crashing input is saved for further root-cause analysis post campaign. Coverage-guided greybox fuzzing extends this idea by instrumenting the target to collect coverage information during program execution. Collected coverage information is then used to further guide input generation. This instrumentation must have low runtime overhead, allowing the fuzzer to maintain a high throughput and maximizing the number of inputs the fuzzer executes during a campaign. In addition to low-overhead instrumentation, modern greybox fuzzers—popularized by AFL++ [7]—use the following techniques to boost performance and maximize throughput. Forkserver. Greybox fuzzers must repeatedly execute the target with new inputs. However, spawning a new process for each target is a costly operation. Moreover, time is wasted waiting for the target process to initialize before an input is executed. To avoid this overhead, modern fuzzers use a forkserver that ensures the target is only initialized once. Future processes are then efficiently cloned (due to fork’s copy-on-write semantics) from the initialized process. Deferred initialization. Target binaries may have complex initialization procedures that slow down the fuzzer. To combat this, the forkserver can be initialized at a user-specified location. This deferred initialization can be used to skip target-specific initialization overheads. Persistent mode. Rather than spawning a new process per input, persistent mode allows one process to execute multiple inputs. This is achieved by wrapping the relevant target code in a loop, with each loop iteration executing a single input. While this increases fuzzer throughput, care must be taken to correctly reset the target’s state after executing each input (preventing future inputs from executing in an invalid state). Shared memory fuzzing. Fuzzers typically send inputs to the target via the filesystem. The target is then expected to open and handle this file. Reading and writing these inputs to the filesystem incurs a performance penalty. However, shared memory fuzzing
3
PeAR
We present a high-level overview of PeAR in Fig. 1. PeAR instruments target binaries using Ddisasm [8] and the GrammaTech Intermediate Representation for Binaries [21] (GTIRB). First, Ddisasm disassembles the input binary and decodes a superset of possible instructions to create a set of Datalog facts. These facts are then analyzed (e.g., for symbolization and to determine control flow) and refined before being translated to GTIRB. We then use the gtirb-rewriting framework [10] to insert coverage instrumentation, the forkserver, and the code necessary to enable deferred initialization, persistent mode, and shared memory fuzzing. The remainder of this section describes these steps in greater depth.
1 https://github.com/pear-labs/PeAR.git
2
PeAR : A Static Binary Rewriting Framework for Binary-Only Fuzzing
Table 1: Comparison of SBI fuzzing frameworks. We compare frameworks across two dimensions: the rewriter they use, and the fuzzing features they support.
Name
Rewriter Name
Type
afl-dyninst [12] E9AFL [9] RetroWrite [4] StochFuzz [23] ZAFL [17]
Dyninst [18] E9 [6] – – Zipr [11]
PeAR
Ddisasm [8]
Fuzzing Forkserver
Defer. init.
Pers. mode
Sh. mem. fuzzing
Trampoline Trampoline Reassembleable disassembler Trampoline Direct rewriter
✓ ✓ ✗ ✓ ✓
✗ ✗ ✗ ✗ ✗
✗ ✗ ✗ ✗ ✗
✗ ✗ ✗ ✗ ✗
Reassembleable disassembler
✓
✓
✓
✓
Static Analysis
Instrumentation - Coverage - Forkserver - Deferred initialization - Persistent mode - Shared memory
Figure 1: PeAR overview. The target binary is disassembled and statically analyzed before being instrumented and rewritten.
3.1
Static Analysis 1
PeAR uses reassembleable disassembly [22] to achieve efficient static binary instrumentation. We use Ddisasm—a fast and accurate disassembler implemented using Datalog—to disassemble the target binary and translate it to the GTIRB intermediate representation for further analysis and instrumentation. We choose Ddisasm because of its high success rates in a range of dissasembly and rewriting benchmarks [14, 19, 20]. Indeed, our evaluation reinforces these findings, with PeAR successfully instrumenting the vast majority of FuzzBench targets. The resulting GTIRB output (a serialized protocol buffer) contains all of the information required for inserting fuzzing instrumentation.
3.2
2 3 4 5 6 7 8 9 10
lea mov mov mov mov call mov mov mov lea
rsp ,[ rsp -0 x98 ] QWORD PTR [ rsp ] , rdx QWORD PTR [ rsp +0 x8 ] , rcx QWORD PTR [ rsp +0 x10 ] , rax rcx , < BLOCK_ID > __afl_trace rax , QWORD PTR [ rsp +0 x10 ] rcx , QWORD PTR [ rsp +0 x8 ] rdx , QWORD PTR [ rsp ] rsp , [ rsp +0 x98 ]
Listing 1: Basic block trampoline. Unlike AFL++—which represents the target as a collection of intraprocedural control-flow graphs (CFG)—GTIRB represents the target as a single interprocedural CFG, where blocks can be terminated by call instructions (not just branches) and “fallthrough” edges connect the caller block to a successor block (containing the instructions following the call). To avoid unnecessary instrumentation, PeAR does not instrument blocks with an incoming fallthrough edge. Listing 2 shows the __afl_trace function called by the trampoline. This function follows the “classic” edge coverage approach used by AFL, xor-ing the previous block identifier (__afl_prev_loc) with the current block identifier (passed in rcx). The result of this xor—an edge identifier—is used as a lookup into the coverage map (pointed to by __afl_area_ptr) where an edge counter is incremented.
Instrumentation
PeAR’s instrumentation consists of: coverage tracing, deferred initialization, persistent mode, and shared memory fuzzing. This instrumentation supports x64 Linux ELF and Windows PE executables and is compatible with the state-of-the-art AFL++ fuzzer. We describe the design and implementation of this instrumentation in the following sections. 3.2.1 Coverage Tracing. PeAR tracks edge coverage by inserting trampolines to a tracer function at the start of a GTIRB basic block.2 This approach is inspired by the __afl_maybe_log function from AFL++’s assembly-level instrumentation. Like __afl_maybe_log, PeAR’s tracing function takes a single argument: a random integer identifying the current block. Listing 1 shows our trampoline code, where <BLOCK_ID> (Section 3.2.1) is the basic block identifier. 2 Unfortunately, gtirb-rewriting does not support inserting instrumentation code
using non-live registers at patch locations, which is required for sound insertion of inlined instrumentation.
3
Alvin Charles, Adrian Herrera, Peter Oslington, and Alwen Tiu
1 2 3
main: ... call foo ...
; Save state lahf seto al
4 5 6 7 8 9 10 11
; Record path in coverage map mov rdx ,[ rip + __afl_area_ptr ] xor rcx , QWORD PTR [ rip + __afl_prev_loc ] xor QWORD PTR [ rip + __afl_prev_loc ] , rcx shr QWORD PTR [ rip + __afl_prev_loc ] , 1 inc BYTE PTR [ rdx + rcx * 1] adc BYTE PTR [ rdx + rcx * 1] , 0 x0
(a) Original function call.
12 13 14 15
main: ... call foo ...
; Restore state add al ,0 x7f sahf
16 17
ret
Listing 2: Edge coverage tracing. 3.2.2
Initialization.
Coverage Map Setup. Listing 3 shows the initialization routine inserted by PeAR.3 A call to this routine is inserted at the target’s entrypoint. This routine sets up a dummy map area (allowing the target to run without a fuzzer) before attaching to the shared memory map (Section 3.2.2). Per Section 3.2.1, this shared memory map stores edge hit counts and is used to communicate coverage information to the fuzzer. Notably, gtirb-rewriting does not support adding data to the target’s .bss section, forcing PeAR to use a dynamic memory allocation for the dummy map area (Section 3.2.2). 1 2 3 4 5 6
(b) After persistent mode application.
Figure 2: Example of PeAR’s persistent mode instrumentation.
Listing 4 shows the persistent mode handler inserted at foo’s entrypoint. The handler first saves the current register state before entering the persistent loop. The first time the loop is entered, the caller’s return address (in main) is saved to memory (Sections 3.2.3 to 3.2.3). In the loop body, the start address of the loop replaces the caller’s return address (Sections 3.2.3 to 3.2.3), ensuring the persistent loop is always executed. After looping for <PERSISTENT_MODE_COUNT> iterations (Sections 3.2.3 to 3.2.3) the original return address and register state are restored (Sections 3.2.3 to 3.2.3).
7
if ( getenv (" AFL_DEBUG " ) ) __afl_debug = 1;
9 10 11 12
__afl_map_shm () ; }
Listing 3: Coverage map initialization.
foo: backup_registers() persistent_loop_start: if first_pass: Backup return address pop; and emulate function call backup_return_address() from loop handler push persistent_loop_start; if not __afl_persistent_loop(): Check loop restore_return_address() restore_registers() Exit loop ret else: sharedmem_hook() restore_registers() Run loop iteration <function code> ret
void __afl_setup ( void ) { __afl_area_ptr_dummy = malloc (0 x10000 ) ; if ( __afl_area_ptr_dummy == NULL ) { perror (" AFL dummy map allocation failed " ) ; _exit (1) ; }
8
foo: <function code> ret
1
; Backup register state ( omitted for brevity )
2
Deferred Initialization. AFL++ recognizes a deferred forkserver by looking for the #SIG_AFL_DEFER_FORKSRV# string in the target binary. PeAR allows the user to specify the forkserver’s location (as a function or address). This location defines when the target process has been fully initialized and can be cloned. The forkserver ensures the target is only initialized once, rather than during the execution of every input. Notably, PeAR does not provide a forkserver on Windows due to the lack of support for fork operations [13]. 3.2.3 Persistent Mode. Figure 2 summarizes PeAR’s persistent mode instrumentation, while Listing 4 shows the corresponding assembly code. In Fig. 2a, the function foo is selected as a fuzz target. If source code were available, we would wrap the call to foo within a loop—e.g., using AFL++’s __afl_persistent_loop macro—signaling to the fuzzer that foo should be executed with multiple inputs without spawning a new proess per input. 3 This code is written in C, compiled into its own object file, and later linked with the
instrumented target.
3 4 5 6 7
; Start of persistent loop .Lsetup_loop : movzx eax , BYTE PTR first_pass [ rip ] test al , al je .Lnot_first_pass
8 9 10 11 12
; On first pass , save and overwrite the return address ; ( first_pass is set in __afl_persistent_loop ) pop rax mov QWORD PTR [ rip + p_mode_ret_addr_backup ], rax
13 14 15 16 17
.Lnot_first_pass : ; On subsequent passes , set new return address lea rax , [ rip + .Lsetup_loop ] push rax
18 19 20 21 22 23 24 25 26 27
4
; Check mov lea and push push mov call pop
whether to continue loop rcx , rsp rsp , [ rsp - 0 x80 ] rsp , 0 xfffffffffffffff0 rcx rcx edi , < PERSISTENT_MODE_COUNT > __afl_persistent_loop rcx
PeAR : A Static Binary Rewriting Framework for Binary-Only Fuzzing
28
mov
rsp , rcx
test jne
eax , eax .Lstart_func
1
29 30 31
2
32 33
3
; Break from loop , restoring the original return address mov rax , QWORD PTR [ rip + p_mode_ret_addr_backup ] lea rsp , [ rsp + 0 x8 ] push rax ; Restore register state ( omitted for brevity ) ret
34 35 36 37 38
4 5 6 7 8 9
39 40
; The function to fuzz .Lstart_func :
41
10 11
Listing 4: Persistent mode handler.
12 13 14 15
3.2.4 Shared Memory Fuzzing. PeAR implements shared memory fuzzing using a shared memory hook, similar to AFL++’s QEMU mode. Listing 5 shows the hook’s function signature, which takes the following arguments: (i) the current state of program registers; (ii) a pointer to the current input; and (iii) the length of the input. 1 2 3 4 5
8 9 10 11
// Hook for process ( char * buf ) , where buffer points to a caller - owned 100 - byte buffer that cannot be redirected . void __pear_sharedmem_hook ( struct x86_64_regs * regs , uint8_t * input_buf , uint32_t input_buf_len ) { memset (( void *) regs - > rdi , 0 , 100) ; if ( input_buf_len > 100) input_buf_len = 100; memcpy (( void *) regs - > rdi , input_buf , input_buf_len ); }
Listing 6: Example shared memory hooks.
4
Evaluation
We evaluate PeAR to determine the feasibility of implementing modern fuzzing techniques using SBI frameworks without compromising on performance. We conduct our evaluation on the FuzzBench [16] benchmark suite, comparing PeAR against other state-of-the-art binary-only fuzzers. Our evaluation focuses on the following metrics: • Robustness: the ability of a binary-only fuzzer to successfully instrument a given target. • Code coverage: the amount of code covered by the fuzzer on a particular target. • Speed: the number of iterations achieved (per unit of time) on a particular target. We evaluate these metrics in FuzzBench (Sections 4.1 to 4.3). Our evaluation focuses on PeAR’s Linux ELF instrumentation and does not include PeAR’s Windows PE support (due to the lack of support for PE files in FuzzBench).
struct __attribute__ (( __packed__ ) ) x86_64_regs { uint64_t rax , rbx , rcx , rdx , rdi , rsi , r8 , r9 , r10 , r11 , r12 , r13 , r14 , r15 ; uint8_t xmm_regs [16][16]; };
6 7
// Hook for LLVMFuzzerTestOneInput ( uint8_t * Data , size_t Size ) , where we can redirect rdi to point at shared memory test case . void __pear_sharedmem_hook ( struct x86_64_regs * regs , uint8_t * input_buf , uint32_t input_buf_len ) { regs - > rdi = ( uint64_t ) input_buf ; regs - > rsi = input_buf_len ; }
void __afl_rewrite_sharedmem_hook ( struct x86_64_regs * saved_regs , uint8_t * input_buf , uint32_t input_buf_len );
Listing 5: Shared memory hook. The hook’s implementation depends on how the target processes the input and where in the program the hook function is called. When instrumenting a target the user can select one of the following locations for the hook call: • At the beginning of the persistent mode loop; • At a user-selected location, specified through code address or function name; or • Immediately after forkserver initialization.
4.1
Experiment 1: Robustness
SBI-based fuzzers have traditionally failed to instrument complex “real-world” targets. Thus, we compare PeAR’s ability to instrument all 25 targets in the FuzzBench benchmark suite, comparing PeAR to four other state-of-the-art SBI-based fuzzers: ZAFL [17], StochFuzz [23], afl-dyninst [12], and E9AFL [9]. We exclude RetroWrite [4] because it only supports position-independent executables (PIE), which FuzzBench does not produce. We use afl-dyninst’s “experimental performance mode” as recommended by the authors, which led to more targets being instrumented (compared to afl-dyninst’s default mode). For two targets (curl and libjpeg-turbo), PeAR required manual Ddisasm hints to produce correct instrumentation: curl contained data tables in its .text section that were misidentified as code, while libjpeg-turbo had a constant misidentified as a symbolic expression. Table 2 summarizes the FuzzBench targets each fuzzer was able to instrument. Failing builds fell into one of two categories: • Failed application. The tool failed to produce an instrumented binary (e.g., due to a build or rewriting failure).
As PeAR operates at a binary level, there is no source-level mechanism (such as AFL++’s __AFL_FUZZ_TESTCASE_BUF macro) to redirect how the target reads its input. Instead, PeAR saves the target’s register state to a global data section (p_mode_reg_backup) before invoking the hook, and restores registers from here afterwards. This allows the hook to modify register values through the x86_64_regs struct - for example, redirecting a function’s pointer argument to the shared memory test case. The modified register value then takes effect when the target function executes. Listing 6 shows two example hooks. In both cases, the target function takes a buffer pointer as its first argument (rdi under the System V ABI). The first hook redirects rdi to point directly at AFL++’s shared memory test case, avoiding any copy. The second hook instead copies the test case into the target’s existing buffer, which is necessary if the target expects the test case to be in a specific area of memory. The hook is compiled as an object file and statically linked with the instrumented binary. 5
Alvin Charles, Adrian Herrera, Peter Oslington, and Alwen Tiu
Table 2: Successfully instrumented FuzzBench targets. ✓ indicates successful instrumentation, ✓ * indicates success with manual Ddisasm hints, ✗ indicates faulty instrumentation, and ✗✗ indicates no instrumented binary was generated. Target
PeAR
ZAFL
StochFuzz
afl-dyninst
E9AFL
bloaty_fuzz_target curl_curl_fuzzer_http freetype2_ftfuzzer harfbuzz_hb-shape-fuzzer jsoncpp_jsoncpp_fuzzer lcms_cms_transform_fuzzer libjpeg-turbo_libjpeg_turbo_fuzzer libpcap_fuzz_both libpng_libpng_read_fuzzer libxml2_xml libxslt_xpath mbedtls_fuzz_dtlsclient mruby_mruby_fuzzer_8c8bbd openh264_decoder_fuzzer openssl_x509 openthread_ot-ip6-send-fuzzer php_php-fuzz-parser_0dbedb proj4_proj_crs_to_crs_fuzzer re2_fuzzer sqlite3_ossfuzz stb_stbi_read_fuzzer systemd_fuzz-link-parser vorbis_decode_fuzzer woff2_convert_woff2ttf_fuzzer zlib_zlib_uncompress_fuzzer
✗ ✓* ✓ ✓ ✓ ✓ ✓* ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✓ ✗ ✓ ✓ ✓ ✓ ✓
✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗✗ ✓ ✓ ✓ ✓ ✓ ✗✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓
✓ ✓ ✓ ✓ ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗✗ ✗✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓
✗ ✓ ✓ ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✗ ✓
✗ ✗ ✗ ✗ ✗ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✓ ✗ ✗
# Successful Instrumentations
22
23
22
15
2
Table 3: Median final edge coverage per benchmark, normalized to AFL++ with compiler instrumentation. Bold indicates the best SBI-based fuzzer per benchmark. “—” indicates the fuzzer could not instrument the target. Benchmark
AFL++ QEMU
PeAR
PeAR p.
PeAR p.+s.
AFL Dyninst
E9AFL
StochFuzz
ZAFL
1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00
0.96 0.67 0.96 1.00 0.33 1.00 0.01 1.00 0.96 0.97 0.98 0.98 1.00 0.94 0.99 0.95 0.92 0.99 0.96 1.00
0.93 0.71 0.96 1.00 0.33 1.00 0.01 1.00 0.96 0.98 0.98 0.98 1.00 0.95 0.99 0.95 0.92 0.98 0.95 1.00
0.96 0.72 0.97 1.00 0.60 1.00 0.01 1.00 0.96 0.98 1.00 0.99 1.00 0.96 1.00 0.95 0.92 0.99 0.96 1.00
0.98 0.73 0.98 1.00 0.53 1.00 0.01 1.00 0.97 0.99 1.00 1.00 1.00 1.00 1.00 0.95 0.92 0.99 0.96 1.00
0.84 0.34 0.95 — 0.31 1.00 0.01 0.99 0.82 0.64 0.94 0.96 1.00 — — — 0.88 0.97 — 0.94
— — — — 0.33 — — — — — — — — — — — — 0.99 — —
0.77 0.58 0.95 1.00 — 1.00 0.01 0.93 0.96 0.76 0.75 0.81 1.00 0.92 0.99 0.93 0.93 0.65 0.92 0.99
0.94 0.71 0.96 1.00 0.74 1.00 0.01 1.00 0.96 — 0.99 0.99 1.00 0.93 1.00 0.95 0.92 1.00 0.94 0.99
Average
1.00
0.88
0.88
0.90
0.90
0.77
0.66
0.83
0.90
(php and mruby) as FuzzBench does not support running bug and coverage benchmarks in the same experiment. Each fuzzerbenchmark pair was fuzzed for 24 h across ten trials. This experiment was run on a local server with four Intel Xeon Gold 6252 2.10 GHz CPUs, 187 GiB of RAM, and running Ubuntu 20.04 LTS. The total compute for this experiment was approximately 4.25 CPUyrs.
• Faulty instrumentation. The tool produced an instrumented target. However, these binaries crashed on benign inputs, indicating an error in the instrumentation. Result 1
4.2.1 Results. Table 3 presents the per-benchmark median final edge coverage, normalized to AFL++ with compiler instrumentation. Among the SBI-based fuzzers, PeAR achieves the highest average normalized coverage score (87.91 %), comparable to AFL++ QEMU mode (87.84 %). ZAFL follows at 85.17 %, then StochFuzz (79.20 %), afl-dyninst (57.79 %), and E9AFL (6.62 %). PeAR with persistent mode and shared memory fuzzing achieves the best or tied-best coverage among SBI fuzzers on the majority of benchmarks, demonstrating that modern SBI frameworks can match DBI approaches in coverage effectiveness. Figure 3 shows the throughput of each fuzzer on every benchmark, normalized to AFL++ with compiler instrumentation. Among the binary-only fuzzers, PeAR with persistent mode and shared memory fuzzing consistently achieves the highest throughput ratio, while base PeAR remains competitive with ZAFL and StochFuzz.
ZAFL, PeAR, and StochFuzz are the most robust SBI-based fuzzers, instrumenting 23, 22, and 22 out of 25 FuzzBench targets respectively. However, PeAR is the only SBI fuzzer that supports deferred initialization, persistent mode, and shared memory fuzzing (Table 1).
4.2
AFL++
curl freetype2 harfbuzz jsoncpp lcms libjpeg-turbo libpcap libpng libxml2 libxslt mbedtls openh264 openssl openthread re2 stb systemd vorbis woff2 zlib
Experiment 2: Coverage and Performance
Here we evaluate the code coverage and throughput achieved by PeAR, comparing PeAR against other state-of-the-art binary-only fuzzers on a large set of FuzzBench targets. Fuzzer Selection. We compare five SBI-based fuzzers: PeAR (base configuration), ZAFL [17], StochFuzz [23], afl-dyninst [12], and E9AFL [9]. As a DBI baseline, we include AFL++’s QEMU mode with persistent mode and shared memory enabled, representing the strongest available DBI configuration. As a compiler baseline, we include AFL++ with compiler instrumentation (using FuzzBench’s aflplusplus configuration), which serves as a performance upper bound. PeAR’s persistent mode and shared memory variants are evaluated separately in the ablation study (Section 4.3).
Result 2 PeAR achieves the highest code coverage among SBI-based fuzzers, with a normalized score of 87.91 %, comparable to AFL++ QEMU mode (87.84 %), while also achieving competitive throughput.
Experimental Setup. We selected 20 FuzzBench4 targets: all targets from the benchmark suite for which at least one binary-only fuzzer could produce a working instrumented binary. We excluded the targets that PeAR failed to instrument (bloaty, proj4, sqlite3). We also excluded the bug-finding benchmarks
4.3
Experiment 3: Ablation Study
A key advantage of PeAR as a framework is its ability to implement advanced fuzzing techniques—persistent mode and shared memory fuzzing—that are unsupported by other SBI-based fuzzers (Table 1). To isolate the contribution of each feature, we compare three PeAR configurations across all 20 benchmarks:
4We forked FuzzBench at commit 2a2ca6ae4c5d171a52b3e20d9b7a72da306fe5b8
to incorporate our fuzzers.
6
PeAR : A Static Binary Rewriting Framework for Binary-Only Fuzzing
1.00
0.55
0.85
0.36 0.05 0.07
0.07 0.10
0.34 0.07 1.00
libxslt
1.00
1.24
0.06 0.05 0.07 0.10 0.15
0.11
0.11 0.12 0.11
libxml2
1.00
libpng
vorbis
0.55
0.51
woff2
0.30 0.09 0.09 0.10
0.10
0.13 0.11
0.11
0.11
0.03 0.13 0.16 0.14 0.11 0.14
0.30
0.33
0.45
0.39 0.47 0.41
systemd
0.62
0.87
1.00 0.21 0.22
0.30 0.45
0.67 0.80
0.26
0.37 0.35 0.35
libpcap
1.00 0.26 0.18
stb
ZAFL (0.17x)
0.52
0.82
1.00 1.13 0.29 0.39 0.34
0.34
0.90
1.00 0.68 0.35
0.24 0.15 0.18
re2
StochFuzz (0.21x)
1.00
E9AFL (0.15x)
1.00
1.35
1.73 0.85
openthread
0.15 0.11 0.18 0.23 0.14 0.10
0.22 0.21
0.57 0.60 0.14 0.22 0.18
openssl
0.23 0.21
1.00 0.92
openh264
0.32
0.29 0.35 0.05 0.16
0.06 0.04 0.16
0.46 0.56 0.13 0.17 0.17
0.28 0.14
mbedtls
AFL Dyninst (0.17x)
libjpeg-turbo
1.00
1.00
lcms
1.30
1.75
0.16 0.25
0.11 0.08
0.10
jsoncpp
harfbuzz
1.00
0.24
0.25
0.13 0.14 0.25 0.25 0.12 0.21 0.16
0.22 0.20 0.21
0.08 0.21
0.02 0.04 0.02
0.02
0.41
0.48
0.55
0.64 0.36 0.22
freetype2
1.00
curl
PeAR (pers. + shmem) (0.72x)
1.00
PeAR (pers.) (0.48x)
1.00
PeAR (0.18x)
0.85
1.00
AFL++ QEMU (pers. + shmem) (0.31x)
1.00
1.00
AFL++ (1.00x)
zlib
Figure 3: Throughput of each fuzzer per benchmark, expressed as a ratio to AFL++ with compiler instrumentation. Higher is better; the dashed line marks parity with AFL++. • PeAR (base): forkserver with coverage tracing only. • PeAR persistent: adds persistent mode, avoiding fork overhead for each test case. • PeAR persistent + shmem: adds shared memory fuzzing on top of persistent mode, avoiding file I/O for test case delivery. 4.3.1 Results. Persistent mode improves PeAR’s throughput by a median of 2.56× across all 20 benchmarks (range: 1.28–17.57×). Adding shared memory fuzzing provides further improvement, achieving a median of 4.07× over the base configuration (range: 1.12–31.23×). The magnitude of improvement is benchmark-dependent: targets with expensive initialization (e.g., curl: 17.57× with persistent mode, 31.23× with shared memory) benefit the most, while targets with lightweight initialization (e.g., vorbis: 1.28×) see smaller gains. These throughput improvements translate into coverage gains. The average normalized coverage score increases from 87.91 % (base) to 89.84 % (persistent) to 89.95 % (persistent + shared memory), demonstrating that the additional throughput allows PeAR to explore more of the target’s state space within the 24-hour campaign. Result 3 Persistent mode improves PeAR’s throughput by a median of 2.56× across 20 benchmarks. Adding shared memory fuzzing provides a further improvement to a median of 4.07×, demonstrating the value of PeAR’s extensible framework architecture.
4.4
Summary of Findings
We summarize here the results of our experiments along the three metrics listed at the beginning of this section. Robustness. ZAFL, PeAR, and StochFuzz are the most robust SBI fuzzers, instrumenting 23, 22, and 22 out of 25 FuzzBench 7
targets respectively. Unlike other SBI fuzzers, PeAR uniquely supports deferred initialization, persistent mode, and shared memory fuzzing. Code coverage. PeAR achieves the highest normalized coverage score among SBI-based fuzzers (87.91 % to 89.95 % depending on configuration), comparable to AFL++ QEMU mode (87.84 %). Speed. PeAR with persistent mode and shared memory fuzzing achieves a median throughput improvement of 4.07× over base PeAR, and a median of 3.56× over the next-fastest SBI fuzzer on each benchmark. These throughput gains translate into measurable coverage improvements across all 20 benchmarks.
5
Conclusions and Future Work
In light of recent advances in SBI frameworks such as GTIRB, we reexamine the viability of using SBI to build an effective and extensible binary-only fuzzing framework. We show that despite the many challenges in implementing SBI fuzzing frameworks [14, 20], it is indeed possible to build an SBI fuzzing framework that is robust, achieves comparable code coverage to compiler instrumentation, and supports advanced fuzzing techniques. Our implementation, PeAR, instruments 22 out of 25 FuzzBench targets, comparable to ZAFL (23/25) and StochFuzz (22/25). However, unlike these tools, PeAR is the only SBI framework that supports deferred initialization, persistent mode, and shared memory fuzzing. Our ablation study demonstrates the value of this extensibility: persistent mode and shared memory fuzzing provide a median throughput improvement of 4.07×, with improvements up to 31× on targets with expensive initialization. For future work, we plan to implement support for a wider range of architetures and platforms. An experimental version of PeAR for Windows PE binary is currently in development. We also plan to add other advanced instrumentations, in particular, adding support to instrument sanitizers into binaries, similar to RetroWrite.
Alvin Charles, Adrian Herrera, Peter Oslington, and Alwen Tiu
and Fast Cloning. In Network and Distributed System Security Symposium (NDSS). The Internet Society. [14] Hyungseok Kim, Soomin Kim, Junoh Lee, Kangkook Jee, and Sang Kil Cha. 2023. Reassembly is Hard: A Reflection on Challenges and Strategies. In USENIX Security Symposium (SEC). USENIX, 1469–1486. [15] Chi-Keung Luk, Robert Cohn, Robert Muth, Harish Patil, Artur Klauser, Geoff Lowney, Steven Wallace, Vijay Janapa Reddi, and Kim Hazelwood. 2005. Pin: building customized program analysis tools with dynamic instrumentation. (2005), 190–200. doi:10.1145/1065010.1065034 [16] Jonathan Metzman, László Szekeres, Laurent Simon, Read Sprabery, and Abhishek Arya. 2021. Fuzzbench: an open fuzzer benchmarking platform and service. In Proceedings of the 29th ACM joint meeting on European software engineering conference and symposium on the foundations of software engineering. 1393–1403. [17] Stefan Nagy, Anh Nguyen-Tuong, Jason D. Hiser, Jack W. Davidson, and Matthew Hicks. 2021. Breaking Through Binaries: Compiler-quality Instrumentation for Better Binary-only Fuzzing. In Security Symposium (SEC). USENIX, 1683–1700. [18] Paradyn Tools Project. 2018. Dyninst. https://dyninst.org/. [19] Soumyakant Priyadarshan, Huan Nguyen, and R. Sekar. 2024. Accurate Disassembly of Complex Binaries Without Use of Compiler Metadata. In Architectural Support for Programming Languages and Operating Systems (ASPLOS). 1–18. doi:10.1145/3623278.3624766 [20] Eric Schulte, Michael D. Brown, and Vlad Folts. 2022. A Broad Comparative Evaluation of X86-64 Binary Rewriters. In Cyber Security Experimentation and Test (CSET). ACM, 129–144. doi:10.1145/3546096.3546112 [21] Eric Schulte, Jonathan Dorn, Antonio Flores-Montoya, Aaron Ballman, and Tom Johnson. 2020. GTIRB: intermediate representation for binaries. arXiv preprint arXiv:1907.02859 (2020). [22] Shuai Wang, Pei Wang, and Dinghao Wu. 2015. Reassembleable Disassembling. In 24th USENIX Security Symposium, USENIX Security 15, Washington, D.C., USA, August 12-14, 2015, Jaeyeon Jung and Thorsten Holz (Eds.). USENIX Association, 627–642. https://www.usenix.org/conference/usenixsecurity15/technicalsessions/presentation/wang-shuai [23] Zhuo Zhang, Wei You, Guanhong Tao, Yousra Aafer, Xuwei Liu, and Xiangyu Zhang. 2021. StochFuzz: Sound and Cost-effective Fuzzing of Stripped Binaries by Incremental and Stochastic Rewriting. In Security and Privacy (S&P). IEEE, 659–676. doi:10.1109/SP40001.2021.00109
References [1] Erick Bauman, Zhiqiang Lin, Kevin W Hamlen, et al. 2018. Superset Disassembly: Statically Rewriting x86 Binaries Without Heuristics. In Network and Distributed System Security Symposium (NDSS). The Internet Society. [2] Fabrice Bellard. 2005. QEMU, a Fast and Portable Dynamic Translator. In Annual Technical Conference (ATC). USENIX, 41. [3] Michael Chesser, Surya Nepal, and Damith C. Ranasinghe. 2023. Icicle: A ReDesigned Emulator for Grey-Box Firmware Fuzzing. In International Symposium on Software Testing and Analysis (ISSTA). ACM, 76–88. doi:10.1145/3597926.3598 039 [4] Sushant Dinesh, Nathan Burow, Dongyan Xu, and Mathias Payer. 2020. Retrowrite: Statically instrumenting cots binaries for fuzzing and sanitization. In Security and Privacy (S&P). IEEE, 1497–1511. doi:10.1109/SP40000.2020.00009 [5] Brendan Dolan-Gavitt, Patrick Hulin, Engin Kirda, Tim Leek, Andrea Mambretti, Wil Robertson, Frederick Ulrich, and Ryan Whelan. 2016. Lava: Large-scale automated vulnerability addition. In Security and Privacy (S&P). IEEE, 110–121. [6] Gregory J Duck, Xiang Gao, and Abhik Roychoudhury. 2020. Binary Rewriting Without Control Flow Recovery. In Programming Language Design and Implementation (PLDI). ACM, 151–163. doi:10.1145/3385412.3385972 [7] Andrea Fioraldi, Dominik Maier, Heiko Eißfeldt, and Marc Heuse. 2020. { AFL++ } : Combining incremental steps of fuzzing research. In Workshop on Offensive Technologies (WOOT). USENIX. [8] Antonio Flores-Montoya and Eric Schulte. 2020. Datalog disassembly. In Security Symposium (SEC). USENIX, 1075–1092. [9] Xiang Gao, Gregory J Duck, and Abhik Roychoudhury. 2021. Scalable fuzzing of program binaries with E9AFL. In Automated Software Engineering (ASE). IEEE, 1247–1251. doi:10.1109/ASE51524.2021.9678913 [10] GrammaTech. [n. d.]. gtirb-rewriting. https://github.com/GrammaTech/gtirbrewriting [11] William H. Hawkins, Jason D. Hiser, Michele Co, Anh Nguyen-Tuong, and Jack W. Davidson. 2017. Zipr: Efficient Static Binary Rewriting for Security. In Dependable Systems and Networks (DSN). 559–566. doi:10.1109/DSN.2017.27 [12] Marc Heuse. 2021. American Fuzzy Lop + Dyninst == AFL Fuzzing blackbox binaries. https://github.com/vanhauser-thc/afl-dyninst. [13] Jinho Jung, Stephen Tong, Hong Hu, Jungwon Lim, Yonghwi Jin, and Taesoo Kim. 2021. WINNIE : Fuzzing Windows Applications with Harness Synthesis
8