A method for fuzz testing based on symbolic execution
By introducing taint analysis and binary modeling of operand dependency graphs into fuzzing, obstacles in fuzzing are overcome, enabling more efficient and accurate fuzzing at the binary level, and allowing for faster and more comprehensive discovery of software vulnerabilities.
Patent Information
- Application Number
- CN202210624907.X
- Authority / Receiving Office
- CN · China
- Patent Type
- Patents(China)
- Current Assignee / Owner
- Filing Date
- 2022-06-02
- Publication Date
- 2025-12-05
- Estimated Expiration
- 2042-06-02
AI Technical Summary
Existing fuzz testing suffers from blindness during the mutation process, making it difficult to overcome obstacles such as magic numbers, checksums, and nested loops in the program. Furthermore, symbolic execution suffers from symbol loss and slow execution when applied at the binary level.
A symbolic execution-based fuzzing method is adopted. By modeling at the binary level through taint analysis and heuristic algorithms, an operand dependency graph is constructed to identify file structure and field semantics, guiding fuzzing to perform precise structural mutations. Hybrid symbolic execution technology is used to reduce information loss and improve mutation efficiency.
It enables more efficient and accurate fuzzing at the binary level, reduces information loss during symbolic execution, improves the accuracy and coverage of mutations, and can discover software vulnerabilities faster and more comprehensively.
Smart Images

