├── .gitignore ├── LICENSE ├── README.md ├── frizzer ├── __init__.py ├── coverage.py ├── frida_script.js ├── fuzzer.py ├── log.py ├── project.py └── target.py ├── setup.py └── tests ├── picohttpparser ├── Makefile ├── indir │ ├── 1 │ └── 2 ├── picohttpparser.c ├── picohttpparser.h ├── run_in_process_fuzzing_test.sh ├── run_tests.sh ├── test └── test.c ├── run_all.sh └── simple_binary ├── Makefile ├── indir ├── 1 ├── 2 ├── 3 └── 4 ├── run_in_process_fuzzing_test.sh ├── run_tests.sh ├── test └── test.c /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled python 2 | *.pyc 3 | 4 | # pip 5 | *.egg-info 6 | 7 | # virtualenv 8 | venv 9 | 10 | # ctags 11 | tags 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dennis Mantz, Birk Kauer (ERNW GmbH) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Frizzer 2 | ======= 3 | 4 | A coverage-guided blackbox fuzzer based on the Frida instrumentation framework. 5 | 6 | 7 | Idea 8 | ---- 9 | 10 | This fuzzer is meant to be quick and relatively easy to set up in scenarios 11 | where no source code is available for a network service. Via Frida it is 12 | possible to retrieve coverage from uninstrumented binaries. Therefore, even 13 | though the fuzzer is not fast or efficient it can still be beneficial during 14 | assessments with restricted time frames. 15 | 16 | The fuzzer is written in Python 3 and runs under Linux. However, the fuzzed 17 | application does not necessarily have to run on the same system as long as 18 | Frida is also available for the respective system. The fuzzer can remotely 19 | connect to the Frida instance which runs on the target system. 20 | 21 | Currently the fuzzer expects a target which communicates via TCP (plain or TLS) 22 | and is already running. The fuzzer won't start (or restart) the service but 23 | only fuzz it until it crashes. 24 | 25 | As the fuzzer is not very efficient it is necessary to restrict the coverage 26 | tracking to the interesting part of the target service. The basic idea is that 27 | the fuzzer will only track coverage during the execution of the main network 28 | protocol handler (i.e. a function which handles the incoming TCP payloads) of 29 | the target service. The address of this function needs to be found via reverse 30 | engineering and has to be provided to the fuzzer. 31 | 32 | 33 | Installation 34 | ------------ 35 | 36 | The fuzzer is written in Python 3 and depends on the python module `frida-tools` 37 | and `toml`. It is recommended to set up a Python virtual environment: 38 | 39 | $ git clone https://cis.ernw.net/dmantz/frida-fuzzer 40 | $ cd frida-fuzzer 41 | $ virtualenv3 venv 42 | $ source venv/bin/activate 43 | $ pip install -e . 44 | $ frizzer --help 45 | 46 | The fuzzer also needs radamsa () to be available on the system. For Debian 47 | based systems it can be installed with the following commands: 48 | 49 | $ sudo apt update && sudo apt install gcc git make wget 50 | $ git clone https://gitlab.com/akihe/radamsa.git 51 | $ cd radamsa 52 | $ make 53 | $ sudo make install 54 | 55 | 56 | 57 | Usage 58 | ----- 59 | 60 | First it is necessary to find the main protocol handler function of the target 61 | service. It is important that this function is called exactly once for every 62 | TCP connection that the fuzzer initiates. For example, the `picohttpparser.c` 63 | file (`tests/picohttpparser/picohttpparser.c`) has a function 64 | `parse_request(...)` which matches the above mentioned requirements. Once the 65 | address of this function has been found via reverse engineering, the fuzzing 66 | can be configured. 67 | 68 | The first step is to create a new project (this command will create a new 69 | project directory `fuzzproject1`): 70 | 71 | $ frizzer init fuzzproject1 72 | 73 | The project directory contains the following files/folders: 74 | - config: Config file for the fuzzer 75 | - corpus: Current fuzzing corpus (contains the payloads) 76 | - crashes: Contains all payloads that have led to a crash 77 | 78 | After creating the project, the config file has to be edited. The generated file 79 | looks like this: 80 | 81 | [fuzzer] 82 | log_level = 3 # debug 83 | debug_mode = false 84 | 85 | [target] 86 | process_name = "myprocess" 87 | function = 0x123456 88 | host = "localhost" 89 | port = 7777 90 | ssl = false 91 | remote_frida = false 92 | recv_timeout = 0.1 93 | fuzz_in_process = false 94 | modules = [ 95 | "/home/dennis/tools/frida-fuzzer/tests/simple_binary/test", 96 | ] 97 | 98 | - Change the `process_name` parameter to the name of the process which should 99 | be fuzzed. The fuzzer will resolve the name to a PID (make sure only one 100 | instance of the service is running!) and attach to it via Frida. 101 | - Change the `function` parameter to the address of the protocol handler 102 | function. The fuzzer will hook this function via Frida and start the Frida 103 | Stalker to record which basic blocks are executed. The Stalker is stopped and 104 | the coverage processed as soon as the protocol handler function returns. 105 | - Change the `host` and `port` parameters to point to the service that shall be 106 | fuzzed. The fuzzer will establish a TCP connection for every new payload. 107 | When setting `ssl` to `true` the fuzzer will establish a TLS connection and 108 | send the payload through the TLS socket instead. 109 | - Change the `modules` parameter so that it contains a list of all modules for 110 | which the Stalker should track coverage (i.e. the main executable and 111 | potentially interesting .so files). The modules have to be given with absolute 112 | path! Do not include shared libraries in which you are not interested (e.g. 113 | the libc or other standard libs) as this will generate a lot of coverage 114 | data and render the fuzzing process inefficient. 115 | 116 | Finally, add one or more initial payload files to the project: 117 | 118 | $ frizzer add -p fuzzproject1 myinitialfiles 119 | 120 | In this command, `-p fuzzproject1` specifies the project directory and 121 | `myinitialfiles` is a directory which contains the payload files that shall 122 | be copied over to the corpus directory. If the protocol format is completely 123 | unknown just start with a single file containing a single 'A' and let radamsa 124 | and the coverage tracking figure out the protocol format. This may be too 125 | inefficient and it is recommended to continue reverse engineering as soon as 126 | the fuzzer is running. New payload files can be added at any time with the 127 | `add` subcommand of frizzer. 128 | 129 | Now the fuzzer can be started with the following command (note that the service 130 | needs to be running already!): 131 | 132 | $ frizzer fuzz -p fuzzproject1 133 | 134 | The output should look like this: 135 | 136 | [+] Project: {'fuzzer': {'log_level': 3, 'debug_mode': False}, 'target': {'process_name': 'test', 'function': 4198998, 'host': 'localhost', 'port': 7777, 'remote_frida': False, 'fuzz_in_process': False, 'modules': ['/home/dennis/frida-fuzzer/tests/simple_binary/test']}} 137 | [+] Loading script: /home/dennis/frida-fuzzer/frizzer/frida_script.js 138 | [+] Attached to pid 502277! 139 | [+] Filter coverage to only include the following modules: 140 | /home/dennis/tools/frida-fuzzer/tests/simple_binary/test 141 | [+] Initializing Corpus... 142 | [*] 2020-06-24 11:34:02 [iteration=1] tmpprojdir/corpus/1 143 | [!] 2020-06-24 11:34:02 [iteration=1] Inconsistent coverage for tmpprojdir/corpus/1! 144 | [*] 2020-06-24 11:34:02 [iteration=1] tmpprojdir/corpus/2 145 | [!] 2020-06-24 11:34:02 [iteration=1] Inconsistent coverage for tmpprojdir/corpus/2! 146 | [*] Using 4 input files which cover a total of 10 basic blocks! 147 | [D] Corpus: ['tmpprojdir/corpus/1', 'tmpprojdir/corpus/2', 'tmpprojdir/corpus/3', 'tmpprojdir/corpus/4'] 148 | [*] [seed=0] speed=[ 67 exec/sec (avg: 67)] coverage=[10 bblocks] corpus=[4 files] last new path: [-1] crashes: [0] 149 | [*] [seed=1] speed=[ 87 exec/sec (avg: 76)] coverage=[10 bblocks] corpus=[4 files] last new path: [-1] crashes: [0] 150 | [*] [seed=2] speed=[ 85 exec/sec (avg: 79)] coverage=[10 bblocks] corpus=[4 files] last new path: [-1] crashes: [0] 151 | [*] [seed=3] speed=[ 90 exec/sec (avg: 81)] coverage=[10 bblocks] corpus=[4 files] last new path: [-1] crashes: [0] 152 | [*] [seed=4] speed=[ 91 exec/sec (avg: 83)] coverage=[10 bblocks] corpus=[4 files] last new path: [-1] crashes: [0] 153 | [*] [seed=5] 2020-06-24 11:34:02 tmpprojdir/corpus/3 154 | [+] Found new path: [5] tmpprojdir/corpus/3 155 | [*] [seed=5] speed=[ 86 exec/sec (avg: 83)] coverage=[12 bblocks] corpus=[4 files] last new path: [5] crashes: [0] 156 | [*] [seed=6] speed=[ 88 exec/sec (avg: 84)] coverage=[12 bblocks] corpus=[5 files] last new path: [5] crashes: [0] 157 | ... 158 | 159 | The `seed` value reveres to the seed which is passed to radamsa. By default the 160 | seed will start at 0 and is increased in each round. Each round will produce 161 | new payloads for all files which are currently in the `corpus` directory. This 162 | is done by passing each file and the current seed to radamsa. The resulting new 163 | payloads are sent to the target. If the coverage for a specific payload 164 | contains new basic blocks (i.e. a `new path`), the payload is added to the 165 | corpus. 166 | 167 | Also have a look at the test cases to get an idea on how to use the fuzzer! 168 | 169 | -------------------------------------------------------------------------------- /frizzer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demantz/frizzer/67ca399b536880366ced8faf068d0dc67f90e8a2/frizzer/__init__.py -------------------------------------------------------------------------------- /frizzer/coverage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Frida-based fuzzer for blackbox fuzzing of network services. 4 | # 5 | # Author: Dennis Mantz (ERNW GmbH) 6 | # Birk Kauer (ERNW GmbH) 7 | 8 | import struct 9 | 10 | # frizzer modules 11 | from frizzer import log 12 | 13 | class BasicBlock: 14 | def __init__(self, start, end, module): 15 | self.start = start 16 | self.end = end 17 | self.module = module 18 | def __hash__(self): 19 | return self.start 20 | def __eq__(self, other): 21 | return self.start == other.__hash__() 22 | def to_drcov(self): 23 | # Data structure for the coverage info itself 24 | # typedef struct _bb_entry_t { 25 | # uint start; // offset of bb start from the image base 26 | # ushort size; 27 | # ushort mod_id; 28 | # } bb_entry_t; 29 | return struct.pack("IHH", self.start-self.module["base"], 30 | self.end-self.start, 31 | self.module["id"]) 32 | def __str__(self): 33 | return hex(self.start) 34 | 35 | def parse_coverage(coverage, modules): 36 | """ 37 | Parse the coverage that is returned from frida_script.exports.getcoverage(). 38 | The RPC function returns a list of basic blocks. The basic block 39 | itself is another list with two entries: start, end. 40 | Both, start and end, are unicode strings of hex addresses (e.g. u'0x7f5157f71a3f') 41 | 42 | The blocks are filtered: If a block does not belong to any module in 43 | it is ignored. 44 | """ 45 | bbs = set() 46 | for bb_list in coverage: 47 | start = int(bb_list[0], 16) 48 | end = int(bb_list[1], 16) 49 | module = None 50 | for m in modules: 51 | if start > m["base"] and end < m["end"]: 52 | module = m 53 | break 54 | if module == None: 55 | #log.debug("Basic block @0x%x does not belong to any module!" % start) 56 | continue 57 | bbs.add(BasicBlock(start, end, module)) 58 | return bbs 59 | 60 | def create_drcov_header(modules): 61 | """ 62 | Takes a module dictionary and formats it as a drcov logfile header. 63 | """ 64 | 65 | if modules == None: 66 | log.warn("create_drcov_header: modules is None!") 67 | return None 68 | 69 | header = '' 70 | header += 'DRCOV VERSION: 2\n' 71 | header += 'DRCOV FLAVOR: frida\n' 72 | header += 'Module Table: version 2, count %d\n' % len(modules) 73 | header += 'Columns: id, base, end, entry, checksum, timestamp, path\n' 74 | 75 | entries = [] 76 | 77 | for m in modules: 78 | # drcov: id, base, end, entry, checksum, timestamp, path 79 | # frida doesnt give us entry, checksum, or timestamp 80 | # luckily, I don't think we need them. 81 | entry = '%3d, %#016x, %#016x, %#016x, %#08x, %#08x, %s' % ( 82 | m['id'], m['base'], m['end'], 0, 0, 0, m['path']) 83 | 84 | entries.append(entry) 85 | 86 | header_modules = '\n'.join(entries) 87 | 88 | return header + header_modules + '\n' 89 | 90 | def create_drcov_coverage(bbs): 91 | # take the recv'd basic blocks, finish the header, and append the coverage 92 | bb_header = 'BB Table: %d bbs\n' % len(bbs) 93 | data = [bb.to_drcov() for bb in bbs] 94 | return bb_header.encode('ascii') + b''.join(data) 95 | 96 | 97 | def write_drcov_file(modules, coverage, filename): 98 | """ 99 | Write the coverage to a file using the drcov file format. 100 | """ 101 | 102 | header = create_drcov_header(modules) 103 | body = create_drcov_coverage(coverage) 104 | 105 | with open(filename, 'wb') as h: 106 | h.write(header.encode("ascii")) 107 | h.write(body) 108 | -------------------------------------------------------------------------------- /frizzer/frida_script.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | var debugging_enabled = false 3 | function debug(msg) { if(debugging_enabled){console.log("[+ ("+Process.id+")] " + msg)} } 4 | function debugCov(msg){ if(debugging_enabled){console.log("[+ ("+Process.id+")] " + msg)} } 5 | function warning(msg) { console.warn("[!] " + msg) } 6 | 7 | debug("loading script...") 8 | 9 | var stalker_attached = false; 10 | var stalker_finished = false; 11 | 12 | var whitelist = ['all']; 13 | 14 | var gc_cnt = 0; 15 | 16 | function make_maps() { 17 | var maps = Process.enumerateModulesSync(); 18 | var i = 0; 19 | // We need to add the module id 20 | maps.map(function(o) { o.id = i++; }); 21 | // .. and the module end point 22 | maps.map(function(o) { o.end = o.base.add(o.size); }); 23 | 24 | return maps; 25 | } 26 | var maps = make_maps() 27 | 28 | // We want to use frida's ModuleMap to create DRcov events, however frida's 29 | // Module object doesn't have the 'id' we added above. To get around this, 30 | // we'll create a mapping from path -> id, and have the ModuleMap look up the 31 | // path. While the ModuleMap does contain the base address, if we cache it 32 | // here, we can simply look up the path rather than the entire Module object. 33 | var module_ids = {}; 34 | maps.map(function (e) { 35 | module_ids[e.path] = {id: e.id, start: e.base}; 36 | }); 37 | 38 | var filtered_maps = new ModuleMap(function (m) { 39 | if (whitelist.indexOf('all') >= 0) { return true; } 40 | 41 | return whitelist.indexOf(m.name) >= 0; 42 | }); 43 | 44 | // Always trust code. #Make it faster 45 | Stalker.trustThreshold = 0; 46 | var stalker_events = [] 47 | 48 | var target_function = undefined 49 | 50 | // ======== For in-process fuzzing ================= 51 | var arg1 = Memory.alloc(0x100000); 52 | var arg2 = Memory.alloc(0x100000); 53 | var zero_0x100000 = new Uint8Array(0x100000); 54 | // ================================================= 55 | 56 | rpc.exports = { 57 | // get the module maps: 58 | makemaps: function(args) { 59 | return maps; 60 | }, 61 | 62 | // get the PID: 63 | getpid: function(args) { 64 | return Process.id; 65 | }, 66 | 67 | // get the absolute address of a function by name 68 | resolvesymbol: function(symbolname) { 69 | return DebugSymbol.fromName(symbolname).address; 70 | }, 71 | 72 | // initialize the address of the target function (to-be-hooked) 73 | // and attach the Interceptor 74 | settarget: function(target) { 75 | debug("Target: " + target) 76 | target_function = ptr(target) 77 | 78 | Interceptor.attach(target_function, { 79 | onEnter: function (args) { 80 | //debug('Called ------func-------: '); 81 | //debug("Stalker.queueCapacity=" + Stalker.queueCapacity) 82 | //debug("Stalker.queueDrainInterval=" + Stalker.queueDrainInterval) 83 | stalker_attached = true 84 | stalker_finished = false 85 | Stalker.queueCapacity = 100000000 86 | Stalker.queueDrainInterval = 1000*1000 87 | 88 | debugCov("follow") 89 | Stalker.follow(Process.getCurrentThreadId(), { 90 | events: { 91 | call: false, 92 | ret: false, 93 | exec: false, 94 | block: false, 95 | compile: true 96 | }, 97 | onReceive: function (events) { 98 | debugCov("onReceive: len(stalker_events)=" + stalker_events.length) 99 | stalker_events.push(events) 100 | } 101 | /*onCallSummary: function (summary) { 102 | console.log("onCallSummary: " + JSON.stringify(summary)) 103 | }*/ 104 | }); 105 | }, 106 | onLeave: function (retval) { 107 | //debug('Leave func '); 108 | debugCov("unfollow") 109 | Stalker.unfollow(Process.getCurrentThreadId()) 110 | debugCov("flush") 111 | Stalker.flush(); 112 | if(gc_cnt % 100 == 0){ 113 | Stalker.garbageCollect(); 114 | } 115 | gc_cnt++; 116 | stalker_finished = true 117 | //send("finished") 118 | } 119 | }); 120 | }, 121 | 122 | // call the target function with fuzzing payload (in-process fuzzing) 123 | fuzz: function (payload_hex) { 124 | var func_handle = undefined 125 | if(target_function == undefined) { 126 | warning("Target Function not defined!") 127 | return false 128 | } 129 | // Create the function handle (specify type and number of arguments) 130 | //func_handle = new NativeFunction(ptr(target_func), 'void', ['pointer', 'pointer']); 131 | func_handle = new NativeFunction(target_function, 'void', ['pointer']); 132 | 133 | var max_len = 100 134 | if(payload_hex.length > max_len*2) { 135 | debug("Payload trunkated from " + payload_hex.length/2 + " bytes!") 136 | payload_hex = payload_hex.substring(0,max_len*2); 137 | } 138 | debug("Payload: " + payload_hex) 139 | debug("arg1 @ " + arg1) 140 | debug("arg2 @ " + arg2) 141 | 142 | var payload = []; 143 | for(var i = 0; i < payload_hex.length; i+=2) 144 | { 145 | payload.push(parseInt(payload_hex.substring(i, i + 2), 16)); 146 | } 147 | 148 | // Prepare function arguments: 149 | payload = new Uint8Array(payload) 150 | Memory.writeByteArray(arg1, zero_0x100000) 151 | Memory.writeByteArray(arg1, payload) // payload goes into data 152 | //Memory.writePointer(arg1, ptr(Number(arg1)+64)) 153 | //Memory.writeInt(ptr(Number(arg1)+8), payload.length) 154 | 155 | //Memory.writePointer(ptr(Number(arg1)+16), ptr(heap1)) 156 | ////Memory.writePointer(ptr(Number(arg1)+16), ptr(0x0)) 157 | // 158 | //Memory.writeInt(ptr(Number(arg1)+24), 3) 159 | //Memory.writePointer(ptr(Number(arg1)+32), ptr(0x0)) 160 | 161 | 162 | //// manage malloc/free 163 | //var next_buffer_index = 0 164 | 165 | //// Intercept malloc in order to free all allocated memory after the call: 166 | //Interceptor.replace(malloc, new NativeCallback(function (size) { 167 | // if(size > buffers_size) { 168 | // warning("malloc(" + size + ") is too large. return 0") 169 | // return ptr(0) 170 | // } 171 | // var buf = buffers[next_buffer_index] 172 | // debug("malloc(" + size + ") => [" + next_buffer_index + "] " + buf) 173 | // next_buffer_index += 1 174 | // return buf; 175 | //}, 'pointer', ['int'])); 176 | 177 | //// Intercept calloc in order to free all allocated memory after the call: 178 | //Interceptor.replace(calloc, new NativeCallback(function (size) { 179 | // if(size > buffers_size) { 180 | // warning("calloc(" + size + ") is too large. return 0") 181 | // return ptr(0) 182 | // } 183 | // var buf = buffers[next_buffer_index] 184 | // debug("calloc(" + size + ") => [" + next_buffer_index + "] " + buf) 185 | // next_buffer_index += 1 186 | // return buf; 187 | //}, 'pointer', ['int'])); 188 | 189 | //// Intercept free as well 190 | //Interceptor.replace(free, new NativeCallback(function (pointer) { 191 | // debug("free(" + pointer + ")") 192 | // return 0; 193 | //}, 'int', ['pointer'])); 194 | 195 | //Interceptor.flush() 196 | 197 | debug('calling...') 198 | var retval = func_handle(arg1); 199 | debug('retval = ' + retval) 200 | 201 | //// free all allocated memory: 202 | //Interceptor.revert(malloc) 203 | //Interceptor.revert(calloc) 204 | //Interceptor.revert(free) 205 | //Interceptor.flush() 206 | 207 | return stalker_events 208 | }, 209 | 210 | // check stalker state 211 | checkstalker: function(args) { 212 | debugCov("checkstalker: len(stalker_events)=" + stalker_events.length + 213 | " stalker_{attached,finished}=" + stalker_attached + "," + stalker_finished) 214 | return [stalker_attached, stalker_finished]; 215 | }, 216 | // get the coverage 217 | getcoverage: function(args) { 218 | debugCov("getcoverage: len(stalker_events)=" + stalker_events.length) 219 | if(stalker_events.length == 0) 220 | return undefined; 221 | var accumulated_events = [] 222 | for(var i = 0; i < stalker_events.length; i++) { 223 | var parsed = Stalker.parse(stalker_events[i], {stringify: false, annotate: false}) 224 | accumulated_events = accumulated_events.concat(parsed); 225 | } 226 | //debugCov("cov: " + accumulated_events) 227 | return accumulated_events; 228 | }, 229 | // clear the coverage (set empty) 230 | clearcoverage: function(args) { 231 | debugCov("clearcoverage") 232 | stalker_events = [] 233 | stalker_attached = false 234 | stalker_finished = false 235 | } 236 | }; 237 | debug("Loading JS complete") 238 | -------------------------------------------------------------------------------- /frizzer/fuzzer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Frida-based fuzzer for blackbox fuzzing of network services. 4 | # 5 | # Author: Dennis Mantz (ERNW GmbH) 6 | # Birk Kauer (ERNW GmbH) 7 | 8 | from subprocess import check_output 9 | import traceback 10 | import argparse 11 | import frida 12 | import socket 13 | import ssl 14 | import time 15 | import shutil 16 | import sys 17 | import os 18 | 19 | # frizzer modules: 20 | from frizzer import log 21 | from frizzer import project 22 | from frizzer.coverage import parse_coverage, write_drcov_file 23 | 24 | 25 | ### 26 | ### FridaFuzzer Class 27 | ### 28 | 29 | class FridaFuzzer: 30 | """ 31 | This class operates the fuzzing process. 32 | """ 33 | 34 | def __init__(self, project): 35 | self.project = project 36 | self.targets = project.targets 37 | self.active_target = project.targets[0] # Target which produced coverage most recently 38 | self.corpus = None 39 | self.accumulated_coverage = set() 40 | self.total_executions = 0 41 | self.start_time = None 42 | self.payload_filter_function = None 43 | 44 | if not os.path.exists(project.coverage_dir): 45 | os.mkdir(project.coverage_dir) 46 | 47 | def loadPayloadFilter(self): 48 | if self.project.payload_filter == None: 49 | return True 50 | if not os.path.exists(self.project.payload_filter): 51 | log.warn("Payload filter (file: '%s') does not exist!" % self.project.payload_filter) 52 | return False 53 | saved_sys_path = sys.path 54 | try: 55 | payload_filter_file_without_ext = os.path.splitext(self.project.payload_filter)[0] 56 | payload_filter_module_path = os.path.dirname(payload_filter_file_without_ext) 57 | payload_filter_module_name = os.path.basename(payload_filter_file_without_ext) 58 | sys.path.insert(0, payload_filter_module_path) 59 | payload_filter_module = __import__(payload_filter_module_name) 60 | self.payload_filter_function = payload_filter_module.payload_filter_function 61 | sys.path = saved_sys_path 62 | except Exception as e: 63 | sys.path = saved_sys_path 64 | log.warn("loadPayloadFilter: " + str(e)) 65 | log.debug("Full Stack Trace:\n" + traceback.format_exc()) 66 | return False 67 | 68 | sys.path = saved_sys_path 69 | return True 70 | 71 | def runPayloadFilterFunction(self, fuzz_pkt): 72 | """ 73 | Returns a filtered version of the fuzz payload (as bytes object) 74 | or None if the payload should not be used by the fuzzer. 75 | The payload is first passed through the user-provided payload 76 | filter (if specified). The filter may modify the payload before 77 | returning or decide to not return any payload (None) in which 78 | case the fuzzer should skip the payload. 79 | """ 80 | if self.payload_filter_function != None: 81 | try: 82 | fuzz_pkt = self.payload_filter_function(fuzz_pkt) 83 | except Exception as e: 84 | log.warn("The payload filter '%s' caused an exception: %s" % (self.project.payload_filter, str(e))) 85 | log.debug("Full Stack Trace:\n" + traceback.format_exc()) 86 | if not isinstance(fuzz_pkt, bytes) and fuzz_pkt != None: 87 | log.warn("The payload filter '%s' returned unsupported type: '%s'." 88 | % (self.project.payload_filter, str(type(fuzz_pkt)))) 89 | return None 90 | 91 | return fuzz_pkt 92 | 93 | def getMutatedPayload(self, pkt_file, seed): 94 | """ 95 | Returns a mutated version of the content inside pkt_file (as bytes object) 96 | or None if the payload should not be used by the fuzzer. 97 | """ 98 | 99 | fuzz_pkt = check_output(["radamsa", "-s", str(seed), pkt_file]) 100 | if self.project.max_payload_size > 0 and len(fuzz_pkt) > self.project.max_payload_size: 101 | fuzz_pkt = fuzz_pkt[:self.project.max_payload_size] 102 | 103 | return fuzz_pkt 104 | 105 | 106 | def sendFuzzPayload(self, payload): 107 | """ 108 | Send fuzzing payload to target process via TCP socket 109 | """ 110 | 111 | dest = (self.project.host, self.project.port) 112 | try: 113 | if not self.project.udp: 114 | # TCP 115 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 116 | s.connect(dest) 117 | if self.project.ssl: 118 | s = ssl.wrap_socket(s) 119 | 120 | s.sendall(payload) 121 | if self.project.recv_timeout: 122 | s.settimeout(self.project.recv_timeout) 123 | s.recv(1) 124 | 125 | else: 126 | # UDP (only send single UDP packets) 127 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 128 | s.sendto(payload, dest) 129 | 130 | except IOError as e: 131 | #log.debug("IOError: " + str(e)) 132 | pass 133 | except EOFError as e: 134 | #log.debug("EOFError: " + str(e)) 135 | pass 136 | s.close() 137 | 138 | def sendFuzzPayloadInProcess(self, payload): 139 | """ 140 | Send fuzzing payload to target[0] process by invoking the target function 141 | directly in frida 142 | """ 143 | 144 | # Call function under fuzz: 145 | encoded = payload.hex() 146 | coverage_blob = self.targets[0].frida_script.exports.fuzz(encoded) 147 | #log.info("sendFuzzPayloadInProcess: len=%d" % len(coverage_blob)) 148 | # the fuzz call may cause a frida.core.RPCException, e.g. when the function 149 | # causes a segfault. we do not catch the exeception here, but in doIteration 150 | # where it is registered as a crash 151 | 152 | def waitForCoverage(self, timeout): 153 | """ 154 | Continiously checks the frida script of all targets if their stalker got attached. 155 | 156 | Returns a tupel: 157 | idx 0: the target if the stalker was attached or None if the timeout was hit. 158 | idx 1: the stalker_attached boolean (stalker has been attached in the target process) 159 | idx 2: the stalker_finished boolean (stalker has completed) 160 | 161 | The target (if found) is also set as the active_target 162 | """ 163 | # Create an ordered list of targets to check (last active target is checked first) 164 | targets = [] 165 | if self.active_target != None: 166 | targets.append(self.active_target) 167 | targets.extend([t for t in self.targets if t != self.active_target]) 168 | else: 169 | targets = self.targets 170 | 171 | # Wait for timeout seconds for any of the stalkers to get attached 172 | # (i.e. we hit the target function) 173 | start = time.time() 174 | while (time.time()-start) < timeout: 175 | for target in self.targets: 176 | stalker_attached, stalker_finished = target.frida_script.exports.checkstalker() 177 | if stalker_attached: 178 | # Found the right target 179 | self.active_target = target 180 | return (target, stalker_attached, stalker_finished) 181 | return (None, False, False) 182 | 183 | def getCoverageOfPayload(self, payload, timeout=0.04, retry=0): 184 | """ 185 | Sends of the payload and checks the returned coverage. 186 | If the payload_filter was specified by the user, the payload 187 | will first be passed through it. 188 | All targets will then be checked for coverage. The function only 189 | succeeds if just one target has produced a coverage. 190 | 191 | Important: 192 | Frida appears to have a bug sometimes in collecting traces with the 193 | stalker.. no idea how to fix this yet.. hence we do a retry. This 194 | can however screw up the replay functionality and should be fixed 195 | in the future. 196 | 197 | Arguments: 198 | payload {bytes} -- payload which shall be sent to the target 199 | 200 | Keyword Arguments: 201 | timeout {float} -- [description] (default: {0.1}) 202 | retry {int} -- [description] (default: {5}) 203 | 204 | Returns: 205 | {set} -- set of basic blocks covered by the payload 206 | """ 207 | 208 | payload = self.runPayloadFilterFunction(payload) 209 | if payload == None: 210 | return set() 211 | 212 | cov = None 213 | cnt = 0 214 | while cnt <= retry: 215 | # Clear coverage info in all targets: 216 | for target in self.targets: 217 | target.frida_script.exports.clearcoverage() 218 | 219 | # Send payload 220 | if self.project.fuzz_in_process: 221 | self.sendFuzzPayloadInProcess(payload) 222 | else: 223 | self.sendFuzzPayload(payload) 224 | 225 | # Wait for timeout seconds for any of the stalkers to get attached 226 | target, stalker_attached, stalker_finished = self.waitForCoverage(timeout) 227 | 228 | if target != None: 229 | # Found a target that has attached their stalker. Wait for the stalker 230 | # to finish and then extract the coverage. 231 | # Wait for 1 second <- maybe this should be adjusted / configurable ? 232 | start = time.time() 233 | while not stalker_finished and (time.time()-start) < 1: 234 | stalker_attached, stalker_finished = target.frida_script.exports.checkstalker() 235 | 236 | if not stalker_finished: 237 | log.info("getCoverageOfPayload: Stalker did not finish after 1 second!") 238 | break 239 | 240 | cov = target.frida_script.exports.getcoverage() 241 | if cov != None and len(cov) > 0: 242 | break 243 | 244 | else: 245 | # None of the targets' function was hit. next try.. 246 | cnt += 1 247 | 248 | if cov == None or len(cov) == 0: 249 | log.debug("getCoverageOfPayload: got nothing!") 250 | return set() 251 | 252 | return parse_coverage(cov, self.active_target.watched_modules) 253 | 254 | 255 | def buildCorpus(self): 256 | log.info("Initializing Corpus...") 257 | 258 | # Resetting Corpus to avoid at restart to have with ASLR more blocks than needed 259 | self.accumulated_coverage = set() 260 | 261 | corpus = [self.project.corpus_dir + "/" + x for x in os.listdir(self.project.corpus_dir)] 262 | corpus.sort() 263 | #log.debug("Corpus: " + str(corpus)) 264 | 265 | if len(corpus) == 0: 266 | log.warn("Corpus is empty, please add files/directories with 'add'") 267 | return False 268 | 269 | for infile in corpus: 270 | fuzz_pkt = open(infile, "rb").read() 271 | coverage_last = None 272 | for i in range(5): 273 | t = time.strftime("%Y-%m-%d %H:%M:%S") 274 | log.update(t + " [iteration=%d] %s" % (i, infile)) 275 | 276 | # send packet to target 277 | coverage = self.getCoverageOfPayload(fuzz_pkt, timeout=1) 278 | if coverage == None or len(coverage) == 0: 279 | log.warn("No coverage was returned! you might want to delete %s from corpus if it happens more often" % infile) 280 | 281 | #log.info("Iteration=%d covlen=%d file=%s" % (i, len(coverage), infile)) 282 | 283 | if coverage_last != None and coverage_last != coverage: 284 | log.warn(t + " [iteration=%d] Inconsistent coverage for %s!" % (i, infile)) 285 | #log.info("diff a-b:" + " ".join([str(x) for x in coverage_last.difference(coverage)])) 286 | #log.info("diff b-a:" + " ".join([str(x) for x in coverage.difference(coverage_last)])) 287 | 288 | coverage_last = coverage 289 | # Accumulate coverage: 290 | self.accumulated_coverage = self.accumulated_coverage.union(coverage_last) 291 | 292 | write_drcov_file(self.active_target.modules, coverage_last, 293 | self.project.coverage_dir + "/" + infile.split("/")[-1]) 294 | 295 | log.finish_update("Using %d input files which cover a total of %d basic blocks!" % ( 296 | len(corpus), len(self.accumulated_coverage))) 297 | self.corpus = corpus 298 | return True 299 | 300 | def doIteration(self, seed=None, corpus=None): 301 | if seed == None: 302 | seed = self.project.seed 303 | if corpus == None: 304 | corpus = self.corpus 305 | 306 | start_time = time.time() 307 | for pkt_file in corpus: 308 | log.update("[seed=%d] " % seed + time.strftime("%Y-%m-%d %H:%M:%S") + " %s" % pkt_file) 309 | fuzz_pkt = self.getMutatedPayload(pkt_file, seed) 310 | if fuzz_pkt == None: 311 | continue 312 | 313 | # Writing History file for replaying 314 | open(self.project.project_dir + "/frida_fuzzer.history", "a").write(str(pkt_file) + "|" + str(seed) + "\n") 315 | 316 | try: 317 | coverage = self.getCoverageOfPayload(fuzz_pkt) 318 | except (frida.TransportError, frida.InvalidOperationError, frida.core.RPCException) as e: 319 | log.warn("doIteration: Got a frida error: " + str(e)) 320 | log.debug("Full Stack Trace:\n" + traceback.format_exc()) 321 | log.info("Current iteration: " + time.strftime("%Y-%m-%d %H:%M:%S") + 322 | " [seed=%d] [file=%s]" % (seed, pkt_file)) 323 | crash_file = self.project.crash_dir + time.strftime("/%Y%m%d_%H%M%S_crash") 324 | with open(crash_file + "_" + str(self.active_target.process_pid), "wb") as f: 325 | f.write(fuzz_pkt) 326 | log.info("Payload is written to " + crash_file) 327 | self.project.crashes += 1 328 | return False 329 | 330 | if coverage == None: 331 | log.warn("No coverage was generated for [%d] %s!" % (seed, pkt_file)) 332 | continue 333 | 334 | if not coverage.issubset(self.accumulated_coverage): 335 | # New basic blocks covered! 336 | log.info("Found new path: [%d] %s" % (seed, pkt_file)) 337 | newfile = open(self.project.corpus_dir + "/" + str(seed) + "_" + pkt_file.split("/")[-1], "wb") 338 | newfile.write(fuzz_pkt) 339 | newfile.close() 340 | 341 | cov_file = self.project.coverage_dir + "/" + str(seed) + "_" + pkt_file.split("/")[-1] 342 | write_drcov_file(self.active_target.modules, coverage, cov_file) 343 | write_drcov_file(self.active_target.modules, coverage.difference(self.accumulated_coverage), 344 | cov_file + "_diff") 345 | 346 | self.project.last_new_path = seed 347 | self.accumulated_coverage = self.accumulated_coverage.union(coverage) 348 | write_drcov_file(self.active_target.modules, self.accumulated_coverage, 349 | self.project.coverage_dir + "/accumulated_coverage.drcov") 350 | 351 | self.total_executions += 1 352 | 353 | end_time = time.time() 354 | speed = len(corpus) / (end_time-start_time) 355 | avg_speed = self.total_executions / (end_time-self.start_time) 356 | 357 | log.finish_update("[seed=%d] speed=[%3d exec/sec (avg: %d)] coverage=[%d bblocks] corpus=[%d files] " 358 | "last new path: [%d] crashes: [%d]" % ( 359 | seed, speed, avg_speed, len(self.accumulated_coverage), len(corpus), 360 | self.project.last_new_path, self.project.crashes)) 361 | return True 362 | 363 | def doReplay(self): 364 | """ 365 | This function replays the last Session. This function will later implement also probes to test when the process is crashing 366 | """ 367 | log.info("Starting the full Replay") 368 | with open(self.project.project_dir + "/frida_fuzzer.history") as fp: 369 | for line in fp: 370 | pkt_file, seed = line.split("|") 371 | try: 372 | fuzz_pkt = self.getMutatedPayload(pkt_file, int(seed.strip())) 373 | if fuzz_pkt == None: 374 | continue 375 | if self.project.debug_mode: 376 | open(self.project.debug_dir + "/history", "a").write("file: {} seed: {} \n{}\n".format( 377 | pkt_file, 378 | seed, 379 | fuzz_pkt, 380 | )) 381 | coverage = self.getCoverageOfPayload(fuzz_pkt) 382 | log.info("Current iteration: " + time.strftime("%Y-%m-%d %H:%M:%S") + 383 | " [seed=%d] [file=%s]" % (int(seed.strip()), pkt_file)) 384 | except (frida.TransportError, frida.InvalidOperationError, frida.core.RPCException) as e: 385 | log.finish_update("doReplay: Got a frida error: " + str(e)) 386 | log.debug("Full Stack Trace:\n" + traceback.format_exc()) 387 | log.finish_update("Current iteration: " + time.strftime("%Y-%m-%d %H:%M:%S") + 388 | " [seed=%d] [file=%s]" % (int(seed.strip()), pkt_file)) 389 | log.finish_update("Server Crashed! Lets narrow it down") 390 | #crash_file = self.crash_dir + time.strftime("/%Y%m%d_%H%M%S_crash") 391 | #with open(crash_file, "wb") as f: 392 | # f.write(fuzz_pkt) 393 | #log.info("Payload is written to " + crash_file) 394 | return False 395 | 396 | if coverage == None: 397 | log.warn("No coverage was generated for [%d] %s!" % (seed, pkt_file)) 398 | 399 | log.warn("Replay did not crash the Server!") 400 | return False 401 | 402 | def doMinimize(self): 403 | """ 404 | This Function will minimize the current Corpus 405 | """ 406 | log.info("Minimizing Corpus...") 407 | # Reset the accumulated coverage 408 | self.accumulated_coverage = set() 409 | 410 | corpus = [self.project.corpus_dir + "/" + x for x in os.listdir(self.project.corpus_dir)] 411 | corpus.sort() 412 | 413 | if len(corpus) == 0: 414 | log.warn("Corpus is empty, please use the 'add' subcommand to add files to it.") 415 | return False 416 | 417 | # Collect coverage 418 | dict_of_infile_coverages = {} 419 | loop_counter = 0 420 | for infile in corpus: 421 | loop_counter += 1 422 | fuzz_pkt = open(infile, "rb").read() 423 | failed_coverage_count = 0 424 | tmp_accu_cov = set() 425 | RETRIES = 5 426 | for i in range(RETRIES): 427 | t = time.strftime("%Y-%m-%d %H:%M:%S") 428 | log.update(t + " Collecting coverage for corpus files (%d/%d) ... [iteration=%d] %s" 429 | % (loop_counter, len(corpus), i, infile)) 430 | 431 | # send packet to target 432 | coverage = self.getCoverageOfPayload(fuzz_pkt, timeout=0.2) 433 | if coverage == None or len(coverage) == 0: 434 | failed_coverage_count += 1 435 | continue 436 | 437 | # Accumulate coverage: 438 | tmp_accu_cov = tmp_accu_cov.union(coverage) 439 | 440 | if failed_coverage_count == RETRIES: 441 | log.warn("Coverage for %s was always 0 (%d retries)" % (infile, RETRIES)) 442 | # note: file will be removed later.. 443 | 444 | dict_of_infile_coverages[infile] = tmp_accu_cov 445 | self.accumulated_coverage = self.accumulated_coverage.union(tmp_accu_cov) 446 | write_drcov_file(self.active_target.modules, tmp_accu_cov, 447 | self.project.coverage_dir + "/" + infile.split("/")[-1]) 448 | 449 | log.finish_update("Collected coverage for corpus (%d basic blocks from %d files in corpus)" 450 | % (len(self.accumulated_coverage), len(corpus))) 451 | 452 | # Filter all corpus files with a coverage that is a direct subset of another corpus file 453 | loop_counter = 0 454 | for infile in corpus: 455 | loop_counter += 1 456 | log.update("(%d/%d) Comparing %s (%d bblocks) against rest of the corpus..." 457 | % (loop_counter, len(corpus), infile, len(dict_of_infile_coverages[infile]))) 458 | for other_infile in [f for f in corpus if f != infile]: 459 | if dict_of_infile_coverages[infile].issubset(dict_of_infile_coverages[other_infile]): 460 | log.info("%s coverage is direct subset of %s. Moving to trash..." % (infile, other_infile)) 461 | backup_file = self.project.corpus_trash_dir + "/" + infile.split("/")[-1] 462 | shutil.move(infile, backup_file) 463 | break 464 | 465 | corpus_new = [self.project.corpus_dir + "/" + x for x in os.listdir(self.project.corpus_dir)] 466 | acc_cov_new = set.union(*dict_of_infile_coverages.values()) 467 | log.finish_update("Remaining input files: %d (total of %d basic blocks)." % ( 468 | len(corpus_new), len(acc_cov_new))) 469 | self.corpus = corpus_new 470 | return True 471 | 472 | def fuzzerLoop(self): 473 | try: 474 | self.start_time = time.time() 475 | self.total_executions = 0 476 | while True: 477 | if not self.doIteration(): 478 | log.info("stopping fuzzer loop") 479 | return False 480 | self.corpus = [self.project.corpus_dir + "/" + f for f in os.listdir(self.project.corpus_dir)] 481 | self.corpus.sort() 482 | self.project.seed += 1 483 | self.project.saveState() 484 | except KeyboardInterrupt: 485 | log.info("Interrupted by user..") 486 | 487 | def attach(self): 488 | """ 489 | Attach frida to all specified targets (project.targets) 490 | """ 491 | scriptfile = os.path.join(os.path.dirname(__file__),'frida_script.js') 492 | log.info("Loading script: %s" % scriptfile) 493 | script_code = open(scriptfile, "r").read() 494 | 495 | for target in self.targets: 496 | if not target.attach(script_code): 497 | return False 498 | if target.getModuleMap() == None: # Query the loaded modules from the target 499 | return False 500 | if not target.createModuleFilterList(): # ... and create the module filter list 501 | return False 502 | return True 503 | 504 | def detach(self): 505 | """ 506 | Detach frida from all targets. 507 | """ 508 | for target in self.targets: 509 | target.detach() 510 | 511 | 512 | ### 513 | ### frizzer sub functions 514 | ### (init, add, fuzz, minimize, ...) 515 | ### 516 | 517 | def init(args): 518 | if os.path.exists(args.project): 519 | log.warn("Project '%s' already exists!" % args.project) 520 | return 521 | log.info("Creating project '%s'!" % args.project) 522 | if not project.createProject(args.project): 523 | log.warn("Could not create project!") 524 | 525 | 526 | def add(args): 527 | infiles = [] 528 | for path in args.input: 529 | if not os.path.exists(path): 530 | log.warn("File or directory '%s' does not exist!" % path) 531 | return 532 | if os.path.isdir(path): 533 | infiles.extend([path + "/" + x for x in os.listdir(path)]) 534 | else: 535 | infiles.append(path) 536 | 537 | corpus_dir = project.getInstance().corpus_dir 538 | for inFile in infiles: 539 | if not os.path.exists(corpus_dir + "/" + inFile.split("/")[-1]): 540 | log.info("Copying '%s' to corpus directory: " % inFile) 541 | shutil.copy2(inFile, corpus_dir) 542 | 543 | 544 | def fuzz(args, fuzzer): 545 | #TODO (maybe? save the old history file.. do we need this?) 546 | # history_file = self.project_dir + "/frida_fuzzer.history" 547 | # if os.path.exists(history_file): 548 | # backup_file = self.project_dir + time.strftime("/%Y%m%d_%H%M%S_frida_fuzzer.history") 549 | # shutil.move(history_file, backup_file) 550 | # log.info("Found old history file. Moving it to %s" % backup_file) 551 | 552 | if fuzzer.buildCorpus(): 553 | log.debug("Corpus: " + str(fuzzer.corpus)) 554 | fuzzer.fuzzerLoop() 555 | 556 | 557 | def replay(args, fuzzer): 558 | log.info("Replay Mode!") 559 | fuzzer.doReplay() 560 | 561 | 562 | def minimize(args, fuzzer): 563 | if fuzzer.doMinimize(): 564 | log.info("Minimized the Corpus. Start again without the minimizing option!") 565 | else: 566 | log.warn("Failed to minimize the corpus!") 567 | 568 | 569 | ### 570 | ### Argument Parsing 571 | ### 572 | 573 | def parse_args(): 574 | parser = argparse.ArgumentParser() 575 | subparsers = parser.add_subparsers(dest="command") 576 | 577 | # Add subcommands 578 | parser_init = subparsers.add_parser('init') 579 | parser_add = subparsers.add_parser('add') 580 | parser_fuzz = subparsers.add_parser('fuzz') 581 | parser_replay = subparsers.add_parser('replay') 582 | parser_minimize = subparsers.add_parser('minimize') 583 | 584 | # Assign functions to subcommands 585 | parser_init.set_defaults(func=init) 586 | parser_add.set_defaults(func=add) 587 | parser_fuzz.set_defaults(func=fuzz) 588 | parser_replay.set_defaults(func=replay) 589 | parser_minimize.set_defaults(func=minimize) 590 | 591 | # Add general options 592 | for p in [parser_init, parser_add, parser_fuzz, parser_replay, parser_minimize]: 593 | p.add_argument("--verbose", "-v", action='store_true', help="Change log level to 'debug'") 594 | p.add_argument("--debug", action='store_true', help="Verbose Debugging in a file (every Request)") 595 | 596 | # Add subcommand specific parser options: 597 | for p in [parser_add, parser_fuzz, parser_replay, parser_minimize]: 598 | p.add_argument("--project", "-p", help="Project directory.") 599 | 600 | for p in [parser_fuzz, parser_replay, parser_minimize]: 601 | p.add_argument("--pid", help="Process ID or name of target program") 602 | p.add_argument("--seed", "-s", help="Seed for radamsa", type=int) 603 | p.add_argument("--function", "-f", help="Function to fuzz and over which the coverage is calculated") 604 | 605 | parser_init.add_argument("project", help="Project name / directory which will be created)") 606 | parser_add.add_argument("input", nargs="*", help="Input files and directories that will be added to the corpus") 607 | 608 | # Parse arguments 609 | args = parser.parse_args() 610 | 611 | if args.command == None: 612 | parser.print_help() 613 | sys.exit(-1) 614 | if args.verbose: 615 | log.log_level = 3 616 | if args.project == None: 617 | log.warn("Please specify a project directory name with --project/-p") 618 | sys.exit(-1) 619 | 620 | return args 621 | 622 | 623 | def main(): 624 | args = parse_args() 625 | 626 | if args.command != "init": 627 | # Load project 628 | if not project.loadProject(args.project, args): 629 | log.warn("Error: Could not load project '%s'!" % args.project) 630 | return 631 | 632 | if project.getInstance().logfile_name != None: 633 | log.logfile = open(project.getInstance().logfile_name, "wb", 0) 634 | 635 | if not project.getInstance().colored_output: 636 | log.use_color = False 637 | log.CLEAR_LINE = "" # no escape sequences for the no-color option! 638 | 639 | if args.command in ["fuzz", "replay", "minimize"]: 640 | # Create Fuzzer and attach to target 641 | fuzzer = FridaFuzzer(project.getInstance()) 642 | if not fuzzer.attach(): 643 | return 644 | if not fuzzer.loadPayloadFilter(): 645 | return 646 | 647 | # Invoke subcommand function with instantiated fuzzer 648 | args.func(args, fuzzer) 649 | 650 | log.info("Detach Fuzzer ...") 651 | fuzzer.detach() 652 | 653 | else: 654 | # Invoke subcommand function 655 | args.func(args) 656 | 657 | log.info("Done") 658 | if log.logfile != None: 659 | log.logfile.close() 660 | return 661 | 662 | 663 | if __name__ == "__main__": 664 | main() 665 | -------------------------------------------------------------------------------- /frizzer/log.py: -------------------------------------------------------------------------------- 1 | # This is a temporary replacement for pwntools' log command 2 | # TODO: Add sophisticated logging + nice ui output etc. 3 | 4 | import sys 5 | 6 | # colors: 7 | COLOR_NC ='\033[0m' # No Color 8 | WHITE ='\033[37m' 9 | BLACK ='\033[30m' 10 | BLUE ='\033[34m' 11 | GREEN ='\033[32m' 12 | CYAN ='\033[36m' 13 | RED ='\033[31m' 14 | PURPLE ='\033[35m' 15 | BROWN ='\033[33m' 16 | YELLOW ='\033[33m' 17 | GRAY ='\033[30m' 18 | 19 | CLEAR_LINE ='\033[K' 20 | 21 | update_ongoing = False 22 | logfile = None 23 | use_color = True 24 | 25 | # debug = 3 info = 2 warn = 1 26 | log_level = 2 27 | 28 | def add_color(msg, color): 29 | if use_color: 30 | return color + msg + COLOR_NC 31 | else: 32 | return msg 33 | 34 | def writeLine(msg, do_update_line=False): 35 | global update_ongoing, logfile 36 | if not do_update_line: 37 | # if we are not called from update() and are currently in update_ongoing 38 | # then stop update_ongoing by going to the next line: 39 | if update_ongoing: 40 | sys.stdout.write("\n") 41 | update_ongoing = False 42 | sys.stdout.write(msg + "\n") 43 | else: 44 | sys.stdout.write(msg) 45 | 46 | if logfile != None: 47 | msg = msg + "\n" 48 | for i in ["\r",COLOR_NC,WHITE,BLACK,BLUE,GREEN,CYAN,RED,PURPLE,BROWN,YELLOW,GRAY,CLEAR_LINE]: 49 | msg = msg.replace(i,"") 50 | logfile.write(msg.encode("ascii")) 51 | 52 | def update(msg): 53 | global update_ongoing, log_level 54 | if log_level >= 2: 55 | update_ongoing = True 56 | clear_line_seq = CLEAR_LINE if use_color else "" 57 | msg = "\r"+clear_line_seq+"[" + add_color("*",YELLOW) + "] " + msg 58 | writeLine(msg, do_update_line=True) 59 | 60 | def finish_update(msg): 61 | global update_ongoing, log_level 62 | if log_level >= 2: 63 | update_ongoing = False 64 | writeLine("\r"+CLEAR_LINE+"[" + add_color("*",GREEN) + "] " + msg) 65 | 66 | def debug(msg): 67 | global log_level 68 | if log_level >= 3: 69 | writeLine("[" + add_color("D",GRAY) + "] " + msg) 70 | 71 | def info(msg): 72 | global log_level 73 | if log_level >= 2: 74 | writeLine("[" + add_color("+",BLUE) + "] " + msg) 75 | 76 | def warn(msg): 77 | global log_level 78 | if log_level >= 1: 79 | writeLine("[" + add_color("!",RED) + "] " + msg) 80 | 81 | -------------------------------------------------------------------------------- /frizzer/project.py: -------------------------------------------------------------------------------- 1 | # FRIZZER - project.py 2 | # 3 | # 4 | # 5 | # Author: Dennis Mantz (ERNW GmbH) 6 | # Birk Kauer (ERNW GmbH) 7 | 8 | import os 9 | import time 10 | import toml 11 | import frida 12 | 13 | # frizzer modules 14 | from frizzer import log 15 | from frizzer.target import Target 16 | 17 | CONFIG_TEMPLATE = """ 18 | [fuzzer] 19 | log_level = 3 # 3=debug 2=info 1=warning 20 | write_logfile = true # writes a .log file to the project directory 21 | debug_mode = false # write additional debug logs 22 | host = "localhost" # host on which the target process listens 23 | port = 7777 # port on which the target process listens 24 | ssl = false # (only for TCP) wraps the TCP socket in a SSL context 25 | udp = false # Use UDP instead of TCP 26 | fuzz_in_process = false # use in-process fuzzing instead of fuzzing over the network 27 | recv_timeout = 0.1 28 | colored_output = true # use ANSI Escape Codes to color terminal output 29 | max_payload_size= 0 # Maximum size for fuzz payloads in bytes. 0 = no limit. 30 | #payload_filter = "path/to/filter.py" # Define a filter for the mutated payloads (written in Python 3) 31 | # The python file must contain the following function: 32 | # def payload_filter_function(payload): 33 | # # do stuff. (payload is 'bytes' object) 34 | # return modified_payload_or_None 35 | 36 | [target] 37 | process_name = "myprocess" # Process name of the target. Must be unique, otherwise use process_pid 38 | #process_pid = 1234 # Specifify the target process via process ID 39 | function = 0x123456 # Function for which the coverage will be traced 40 | # Can either be an absolute address (integer, e.g. 0x12345) 41 | # or a symbol name (string, e.g. "handleClient") 42 | remote_frida = false # Connect to a "remote" frida server (needs ssh portforwarding of the 43 | # frida server port to localhost) 44 | frida_port = 27042 # port for the remote frida connection. frizzer will connect to localhost:$PORT 45 | 46 | # 'modules' is a filter list. Coverage will be traced only for modules / shared 47 | # libs which match one of the search terms in 'modules'. It is important that 48 | # the filter matches at least one module! 49 | modules = [ 50 | "tests/simple_binary/test", 51 | ] 52 | 53 | # Multiple targets are supported but you probably don't want this 54 | # (only meant for load-balancing setups were multiple processes handle the network traffic) 55 | #[target2] 56 | #process_name = "myprocess" 57 | #function = 0x123456 58 | #remote_frida = true 59 | #frida_port = 27042 60 | #modules = [ 61 | # "tests/simple_binary/test", 62 | # ] 63 | """ 64 | 65 | # Singleton instance (can be accessed from everywhere) 66 | instance = None 67 | 68 | def getInstance(): 69 | global instance 70 | if instance == None: 71 | log.warn("Project instance was not yet created!") 72 | return instance 73 | 74 | def loadProject(project_dir, args=None): 75 | """ 76 | Loads the project given by 'project_dir' (directory name) as global singleton instance 77 | which can be retrieved by getInstance() afterwards. 78 | If args is not None, it will be used to temporarily overwrite certain project settings 79 | like pid, seed, etc. 80 | """ 81 | global instance 82 | if instance != None: 83 | log.warn("Project instance does already exist!") 84 | return False 85 | 86 | proj = Project(project_dir) 87 | if not proj.loadProject(args): 88 | log.warn("Could not load project") 89 | return False 90 | 91 | instance = proj 92 | return True 93 | 94 | def createProject(project_dir): 95 | os.mkdir(project_dir) 96 | proj = Project(project_dir) 97 | if not proj.checkAndCreateSubfolders(): 98 | return False 99 | with open(proj.config_file, "w") as f: 100 | f.write(CONFIG_TEMPLATE) 101 | return True 102 | 103 | 104 | class Project(): 105 | """ 106 | This class holds all project settings and states. It provides functions to 107 | parse the project config file and read/write the state file. 108 | """ 109 | 110 | def __init__(self, project_dir): 111 | self.project_dir = project_dir 112 | 113 | # Settings from the config file 114 | self.targets = [] 115 | self.port = None 116 | self.host = None 117 | self.ssl = False 118 | self.udp = False 119 | self.recv_timeout = None 120 | self.fuzz_in_process = False 121 | self.corpus = None 122 | self.corpus_dir = project_dir + "/corpus" 123 | self.corpus_trash_dir = project_dir + "/corpus_trash" 124 | self.crash_dir = project_dir + "/crashes" 125 | self.coverage_dir = project_dir + time.strftime("/%Y%m%d_%H%M%S_coverage") 126 | self.debug_dir = project_dir + "/debug" 127 | self.config_file = project_dir + "/config" 128 | self.state_file = project_dir + "/state" 129 | self.debug_mode = False 130 | self.logfile_name = None 131 | self.colored_output = True 132 | self.max_payload_size = 0 133 | self.payload_filter = None 134 | 135 | 136 | # State 137 | self.pid = None 138 | self.seed = 0 139 | self.crashes = 0 140 | self.last_new_path = -1 141 | 142 | 143 | def loadProject(self, args): 144 | 145 | # Load config file 146 | if not os.path.exists(self.config_file): 147 | log.warn("Config file %s does not exist!" % self.config_file) 148 | return False 149 | proj = toml.loads(open(self.config_file).read()) 150 | 151 | log.info("Project: " + repr(proj)) 152 | 153 | if not "fuzzer" in proj: 154 | log.warn("Section 'fuzzer' was not found in config file.") 155 | return False 156 | fuzzer = proj["fuzzer"] 157 | if "fuzzer" in proj: 158 | if "log_level" in fuzzer: 159 | log.log_level = fuzzer["log_level"] 160 | if "write_logfile" in fuzzer: 161 | if fuzzer["write_logfile"]: 162 | self.logfile_name = self.project_dir + time.strftime("/%Y%m%d_%H%M%S_stdout.log") 163 | if "colored_output" in fuzzer: 164 | self.colored_output = fuzzer["colored_output"] 165 | if "debug_mode" in fuzzer: 166 | self.debug_mode = fuzzer["debug_mode"] 167 | if "host" in fuzzer: 168 | self.host = fuzzer["host"] 169 | if "port" in fuzzer: 170 | self.port = fuzzer["port"] 171 | if "ssl" in fuzzer: 172 | self.ssl = fuzzer["ssl"] 173 | if "udp" in fuzzer: 174 | self.udp = fuzzer["udp"] 175 | if self.udp and 'ssl' in fuzzer and self.ssl: 176 | log.warn("SSL can not be used with UDP sockets. SSL will be ignored.") 177 | if "recv_timeout" in fuzzer: 178 | self.recv_timeout = fuzzer["recv_timeout"] 179 | if "fuzz_in_process" in fuzzer: 180 | self.fuzz_in_process = fuzzer["fuzz_in_process"] 181 | if "max_payload_size" in fuzzer: 182 | self.max_payload_size = fuzzer["max_payload_size"] 183 | if "payload_filter" in fuzzer: 184 | self.payload_filter = fuzzer["payload_filter"] 185 | 186 | targets = [t for t in proj.keys() if t.startswith('target')] 187 | if len(targets) == 0: 188 | log.warn("No 'target' sections were not found in config file (section starting with 'target...').") 189 | return False 190 | 191 | for target in targets: 192 | targetobj = Target(target, proj[target]) 193 | self.targets.append(targetobj) 194 | 195 | # Load state file 196 | if os.path.exists(self.state_file): 197 | state = toml.loads(open(self.state_file).read()) 198 | if "seed" in state: 199 | self.seed = state["seed"] 200 | if "crashes" in state: 201 | self.crashes = state["crashes"] 202 | if "last_new_path" in state: 203 | self.last_new_path = state["last_new_path"] 204 | log.info("Found old state. Continuing at seed=%d" % (self.seed)) 205 | 206 | # Load command line parameters 207 | if args != None: 208 | if 'pid' in args and args.pid != None: 209 | self.pid = args.pid 210 | if 'seed' in args and args.seed != None: 211 | self.seed = args.seed 212 | if 'function' in args and args.function != None: 213 | self.target_function = args.function 214 | if 'debug' in args and args.debug == True: 215 | self.debug_mode = True 216 | 217 | return True 218 | 219 | def saveState(self): 220 | state = {"seed": self.seed, 221 | "crashes": self.crashes, 222 | "last_new_path": self.last_new_path} 223 | open(self.state_file, "w").write(toml.dumps(state)) 224 | return True 225 | 226 | def checkAndCreateSubfolders(self): 227 | """ 228 | Check whether alls necessary subdirectories exist in the 229 | project folder. Create them if necessary. 230 | """ 231 | if not os.path.exists(self.project_dir): 232 | log.warn("Project directory '%s' does not exist." % self.project_dir) 233 | return False 234 | 235 | if not os.path.exists(self.debug_dir): 236 | os.mkdir(self.debug_dir) 237 | 238 | if os.path.exists(self.debug_dir + "/history"): 239 | log.debug("Deleting old Debug file: " + self.debug_dir + "/history") 240 | os.remove(self.debug_dir + "/history") 241 | 242 | #if not os.path.exists(self.coverage_dir): 243 | # os.mkdir(self.coverage_dir) 244 | 245 | if not os.path.exists(self.crash_dir): 246 | os.mkdir(self.crash_dir) 247 | 248 | if not os.path.exists(self.corpus_dir): 249 | os.mkdir(self.corpus_dir) 250 | 251 | if not os.path.exists(self.corpus_trash_dir): 252 | os.mkdir(self.corpus_trash_dir) 253 | 254 | return True 255 | 256 | -------------------------------------------------------------------------------- /frizzer/target.py: -------------------------------------------------------------------------------- 1 | # FRIZZER - target.py 2 | # 3 | # 4 | # 5 | # Author: Dennis Mantz (ERNW GmbH) 6 | # Birk Kauer (ERNW GmbH) 7 | 8 | import frida 9 | 10 | # frizzer modules 11 | from frizzer import log 12 | 13 | class Target(): 14 | """ 15 | This class represents a fuzz-target to which frizzer will attach 16 | with frida. 17 | """ 18 | 19 | def __init__(self, name, target_dict): 20 | 21 | self.name = name 22 | self.frida_session = None 23 | self.frida_script = None 24 | self.process_name = None 25 | self.process_pid = None 26 | self.remote_frida = False 27 | self.frida_port = 27042 28 | self.modules = None # Modules which are loaded in the process 29 | self.modules_to_watch = None # Modules which where given in the config file 30 | # for which the coverage should be tracked 31 | self.watched_modules = None # intersection of self.modules and self.modules_to_watch 32 | 33 | if "function" in target_dict: 34 | self.function = target_dict["function"] 35 | else: 36 | log.warn("No 'function' in target-section '%s'!" % name) 37 | return False 38 | 39 | if "process_name" in target_dict: 40 | self.process_name = target_dict["process_name"] 41 | if "process_pid" in target_dict: 42 | self.process_pid = target_dict["process_pid"] 43 | if "remote_frida" in target_dict: 44 | self.remote_frida = target_dict["remote_frida"] 45 | if "frida_port" in target_dict: 46 | self.frida_port = target_dict["frida_port"] 47 | if "modules" in target_dict: 48 | self.modules_to_watch = target_dict["modules"] 49 | 50 | if self.remote_frida: 51 | self.frida_instance = frida.get_device_manager().add_remote_device('%s:%d' % ('localhost', self.frida_port)) 52 | else: 53 | self.frida_instance = frida 54 | 55 | 56 | 57 | def loadScript(self, script_code): 58 | script = self.frida_session.create_script(script_code) 59 | 60 | def on_message(message, data): 61 | if 'payload' in message.keys() and str(message['payload']) == "finished": 62 | pass 63 | else: 64 | log.info("on_message: " + str(message)) 65 | #log.info("on_message: " + str(message['payload'])) 66 | #log.info("on_message (data): " + str(data)) 67 | 68 | script.on('message', on_message) 69 | script.load() 70 | self.frida_script = script 71 | return True 72 | 73 | def attach(self, script_code): 74 | """ 75 | Attach frida to the target 76 | """ 77 | 78 | if self.process_pid != None: 79 | target_process = int(self.process_pid) 80 | elif self.process_name != None: 81 | target_process = self.process_name 82 | else: 83 | log.warn("'%s'.attach: No process specified with 'process_name' or 'pid'!" % self.name) 84 | return False 85 | 86 | try: 87 | if self.remote_frida: 88 | self.frida_session = self.frida_instance.attach(target_process) 89 | else: 90 | self.frida_session = self.frida_instance.attach(target_process) 91 | except frida.ProcessNotFoundError as e: 92 | log.warn("'%s'.attach: %s" % (self.name, str(e))) 93 | return False 94 | 95 | self.loadScript(script_code) 96 | 97 | pid = self.frida_script.exports.getpid() 98 | log.info("'%s'.attach: Attached to pid %d!" % (self.name, pid)) 99 | self.process_pid = pid 100 | 101 | function_address = self.function 102 | if isinstance(function_address, str): 103 | function_address = int(self.frida_script.exports.resolvesymbol(self.function), 0) 104 | if function_address > 0: 105 | log.info("Target function '%s' is at address %s!" % (self.function, function_address)) 106 | else: 107 | log.warn("No symbol with name '%s' was found!" % self.function) 108 | self.detach() 109 | return False 110 | 111 | self.frida_script.exports.settarget(function_address) 112 | return True 113 | 114 | def detach(self): 115 | try: 116 | self.frida_script.unload() 117 | except frida.InvalidOperationError as e: 118 | log.warn("'%s'.detach: Could not unload frida script: %s" % (self.name,str(e))) 119 | 120 | self.frida_session.detach() 121 | 122 | 123 | def getModuleMap(self): 124 | if self.frida_script == None: 125 | log.warn("'%s'.getModuleMap: self.frida_script is None!" % self.name) 126 | return None 127 | 128 | try: 129 | modulemap = self.frida_script.exports.makemaps() 130 | except frida.core.RPCException as e: 131 | log.info("RPCException: " + repr(e)) 132 | return None 133 | 134 | self.modules = [] 135 | for image in modulemap: 136 | idx = image['id'] 137 | path = image['path'] 138 | base = int(image['base'], 0) 139 | end = int(image['end'], 0) 140 | size = image['size'] 141 | 142 | m = { 143 | 'id' : idx, 144 | 'path' : path, 145 | 'base' : base, 146 | 'end' : end, 147 | 'range' : range(base, end), 148 | 'size' : size} 149 | 150 | self.modules.append(m) 151 | return self.modules 152 | 153 | def createModuleFilterList(self): 154 | """ 155 | Creates the list of modules in which coverage information 156 | should be collected. This list is created by querying frida 157 | for the loaded modules and comparing them to the modules 158 | the user selected in the project settings. 159 | 160 | Must be called after frida was attached to the target and 161 | before any coverage is collected. 162 | """ 163 | 164 | if self.modules == None: 165 | log.warn("'%s'.createModuleFilterList: self.modules is None!" % self.name) 166 | return False 167 | 168 | self.watched_modules = [] 169 | for module in self.modules: 170 | for search_term in self.modules_to_watch: 171 | if search_term in module["path"]: 172 | self.watched_modules.append(module) 173 | 174 | if len(self.watched_modules) == 0: 175 | paths = "\n".join([m["path"] for m in self.modules]) 176 | log.warn("'%s'.createModuleFilterList: No module was selected! Possible choices:\n%s" % (self.name, paths)) 177 | return False 178 | else: 179 | paths = "\n".join([m["path"] for m in self.watched_modules]) 180 | log.info("'%s'.createModuleFilterList: Filter coverage to only include the following modules:\n%s" % (self.name, paths)) 181 | return True 182 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup 4 | 5 | setup(name='frizzer', 6 | version='0.1', 7 | description='A coverage-guided blackbox fuzzer based on the frida instrumentation framework', 8 | url='http://github.com/ernw/frizzer', 9 | author='Dennis Mantz', 10 | author_email='dmantz@ernw.de', 11 | license='MIT', 12 | packages=['frizzer'], 13 | package_data={ 14 | '': ['*.js'], 15 | }, 16 | install_requires=[ 17 | 'frida-tools', 18 | 'toml' 19 | ], 20 | entry_points = { 21 | 'console_scripts': ['frizzer=frizzer.fuzzer:main'] 22 | }, 23 | zip_safe=False) 24 | -------------------------------------------------------------------------------- /tests/picohttpparser/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | gcc -no-pie -o test test.c picohttpparser.c -Wstringop-overflow=0 3 | 4 | clean: 5 | rm test 6 | rm -rf ./tmprojdir 7 | -------------------------------------------------------------------------------- /tests/picohttpparser/indir/1: -------------------------------------------------------------------------------- 1 | GET / HTTP/1.1 2 | Host: localhost:8888 3 | Connection: keep-alive 4 | Upgrade-Insecure-Requests: 1 5 | User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36 6 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 7 | Accept-Encoding: gzip, deflate, br 8 | Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7,fr;q=0.6 9 | Cookie: org.cups.sid=4c5eeaee36f46c9d32de76e4f1fde0e2 10 | 11 | -------------------------------------------------------------------------------- /tests/picohttpparser/indir/2: -------------------------------------------------------------------------------- 1 | POST /temp.api HTTP/1.1 2 | Host: localhost:8888 3 | Connection: keep-alive 4 | Content-Length: 0 5 | Accept: */* 6 | Origin: http://localhost:8888 7 | X-Requested-With: XMLHttpRequest 8 | User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36 9 | Referer: http://localhost:8888/ 10 | Accept-Encoding: gzip, deflate, br 11 | Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7,fr;q=0.6 12 | Cookie: org.cups.sid=4c5eeaee36f46c9d32de76e4f1fde0e2 13 | 14 | -------------------------------------------------------------------------------- /tests/picohttpparser/picohttpparser.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009-2014 Kazuho Oku, Tokuhiro Matsuno, Daisuke Murase, 3 | * Shigeo Mitsunari 4 | * 5 | * The software is licensed under either the MIT License (below) or the Perl 6 | * license. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to 10 | * deal in the Software without restriction, including without limitation the 11 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | * sell copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | * IN THE SOFTWARE. 25 | */ 26 | 27 | #include 28 | #include 29 | #include 30 | #ifdef __SSE4_2__ 31 | #ifdef _MSC_VER 32 | #include 33 | #else 34 | #include 35 | #endif 36 | #endif 37 | #include "picohttpparser.h" 38 | 39 | #if __GNUC__ >= 3 40 | #define likely(x) __builtin_expect(!!(x), 1) 41 | #define unlikely(x) __builtin_expect(!!(x), 0) 42 | #else 43 | #define likely(x) (x) 44 | #define unlikely(x) (x) 45 | #endif 46 | 47 | #ifdef _MSC_VER 48 | #define ALIGNED(n) _declspec(align(n)) 49 | #else 50 | #define ALIGNED(n) __attribute__((aligned(n))) 51 | #endif 52 | 53 | #define IS_PRINTABLE_ASCII(c) ((unsigned char)(c)-040u < 0137u) 54 | 55 | #define CHECK_EOF() \ 56 | if (buf == buf_end) { \ 57 | *ret = -2; \ 58 | return NULL; \ 59 | } 60 | 61 | #define EXPECT_CHAR_NO_CHECK(ch) \ 62 | if (*buf++ != ch) { \ 63 | *ret = -1; \ 64 | return NULL; \ 65 | } 66 | 67 | #define EXPECT_CHAR(ch) \ 68 | CHECK_EOF(); \ 69 | EXPECT_CHAR_NO_CHECK(ch); 70 | 71 | #define ADVANCE_TOKEN(tok, toklen) \ 72 | do { \ 73 | const char *tok_start = buf; \ 74 | static const char ALIGNED(16) ranges2[16] = "\000\040\177\177"; \ 75 | int found2; \ 76 | buf = findchar_fast(buf, buf_end, ranges2, 4, &found2); \ 77 | if (!found2) { \ 78 | CHECK_EOF(); \ 79 | } \ 80 | while (1) { \ 81 | if (*buf == ' ') { \ 82 | break; \ 83 | } else if (unlikely(!IS_PRINTABLE_ASCII(*buf))) { \ 84 | if ((unsigned char)*buf < '\040' || *buf == '\177') { \ 85 | *ret = -1; \ 86 | return NULL; \ 87 | } \ 88 | } \ 89 | ++buf; \ 90 | CHECK_EOF(); \ 91 | } \ 92 | tok = tok_start; \ 93 | toklen = buf - tok_start; \ 94 | } while (0) 95 | 96 | static const char *token_char_map = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 97 | "\0\1\0\1\1\1\1\1\0\0\1\1\0\1\1\0\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0" 98 | "\0\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\1\1" 99 | "\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\1\0\1\0" 100 | "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 101 | "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 102 | "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" 103 | "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; 104 | 105 | static const char *findchar_fast(const char *buf, const char *buf_end, const char *ranges, size_t ranges_size, int *found) 106 | { 107 | *found = 0; 108 | #if __SSE4_2__ 109 | if (likely(buf_end - buf >= 16)) { 110 | __m128i ranges16 = _mm_loadu_si128((const __m128i *)ranges); 111 | 112 | size_t left = (buf_end - buf) & ~15; 113 | do { 114 | __m128i b16 = _mm_loadu_si128((const __m128i *)buf); 115 | int r = _mm_cmpestri(ranges16, ranges_size, b16, 16, _SIDD_LEAST_SIGNIFICANT | _SIDD_CMP_RANGES | _SIDD_UBYTE_OPS); 116 | if (unlikely(r != 16)) { 117 | buf += r; 118 | *found = 1; 119 | break; 120 | } 121 | buf += 16; 122 | left -= 16; 123 | } while (likely(left != 0)); 124 | } 125 | #else 126 | /* suppress unused parameter warning */ 127 | (void)buf_end; 128 | (void)ranges; 129 | (void)ranges_size; 130 | #endif 131 | return buf; 132 | } 133 | 134 | static const char *get_token_to_eol(const char *buf, const char *buf_end, const char **token, size_t *token_len, int *ret) 135 | { 136 | const char *token_start = buf; 137 | 138 | #ifdef __SSE4_2__ 139 | static const char ALIGNED(16) ranges1[16] = "\0\010" /* allow HT */ 140 | "\012\037" /* allow SP and up to but not including DEL */ 141 | "\177\177"; /* allow chars w. MSB set */ 142 | int found; 143 | buf = findchar_fast(buf, buf_end, ranges1, 6, &found); 144 | if (found) 145 | goto FOUND_CTL; 146 | #else 147 | /* find non-printable char within the next 8 bytes, this is the hottest code; manually inlined */ 148 | while (likely(buf_end - buf >= 8)) { 149 | #define DOIT() \ 150 | do { \ 151 | if (unlikely(!IS_PRINTABLE_ASCII(*buf))) \ 152 | goto NonPrintable; \ 153 | ++buf; \ 154 | } while (0) 155 | DOIT(); 156 | DOIT(); 157 | DOIT(); 158 | DOIT(); 159 | DOIT(); 160 | DOIT(); 161 | DOIT(); 162 | DOIT(); 163 | #undef DOIT 164 | continue; 165 | NonPrintable: 166 | if ((likely((unsigned char)*buf < '\040') && likely(*buf != '\011')) || unlikely(*buf == '\177')) { 167 | goto FOUND_CTL; 168 | } 169 | ++buf; 170 | } 171 | #endif 172 | for (;; ++buf) { 173 | CHECK_EOF(); 174 | if (unlikely(!IS_PRINTABLE_ASCII(*buf))) { 175 | if ((likely((unsigned char)*buf < '\040') && likely(*buf != '\011')) || unlikely(*buf == '\177')) { 176 | goto FOUND_CTL; 177 | } 178 | } 179 | } 180 | FOUND_CTL: 181 | if (likely(*buf == '\015')) { 182 | ++buf; 183 | EXPECT_CHAR('\012'); 184 | *token_len = buf - 2 - token_start; 185 | } else if (*buf == '\012') { 186 | *token_len = buf - token_start; 187 | ++buf; 188 | } else { 189 | *ret = -1; 190 | return NULL; 191 | } 192 | *token = token_start; 193 | 194 | return buf; 195 | } 196 | 197 | static const char *is_complete(const char *buf, const char *buf_end, size_t last_len, int *ret) 198 | { 199 | int ret_cnt = 0; 200 | buf = last_len < 3 ? buf : buf + last_len - 3; 201 | 202 | while (1) { 203 | CHECK_EOF(); 204 | if (*buf == '\015') { 205 | ++buf; 206 | CHECK_EOF(); 207 | EXPECT_CHAR('\012'); 208 | ++ret_cnt; 209 | } else if (*buf == '\012') { 210 | ++buf; 211 | ++ret_cnt; 212 | } else { 213 | ++buf; 214 | ret_cnt = 0; 215 | } 216 | if (ret_cnt == 2) { 217 | return buf; 218 | } 219 | } 220 | 221 | *ret = -2; 222 | return NULL; 223 | } 224 | 225 | #define PARSE_INT(valp_, mul_) \ 226 | if (*buf < '0' || '9' < *buf) { \ 227 | buf++; \ 228 | *ret = -1; \ 229 | return NULL; \ 230 | } \ 231 | *(valp_) = (mul_) * (*buf++ - '0'); 232 | 233 | #define PARSE_INT_3(valp_) \ 234 | do { \ 235 | int res_ = 0; \ 236 | PARSE_INT(&res_, 100) \ 237 | *valp_ = res_; \ 238 | PARSE_INT(&res_, 10) \ 239 | *valp_ += res_; \ 240 | PARSE_INT(&res_, 1) \ 241 | *valp_ += res_; \ 242 | } while (0) 243 | 244 | /* returned pointer is always within [buf, buf_end), or null */ 245 | static const char *parse_http_version(const char *buf, const char *buf_end, int *minor_version, int *ret) 246 | { 247 | /* we want at least [HTTP/1.] to try to parse */ 248 | if (buf_end - buf < 9) { 249 | *ret = -2; 250 | return NULL; 251 | } 252 | EXPECT_CHAR_NO_CHECK('H'); 253 | EXPECT_CHAR_NO_CHECK('T'); 254 | EXPECT_CHAR_NO_CHECK('T'); 255 | EXPECT_CHAR_NO_CHECK('P'); 256 | EXPECT_CHAR_NO_CHECK('/'); 257 | EXPECT_CHAR_NO_CHECK('1'); 258 | EXPECT_CHAR_NO_CHECK('.'); 259 | PARSE_INT(minor_version, 1); 260 | return buf; 261 | } 262 | 263 | static const char *parse_headers(const char *buf, const char *buf_end, struct phr_header *headers, size_t *num_headers, 264 | size_t max_headers, int *ret) 265 | { 266 | for (;; ++*num_headers) { 267 | CHECK_EOF(); 268 | if (*buf == '\015') { 269 | ++buf; 270 | EXPECT_CHAR('\012'); 271 | break; 272 | } else if (*buf == '\012') { 273 | ++buf; 274 | break; 275 | } 276 | if (*num_headers == max_headers) { 277 | *ret = -1; 278 | return NULL; 279 | } 280 | if (!(*num_headers != 0 && (*buf == ' ' || *buf == '\t'))) { 281 | /* parsing name, but do not discard SP before colon, see 282 | * http://www.mozilla.org/security/announce/2006/mfsa2006-33.html */ 283 | headers[*num_headers].name = buf; 284 | static const char ALIGNED(16) ranges1[] = "\x00 " /* control chars and up to SP */ 285 | "\"\"" /* 0x22 */ 286 | "()" /* 0x28,0x29 */ 287 | ",," /* 0x2c */ 288 | "//" /* 0x2f */ 289 | ":@" /* 0x3a-0x40 */ 290 | "[]" /* 0x5b-0x5d */ 291 | "{\377"; /* 0x7b-0xff */ 292 | int found; 293 | buf = findchar_fast(buf, buf_end, ranges1, sizeof(ranges1) - 1, &found); 294 | if (!found) { 295 | CHECK_EOF(); 296 | } 297 | while (1) { 298 | if (*buf == ':') { 299 | break; 300 | } else if (!token_char_map[(unsigned char)*buf]) { 301 | *ret = -1; 302 | return NULL; 303 | } 304 | ++buf; 305 | CHECK_EOF(); 306 | } 307 | if ((headers[*num_headers].name_len = buf - headers[*num_headers].name) == 0) { 308 | *ret = -1; 309 | return NULL; 310 | } 311 | ++buf; 312 | for (;; ++buf) { 313 | CHECK_EOF(); 314 | if (!(*buf == ' ' || *buf == '\t')) { 315 | break; 316 | } 317 | } 318 | } else { 319 | headers[*num_headers].name = NULL; 320 | headers[*num_headers].name_len = 0; 321 | } 322 | const char *value; 323 | size_t value_len; 324 | if ((buf = get_token_to_eol(buf, buf_end, &value, &value_len, ret)) == NULL) { 325 | return NULL; 326 | } 327 | /* remove trailing SPs and HTABs */ 328 | const char *value_end = value + value_len; 329 | for (; value_end != value; --value_end) { 330 | const char c = *(value_end - 1); 331 | if (!(c == ' ' || c == '\t')) { 332 | break; 333 | } 334 | } 335 | headers[*num_headers].value = value; 336 | headers[*num_headers].value_len = value_end - value; 337 | } 338 | return buf; 339 | } 340 | 341 | static const char *parse_request(const char *buf, const char *buf_end, const char **method, size_t *method_len, const char **path, 342 | size_t *path_len, int *minor_version, struct phr_header *headers, size_t *num_headers, 343 | size_t max_headers, int *ret) 344 | { 345 | /* skip first empty line (some clients add CRLF after POST content) */ 346 | CHECK_EOF(); 347 | if (*buf == '\015') { 348 | ++buf; 349 | EXPECT_CHAR('\012'); 350 | } else if (*buf == '\012') { 351 | ++buf; 352 | } 353 | 354 | /* parse request line */ 355 | ADVANCE_TOKEN(*method, *method_len); 356 | do { 357 | ++buf; 358 | } while (*buf == ' '); 359 | ADVANCE_TOKEN(*path, *path_len); 360 | do { 361 | ++buf; 362 | } while (*buf == ' '); 363 | if (*method_len == 0 || *path_len == 0) { 364 | *ret = -1; 365 | return NULL; 366 | } 367 | if ((buf = parse_http_version(buf, buf_end, minor_version, ret)) == NULL) { 368 | return NULL; 369 | } 370 | if (*buf == '\015') { 371 | ++buf; 372 | EXPECT_CHAR('\012'); 373 | } else if (*buf == '\012') { 374 | ++buf; 375 | } else { 376 | *ret = -1; 377 | return NULL; 378 | } 379 | 380 | return parse_headers(buf, buf_end, headers, num_headers, max_headers, ret); 381 | } 382 | 383 | int phr_parse_request(const char *buf_start, size_t len, const char **method, size_t *method_len, const char **path, 384 | size_t *path_len, int *minor_version, struct phr_header *headers, size_t *num_headers, size_t last_len) 385 | { 386 | const char *buf = buf_start, *buf_end = buf_start + len; 387 | size_t max_headers = *num_headers; 388 | int r; 389 | 390 | *method = NULL; 391 | *method_len = 0; 392 | *path = NULL; 393 | *path_len = 0; 394 | *minor_version = -1; 395 | *num_headers = 0; 396 | 397 | /* if last_len != 0, check if the request is complete (a fast countermeasure 398 | againt slowloris */ 399 | if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { 400 | return r; 401 | } 402 | 403 | if ((buf = parse_request(buf, buf_end, method, method_len, path, path_len, minor_version, headers, num_headers, max_headers, 404 | &r)) == NULL) { 405 | return r; 406 | } 407 | 408 | return (int)(buf - buf_start); 409 | } 410 | 411 | static const char *parse_response(const char *buf, const char *buf_end, int *minor_version, int *status, const char **msg, 412 | size_t *msg_len, struct phr_header *headers, size_t *num_headers, size_t max_headers, int *ret) 413 | { 414 | /* parse "HTTP/1.x" */ 415 | if ((buf = parse_http_version(buf, buf_end, minor_version, ret)) == NULL) { 416 | return NULL; 417 | } 418 | /* skip space */ 419 | if (*buf != ' ') { 420 | *ret = -1; 421 | return NULL; 422 | } 423 | do { 424 | ++buf; 425 | } while (*buf == ' '); 426 | /* parse status code, we want at least [:digit:][:digit:][:digit:] to try to parse */ 427 | if (buf_end - buf < 4) { 428 | *ret = -2; 429 | return NULL; 430 | } 431 | PARSE_INT_3(status); 432 | 433 | /* get message includig preceding space */ 434 | if ((buf = get_token_to_eol(buf, buf_end, msg, msg_len, ret)) == NULL) { 435 | return NULL; 436 | } 437 | if (*msg_len == 0) { 438 | /* ok */ 439 | } else if (**msg == ' ') { 440 | /* remove preceding space */ 441 | do { 442 | ++*msg; 443 | --*msg_len; 444 | } while (**msg == ' '); 445 | } else { 446 | /* garbage found after status code */ 447 | *ret = -1; 448 | return NULL; 449 | } 450 | 451 | return parse_headers(buf, buf_end, headers, num_headers, max_headers, ret); 452 | } 453 | 454 | int phr_parse_response(const char *buf_start, size_t len, int *minor_version, int *status, const char **msg, size_t *msg_len, 455 | struct phr_header *headers, size_t *num_headers, size_t last_len) 456 | { 457 | const char *buf = buf_start, *buf_end = buf + len; 458 | size_t max_headers = *num_headers; 459 | int r; 460 | 461 | *minor_version = -1; 462 | *status = 0; 463 | *msg = NULL; 464 | *msg_len = 0; 465 | *num_headers = 0; 466 | 467 | /* if last_len != 0, check if the response is complete (a fast countermeasure 468 | against slowloris */ 469 | if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { 470 | return r; 471 | } 472 | 473 | if ((buf = parse_response(buf, buf_end, minor_version, status, msg, msg_len, headers, num_headers, max_headers, &r)) == NULL) { 474 | return r; 475 | } 476 | 477 | return (int)(buf - buf_start); 478 | } 479 | 480 | int phr_parse_headers(const char *buf_start, size_t len, struct phr_header *headers, size_t *num_headers, size_t last_len) 481 | { 482 | const char *buf = buf_start, *buf_end = buf + len; 483 | size_t max_headers = *num_headers; 484 | int r; 485 | 486 | *num_headers = 0; 487 | 488 | /* if last_len != 0, check if the response is complete (a fast countermeasure 489 | against slowloris */ 490 | if (last_len != 0 && is_complete(buf, buf_end, last_len, &r) == NULL) { 491 | return r; 492 | } 493 | 494 | if ((buf = parse_headers(buf, buf_end, headers, num_headers, max_headers, &r)) == NULL) { 495 | return r; 496 | } 497 | 498 | return (int)(buf - buf_start); 499 | } 500 | 501 | enum { 502 | CHUNKED_IN_CHUNK_SIZE, 503 | CHUNKED_IN_CHUNK_EXT, 504 | CHUNKED_IN_CHUNK_DATA, 505 | CHUNKED_IN_CHUNK_CRLF, 506 | CHUNKED_IN_TRAILERS_LINE_HEAD, 507 | CHUNKED_IN_TRAILERS_LINE_MIDDLE 508 | }; 509 | 510 | static int decode_hex(int ch) 511 | { 512 | if ('0' <= ch && ch <= '9') { 513 | return ch - '0'; 514 | } else if ('A' <= ch && ch <= 'F') { 515 | return ch - 'A' + 0xa; 516 | } else if ('a' <= ch && ch <= 'f') { 517 | return ch - 'a' + 0xa; 518 | } else { 519 | return -1; 520 | } 521 | } 522 | 523 | ssize_t phr_decode_chunked(struct phr_chunked_decoder *decoder, char *buf, size_t *_bufsz) 524 | { 525 | size_t dst = 0, src = 0, bufsz = *_bufsz; 526 | ssize_t ret = -2; /* incomplete */ 527 | 528 | while (1) { 529 | switch (decoder->_state) { 530 | case CHUNKED_IN_CHUNK_SIZE: 531 | for (;; ++src) { 532 | int v; 533 | if (src == bufsz) 534 | goto Exit; 535 | if ((v = decode_hex(buf[src])) == -1) { 536 | if (decoder->_hex_count == 0) { 537 | ret = -1; 538 | goto Exit; 539 | } 540 | break; 541 | } 542 | if (decoder->_hex_count == sizeof(size_t) * 2) { 543 | ret = -1; 544 | goto Exit; 545 | } 546 | decoder->bytes_left_in_chunk = decoder->bytes_left_in_chunk * 16 + v; 547 | ++decoder->_hex_count; 548 | } 549 | decoder->_hex_count = 0; 550 | decoder->_state = CHUNKED_IN_CHUNK_EXT; 551 | /* fallthru */ 552 | case CHUNKED_IN_CHUNK_EXT: 553 | /* RFC 7230 A.2 "Line folding in chunk extensions is disallowed" */ 554 | for (;; ++src) { 555 | if (src == bufsz) 556 | goto Exit; 557 | if (buf[src] == '\012') 558 | break; 559 | } 560 | ++src; 561 | if (decoder->bytes_left_in_chunk == 0) { 562 | if (decoder->consume_trailer) { 563 | decoder->_state = CHUNKED_IN_TRAILERS_LINE_HEAD; 564 | break; 565 | } else { 566 | goto Complete; 567 | } 568 | } 569 | decoder->_state = CHUNKED_IN_CHUNK_DATA; 570 | /* fallthru */ 571 | case CHUNKED_IN_CHUNK_DATA: { 572 | size_t avail = bufsz - src; 573 | if (avail < decoder->bytes_left_in_chunk) { 574 | if (dst != src) 575 | memmove(buf + dst, buf + src, avail); 576 | src += avail; 577 | dst += avail; 578 | decoder->bytes_left_in_chunk -= avail; 579 | goto Exit; 580 | } 581 | if (dst != src) 582 | memmove(buf + dst, buf + src, decoder->bytes_left_in_chunk); 583 | src += decoder->bytes_left_in_chunk; 584 | dst += decoder->bytes_left_in_chunk; 585 | decoder->bytes_left_in_chunk = 0; 586 | decoder->_state = CHUNKED_IN_CHUNK_CRLF; 587 | } 588 | /* fallthru */ 589 | case CHUNKED_IN_CHUNK_CRLF: 590 | for (;; ++src) { 591 | if (src == bufsz) 592 | goto Exit; 593 | if (buf[src] != '\015') 594 | break; 595 | } 596 | if (buf[src] != '\012') { 597 | ret = -1; 598 | goto Exit; 599 | } 600 | ++src; 601 | decoder->_state = CHUNKED_IN_CHUNK_SIZE; 602 | break; 603 | case CHUNKED_IN_TRAILERS_LINE_HEAD: 604 | for (;; ++src) { 605 | if (src == bufsz) 606 | goto Exit; 607 | if (buf[src] != '\015') 608 | break; 609 | } 610 | if (buf[src++] == '\012') 611 | goto Complete; 612 | decoder->_state = CHUNKED_IN_TRAILERS_LINE_MIDDLE; 613 | /* fallthru */ 614 | case CHUNKED_IN_TRAILERS_LINE_MIDDLE: 615 | for (;; ++src) { 616 | if (src == bufsz) 617 | goto Exit; 618 | if (buf[src] == '\012') 619 | break; 620 | } 621 | ++src; 622 | decoder->_state = CHUNKED_IN_TRAILERS_LINE_HEAD; 623 | break; 624 | default: 625 | assert(!"decoder is corrupt"); 626 | } 627 | } 628 | 629 | Complete: 630 | ret = bufsz - src; 631 | Exit: 632 | if (dst != src) 633 | memmove(buf + dst, buf + src, bufsz - src); 634 | *_bufsz = dst; 635 | return ret; 636 | } 637 | 638 | int phr_decode_chunked_is_in_data(struct phr_chunked_decoder *decoder) 639 | { 640 | return decoder->_state == CHUNKED_IN_CHUNK_DATA; 641 | } 642 | 643 | #undef CHECK_EOF 644 | #undef EXPECT_CHAR 645 | #undef ADVANCE_TOKEN 646 | -------------------------------------------------------------------------------- /tests/picohttpparser/picohttpparser.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009-2014 Kazuho Oku, Tokuhiro Matsuno, Daisuke Murase, 3 | * Shigeo Mitsunari 4 | * 5 | * The software is licensed under either the MIT License (below) or the Perl 6 | * license. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to 10 | * deal in the Software without restriction, including without limitation the 11 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | * sell copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | * IN THE SOFTWARE. 25 | */ 26 | 27 | #ifndef picohttpparser_h 28 | #define picohttpparser_h 29 | 30 | #include 31 | 32 | #ifdef _MSC_VER 33 | #define ssize_t intptr_t 34 | #endif 35 | 36 | #ifdef __cplusplus 37 | extern "C" { 38 | #endif 39 | 40 | /* contains name and value of a header (name == NULL if is a continuing line 41 | * of a multiline header */ 42 | struct phr_header { 43 | const char *name; 44 | size_t name_len; 45 | const char *value; 46 | size_t value_len; 47 | }; 48 | 49 | /* returns number of bytes consumed if successful, -2 if request is partial, 50 | * -1 if failed */ 51 | int phr_parse_request(const char *buf, size_t len, const char **method, size_t *method_len, const char **path, size_t *path_len, 52 | int *minor_version, struct phr_header *headers, size_t *num_headers, size_t last_len); 53 | 54 | /* ditto */ 55 | int phr_parse_response(const char *_buf, size_t len, int *minor_version, int *status, const char **msg, size_t *msg_len, 56 | struct phr_header *headers, size_t *num_headers, size_t last_len); 57 | 58 | /* ditto */ 59 | int phr_parse_headers(const char *buf, size_t len, struct phr_header *headers, size_t *num_headers, size_t last_len); 60 | 61 | /* should be zero-filled before start */ 62 | struct phr_chunked_decoder { 63 | size_t bytes_left_in_chunk; /* number of bytes left in current chunk */ 64 | char consume_trailer; /* if trailing headers should be consumed */ 65 | char _hex_count; 66 | char _state; 67 | }; 68 | 69 | /* the function rewrites the buffer given as (buf, bufsz) removing the chunked- 70 | * encoding headers. When the function returns without an error, bufsz is 71 | * updated to the length of the decoded data available. Applications should 72 | * repeatedly call the function while it returns -2 (incomplete) every time 73 | * supplying newly arrived data. If the end of the chunked-encoded data is 74 | * found, the function returns a non-negative number indicating the number of 75 | * octets left undecoded at the tail of the supplied buffer. Returns -1 on 76 | * error. 77 | */ 78 | ssize_t phr_decode_chunked(struct phr_chunked_decoder *decoder, char *buf, size_t *bufsz); 79 | 80 | /* returns if the chunked decoder is in middle of chunked data */ 81 | int phr_decode_chunked_is_in_data(struct phr_chunked_decoder *decoder); 82 | 83 | #ifdef __cplusplus 84 | } 85 | #endif 86 | 87 | #endif 88 | -------------------------------------------------------------------------------- /tests/picohttpparser/run_in_process_fuzzing_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Not clear why this does not behave the same as without in-process fuzzing... 4 | 5 | # Enable job control for shell script (so we can use 'fg', etc) 6 | set -m 7 | 8 | exitfn () { 9 | trap SIGINT 10 | echo 'Interrupted by user!' 11 | kill $test_pid 12 | kill $frizzer_pid 13 | exit 14 | } 15 | 16 | trap "exitfn" INT # Set up SIGINT trap to call function. 17 | 18 | ./test > /dev/null & 19 | test_pid=$! 20 | 21 | rm -rf tmpprojdir 22 | frizzer init tmpprojdir 23 | cat > tmpprojdir/config < /dev/null & 23 | test_pid=$! 24 | 25 | rm -rf tmpprojdir 26 | 27 | # new: 28 | frizzer init tmpprojdir 29 | cat > tmpprojdir/config < 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include"picohttpparser.h" 10 | 11 | 12 | void crash() { 13 | char a[10]; 14 | strcpy(a, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); 15 | return; 16 | } 17 | 18 | void handleClient(char* buf) { 19 | char *method, *path; 20 | int pret, minor_version; 21 | struct phr_header headers[100]; 22 | size_t buflen = 0, prevbuflen = 0, method_len, path_len, num_headers; 23 | ssize_t rret; 24 | printf("buf: %s\n", buf); 25 | 26 | buflen = strlen(buf); 27 | prevbuflen = buflen; 28 | num_headers = sizeof(headers) / sizeof(headers[0]); 29 | pret = phr_parse_request(buf, buflen, &method, &method_len, &path, &path_len, 30 | &minor_version, headers, &num_headers, prevbuflen); 31 | printf("method=%s path=%s minor_version=%d num_headers=%d\n", method, path, minor_version, num_headers); 32 | } 33 | 34 | int main(int argc, char** argv) { 35 | char buf[1000]; 36 | socklen_t len; 37 | int sock_desc,temp_sock_desc; 38 | struct sockaddr_in client,server; 39 | memset(&client,0,sizeof(client)); 40 | memset(&server,0,sizeof(server)); 41 | sock_desc = socket(AF_INET,SOCK_STREAM,0); 42 | server.sin_family = AF_INET; server.sin_addr.s_addr = inet_addr("127.0.0.1"); 43 | server.sin_port = htons(7777); 44 | bind(sock_desc,(struct sockaddr*)&server,sizeof(server)); 45 | listen(sock_desc,20); len = sizeof(client); 46 | while(1) 47 | { 48 | temp_sock_desc = -1; 49 | memset(buf, 0, 1000); 50 | temp_sock_desc = accept(sock_desc,(struct sockaddr*)&client,&len); 51 | recv(temp_sock_desc,buf,1000,0); 52 | handleClient(buf); 53 | close(temp_sock_desc); 54 | } 55 | close(sock_desc); 56 | return 0; 57 | 58 | } 59 | -------------------------------------------------------------------------------- /tests/run_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | all_test_dirs=`ls -d */` 4 | 5 | dir_count=`echo $all_test_dirs | wc -w` 6 | 7 | dir_number=1 8 | 9 | for current_dir in $all_test_dirs 10 | do 11 | pushd $current_dir > /dev/null 12 | 13 | all_tests=`ls run_*.sh` 14 | test_count=`echo $all_tests | wc -w` 15 | test_number=1 16 | 17 | for current_test in $all_tests 18 | do 19 | echo -en "[DIR: $dir_number/$dir_count TEST: $test_number/$test_count] ($current_dir$current_test) ... " 20 | 21 | ./$current_test > /dev/null 22 | 23 | if [ $? -eq 0 ]; then 24 | echo -e '\033[32m OK \033[0m' 25 | else 26 | echo -e '\033[31m FAILED \033[0m' 27 | fi 28 | 29 | test_number=$(($test_number+1)) 30 | done 31 | popd > /dev/null 32 | dir_number=$(($dir_number+1)) 33 | done 34 | -------------------------------------------------------------------------------- /tests/simple_binary/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | gcc -no-pie -o test test.c -Wstringop-overflow=0 3 | 4 | clean: 5 | rm test 6 | rm -rf ./tmprojdir 7 | -------------------------------------------------------------------------------- /tests/simple_binary/indir/1: -------------------------------------------------------------------------------- 1 | AAAA 2 | -------------------------------------------------------------------------------- /tests/simple_binary/indir/2: -------------------------------------------------------------------------------- 1 | BBBB 2 | -------------------------------------------------------------------------------- /tests/simple_binary/indir/3: -------------------------------------------------------------------------------- 1 | CCCC 2 | -------------------------------------------------------------------------------- /tests/simple_binary/indir/4: -------------------------------------------------------------------------------- 1 | DDDD 2 | -------------------------------------------------------------------------------- /tests/simple_binary/run_in_process_fuzzing_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Expected bahavior: 4 | # Find new paths at seeds: 5, 111, 135, ... 5 | # Find crash at seed 17?? 6 | # Average Speed: between 40 and 50 7 | 8 | # Enable job control for shell script (so we can use 'fg', etc) 9 | set -m 10 | 11 | exitfn () { 12 | trap SIGINT 13 | echo 'Interrupted by user!' 14 | kill $test_pid 15 | kill $frizzer_pid 16 | exit 17 | } 18 | 19 | trap "exitfn" INT # Set up SIGINT trap to call function. 20 | 21 | ./test > /dev/null & 22 | test_pid=$! 23 | 24 | rm -rf tmpprojdir 25 | frizzer init tmpprojdir 26 | cat > tmpprojdir/config < /dev/null & 31 | test_pid=$! 32 | 33 | rm -rf tmpprojdir 34 | 35 | # new: 36 | frizzer init tmpprojdir 37 | cat > tmpprojdir/config < 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | void crash() { 11 | char a[10]; 12 | strcpy(a, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); 13 | return; 14 | } 15 | 16 | void handleClient(char* buf) { 17 | if(buf[0]%5 == 1) { 18 | puts("--A--"); 19 | if(buf[1]%6 == 1) { 20 | puts("--AB--"); 21 | if(buf[2]%7 == 1) { 22 | puts("--ABC--"); 23 | if(buf[3]%8 == 1) { 24 | puts("--ABCD--"); 25 | if(buf[4]%9 == 1) { 26 | puts("--ABCDE--"); 27 | if(buf[5]%10 == 1) { 28 | puts("--ABCDEF--"); 29 | if(buf[6]%11 == 1) { 30 | puts("--CRASH--"); 31 | crash(); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | printf("%s",buf); 40 | } 41 | 42 | int main(int argc, char** argv) { 43 | char buf[100]; 44 | socklen_t len; 45 | int sock_desc,temp_sock_desc; 46 | struct sockaddr_in client,server; 47 | memset(&client,0,sizeof(client)); 48 | memset(&server,0,sizeof(server)); 49 | sock_desc = socket(AF_INET,SOCK_STREAM,0); 50 | server.sin_family = AF_INET; server.sin_addr.s_addr = inet_addr("127.0.0.1"); 51 | server.sin_port = htons(7777); 52 | bind(sock_desc,(struct sockaddr*)&server,sizeof(server)); 53 | listen(sock_desc,20); len = sizeof(client); 54 | while(1) 55 | { 56 | temp_sock_desc = -1; 57 | memset(buf, 0, 100); 58 | temp_sock_desc = accept(sock_desc,(struct sockaddr*)&client,&len); 59 | recv(temp_sock_desc,buf,100,0); 60 | handleClient(buf); 61 | close(temp_sock_desc); 62 | } 63 | close(sock_desc); 64 | return 0; 65 | 66 | } 67 | --------------------------------------------------------------------------------