Figure CN115017516B_ABST
Abstract
Description
Technical Field
[0001] This invention relates to semi-automated testing techniques, and more particularly to fuzz testing techniques based on symbolic execution. Background Technology
[0002] With the rapid development and widespread application of computers and the internet, an increasing amount of software is being developed and deployed across various industries, from personal computers, mobile phones, online shopping, and office systems to commercial markets, the medical industry, big data centers, and the aviation sector. Our world is driven by a wide variety of software, and this vast amount of software contains numerous security vulnerabilities, ranging from minor to serious. For individuals, these vulnerabilities may lead to privacy breaches or financial losses; for companies, they may result in lost market share; for hospitals, they may mean the loss of patients' lives; and for aerospace, they represent a significant loss of national funds and personnel. Therefore, discovering and eliminating these vulnerabilities is of great significance to individuals, society, and the nation.
[0003] Currently, methods for discovering vulnerabilities include unit testing, integration testing, symbolic execution, formal verification, and fuzzing.
[0004] Fuzzing is a semi-automated testing method that generates large amounts of input data through rules or mutation algorithms, runs the program with this input, and monitors its abnormal states to discover vulnerabilities. Currently, fuzzing is the mainstream and most effective vulnerability discovery method, characterized by its high speed, high degree of automation, and ability to work well with or without source code. However, obstacles such as magic numbers, checksums, and nested loops exist in programs, making it difficult for fuzzing mutation strategies to overcome these barriers and hindering further vulnerability discovery. To address these issues, static analysis, taint analysis, and symbolic execution techniques have been applied to fuzzing with significant progress. This patent utilizes the precise solution capabilities of symbolic execution, avoiding its drawbacks, and further improving the efficiency of fuzzing for faster and more comprehensive vulnerability discovery, which is of great significance for software vulnerability discovery.
[0005] Symbolic execution is a technique that uses static analysis of conditional statements in a program to determine whether a path is reachable or to find potential vulnerabilities without running the program. It is a precise analysis method that can accurately determine which paths the input can cover and how the input can satisfy conditional statement constraints.
[0006] Traditional symbolic execution, also known as static symbolic execution, does not run the target program. Instead, it continuously analyzes and traverses the target program's execution tree during symbolic execution until all paths have been traversed or a specified time has elapsed. Symbolic execution maintains a runtime state set ES during execution, which stores copies of the states at the conditional statements. A state is a triple containing a program counter ip, a symbolic mapping set σ, and a path constraint pc, denoted as s = (ip, σ, pc). A symbolic mapping is a mapping from symbolic variables to expressions; for example, σ(t) → 4x + y represents the expression t = 4x + y.
[0007] Unlike traditional symbolic execution, which uses symbolic representations for all variables, hybrid symbolic execution replaces certain variables with actual values to eliminate constraints that are difficult to solve. For example, consider the hash function hash(x), which is almost impossible to represent symbolically, and even if it could, it would be impossible to solve. With hybrid symbolic execution, if x is replaced with a concrete value (x = 42) and hash(x) = 173, the constraint can be expressed as 173 = y and 173 ≠ y, which the constraint solver can then solve. However, this also loses the path within the hash function. Another characteristic of hybrid symbolic execution is its faster speed compared to traditional symbolic execution. This is because simulating and analyzing programs to build symbolic expressions is much slower than running expressions with concrete values to obtain concrete values. Hybrid symbolic execution avoids the symbolic representation of complex functions and external functions by using concrete values.
[0008] Currently, there are two main practices in the field of hybrid symbolic execution: hybrid symbolic execution represented by DART and CUTE, and execution generation testing represented by EXE and KLEE. We will first introduce hybrid symbolic execution, and then introduce execution generation testing.
[0009] Hybrid execution maintains both the concrete value mapping M and the symbolic mapping S of variables during runtime. First, an input I is randomly generated or given, and then the program is run with I, updating both the concrete value mapping M and the symbolic mapping S simultaneously. During the establishment of the symbolic mapping, if external functions or unsolvable constraints are encountered, the expressions are replaced with concrete values from M. After the program finishes running, the path constraints corresponding to the input I are obtained. Then, according to a certain strategy, the constraints corresponding to the uncovered conditional statements are inverted to obtain new path constraints, which are then solved. This process yields different but approximate inputs I′, which are then used to repeat the process.
[0010] Hybrid symbolic execution is characterized by replacing symbolic expressions with concrete values, then negating a sub-constraint in the resulting path constraints to obtain the input covering the new path. This avoids the need to store a large amount of runtime state, reducing memory usage and preventing path explosion, which is a problem inherent in traditional symbolic execution. However, a randomly generated seed cannot cover all paths (in this example, it only happens to cover them completely). This requires regenerating a new seed and performing the same analysis when the currently generated seed cannot cover the new path. However, the newly generated seed is very likely to execute the same path again, leading to unnecessary execution. This is a trade-off between time and space to avoid path explosion.
[0011] Execution Generated Testing (GPT) is also a hybrid symbolic execution technique, but its working mode differs significantly from hybrid symbolic execution. It resembles traditional symbolic execution with concrete values as input. It was first proposed by Cristian Cadar in EXE and then popularized in KLEE. GPT also maintains a map of concrete values (M) and a map of symbols (S). However, it doesn't require all concrete values as input; instead, it allows only some concrete values, and a symbol can only be concrete or symbolic. In the process of traditional symbolic execution analysis, if the encountered conditional expression is concrete, GPT doesn't establish true / false constraints; instead, it directly runs with concrete values, proceeding along the true / false path. An expression is concrete if and only if all its symbols are concrete; otherwise, it is symbolic.
[0012] Executing generated tests can continue from the last branch, avoiding repeated program runs and saving time compared to regular mixed symbolic execution. However, the existence of a running state set makes it more memory-intensive than regular mixed symbolic execution.
[0013] Both hybrid symbolic execution and execution generation testing employ partial input to eliminate constraints that are difficult to symbolize or solve, thus enabling practical applications of symbolic execution. The difference lies in how they are implemented: hybrid symbolic execution runs directly with the complete input, obtains a path, and then negates it to solve; execution generation testing, on the other hand, provides only partial input and solves for the unknown input by traversing the execution tree. Both can use common optimization techniques and suffer from path loss issues, requiring hybrid symbolic execution to strike a balance between ease of solution and ensuring accuracy.
[0014] Fuzzing differs from symbolic execution; it is a fully dynamic program testing technique. It generates a large number of inputs through mutation algorithms or based on rules, and then runs the program. By instrumenting the program, it obtains internal runtime information to guide input generation and detect abnormal program states, thereby discovering vulnerabilities.
[0015] Fuzzing originated from automated testing programs, first appearing in the 1990s to test the stability of Unix programs by randomly generating input. Fuzzing is categorized based on the amount of internal program information obtained: black-box fuzzing, gray-box fuzzing, and white-box fuzzing. It is also categorized based on whether the target program's source code is required: binary fuzzing and non-binary fuzzing. Binary fuzzing is typically black-box or gray-box, while source code fuzzing is usually white-box. Furthermore, it can be categorized based on the target program: command-line program fuzzing, network protocol fuzzing, graphical user interface program fuzzing, kernel fuzzing, and embedded fuzzing. Currently, the mainstream research direction is gray-box fuzzing, especially coverage-oriented or vulnerability-oriented gray-box fuzzing.
[0016] Black-box fuzzing refers to fuzzing where the target program's internal information is completely unknown; the target program is treated as a black box. Because internal information is unavailable, it cannot help generate input for fuzzing. In this case, the input generator, for efficiency, often blindly generates input based on user-specified rules, resulting in significant waste. Because it doesn't require information about the target program, black-box fuzzing boasts high usability, can test various targets, and is significantly more efficient than manual testing. Early fuzzing programs were almost all of this type, and black-box fuzzing still plays a crucial role in network protocol testing.
[0017] White-box fuzzing is the complete opposite of black-box fuzzing. It can obtain all the information inside a program, even the source code. Because the source code is available, the program's control flow graph, function call graph, and code structure characteristics can be analyzed. This allows for significant guidance in fuzzing mutations, achieving much higher coverage than black-box testing and a greater chance of discovering vulnerabilities. However, many programs in the real world do not provide source code, making truly white-box fuzzing difficult. Even through binary reverse engineering, compiler optimizations and deliberate code obfuscation can prevent obtaining the original code structure, failing to achieve the effectiveness of white-box fuzzing with source code.
[0018] Gray-box fuzzing lies between black-box and white-box fuzzing. It uses instrumentation techniques—either source code instrumentation or binary dynamic instrumentation—to obtain runtime coverage information, operand information, and even control flow graph information. Coverage information, ranging from coarse to detailed, can be categorized as basic block coverage, instruction coverage, edge coverage, and path coverage. This coverage information helps determine the quality of inputs and prioritizes selecting seeds that lead towards high coverage or specific paths. Simultaneously, operand information or the control flow graph guides the fuzzing mutation algorithm to determine the appropriate mutation operation at which point in the input, thus overcoming certain difficult conditions. Gray-box fuzzing, possessing both the usability of black-box fuzzing and a degree of accuracy of white-box fuzzing, has become a mainstream research direction, uncovering numerous vulnerabilities in practice.
[0019] Currently, most mainstream fuzz testing methods follow... Figure 1 The workflow is as follows: First, suitable seeds are selected from the initial seed set provided by the user to form a seed queue; this step is called seed distillation. Then, a seed is selected from the seed queue according to a certain strategy and allocated a certain amount of energy (i.e., how many new inputs this seed can derive). The mutation algorithm generates a corresponding number of test cases as input based on the energy and the seed. If it is a generation-based algorithm, it will generate a certain number of test cases based on the input description. The target program runs each generated input, and fuzzing obtains certain information through the program's internal stub code and feeds it back to the mutation algorithm. At the same time, fuzzing detects whether the program has encountered anomalies, commonly crashes or infinite loops. If an anomaly occurs, the input is collected and added to the crash set; otherwise, it is determined whether the input has improved the coverage or moved towards the target path. If so, the input is added to the seed queue as a new seed. The following mainly explains the fuzzing process based on the mutation strategy in detail.
[0020] Seed distillation is the process of selecting the most suitable seed from a large pool of initial seeds for the target program. Initial seeds typically come from test cases, publicly available exploit samples online, or, for programs that parse specific files, from downloaded file samples. Some seeds may be equivalent for the target program, so a streamlined set of seeds needs to be selected. For example, AFL provides two streamlined programs, afl-tmin and afl-cmin, which consider two seeds equivalent if they produce the same edge cover. The former selects the seed with the shortest runtime from the equivalent seeds, while the latter selects the seed with the smallest size.
[0021] Seed selection follows a specific strategy to choose suitable seeds from a seed queue for subsequent mutation. Common selection strategies include: random selection, sequential selection, selection based on the product of seed length and runtime, selection based on the number of times the seed has been selected and the number of accessed paths, selection based on the seed's effectiveness in relation to file structure, and selection based on the number of times the seed accesses memory. Choosing different seeds for subsequent mutation based on different objectives and fuzzing strategies can improve the efficiency of fuzzing.
[0022] Energy allocation refers to the number of mutation opportunities given to a selected seed during mutation. The probability that a seed, through a limited number of mutations, can cover a new path is extremely low. Therefore, a seed needs to undergo many mutation attempts, and after each mutation, it needs to be tested to determine whether the coverage rate has improved. However, it is also important not to allocate too much energy to it, thus wasting resources. In the AFLFast strategy, it is considered that the energy E of a seed should be the reciprocal of its probability p(i) of obtaining a new path through mutation, i.e. In AFL, the energy of each seed is constant, which can lead to some seeds receiving insufficient energy while others receive excessive energy. Therefore, AFLFast employs a truncated exponential growth method to quickly approximate the appropriate energy, as shown in the following equation, where α(i) represents the initial fixed energy assigned to seed i in AFL; β is a constant factor; f(i) represents the number of times the path corresponding to the seed is executed; s(i) is the number of times the seed is selected; and M is the energy cap.
[0023]
[0024] There are two main methods for test case generation: rule-based generation and mutation-based generation. Most rule-based generation methods are black-box fuzzing, requiring users to provide a specification file. The fuzzer then generates input based on the descriptions of each field or a template in the specification file. For example, SPIKE, a black-box network protocol fuzzing tool, requires a file describing network protocol fields; Peach requires an XML file named PeachPit to describe the input type, test logic transformations, and target program responses. Both methods require users to have some prior knowledge and write corresponding rule files to maximize the effectiveness of the fuzzing program. There are also fuzzing programs with built-in rules for specific programs, such as jsfunfuzz and LangFuzz. The former has built-in JavaScript rules and can automatically generate JavaScript code to fuzz the browser's JavaScript interpreter; the latter is more general, applicable to all programming languages. Users only need to provide the context-free syntax of the corresponding language, and LangFuzz can automatically generate code snippets and perform structured mutations based on these snippets for testing.
[0025] Mutation-based test case generation is primarily used in gray-box and white-box fuzzing because fuzzing programs can acquire certain information about the target program, thus requiring no prior knowledge from the user. Mutation-based fuzzing programs internally incorporate simple mutation operations, such as flipping bits or bytes, treating bytes as integers for addition or subtraction, deleting or adding bytes, replacing bytes with random numbers, or cloning bytes and inserting them into other positions. Then, a random seed is selected for crossover. These steps simulate the mutation and crossover of genes in nature to obtain superior seeds. Since different mutation operations have varying efficiencies for different programs, using mutation operations appropriately can improve fuzzing efficiency. Mutation operations are typically performed on bits and bytes, which is a low-level mutation method, making it only suitable for intensive file formats such as images, audio, and video. It is inefficient for text or programming languages.
[0026] Gray-box fuzzing heavily relies on the program's internal runtime state and information, which can only be obtained through instrumentation. Instrumentation involves inserting code (trampolines) into the target program that doesn't affect its normal operation to obtain the desired information. While instrumentation doesn't affect functionality, it does impact runtime efficiency, so trampolines should be as small and efficient as possible. Currently, instrumentation is divided into source code instrumentation and binary instrumentation. Source code instrumentation typically utilizes the compiler interface provided by the LLVM compiler suite, directly inserting the IR code of the instrumentation during the Intermediate Representation (IR) stage of compilation, and then having the compiler generate the binary file. Because the intermediate representation is inserted, subsequent compiler optimizations can be utilized, making source code instrumentation relatively efficient. Typical implementations include AFL's LLVM instrumentation, InsTrim's lightweight instrumentation, and CollAFL's optimizations for trampolines. Source code instrumentation can also utilize the compiler to generate assembly code.
[0027] The assembly code is then parsed and stub code generated by GCC is inserted. For example, AFL's default working mode inserts stub code into the assembly code generated by GCC. This method cannot utilize the compiler to optimize the stub code, and different assembly code needs to be written for different target platforms, resulting in low portability.
[0028] Binary instrumentation is more difficult than source code instrumentation. Firstly, different architectures have different instructions; secondly, disassembling binary code is inaccurate. Currently, there are three main methods of binary instrumentation: static decompilation instrumentation, dynamic execution instrumentation, and hardware-assisted instrumentation. Static decompilation has two forms: the first is to statically decompile to an intermediate representation, then instrument it, and finally compile it back into binary; the second is to directly modify the binary program and insert instrumentation code through static binary analysis. Both of these methods involve instrumentation through static analysis of the binary, but binary code is usually highly optimized and may contain instruction obfuscation and junk code, which can cause problems during static analysis. Dynamic execution instrumentation utilizes the instruction virtual machine to run the binary file and uses the virtual machine's interface to detect the program's internal state to obtain information. Typical examples include Intel's dynamic instruction instrumentation tool Pin, the program dynamic analysis tool Valgrind, and the hardware virtual machine QEMU. This method is significantly more accurate than static instrumentation, but its execution efficiency is relatively low. However, QEMU utilizes JITx, so its execution speed is relatively good. The fastest method is hardware-assisted instrumentation, which utilizes… Processor-provided ProcessTrace technology allows the processor to record program instruction execution information during program execution, and then analyze it. Currently, PT-Fuzz and kAFL use this technology. This technology is extremely fast and does not require any modification to the target program, but currently only... This technology can be used in processors from the 5th generation onwards, but it is only applicable to the x86 architecture.
[0029] Anomaly detection involves fuzzing programs monitoring abnormal behavior in a target program given input, typically resulting in crashes or infinite loops. Program anomalies can occur in many ways, including business logic errors, incorrect output, out-of-bounds read / write errors, uninitialized pointer dereferencing, stack overflows, integer overflows, and multiple free errors. Currently, fuzzing anomaly detection is only effective against anomalies that cause crashes or infinite loops; it is ineffective against non-internal errors such as incorrect business logic or output. Even internal errors do not necessarily lead to abnormal program behavior; for example, reading data out of bounds usually does not cause a crash. To capture internal errors as much as possible, developers often use assertions to guarantee internal logic, making errors more likely to be exposed and caught during testing. Source code or binary instrumentation techniques can also be used, employing sanitizer techniques to detect address access errors (Address Sanitizer), memory access errors (Memory Sanitizer), or undefined behavior errors (Undefine Behavior Sanitizer). Capturing anomalies in graphical user interface (GUI) programs is more difficult. Firstly, drawing the GUI significantly impacts testing efficiency. Secondly, GUI programs typically rely on various events for input, rather than ordinary file input, making it difficult for fuzzing programs to generate such input. Even when an anomaly occurs, the program often enters an idle state, requiring special methods for analysis.
[0030] Fuzzing programs identify inputs that cause anomalies through anomaly detection. However, different inputs may trigger the same vulnerability, so a method for vulnerability classification is needed. AFL (Automatic Function Loading) calculates the edge-cover bitmap of the inputs that cause crashes to distinguish between different crashes. That is, if two inputs both cause a crash but their edge covers are different, then the inputs are considered to have triggered two different vulnerabilities. However, because AFL's bitmaps can have conflicts, this method is not accurate enough. Currently, context-dependent methods are also used to identify different crashes, specifically by calculating the hash value of the function call stack information.
[0031] In general, fuzzing is a more practice-oriented technique compared to symbolic execution. Each stage of fuzzing has room for optimization and is closely related to practical application. Because it originates from and develops through practice, it has become a powerful tool for discovering vulnerabilities.
[0032] Symbolic execution-based fuzz testing, also known as hybrid testing, is currently a hot research topic. The purpose of introducing symbolic execution into fuzz testing is to address the blindness in the mutation process of fuzz testing and improve the ability to overcome difficult verifications. However, this also introduces problems such as slow program execution and difficulty in solving constraints. Summary of the Invention
[0033] The technical problem to be solved by the present invention is to provide a fuzz testing method that models at the binary level through binary dynamic execution.
[0034] The technical solution adopted by this invention to solve the above-mentioned technical problems is a fuzz testing method based on symbolic execution, comprising the following steps:
[0035] 1) The fuzzing tool first initiates the symbolic execution and analysis program, and then sends a request to it; the symbolic execution and analysis program is built at the binary level;
[0036] 2) Upon receiving the request, the symbol execution and analysis program calls the taint analysis program to perform taint analysis based on binary symbols;
[0037] 3) After completing the taint analysis, the taint analysis program generates a record file of the tainted instructions and stores the record file in the symbolic execution and analysis program in the output directory of the fuzzing tool;
[0038] 4) The symbolic execution and analysis program analyzes the record file, identifies the record file structure through heuristic algorithms to obtain field and data block information, and constructs an operand dependency graph;
[0039] 5) The symbolic execution and analysis program traverses the operand dependency graph in reverse topological order, establishes a symbolic representation for each binary instruction, completes hybrid symbolic execution, and then responds to the fuzzy testing tool.
[0040] The applicant analyzed existing hybrid fuzzing methods. While symbolic execution has been introduced, it doesn't address the issue of symbol loss, and using concrete values for substitution leads to precision loss. Secondly, current mutation-based fuzzing focuses on low-level mutations and obstacle breaking, with less attention paid to semantic and structural analysis of the input for structuring. Traditional symbolic fuzzing tools introduce structured mutations in their mutation algorithms, but lack the precise obstacle-breaking capabilities of symbolic execution. Thirdly, symbolizing each instruction during symbolic execution is an expensive operation. While taint analysis replaces the symbolization of MOV-type instruction transfers, its field-tree-based constraint building uses too many concrete values, leading to accuracy issues. Finally, current symbolic execution methods rarely directly target binary symbols.
[0041] The beneficial effects of this invention are as follows: First, it introduces hybrid symbolic execution technology to model at the binary level, thereby reducing information loss during taint analysis and symbolic execution; then, during binary symbolic execution analysis, it uses heuristic algorithms to identify file structure and field semantics, thereby guiding fuzz testing to perform precise structural mutations; finally, it traverses the operand dependency graph in reverse topological order, where the operands have already been symbolized or replaced with concrete values, which avoids the recursive symbolization of other fields in the instructions by Intriguer, resulting in good efficiency and high accuracy. Attached Figure Description
[0042] Figure 1 For the existing fuzz testing process;
[0043] Figure 2 This is a request-response process for a symbol-based fuzz test.
[0044] Figure 3 A flowchart for the taint analysis program leetaint;
[0045] Figure 4 Sequence diagram of the communication protocol between LeeSym and AFL;
[0046] Figure 5 This is a diagram illustrating the data flow in the program;
[0047] Figure 6 This is a diagram illustrating operand dependencies.
[0048] Figure 7 This is a schematic diagram of a directed acyclic graph. Detailed Implementation
[0049] Symbolic execution-based fuzzing involves three parts: the taint analyzer leetaint, the symbolic execution and analysis program LeeSym, and the fuzzing tool AFL.
[0050] like Figure 2 As shown, the method for performing a fuzz test in this embodiment is as follows:
[0051] 1) AFL first starts LeeSym and then sends a request to it;
[0052] 2) After receiving the request, LeeSym calls leetaint to perform taint analysis;
[0053] 3) After leetaint completes the taint analysis, it generates a log file of the contaminated instructions and stores the log file in the AFL output directory. <outdir>In leesym / trace.txt;
[0054] 4) LeeSym analyzes the record file to obtain field and data block information, and then constructs an operand dependency graph;
[0055] 5) LeeSym completes the symbolic execution process, receives new input, and responds to AFL;
[0056] Steps 1) through 5) can be repeated continuously.
[0057] Leetaint includes a system call module, an instruction analysis module, and a taint information maintenance module, such as... Figure 3 As shown. The dynamic instruction instrumentation tool IntelPin receives the target binary file and the corresponding program input file, and then runs the target binary. The system call module calls the callback functions implemented by leetaint for each system call (syscall) and each instruction, respectively simulating system calls, analyzing instruction semantics, and maintaining taint information. The instruction analysis module calls the corresponding function for each analyzed instruction. The instruction function simulates the information propagation of each instruction and records data of tainted instructions in a certain format for subsequent analysis. The taint information maintenance module generates a log file based on the tainted instructions recorded by the instruction analysis module. The global variables inodes{} and opened_table{} record the taint status of the file and are maintained by the system call module. The global variables g_registers{} and g_pages{} record the taint status of registers and memory and are maintained by the corresponding interface functions, provided to the taint information maintenance module.
[0058] LeeSym's server mode differs from the normal server mode in that it operates on sockets; it's more like a language server. Normally, once in server mode, everything from standard input is treated as a protocol request, and responses are sent via standard output. When LeeSym enters server mode, it waits for requests from AFLs, such as time-series requests. Figure 4 As shown:
[0059] 1) AFL sends a message to LeeSym in cmdline format: <targetbinarypathwitharguments>The request begins with the keyword `cmdline:`, indicating the path and parameters of the target program being tested; then it is followed by sending `outdir:`. <pathofoutputdirectory>The request specifies the path to the AFL output directory;
[0060] 2) After LeeSym correctly accepts and processes the request, it responds with "OK", and the handshake is complete;
[0061] 3) AFL sends the seed file path (path-to-seed-file) to LeeSym, then waits for LeeSym to process it and perform symbolic execution to obtain new seed and field information;
[0062] 4) LeeSym generates via callback: <numer>This indicates how many new seeds were generated. Figure 4 The number is 42, followed by the path to each generated seed: input0: path-to-generated-input and by field: <type>,<start,end> Provide field information.
[0063] AFL can request LeeSym multiple times to obtain the desired seed information until AFL completes the fuzz test;
[0064] 5) AFL sends Bye to LeeSym;
[0065] 6) LeeSym responds to Bye and exits the program.
[0066] The following detailed improvements were made to the implementation of the example:
[0067] 1. Modeling system calls
[0068] Both taint analysis and symbolic execution suffer from information loss during propagation. Ideally, a computer system stores program execution information only in registers and memory. This allows for precise knowledge of how data and symbols are propagated during taint analysis or symbolic execution simulations. However, real computer programs run in user mode, interacting with the environment to complete tasks. These interactions occur in kernel mode via system calls from the operating system. This can lead to data and symbol information being propagated outside the program, a problem that taint analysis and symbolic execution tools cannot solve. Therefore, modeling system calls is necessary.
[0069] Although data transmission during actual program execution varies, it can be categorized into six types, such as... Figure 5 The diagram illustrates the data loss caused by reading data from a file into memory, transferring data between memory locations, transferring data between memory and registers, transferring data between registers, transferring data from memory to a file, and releasing memory. Data transfers within a program—namely, transfers between memory locations, transfers between registers, and transfers between registers—usually do not cause the loss of dirty data; data loss only occurs when the data is overwritten by uncontaminated data.
[0070] Let R denote the mapping from register name to input data offset and length, and let r denote the register name. Then R(r) → {offset, size} means that register r is corrupted by [offset, offset + size) bytes from the input. The value of r is taken from the set of register names R, and the specific value of R depends on the instruction set. The values for x86-64 architecture are as follows:
[0071] R={rax,rbx,rcx,rdx,rbp,...,r8,r8,r10,...}
[0072] `offset` represents the file offset, and `size` typically takes values of 1, 2, 4, 8, 16, 32, etc. Values 16 and 32 are chosen because the SIMD instruction set has 128-bit registers of class `xmm0`. Similarly, let `M` denote the mapping from memory address to input data offset. `M(a) → offset` means that the byte corresponding to memory address `a` is corrupted by the `offset`-th byte of the input. Let `dom R` denote the domain of mapping `R`. If register `r` is corrupted, it can be represented as `r ∈ dom R`. Likewise, if address `a` is corrupted, it can be represented as `a ∈ dom M`. The operation `R(r) ← {offset, size}` represents updating the mapping `R`, adding or modifying information about the corruption in register `r`. `R ← Rr` means removing the corruption from register `r`. The same applies to memory.
[0073] Next, we model the file system, whose structure is similar to the inode in a file system. Let the file open table F be the mapping from file descriptors (fd) to triples {rdcur, wrcur, N}, where rdcur and wrcur represent the offsets of the file read / write pointers corresponding to this file descriptor, and N is the file information node. The so-called file information node is also a triple containing {src, name, I}, where src indicates whether this file is a polluted source file (usually the specified input file), name is the filename, and I is a mapping similar to M, recording which bytes of this file were polluted by which bytes of the input. Specifically, I(s)→offset means that the s-th byte of this file was polluted by the offset-th byte of the input. The reason F is designed as a mapping from file descriptors to file information nodes, rather than a direct mapping to the polluting information I, is to facilitate the implementation of system calls such as dup (copying file descriptors) and rename (renaming files). In addition, the file system set B records the information nodes N of all polluted files, and B←B+N and B←BN represent operations of adding and retrieving file information nodes from the file system.
[0074] 2. Precise structural variation
[0075] 2.1. Constructing the operand dependency graph
[0076] After completing the preceding modeling, taint analysis traces the input taint of the binary program, obtaining information on all tainted instructions and which bytes were tainted. Using this taint information, the next step is to extract structural information from the input to guide more effective and advanced fuzzing. This section first introduces the format of the records generated by taint analysis, then describes the algorithms used to obtain input field information, and further determines the field types, such as magic numbers, checksum fields, length fields, and type fields. Finally, it identifies data chunks in the file and combines this information with field information to provide structured mutations.
[0077] We know that the operands of an instruction must originate from the result of a preceding instruction. Therefore, this paper adopts a natural approach: if the result of a preceding instruction is used as an operand in a subsequent instruction, then the two operands must be equal. However, since many instructions may produce the same result, this simple relationship is insufficient. We utilize offset information obtained from taint analysis. If the operand of an instruction is tainted by certain bytes, and the preceding instructions also have these offsets, and the result of this instruction is equal to the operand, then we consider which instruction the operand directly originates from. This relationship is called instruction operand dependency, or simply instruction dependency. Formally, let the current instruction be sequentially numbered (or simply sequence number) i in the record, and let the instruction be denoted as I. i Its left operand is And the input offset is in the set Byte pollution (these bytes are not necessarily consecutive), similarly, the right operand is The corresponding set of input offsets is Then instruction I i Direct dependency instruction I of the left operand k The following relationship is satisfied to minimize the value of ik:
[0078]
[0079] Where op k Indicates instruction I k The actual calculation operation, op k This only applies to non-comparison and jump instructions. Similarly, instruction I... i Similarly, the right operand can be used to obtain directly dependent instructions. Let the function... This indicates the direct dependency instruction number obtained from the left operand.
[0080] The algorithm for constructing the operand dependency graph first groups each instruction according to the contaminated information. This reduces the complexity when obtaining the direct source instruction for each operand. For instructions with dependencies, the instruction can be found by searching upwards a short distance (guaranteed by the principle of locality of reference). If there are no dependent instructions, the entire record will be traversed, resulting in an overall time complexity of O(n^2). Therefore, grouping can greatly speed up the process of constructing the dependency graph.
[0081] In this algorithm, `size` obtains the size of the set or list, and `map` is a mapping from offsets to a list of instruction numbers, sorted in ascending order. The algorithm first groups all instructions by their contaminated offsets and records them in `map`. Then, it processes each instruction in the order they are recorded. If the instruction is an arithmetic instruction (not a comparison, LEAL, or jump instruction), the result of the calculation is recorded in `I`. i In the .result file, the direct source instruction is obtained for each operand. Based on the offset information of the operand being contaminated, all instructions affected by this offset are found in the map, and the closest one is the desired instruction.
[0082] 2.2. Identify file structure
[0083] Before identifying checksum fields, let's define a field. A field is a set of consecutive bytes in the offsets of an operand that have been contaminated by the input. Taking offsets {0x4, 0x5, 0x6, x07, 0x2, x01, ..., 0x42} as an example, x4, 0x5, 0x6, and 0x7 form a field of length 4, denoted as F(4,7). This indicates that the 4th to 7th bytes of the input are divided into a field, and they appear together in sequence during the operation, signifying they form a single unit. Similarly, 0x2 and 0x1 are also a field, denoted as F(2,1), indicating they are formed by reversing the 1st and 2nd bytes of the input. In particular, this paper also considers a single byte appearing in the offsets as a field, such as 0x42 denoted as F(66) in the example. As the definition shows, a field must appear in the offsets of an operand that has been contaminated by the input; a field implicitly indicates that the operand has been contaminated.
[0084] Magic numbers typically appear as constants in the code, which are reflected in comparison instructions as immediate values. Therefore, magic numbers can be identified using the following rules:
[0085] (1) The instruction is a comparison instruction such as cmp or test;
[0086] (2) An instruction has one and only one field for each operand, and this field has I2S properties.
[0087] (3) The other operand of the instruction is an immediate number, which is the magic number.
[0088] The so-called I2S characteristic (Input-to-State Correspondence, I2S) refers to the consistency between the input and the program's internal state. Simply put, it means that the input does not undergo complex transformations before appearing in the instruction. For example, the input content may appear in the instruction without any change, or with only a simple byte order change.
[0089] Next, checksum identification and judgment are performed. Checksum judgment is intuitive: in a comparison instruction, one operand depends on multiple bytes of input, and the other operand has I2S characteristics or is an immediate value. The key is how to identify the multiple bytes of input that the operand depends on. This patent uses operand dependency graph analysis to determine which input bytes the operand depends on. This allows information to be obtained in a single run of the target program, avoiding the overhead of repeated program runs.
[0090] The following describes a dynamic programming algorithm for identifying whether an operand depends on multiple bytes of input. This algorithm maintains information about the input bytes that an operand depends on. It iterates through each instruction record in the order of instruction execution, and for comparison statements, checks the input byte information that is merged with the previous comparison instruction. If the number of bytes dependent on an operand exceeds the constant CksumMinBytes, then this operand is marked as dependent on multiple bytes of input for that instruction.
[0091] Many binary file formats and network protocols include a length field to control the length of file data blocks. If this field can be identified, it can guide fuzzing to generate data blocks of the corresponding length, enabling the use of data length validation conditional statements. Currently, fuzzing pays little attention to this field, and even symbolic execution, which can calculate the value of the length field, cannot reveal its meaning, thus failing to accurately guide the fuzzing program's mutation.
[0092] Based on the observation that length fields, as control fields, often appear in loop conditions to control the code's traversal of the data body, this paper proposes to analyze and compare the time and space characteristics of instructions to determine whether the operands involved are likely to be length fields. The judgment rules are as follows:
[0093] (1) The instruction is a comparison instruction and appears repeatedly;
[0094] (2) At least one operand in the instruction contains a field and the operand depends only on this field, and the field does not change when it is repeated;
[0095] (3) The operands show a monotonically increasing or decreasing trend, usually a continuous integer sequence;
[0096] The maximum value of the operand variation sequence is denoted as the length-related quantity. The specific length value and this quantity have a simple linear function relationship, usually involving addition or subtraction of a constant or being equal. Therefore, the field involved in the instruction is called the length field.
[0097] The above rules essentially identify input-related quantities appearing in loop conditions and determine that these quantities are closely related to the length field. Rule 2 indicates that the loop condition depends on only one field of the input, and Rule 3 determines that the loop depends on this input field to process the data.
[0098] In addition to the magic number in the file header, the checksum of the data portion, and the length field indicating the length of the data blocks, the metadata fields of a file typically include a type field indicating the type of data block. The type field determines the type of data block and which part of the code processes it. Identifying the type field and enumerating the types can help fuzzing explore different data block processing code, potentially uncovering more vulnerabilities. The code that handles the type field is usually implemented using one of three methods: multiple if-else conditional statements, a jump table converted from a switch statement, or traversing and querying a mapping table from the field to the processing function and calling the corresponding processing function.
[0099] Next, we will attempt to identify the type field in the jump table generated by the switch statement. Unlike ordinary function jumps or conditional jumps, where the address after the jump is a compile-time constant, the jmp address in the jump table is a variable related to the input. Therefore, by checking if the jmp instruction operand is contaminated by a certain field in the input, that field is very likely to be a type field. To minimize the possibility of false positives, we will actually analyze whether the jmp instructions at the same address, after multiple runs, satisfy the pattern of unchanged fields but changing jump target addresses.
[0100] 3. Mixed symbol execution
[0101] Without considering optimization, the operand dependency graph is traversed in reverse topological order, and a symbolic representation is established for each instruction. Because the instructions are traversed in reverse topological order, the operands of each instruction have already been symbolized or replaced with concrete values, which avoids the need for Intriguer recursion to symbolize other fields in the instructions.
[0102] input(O i ) represents the value of the corresponding offset byte in the input, similarly S(O i The algorithm first initializes the symbolic representation of each input byte as a symbol array S, and then initializes the constraints of each instruction to True. Next, it iterates through each instruction in execution order, processing each operand individually. If the operand is not corrupted, it is represented by a concrete value; if the operand is an initial value, it is represented by the initial symbol; otherwise, the operand is replaced by the symbolic representation of the previous instruction in the diagram DG. Then, it performs different processing based on the instruction type. If it is a comparison instruction, it obtains the nearest comparison instruction j through the diagram PCG. If it can be obtained, the previous constraint pc... j Conjunction is the expression that is negated by the current comparison operation (by comparing specific values). and If the relationship is known, then if a solution exists, the PC is obtained, and then the PC is updated. i If it's a jump instruction, only retrieve the upper and lower bounds of the type field. If it's a division instruction, and the divisor of the current path constraint is 0, attempt to find an input that would trigger a division-by-zero error. Other cases depend on the instruction operation and the left and right operands.
[0103] Establish a symbolic representation of the current instruction.
[0104] Next, we introduce an efficient algorithm for identifying repetitive and monotonous expressions and replacing them with concrete values to optimize the symbolic execution process.
[0105] In a directed acyclic graph, the depth of a node is defined as the longest distance from that node to a node with an out-degree of 0. For example... Figure 7 The depth of node H is 5 because its longest distance to node A, which has an out-degree of 0, is 5.
[0106] The algorithm for identifying and optimizing repeated instructions in loops is based on the longest path. It judges the repetition by the number of instructions with the same address. If the number of repetitions is greater than the LoopInsMax constant, the instruction is marked as needing optimization (optimized←True); otherwise, it is marked as not needing optimization (optimized←False).
[0107] The algorithm first counts the number of times an instruction is repeated at each address and records this count in the AddrCnt mapping. Then, it iterates through each instruction in reverse topological sorting. If an instruction's occurrence count or depth is less than LoopInsMax, then that instruction doesn't need optimization at this point, as the condition is never met. This rapid elimination removes most instructions. Next, it traverses the longest path, finding the nearest instruction with the same address. If it exists, the occurrence count of that instruction is incremented by 1; otherwise, the entire path is traversed. This traversal of the entire path may impact performance. In practice, the entire path is only traversed once when the instruction's depth is just greater than LoopInsMax; subsequent traversals almost always stop at the previous instruction with the same address. Furthermore, due to the principle of locality of reference, if an instruction truly occurs within a loop, a short upward traversal will find the instruction with the same address. Instructions that are unlikely to repeat on the path have extremely low occurrence counts and are eliminated early on, thus ensuring the algorithm's efficiency. Finally, the relationship between the occurrence count of instructions on the path and LoopInsMax determines whether optimization is needed. Therefore, this algorithm can guarantee a complexity of O(n) most of the time.
[0108] This algorithm also has some compatibility with repeated function calls, where the parameters are usually different. These parameter differences are reflected in the operand dependency graph as different nodes with an out-degree of 0. Therefore, even instructions at the same address appear in different paths, preventing misjudgments. Compared to Intriguer's approach of simply eliminating redundant instructions at the same address, which can lead to incorrect optimizations for repeatedly called functions, this algorithm is efficient and accurate.< / type> < / numer> < / pathofoutputdirectory> < / targetbinarypathwitharguments> < / outdir>
Claims
1. A method of fuzz testing based on symbolic execution, characterized in that, Includes the following steps: The fuzzing tool first starts the symbolic execution and analysis program, and then sends a request to it; The symbol execution and analysis program is built at the binary level; Upon receiving the request, the symbol execution and analysis program calls the taint analysis program to perform taint analysis based on binary symbols. After completing the taint analysis, the taint analysis program generates a log file of the tainted instructions and stores the log file in the symbolic execution and analysis program in the output directory of the fuzzing tool; The symbolic execution and analysis program analyzes the record file, identifies the record file structure through heuristic algorithms to obtain field and data block information, and constructs an operand dependency graph; The symbolic execution and analysis program traverses the operand dependency graph in reverse topological order, establishes a symbolic representation for each binary instruction, completes hybrid symbolic execution, and then responds to the fuzz testing tool. In taint analysis, data streams are divided into six categories: data read from a file into memory, data transfer between memory and memory, data transfer between memory and registers, data transfer between registers, data transfer from memory to a file, and data loss due to memory release. System call modeling is as follows: Let R denote the mapping from register name to input data offset and length, let r denote the register name, and let R(r)→{offset,size} indicate that register r is corrupted by [offset,offset+size) bytes from the input. r takes the value of the register name set R, and the specific value of R depends on the instruction set. offset is the file offset, and size usually takes the value 1, 2, 4, 8, 16 or 32. The symbol M represents the mapping from memory address to input data offset. M(a)→offset means that the byte corresponding to memory address a is corrupted by the input offset byte. The file system is modeled as follows: Let the file open table F be the mapping from file descriptors fd to triples {rdcur, wrcur, N}, where rdcur and wrcur represent the offsets of the file read / write pointers corresponding to this file descriptor, and N is the file information node; the file information node is a triple containing {src, name, I}, where src indicates whether this file is a pollution source file, name is the file name, and I is used to record the mapping of the file's bytes being polluted by the input bytes, I(s)→offset means that the s-th byte of this file is polluted by the offset-th byte of the input; the file system set B records the information nodes N of all polluted files; The heuristic algorithm first identifies a dependency relationship by finding that the source of taint in the operand of the current instruction intersects with the source of taint in the result of the previous instruction, and that the values are equal. This allows the algorithm to construct an operand dependency graph. Then, it identifies the file structure by recognizing magic numbers, checksum fields, and length fields through rules. The rules for identifying magic numbers are as follows: the instruction is a comparison instruction; one operand of the instruction has one and only one field, and this field has the I2S property, which means that the input and the state within the program are consistent; the other operand of the instruction is an immediate value, and this immediate value is the magic number. The rule for identifying the checksum field is that the instruction is a compare instruction; one operand in the compare instruction depends on multiple bytes of input, and the other operand has I2S characteristics or is an immediate number; The rule for identifying the length field is that the instruction is a compare instruction and is repeatedly present; at least one operand in the instruction contains a field and the operand only depends on this field, and the contained field does not change in the repeated presence; the operand appears in a monotonically increasing or decreasing trend, which is usually a continuous integer sequence.