├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── code_coverage.png ├── crash_saving.png ├── ida_coloring.png └── meso_bag.png ├── coverage_scripts └── ida.py ├── examples └── benchmark │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── fake_program │ ├── .gitignore │ ├── Makefile │ ├── program.asm │ └── program.exe │ └── src │ └── main.rs ├── generate_mesos.py ├── libs └── debugger │ ├── Cargo.toml │ └── src │ ├── debugger.rs │ ├── ffi_helpers.rs │ ├── handles.rs │ ├── lib.rs │ ├── minidump.rs │ └── sedebug.rs ├── mesogen_scripts ├── ghidra.py ├── ida.py └── ida_detect.py ├── offline_meso.ps1 └── src ├── main.rs └── mesofile.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | *.i64 4 | *.idb 5 | *.meso 6 | cache 7 | coverage.txt 8 | *.zip 9 | *.dmp 10 | 11 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "debugger" 5 | version = "0.1.0" 6 | dependencies = [ 7 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 8 | ] 9 | 10 | [[package]] 11 | name = "mesos" 12 | version = "0.1.0" 13 | dependencies = [ 14 | "debugger 0.1.0", 15 | ] 16 | 17 | [[package]] 18 | name = "winapi" 19 | version = "0.3.6" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | dependencies = [ 22 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 23 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 24 | ] 25 | 26 | [[package]] 27 | name = "winapi-i686-pc-windows-gnu" 28 | version = "0.4.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | 31 | [[package]] 32 | name = "winapi-x86_64-pc-windows-gnu" 33 | version = "0.4.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | 36 | [metadata] 37 | "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" 38 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 39 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 40 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mesos" 3 | version = "0.1.0" 4 | authors = ["bfalk"] 5 | license = "MIT" 6 | 7 | [dependencies] 8 | debugger = { path = "libs/debugger" } 9 | 10 | [profile.release] 11 | debug = true 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gamozo Labs, LLC 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Bag of Mesos][meso]![Bag of Mesos][meso]![Bag of Mesos][meso]![Bag of Mesos][meso]![Bag of Mesos][meso]![Bag of Mesos][meso]![Bag of Mesos][meso]![Bag of Mesos][meso]![Bag of Mesos][meso]![Bag of Mesos][meso]![Bag of Mesos][meso]![Bag of Mesos][meso]![Bag of Mesos][meso] 2 | 3 | # Summary 4 | 5 | Mesos is a tool to gather binary code coverage on all user-land Windows targets without need for source or recompilation. It also provides an automatic mechanism to save a full minidump of a process if it crashes under mesos. 6 | 7 | Mesos is technically just a really fast debugger, capable of handling tens of millions of breakpoints. Using this debugger, we apply breakpoints to every single basic block in a program. These breakpoints are removed as they are hit. Thus, mesos converges to 0-cost coverage as gathering coverage only has a cost the first time the basic block is hit. 8 | 9 | # Why? 10 | 11 | This is effectively the successor of my 5+ year old Chrome IPC fuzzer. It doesn't have any fuzz components in it, but it is a high-performance debugger. This debugger can apply millions of breakpoints to gather coverage, and handle thousands of breakpoints per second to modify memory to inject inputs. 12 | 13 | This strategy has worked out well for me historically and still is my go-to tooling for fuzzing targets on live systems. 14 | 15 | Out of the box it can be used to gather simple code coverage but it's designed to be easily modified to add fast breakpoint handlers to inject inputs. For example, put a breakpoint after `NtReadFile()` returns and modify the buffer in flight. I used this in Chrome to modify inbound IPC traffic in the browser. 16 | 17 | # Features 18 | 19 | ## Code coverage 20 | 21 | ![code coverage][code coverage] 22 | 23 | ## Automatic full minidump saving 24 | 25 | ![Crash being saved][crash saving] 26 | 27 | ## IDA Coloring 28 | 29 | ![IDA gettin colored up][ida coloring] 30 | 31 | # Quick Usage Guide 32 | 33 | Set `%PATH%` such that `idat64.exe` is in it: 34 | 35 | ``` 36 | path %PATH%;"C:\Program Files\IDA 7.2" 37 | ``` 38 | 39 | Generate mesos (the first time will be slow): 40 | 41 | ``` 42 | powershell .\offline_meso.ps1 43 | python generate_mesos.py process_ida 44 | ``` 45 | 46 | Gather coverage on target! 47 | 48 | ``` 49 | cargo build --release 50 | target\release\mesos.exe 51 | ``` 52 | 53 | Applying 1.6 million breakpoints? No big deal. 54 | 55 | ``` 56 | C:\dev\mesos>target\release\mesos.exe 13828 57 | mesos is 64-bit: true 58 | target is 64-bit: true 59 | [ 0.003783] Applied 5629 breakpoints ( 5629 total breakpoints) notepad.exe 60 | [ 0.028071] Applied 61334 breakpoints ( 66963 total breakpoints) ntdll.dll 61 | [ 0.035298] Applied 25289 breakpoints ( 92252 total breakpoints) kernel32.dll 62 | [ 0.058815] Applied 55611 breakpoints ( 147863 total breakpoints) kernelbase.dll 63 | ... 64 | [ 0.667417] Applied 11504 breakpoints ( 1466344 total breakpoints) oleacc.dll 65 | [ 0.676151] Applied 19557 breakpoints ( 1485901 total breakpoints) textinputframework.dll 66 | [ 0.705431] Applied 66650 breakpoints ( 1552551 total breakpoints) coreuicomponents.dll 67 | [ 0.717276] Applied 25202 breakpoints ( 1577753 total breakpoints) coremessaging.dll 68 | [ 0.720487] Applied 7557 breakpoints ( 1585310 total breakpoints) ntmarta.dll 69 | [ 0.732045] Applied 28569 breakpoints ( 1613879 total breakpoints) iertutil.dll 70 | ``` 71 | 72 | # API 73 | 74 | Currently this tool has a `debugger` lib you can easily bring in and start using to make custom debuggers. However this API is not finalized yet. I suggest not building anything using it yet as it may change very quickly. Once this message is removed it's probably stable :) 75 | 76 | # Performance 77 | 78 | - We can register (request breakpoints to be at module load) about ~6 million/second 79 | - We can apply them (actually install the breakpoints into the target at about ~3 million/second 80 | - We can clear breakpoints at about 15 million/second 81 | - We can hit and handle about 10k breakpoints/second 82 | 83 | Given breakpoints are cleared as they're hit for coverage, that means you can observe 10k _new_ blocks per second. Once you've hit a breakpoint they no longer have a performance cost! 84 | 85 | ``` 86 | C:\dev\mesos\examples\benchmark>cargo run --release 87 | Finished release [optimized + debuginfo] target(s) in 0.03s 88 | Running `target\release\benchmark.exe` 89 | mesos is 64-bit: true 90 | target is 64-bit: true 91 | Registered 1000000 breakpoints in 0.162230 seconds | 6164072.8 / second 92 | Applied 1000000 breakpoints in 0.321347 seconds | 3111897.0 / second 93 | Cleared 1000000 breakpoints in 0.067024 seconds | 14920028.6 / second 94 | Hit 100000 breakpoints in 10.066440 seconds | 9934.0 / second 95 | ``` 96 | 97 | # Usage 98 | 99 | To use mesos there are 3 major steps. First, the modules of a running process are saved. Second, these modules are loaded in IDA which then outputs a list of all basic blocks into the `meso` format. And finally, `mesos` is run against a target process to gather coverage! 100 | 101 | ## Creating meso_deps.zip 102 | 103 | This step is the first thing we have to do. We create a ZIP file containing all of the modules loaded into a given PID. 104 | 105 | This script requires no internet and is designed to be easily dropped onto new VMs so mesos can be generated for your target application. It depends on PowerShell v5.0 or later which is installed by default on Windows 10 and Windows Server 2016. 106 | 107 | Run, with `` replaced with the process ID you want to gather coverage on: 108 | 109 | ``` 110 | C:\dev\mesos>powershell .\offline_meso.ps1 8484 111 | Powershell is 64-bit: True 112 | Target is 64-bit: True 113 | 114 | C:\dev\mesos> 115 | ``` 116 | 117 | _Optionally you can supply `-OutputZip ` to change the output zip file name_ 118 | 119 | This will create a `meso_deps.zip` that if you look at contains all of the modules used in the process you ran the script targeting. 120 | 121 | ### Example output: 122 | 123 | ``` 124 | C:\dev\mesos>powershell .\offline_meso.ps1 8484 -OutputZip testing.zip 125 | Powershell is 64-bit: True Target is 64-bit: True C:\dev\mesos>powershell Expand-Archive testing.zip -DestinationPath example 126 | C:\dev\mesos>powershell Get-ChildItem example -rec -File -Name 127 | cache\c_\program files\common files\microsoft shared\ink\tiptsf.dll 128 | cache\c_\program files\intel\optaneshellextensions\iastorafsserviceapi.dll 129 | cache\c_\program files\widcomm\bluetooth software\btmmhook.dll 130 | cache\c_\program files (x86)\common files\adobe\coresyncextension\coresync_x64.dll 131 | ... 132 | ``` 133 | 134 | ## Generating meso files 135 | 136 | To generate meso files we operate on the `meso_deps.zip` we created in the last step. It doesn't matter where this zip came from. This allows the zip to have come from a VM that the PowerShell script was run on. 137 | 138 | Basic usage is: 139 | 140 | ``` 141 | python generate_mesos.py process_ida 142 | ``` 143 | 144 | This will use the `meso_deps.zip` file as an input, and use IDA to process all executables in the zip file and figure out where their basic blocks are. 145 | 146 | This will create a cache folder with a bunch of files in it. These files are named based on the module name, the modules TimeDateStamp in the PE header, and the ImageSize field in the PE header. This is what DLLs are uniqued by in the PDB symbol store, so it should be good enough for us here too. 147 | 148 | You'll see there are files with no extension (these are the original binaries), there are files with `.meso` extensions (the breakpoint lists), and `.i64` files (the cached IDA database for the original binary). 149 | 150 | ### Symbol resolution 151 | 152 | There is no limitation on what can make these meso files. The quality of the symbol resolution depends on the tool you used to generate and it's ability to resolve symbols. For example with IDA if you have public/private symbols your `_NT_SYMBOL_PATH` should be configured correctly. 153 | 154 | ### More advanced usage 155 | 156 | Check the programs usage for the most recent usage. But there are `_whitelist` and `_blacklist` options that allow you to use a list of strings to filter the amount of mesos generated. 157 | 158 | This is helpful as coverage outside of your target module is probably not relevant and just introduces overheads and unnecessary processing. 159 | 160 | ``` 161 | C:\dev\mesos>python generate_mesos.py 162 | Usage: 163 | generate_mesos.py process_ida 164 | Processes all files in the meso_deps.zip file 165 | 166 | generate_mesos.py process_ida_whitelist 167 | Processes files only containing one of the strings provided 168 | 169 | generate_mesos.py process_ida_blacklist 170 | Processes files all files except for those containing one of the provided strings 171 | 172 | Examples: 173 | 174 | python generate_mesos.py process_ida_whitelist system32 175 | Only processes files in `system32` 176 | 177 | python generate_mesos.py process_ida_blacklist ntdll.dll 178 | Process all files except for `ntdll.dll` 179 | 180 | Path requirements for process_ida_*: must have `idat64.exe` in your PATH 181 | ``` 182 | 183 | ### Example usage 184 | 185 | ``` 186 | C:\dev\mesos>python generate_mesos.py process_ida_whitelist system32 187 | Processing cache/c_/windows/system32/advapi32.dll 188 | Processing cache/c_/windows/system32/bcryptprimitives.dll 189 | Processing cache/c_/windows/system32/cfgmgr32.dll 190 | ... 191 | Processing cache/c_/windows/system32/user32.dll 192 | Processing cache/c_/windows/system32/uxtheme.dll 193 | Processing cache/c_/windows/system32/win32u.dll 194 | Processing cache/c_/windows/system32/windows.storage.dll 195 | Processing cache/c_/windows/system32/wintypes.dll 196 | ``` 197 | 198 | ## Meso usage 199 | 200 | Now we're onto the actual debugger. We've created meso files to tell it where to put breakpoints in each module. 201 | 202 | First we need to build it with Rust! 203 | 204 | ``` 205 | cargo build --release 206 | ``` 207 | 208 | And then we can simply run it with a PID! 209 | 210 | ``` 211 | target\release\mesos.exe 212 | ``` 213 | 214 | ### Command-line options 215 | 216 | Currently there are few options to mesos, run mesos without arguments to get the most recent list. 217 | 218 | ``` 219 | C:\dev\mesos>target\release\mesos.exe 220 | Usage: mesos.exe [--freq | --verbose | --print] 221 | --freq - Treats all breakpoints as frequency breakpoints 222 | --verbose - Enables verbose prints for debugging 223 | --print - Prints breakpoint info on every single breakpoint 224 | [explicit meso file] - Load a specific meso file regardless of loaded modules 225 | 226 | Standard usage: mesos.exe 227 | ``` 228 | 229 | ### Example usage 230 | 231 | ``` 232 | C:\dev\mesos>target\release\mesos.exe 13828 233 | mesos is 64-bit: true 234 | target is 64-bit: true 235 | [ 0.004033] Applied 5629 breakpoints ( 5629 total breakpoints) notepad.exe 236 | [ 0.029248] Applied 61334 breakpoints ( 66963 total breakpoints) ntdll.dll 237 | [ 0.037032] Applied 25289 breakpoints ( 92252 total breakpoints) kernel32.dll 238 | [ 0.062844] Applied 55611 breakpoints ( 147863 total breakpoints) kernelbase.dll 239 | ... 240 | [ 0.739059] Applied 66650 breakpoints ( 1552551 total breakpoints) coreuicomponents.dll 241 | [ 0.750266] Applied 25202 breakpoints ( 1577753 total breakpoints) coremessaging.dll 242 | [ 0.754485] Applied 7557 breakpoints ( 1585310 total breakpoints) ntmarta.dll 243 | [ 0.766119] Applied 28569 breakpoints ( 1613879 total breakpoints) iertutil.dll 244 | ... 245 | [ 23.544097] Removed 5968 breakpoints in imm32.dll 246 | [ 23.551529] Syncing code coverage database... 247 | [ 23.675103] Sync complete (169694 total unique coverage entries) 248 | Detached from process 13828 249 | ``` 250 | 251 | #### Why not use `cargo run`? 252 | 253 | When running in `cargo run` the Ctrl+C handler does not work correctly, and does not allow us to detach from the target program cleanly. 254 | 255 | # Limitations 256 | 257 | Since this relies on a tool (IDA) to identify blocks, if the tool incorrectly identifies a block it could result in us inserting a breakpoint over data. Further it's possible to miss coverage if a block is not correctly found. 258 | 259 | # Why doesn't it do more? 260 | 261 | Well. It really just allows fast breakpoints. Feel free to rip it apart and add your own hooks to functions. It could easily be used to fuzz things :) 262 | 263 | # Why IDA? 264 | 265 | I tried a bunch of tools and IDA was the only one that seemed to work well. Binja probably would also work well but I don't have it installed and I'm not familiar with the API. I have a coworker who wrote a plugin for it and that'll probably get pull requested in soon. 266 | 267 | _The meso files are just simple files, anyone can generate them from any tool_ 268 | 269 | # Technical Details 270 | 271 | ## Minidump autogenned filenames 272 | 273 | The generated minidump filenames are designed to give a high-level of glance value at crashes. It includes things like the exception type, faulting address, and rough classification of the bug. 274 | 275 | Currently if it's an access violation we apply the following classification: 276 | 277 | - Determine the access type (read, write, execute) 278 | - For reads the filename contains: "read" 279 | - For writes the filename contains: "WRITE" 280 | - For execute the filename contains: "DEP" 281 | - Determine if it's a non-canonical 64-bit address 282 | - For non-canonical addresses the filename contains: NONCANON 283 | - Otherwise determine if it's a NULL dereference (within 32 KiB +- of NULL) 284 | - Will put "null" in the filename 285 | - Otherwise it's considered a non-null deref and "HIGH" appears in the filename 286 | 287 | It's intended that more severe things are in all caps to give higher glance value of prioritizing which crash dumps to look into more. 288 | 289 | Example minidump filename for chrome: 290 | 291 | ``` 292 | crash_c0000005_chrome_child.dll+0x2c915c0_WRITE_null.dmp 293 | ``` 294 | 295 | ## Meso file format 296 | 297 | Coming soon (once it's stable) 298 | 299 | [meso]: assets/meso_bag.png 300 | [crash saving]: assets/crash_saving.png 301 | [code coverage]: assets/code_coverage.png 302 | [ida coloring]: assets/ida_coloring.png 303 | -------------------------------------------------------------------------------- /assets/code_coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamozolabs/mesos/a6ca31d7227eaf94fe03d75b065e1130ba87e221/assets/code_coverage.png -------------------------------------------------------------------------------- /assets/crash_saving.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamozolabs/mesos/a6ca31d7227eaf94fe03d75b065e1130ba87e221/assets/crash_saving.png -------------------------------------------------------------------------------- /assets/ida_coloring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamozolabs/mesos/a6ca31d7227eaf94fe03d75b065e1130ba87e221/assets/ida_coloring.png -------------------------------------------------------------------------------- /assets/meso_bag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamozolabs/mesos/a6ca31d7227eaf94fe03d75b065e1130ba87e221/assets/meso_bag.png -------------------------------------------------------------------------------- /coverage_scripts/ida.py: -------------------------------------------------------------------------------- 1 | import collections, re, math 2 | 3 | def addr2block(addr): 4 | f = idaapi.get_func(addr) 5 | if not f: 6 | print "No function at 0x%x" % (addr) 7 | return None 8 | 9 | fc = idaapi.FlowChart(f) 10 | 11 | for block in fc: 12 | if (block.startEA <= addr) and (block.endEA > addr): 13 | return (block.startEA, block.endEA) 14 | return None 15 | 16 | fft = re.compile("[0-9a-f]{16} \| Freq: +([0-9]+) \| +(.*?)\+0x([0-9a-f]+) \| (.*?)\n") 17 | 18 | image_base = idaapi.get_imagebase() 19 | ida_modname = GetInputFile().lower() 20 | 21 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 22 | 23 | inp = open(os.path.join(SCRIPT_DIR, "..", "coverage.txt"), "r").read() 24 | 25 | # Reset all coloring in all functions 26 | for funcea in Functions(): 27 | f = idaapi.get_func(funcea) 28 | fc = idaapi.FlowChart(f) 29 | 30 | for block in fc: 31 | ea = block.startEA 32 | while ea <= block.endEA: 33 | set_color(ea, CIC_ITEM, DEFCOLOR) 34 | ea = idc.NextHead(ea) 35 | 36 | freqs = collections.Counter() 37 | 38 | # Parse input coverage file 39 | for thing in fft.finditer(inp): 40 | freq = int(thing.group(1), 10) 41 | module = thing.group(2) 42 | offset = int(thing.group(3), 16) 43 | 44 | # Skip non-matching modules 45 | if module.lower() not in ida_modname: 46 | continue 47 | 48 | freqs[image_base + offset] = freq 49 | 50 | # Apply coloring 51 | for addr, freq in freqs.most_common()[::-1]: 52 | function_addr = get_func_attr(addr, FUNCATTR_START) 53 | func_entry_freq = freqs[function_addr] 54 | 55 | if func_entry_freq == 0: 56 | func_entry_freq = 1 57 | 58 | # Log value between [0.0, 1.0) 59 | dist = math.log(float(freq) / float(func_entry_freq) + 1.0) 60 | dist = min(dist, float(1.0)) 61 | 62 | color = 0x808080 + (int((1 - dist) * 100.0) << 8) + (int(dist * 100.0) << 0) 63 | print("%10d | 0x%.16x | %s" % (freq, addr, get_func_off_str(addr))) 64 | 65 | blockbounds = addr2block(addr) 66 | if blockbounds == None: 67 | # Color just the single PC, we don't know what block it belongs to 68 | set_color(addr, CIC_ITEM, color) 69 | else: 70 | # Color in the entire block 71 | (ea, block_end) = blockbounds 72 | while ea < block_end: 73 | set_color(ea, CIC_ITEM, color) 74 | ea = idc.NextHead(ea) 75 | 76 | set_cmt(addr, "Freq: %d | Func entry: %.2f" % \ 77 | (freq, float(freq) / float(func_entry_freq)), False) 78 | -------------------------------------------------------------------------------- /examples/benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | 4 | -------------------------------------------------------------------------------- /examples/benchmark/Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "benchmark" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "debugger 0.1.0", 6 | ] 7 | 8 | [[package]] 9 | name = "debugger" 10 | version = "0.1.0" 11 | dependencies = [ 12 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 13 | ] 14 | 15 | [[package]] 16 | name = "winapi" 17 | version = "0.3.6" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | dependencies = [ 20 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 21 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 22 | ] 23 | 24 | [[package]] 25 | name = "winapi-i686-pc-windows-gnu" 26 | version = "0.4.0" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | 29 | [[package]] 30 | name = "winapi-x86_64-pc-windows-gnu" 31 | version = "0.4.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | 34 | [metadata] 35 | "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" 36 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 37 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 38 | -------------------------------------------------------------------------------- /examples/benchmark/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "benchmark" 3 | version = "0.1.0" 4 | authors = ["Brandon Falk "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | debugger = { path = "../../libs/debugger" } 9 | 10 | [profile.release] 11 | debug = true 12 | -------------------------------------------------------------------------------- /examples/benchmark/fake_program/.gitignore: -------------------------------------------------------------------------------- 1 | *.obj 2 | -------------------------------------------------------------------------------- /examples/benchmark/fake_program/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | nasm -f win64 -o program.obj program.asm 3 | link /nologo /SUBSYSTEM:CONSOLE /OUT:program.exe program.obj 4 | 5 | -------------------------------------------------------------------------------- /examples/benchmark/fake_program/program.asm: -------------------------------------------------------------------------------- 1 | [bits 64] 2 | 3 | section .code 4 | 5 | global mainCRTStartup 6 | mainCRTStartup: 7 | ; This should be at program.exe+0x1000 8 | nop 9 | 10 | ; This should be at program.exe+0x1001 11 | jmp mainCRTStartup 12 | 13 | -------------------------------------------------------------------------------- /examples/benchmark/fake_program/program.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamozolabs/mesos/a6ca31d7227eaf94fe03d75b065e1130ba87e221/examples/benchmark/fake_program/program.exe -------------------------------------------------------------------------------- /examples/benchmark/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | use std::sync::Arc; 3 | use std::time::Instant; 4 | use debugger::{Debugger, BreakpointType}; 5 | 6 | const NUM_BREAKPOINTS_TO_HIT: u64 = 100000; 7 | 8 | /// Get elapsed time in seconds 9 | fn elapsed_from(start: &Instant) -> f64 { 10 | let dur = start.elapsed(); 11 | dur.as_secs() as f64 + dur.subsec_nanos() as f64 / 1_000_000_000.0 12 | } 13 | 14 | fn bp_handler(_dbg: &mut Debugger, _tid: u32, _rip: usize, freq: u64) -> bool { 15 | if freq == NUM_BREAKPOINTS_TO_HIT { 16 | return false; 17 | } 18 | 19 | true 20 | } 21 | 22 | fn benchmark_bp_creation() { 23 | const BREAKPOINTS_TO_APPLY: usize = 1000000; 24 | 25 | // Create new fake process to test performance on 26 | let mut process = Command::new("fake_program\\program.exe").spawn() 27 | .expect("Failed to run program"); 28 | 29 | // Attach to program process 30 | let mut dbg = Debugger::attach(process.id()); 31 | 32 | let modname = Arc::new(String::from("program.exe")); 33 | let name = Arc::new(String::from("wootboot")); 34 | 35 | // Register breakpoints 36 | let start = Instant::now(); 37 | for offset in 0..BREAKPOINTS_TO_APPLY { 38 | // These breakpoints are just bogus and don't matter 39 | let offset = offset + 0x10000; 40 | dbg.register_breakpoint(modname.clone(), offset, name.clone(), 41 | offset, BreakpointType::Single, None); 42 | } 43 | let elapsed = elapsed_from(&start); 44 | 45 | print!("Registered {:10} breakpoints in {:10.6} seconds | {:10.1} / second\n", 46 | BREAKPOINTS_TO_APPLY, elapsed, BREAKPOINTS_TO_APPLY as f64 / elapsed); 47 | 48 | // Kill process 49 | let _ = process.kill(); 50 | 51 | // Wait until process exits, which will require all breakpoints to be 52 | // applied 53 | let start = Instant::now(); 54 | dbg.run(); 55 | let elapsed = elapsed_from(&start); 56 | 57 | print!("Applied {:10} breakpoints in {:10.6} seconds | {:10.1} / second\n", 58 | BREAKPOINTS_TO_APPLY, elapsed, BREAKPOINTS_TO_APPLY as f64 / elapsed); 59 | 60 | // Drop the debugger, clearing breakpoints and detaching 61 | let start = Instant::now(); 62 | std::mem::drop(dbg); 63 | let elapsed = elapsed_from(&start); 64 | 65 | print!("Cleared {:10} breakpoints in {:10.6} seconds | {:10.1} / second\n", 66 | BREAKPOINTS_TO_APPLY, elapsed, BREAKPOINTS_TO_APPLY as f64 / elapsed); 67 | } 68 | 69 | fn benchmark_bp_hit() { 70 | // Create new fake process to test performance on 71 | let process = Command::new("fake_program\\program.exe").spawn() 72 | .expect("Failed to run program"); 73 | 74 | // Attach to program process 75 | let mut dbg = Debugger::attach(process.id()); 76 | 77 | let modname = Arc::new(String::from("program.exe")); 78 | let name = Arc::new(String::from("wootboot")); 79 | 80 | // Register a real breakpoint, this will get hit in a loop in the fake 81 | // program we made 82 | dbg.register_breakpoint(modname.clone(), 0x1000, 83 | name.clone(), 0, BreakpointType::Freq, Some(bp_handler)); 84 | 85 | // Run 86 | let start = Instant::now(); 87 | dbg.run(); 88 | let elapsed = elapsed_from(&start); 89 | 90 | print!("Hit {:10} breakpoints in {:10.6} seconds | {:10.1} / second\n", 91 | NUM_BREAKPOINTS_TO_HIT, elapsed, 92 | NUM_BREAKPOINTS_TO_HIT as f64 / elapsed); 93 | } 94 | 95 | fn main() { 96 | benchmark_bp_creation(); 97 | benchmark_bp_hit(); 98 | } 99 | -------------------------------------------------------------------------------- /generate_mesos.py: -------------------------------------------------------------------------------- 1 | import sys, subprocess, os, zipfile, threading, time, struct 2 | 3 | # Maximum number of processing threads to use 4 | # idat64.exe fails silently on OOMs and stuff so we just keep this reasonable 5 | MAX_JOBS = 4 6 | 7 | # Name of the input zip file created by `offline_meso.ps1` 8 | PREP_ZIP = "meso_deps.zip" 9 | 10 | # Name of IDA executable 11 | IDA_NAME = "idat64.exe" 12 | 13 | # Name of the folder to write the mesos to 14 | CACHE_FOLDER_NAME = "cache" 15 | 16 | def usage(): 17 | print("Usage:") 18 | print(" generate_mesos.py process_ida") 19 | print(" Processes all files in the meso_deps.zip file\n") 20 | 21 | print(" generate_mesos.py process_ida_whitelist ") 22 | print(" Processes files only containing one of the strings provided\n") 23 | 24 | print(" generate_mesos.py process_ida_blacklist ") 25 | print(" Processes files all files except for those containing one of the provided strings\n") 26 | 27 | print("Examples:\n") 28 | print(" python generate_mesos.py process_ida_whitelist system32") 29 | print(" Only processes files in `system32`\n") 30 | print(" python generate_mesos.py process_ida_blacklist ntdll.dll") 31 | print(" Process all files except for `ntdll.dll`\n") 32 | 33 | print("Path requirements for process_ida_*: must have `idat64.exe` in your PATH") 34 | quit() 35 | 36 | # Make sure ida is installed and working 37 | def ida_probe(): 38 | PROBENAME = os.path.join("mesogen_scripts", ".idaprobe") 39 | 40 | def ida_error(): 41 | # Validate it worked! 42 | assert os.path.exists(PROBENAME), \ 43 | "idat64.exe is not in your PATH or it's not functioning. \ 44 | Perhaps you need to accept a license agreement" 45 | 46 | # Remove probe file 47 | try: 48 | os.unlink(PROBENAME) 49 | except FileNotFoundError: 50 | pass 51 | 52 | # Invoke IDA to generate the idaprobe file 53 | try: 54 | subprocess.check_call([IDA_NAME, "-t", "-A", 55 | "-Smesogen_scripts/ida_detect.py"], shell=True) 56 | except: 57 | pass 58 | 59 | # Display error if file did not get created 60 | ida_error() 61 | 62 | # Remove file 63 | os.unlink(PROBENAME) 64 | 65 | def process_ida(orig_name, cache_fn, cache_fn_bin, contents): 66 | if not os.path.exists(cache_fn): 67 | # Make the hirearchy for the cache file 68 | try: 69 | os.makedirs(os.path.dirname(cache_fn)) 70 | except FileExistsError: 71 | pass 72 | 73 | # Save file to disk 74 | with open(cache_fn_bin, "wb") as fd: 75 | fd.write(contents) 76 | 77 | # Invoke IDA to generate the meso file 78 | subprocess.check_call([ 79 | IDA_NAME, "-o%s.idb" % cache_fn, "-A", 80 | "-Smesogen_scripts/ida.py cmdline \"%s\" \"%s\"" % \ 81 | (cache_fn, os.path.basename(orig_name)), 82 | "-c", cache_fn_bin], shell=True) 83 | 84 | def process(whitelist, blacklist): 85 | # Open the zip file generated by an offline meso script 86 | tf = zipfile.ZipFile(PREP_ZIP, "r") 87 | 88 | for member in tf.infolist(): 89 | # If there's no file size (it's a directory) skip it 90 | if member.file_size == 0: 91 | continue 92 | 93 | # Check if the blacklist excludes this file 94 | if blacklist is not None: 95 | in_blacklist = filter(lambda x: x in member.filename, blacklist) 96 | if len(list(in_blacklist)) > 0: 97 | continue 98 | 99 | # Check if the whitelist includes a file 100 | if whitelist is not None: 101 | in_whitelist = filter(lambda x: x in member.filename, whitelist) 102 | if len(list(in_whitelist)) == 0: 103 | continue 104 | 105 | print("Processing %s" % member.filename) 106 | 107 | # Read the file from the archive 108 | contents = None 109 | with tf.open(member, "r") as fd: 110 | contents = fd.read() 111 | 112 | # Parse out the TimeDateStamp and SizeOfImage from the PE header 113 | assert contents[:2] == b"MZ" 114 | pe_ptr = struct.unpack(" MAX_JOBS: 131 | time.sleep(0.1) 132 | 133 | # Create thread 134 | threading.Timer(0.0, process_ida, \ 135 | args=[image_name, cache_fn, cache_fn_bin, contents]) \ 136 | .start() 137 | 138 | # Wait for all jobs to finish 139 | while threading.active_count() > 1: 140 | time.sleep(0.1) 141 | 142 | # Check that IDA is installed and working 143 | ida_probe() 144 | 145 | args = sys.argv[1:] 146 | 147 | # Super secret options 148 | while len(args) >= 2: 149 | if args[0] == "--zipfile": 150 | # Use a custom zipfile as input 151 | PREP_ZIP = args[1] 152 | args = args[2:] 153 | continue 154 | elif args[0] == "--threads": 155 | # Change the number of worker jobs for meso generation 156 | MAX_JOBS = int(args[1]) 157 | args = args[2:] 158 | continue 159 | break 160 | 161 | if len(args) == 1 and args[0] == "process_ida": 162 | process(None, None) 163 | elif len(args) >= 2 and args[0] == "process_ida_whitelist": 164 | process(args[1:], None) 165 | elif len(args) >= 2 and args[0] == "process_ida_blacklist": 166 | process(None, args[1:]) 167 | else: 168 | usage() 169 | -------------------------------------------------------------------------------- /libs/debugger/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "debugger" 3 | version = "0.1.0" 4 | authors = ["Brandon Falk "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | winapi = { version = "0.3.5", features = ["debugapi", "winbase", "memoryapi", "processthreadsapi", "errhandlingapi", "handleapi", "securitybaseapi", "consoleapi", "winerror", "wow64apiset", "psapi"] } 9 | -------------------------------------------------------------------------------- /libs/debugger/src/debugger.rs: -------------------------------------------------------------------------------- 1 | /// High performance debugger for fuzzing and gathering code coverage on 2 | /// Windows 3 | 4 | use winapi::um::memoryapi::ReadProcessMemory; 5 | use winapi::um::memoryapi::WriteProcessMemory; 6 | use winapi::um::winnt::CONTEXT; 7 | use winapi::um::winnt::DBG_CONTINUE; 8 | use winapi::um::winnt::DBG_EXCEPTION_NOT_HANDLED; 9 | use winapi::um::winnt::CONTEXT_ALL; 10 | use winapi::um::winnt::EXCEPTION_RECORD; 11 | use winapi::um::errhandlingapi::GetLastError; 12 | use winapi::um::minwinbase::CREATE_PROCESS_DEBUG_EVENT; 13 | use winapi::um::minwinbase::CREATE_THREAD_DEBUG_EVENT; 14 | use winapi::um::minwinbase::EXCEPTION_DEBUG_EVENT; 15 | use winapi::um::minwinbase::LOAD_DLL_DEBUG_EVENT; 16 | use winapi::um::minwinbase::EXIT_THREAD_DEBUG_EVENT; 17 | use winapi::um::minwinbase::EXIT_PROCESS_DEBUG_EVENT; 18 | use winapi::um::minwinbase::UNLOAD_DLL_DEBUG_EVENT; 19 | use winapi::um::minwinbase::OUTPUT_DEBUG_STRING_EVENT; 20 | use winapi::um::minwinbase::RIP_EVENT; 21 | use winapi::shared::winerror::ERROR_SEM_TIMEOUT; 22 | use winapi::um::debugapi::WaitForDebugEvent; 23 | use winapi::um::debugapi::DebugActiveProcessStop; 24 | use winapi::um::debugapi::DebugActiveProcess; 25 | use winapi::um::debugapi::ContinueDebugEvent; 26 | use winapi::um::winbase::InitializeContext; 27 | use winapi::um::processthreadsapi::GetProcessId; 28 | use winapi::um::processthreadsapi::GetCurrentProcess; 29 | use winapi::um::processthreadsapi::SetThreadContext; 30 | use winapi::um::processthreadsapi::GetThreadContext; 31 | use winapi::um::processthreadsapi::FlushInstructionCache; 32 | use winapi::um::processthreadsapi::TerminateProcess; 33 | use winapi::um::processthreadsapi::OpenProcess; 34 | use winapi::um::processthreadsapi::CreateProcessA; 35 | use winapi::um::wow64apiset::IsWow64Process; 36 | use winapi::um::winnt::PROCESS_QUERY_LIMITED_INFORMATION; 37 | use winapi::um::psapi::GetMappedFileNameW; 38 | use winapi::um::winnt::HANDLE; 39 | use winapi::um::minwinbase::DEBUG_EVENT; 40 | use winapi::um::winbase::DEBUG_PROCESS; 41 | use winapi::um::winbase::DEBUG_ONLY_THIS_PROCESS; 42 | 43 | use std::time::{Duration, Instant}; 44 | use std::collections::{HashSet, HashMap}; 45 | use std::path::Path; 46 | use std::sync::Arc; 47 | use std::fs::File; 48 | use std::ffi::CString; 49 | use std::io::Write; 50 | use std::io::BufWriter; 51 | use std::sync::atomic::{AtomicBool, Ordering}; 52 | use winapi::um::consoleapi::SetConsoleCtrlHandler; 53 | 54 | 55 | use crate::minidump::dump; 56 | use crate::handles::Handle; 57 | 58 | /// Tracks if an exit has been requested via the Ctrl+C/Ctrl+Break handler 59 | static EXIT_REQUESTED: AtomicBool = AtomicBool::new(false); 60 | 61 | /// Function invoked on module loads 62 | /// (debugger, module filename, module base) 63 | type ModloadFunc = Box; 64 | 65 | /// Function invoked on debug events 66 | type DebugEventFunc = Box; 67 | 68 | /// Function invoked on breakpoints 69 | /// (debugger, tid, address of breakpoint, 70 | /// number of times breakpoint has been hit) 71 | /// If this returns false the debuggee is terminated 72 | type BreakpointCallback = fn(&mut Debugger, u32, usize, u64) -> bool; 73 | 74 | /// Ctrl+C handler so we can remove breakpoints and detach from the debugger 75 | unsafe extern "system" fn ctrl_c_handler(_ctrl_type: u32) -> i32 { 76 | // Store that an exit was requested 77 | EXIT_REQUESTED.store(true, Ordering::SeqCst); 78 | 79 | // Sleep forever 80 | loop { 81 | std::thread::sleep(Duration::from_secs(100)); 82 | } 83 | } 84 | 85 | /// Different types of breakpoints 86 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 87 | pub enum BreakpointType { 88 | /// Keep the breakpoint in place and keep track of how many times it was 89 | /// hit 90 | Freq, 91 | 92 | /// Delete the breakpoint after it has been hit once 93 | Single, 94 | } 95 | 96 | /// Structure to represent breakpoints 97 | #[derive(Clone)] 98 | pub struct Breakpoint { 99 | /// Offset from module base 100 | offset: usize, 101 | 102 | /// Tracks if this breakpoint is currently active 103 | enabled: bool, 104 | 105 | /// Original byte that was at this location, only set if breakpoint was 106 | /// ever applied 107 | orig_byte: Option, 108 | 109 | /// Tracks if this breakpoint should stick around after it's hit once 110 | typ: BreakpointType, 111 | 112 | /// Name of the function this breakpoint is in 113 | funcname: Arc, 114 | 115 | /// Offset into the function that this breakpoint addresses 116 | funcoff: usize, 117 | 118 | /// Module name 119 | modname: Arc, 120 | 121 | /// Callback to invoke if this breakpoint is hit 122 | callback: Option, 123 | 124 | /// Number of times this breakpoint has been hit 125 | freq: u64, 126 | } 127 | 128 | /// Debugger for a single process 129 | pub struct Debugger<'a> { 130 | /// List of breakpoints we want to apply, keyed by module 131 | /// This is not the _active_ list of breakpoints, it only refers to things 132 | /// we would like to apply if we see this module show up 133 | target_breakpoints: HashMap>, 134 | 135 | /// List of potentially active breakpoints, keyed by linear address 136 | /// They may be optionally disabled via `Breakpoint.enabled` 137 | breakpoints: HashMap, 138 | 139 | /// Tracks the minimum and maximum addresses for breakpoints per module 140 | minmax_breakpoint: HashMap, 141 | 142 | /// Handle to the process, given by the first create process event so it 143 | /// is not present until `run()` is used 144 | process_handle: Option, 145 | 146 | /// List of callbacks to invoke when a module is loaded 147 | module_load_callbacks: Option>, 148 | 149 | /// List of callbacks to invoke when a debug event is fired 150 | debug_event_callbacks: Option>, 151 | 152 | /// Thread ID to handle map 153 | thread_handles: HashMap, 154 | 155 | /// List of all PCs we hit during execution 156 | /// Keyed by PC 157 | /// Tuple is (module, offset, symbol+offset, frequency) 158 | coverage: HashMap, usize, String, u64)>, 159 | 160 | /// Set of DLL names and the corresponding DLL base 161 | modules: HashSet<(String, usize)>, 162 | 163 | /// TIDs actively single stepping mapped to the PC they stepped from 164 | single_step: HashMap, 165 | 166 | /// Always do frequency tracking. Disables printing to screen and updates 167 | /// the coverage database on an interval to decrease I/O 168 | always_freq: bool, 169 | 170 | /// Last time we saved the coverage database 171 | last_db_save: Instant, 172 | 173 | /// Prints some more status information during runtime 174 | verbose: bool, 175 | 176 | /// Process ID of the process we're debugging 177 | pid: u32, 178 | 179 | /// Time we attached to the target at 180 | start_time: Instant, 181 | 182 | /// Prints breakpoints as we hit them if set 183 | bp_print: bool, 184 | 185 | /// Tracks if we want to kill the debuggee 186 | kill_requested: bool, 187 | 188 | /// Pointer to aligned context structure 189 | context: &'a mut CONTEXT, 190 | _context_backing: Vec, 191 | } 192 | 193 | /// Get elapsed time in seconds 194 | fn elapsed_from(start: &Instant) -> f64 { 195 | let dur = start.elapsed(); 196 | dur.as_secs() as f64 + dur.subsec_nanos() as f64 / 1_000_000_000.0 197 | } 198 | 199 | // Mesos print with uptime prefix 200 | macro_rules! mprint { 201 | ($x:ident, $($arg:tt)*) => { 202 | print!("[{:14.6}] ", elapsed_from(&$x.start_time)); 203 | print!($($arg)*); 204 | } 205 | } 206 | 207 | impl<'a> Debugger<'a> { 208 | /// Create a new debugger and attach to `pid` 209 | pub fn attach(pid: u32) -> Debugger<'a> { 210 | Debugger::attach_internal(pid, false) 211 | } 212 | 213 | /// Create a new process argv[0], with arguments argv[1..] and attach to it 214 | pub fn spawn_proc(argv: &[String], follow_fork: bool) -> Debugger<'a> { 215 | let mut startup_info = unsafe { std::mem::zeroed() }; 216 | let mut proc_info = unsafe { std::mem::zeroed() }; 217 | 218 | let cmdline = CString::new(argv.join(" ")).unwrap(); 219 | 220 | let cmdline_ptr = cmdline.into_raw(); 221 | 222 | let flags = if follow_fork { 223 | DEBUG_PROCESS 224 | } 225 | else { 226 | DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS 227 | }; 228 | 229 | unsafe { 230 | assert!(CreateProcessA( 231 | std::ptr::null_mut(), // lpApplicationName 232 | cmdline_ptr, // lpCommandLine 233 | std::ptr::null_mut(), // lpProcessAttributes 234 | std::ptr::null_mut(), // lpThreadAttributes 235 | 0, // bInheritHandles 236 | flags, // dwCreationFlags 237 | std::ptr::null_mut(), // lpEnvironment 238 | std::ptr::null_mut(), // lpCurrentDirectory 239 | &mut startup_info, // lpStartupInfo 240 | &mut proc_info) != 0, // lpProcessInformation 241 | "Failed to create process."); 242 | } 243 | 244 | let pid = unsafe { GetProcessId(proc_info.hProcess) }; 245 | 246 | Debugger::attach_internal(pid, true) 247 | } 248 | 249 | /// Create a new debugger 250 | pub fn attach_internal(pid: u32, attached: bool) -> Debugger<'a> { 251 | // Save the start time 252 | let start_time = Instant::now(); 253 | 254 | // Enable ability to debug system services 255 | crate::sedebug::sedebug(); 256 | 257 | // Register ctrl-c handler 258 | unsafe { 259 | assert!(SetConsoleCtrlHandler(Some(ctrl_c_handler), 1) != 0, 260 | "SetConsoleCtrlHandler() failed"); 261 | } 262 | 263 | unsafe { 264 | let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid); 265 | assert!(handle != std::ptr::null_mut(), "OpenProcess() failed"); 266 | 267 | // Wrap up the handle for drop tracking 268 | let handle = Handle::new(handle).expect("Failed to get handle"); 269 | 270 | let mut cur_is_wow64 = 0i32; 271 | let mut target_is_wow64 = 0i32; 272 | 273 | // Query if the target process is 32-bit 274 | assert!(IsWow64Process(handle.raw(), &mut target_is_wow64) != 0, 275 | "IsWow64Process() failed"); 276 | 277 | // Query if our process is 32-bit 278 | assert!(IsWow64Process(GetCurrentProcess(), 279 | &mut cur_is_wow64) != 0, "IsWow64Process() failed"); 280 | 281 | print!("mesos is 64-bit: {}\n", cur_is_wow64 == 0); 282 | print!("target is 64-bit: {}\n", target_is_wow64 == 0); 283 | 284 | // Validate target process is the same bitness as we are 285 | assert!(cur_is_wow64 == target_is_wow64, 286 | "Target process does not match mesos bitness"); 287 | 288 | if !attached { 289 | // Attach to the target! 290 | assert!(DebugActiveProcess(pid) != 0, 291 | "Failed to attach to process, is your PID valid \ 292 | and do you have correct permissions?"); 293 | } 294 | } 295 | 296 | // Correctly initialize a context so it's aligned. We overcommit 297 | // with `buf` to give room for the CONTEXT to slide for alignment 298 | let mut cptr: *mut CONTEXT = std::ptr::null_mut(); 299 | let mut context_backing = 300 | vec![0u8; std::mem::size_of::() + 4096]; 301 | let mut clen: u32 = context_backing.len() as u32; 302 | 303 | // Use InitializeContext() to correct align the CONTEXT structure 304 | unsafe { 305 | assert!(InitializeContext(context_backing.as_mut_ptr() as *mut _, 306 | CONTEXT_ALL, &mut cptr, &mut clen) != 0, 307 | "InitializeContext() failed"); 308 | } 309 | 310 | // Construct the new Debugger object :) 311 | Debugger { 312 | target_breakpoints: HashMap::new(), 313 | breakpoints: HashMap::new(), 314 | process_handle: None, 315 | thread_handles: HashMap::new(), 316 | coverage: HashMap::new(), 317 | minmax_breakpoint: HashMap::new(), 318 | modules: HashSet::new(), 319 | single_step: HashMap::new(), 320 | module_load_callbacks: Some(Vec::new()), 321 | debug_event_callbacks: Some(Vec::new()), 322 | always_freq: false, 323 | kill_requested: false, 324 | last_db_save: Instant::now(), 325 | verbose: false, 326 | bp_print: false, 327 | pid, start_time, 328 | 329 | context: unsafe { &mut *cptr }, 330 | _context_backing: context_backing, 331 | } 332 | } 333 | 334 | /// Get a list of all thread IDs currently active on this process 335 | pub fn get_thread_list(&self) -> Vec { 336 | self.thread_handles.keys().cloned().collect() 337 | } 338 | 339 | /// Register a function to be invoked on module loads 340 | pub fn register_modload_callback(&mut self, func: ModloadFunc) { 341 | self.module_load_callbacks.as_mut() 342 | .expect("Cannot add callback during callback").push(func); 343 | } 344 | 345 | /// Register a function to be invoked on debug events 346 | pub fn register_debug_event_callback(&mut self, func: DebugEventFunc) { 347 | self.debug_event_callbacks.as_mut() 348 | .expect("Cannot add callback during callback").push(func); 349 | } 350 | 351 | /// Registers a breakpoint for a specific file 352 | /// `module` is the name of the module we want to apply the breakpoint to, 353 | /// for example "notepad.exe", `offset` is the byte offset in this module 354 | /// to apply the breakpoint to 355 | /// 356 | /// `name` and `nameoff` are completely user controlled and are used to 357 | /// give this breakpoint a unique name. Often if used from mesos `name` 358 | /// will correspond to the function name and `nameoff` will be the offset 359 | /// into the function. However these can be whatever you like. It's only 360 | /// for readability of the coverage data 361 | pub fn register_breakpoint(&mut self, module: Arc, offset: usize, 362 | name: Arc, nameoff: usize, typ: BreakpointType, 363 | callback: Option) { 364 | // Create a new entry if none exists 365 | if !self.target_breakpoints.contains_key(&**module) { 366 | self.target_breakpoints.insert(module.to_string(), Vec::new()); 367 | } 368 | 369 | if !self.minmax_breakpoint.contains_key(&**module) { 370 | self.minmax_breakpoint.insert(module.to_string(), (!0, 0)); 371 | } 372 | 373 | let mmbp = self.minmax_breakpoint.get_mut(&**module).unwrap(); 374 | mmbp.0 = std::cmp::min(mmbp.0, offset as usize); 375 | mmbp.1 = std::cmp::max(mmbp.1, offset as usize); 376 | 377 | // Append this breakpoint 378 | self.target_breakpoints.get_mut(&**module).unwrap().push( 379 | Breakpoint { 380 | offset: offset as usize, 381 | enabled: false, 382 | typ: typ, 383 | orig_byte: None, 384 | funcname: name.clone(), 385 | funcoff: nameoff, 386 | modname: module.clone(), 387 | freq: 0, 388 | callback, 389 | } 390 | ); 391 | } 392 | 393 | /// Gets a raw `HANDLE` to the process we are attached to 394 | fn process_handle(&self) -> HANDLE { 395 | self.process_handle.expect("No process handle present") 396 | } 397 | 398 | pub fn set_always_freq(&mut self, val: bool) { self.always_freq = val; } 399 | pub fn set_verbose(&mut self, val: bool) { self.verbose = val; } 400 | pub fn set_bp_print(&mut self, val: bool) { self.bp_print = val; } 401 | 402 | /// Resolves the file name of a given memory mapped file in the target 403 | /// process 404 | fn filename_from_module_base(&self, base: usize) -> String { 405 | // Use GetMappedFileNameW() to get the mapped file name 406 | let mut buf = [0u16; 4096]; 407 | let fnlen = unsafe { 408 | GetMappedFileNameW(self.process_handle(), 409 | base as *mut _, buf.as_mut_ptr(), buf.len() as u32) 410 | }; 411 | assert!(fnlen != 0 && (fnlen as usize) < buf.len(), 412 | "GetMappedFileNameW() failed"); 413 | 414 | // Convert the name to utf-8 and lowercase it 415 | let path = String::from_utf16(&buf[..fnlen as usize]).unwrap() 416 | .to_lowercase(); 417 | 418 | // Get the filename from the path 419 | Path::new(&path).file_name().unwrap().to_str().unwrap().into() 420 | } 421 | 422 | /// Reads from `addr` in the process we're debugging into `buf` 423 | /// Returns number of bytes read 424 | pub fn read_mem(&self, addr: usize, buf: &mut [u8]) -> usize { 425 | let mut offset = 0; 426 | 427 | // Read until complete 428 | while offset < buf.len() { 429 | let mut bread = 0; 430 | 431 | unsafe { 432 | // Issue a read 433 | if ReadProcessMemory( 434 | self.process_handle(), (addr + offset) as *mut _, 435 | buf.as_mut_ptr().offset(offset as isize) as *mut _, 436 | buf.len() - offset, &mut bread) == 0 { 437 | // Return out on error 438 | return offset; 439 | } 440 | assert!(bread > 0); 441 | } 442 | 443 | offset += bread; 444 | } 445 | 446 | offset 447 | } 448 | 449 | /// Writes `buf` to `addr` in the process we're debugging 450 | /// Returns number of bytes written 451 | pub fn write_mem(&self, addr: usize, buf: &[u8]) -> usize { 452 | let mut offset = 0; 453 | 454 | // Write until complete 455 | while offset < buf.len() { 456 | let mut bread = 0; 457 | 458 | unsafe { 459 | // Issue a write 460 | if WriteProcessMemory( 461 | self.process_handle(), (addr + offset) as *mut _, 462 | buf.as_ptr().offset(offset as isize) as *const _, 463 | buf.len() - offset, &mut bread) == 0 { 464 | // Return out on error 465 | return offset; 466 | } 467 | assert!(bread > 0); 468 | } 469 | 470 | offset += bread; 471 | } 472 | 473 | offset 474 | } 475 | 476 | /// Flush all instruction caches in the target process 477 | fn flush_instruction_caches(&self) { 478 | unsafe { 479 | // Flush all instruction caches for the process 480 | assert!(FlushInstructionCache( 481 | self.process_handle(), std::ptr::null(), 0) != 0); 482 | } 483 | } 484 | 485 | /// Add the module loaded at `base` in the target process to our module 486 | /// list 487 | fn register_module(&mut self, base: usize) { 488 | let filename = self.filename_from_module_base(base); 489 | 490 | // Insert into the module list 491 | self.modules.insert((filename.into(), base)); 492 | } 493 | 494 | /// Remove the module loaded at `base` in the target process from our 495 | /// module list 496 | fn unregister_module(&mut self, base: usize) { 497 | let mut to_remove = None; 498 | 499 | // Find the corresponding module to this base 500 | for module in self.modules.iter() { 501 | if module.1 == base { 502 | to_remove = Some(module.clone()); 503 | } 504 | } 505 | 506 | if let Some(to_remove) = to_remove { 507 | if self.minmax_breakpoint.contains_key(&to_remove.0) { 508 | // If there are breakpoints in this module, unregister those too 509 | 510 | // Get minimum and maximum offsets into the module where 511 | // breakpoints are applied 512 | let minmax = self.minmax_breakpoint[&to_remove.0]; 513 | 514 | let start_addr = base + minmax.0; 515 | let end_addr = base + minmax.1; 516 | 517 | // Remove any breakpoints which are present in this range 518 | self.breakpoints.retain(|&k, _| { 519 | k < start_addr || k > end_addr 520 | }); 521 | } 522 | 523 | // Remove the module and breakpoint info for the module 524 | self.modules.remove(&to_remove); 525 | } else { 526 | // Got unregister module for unknown DLL 527 | // Our database is out of sync with reality 528 | panic!("Unexpected DLL unload of base 0x{:x}\n", base); 529 | } 530 | } 531 | 532 | /// Given a `base` of a module in the target process, identify the module 533 | /// and attempt to apply breakpoints to it if we have any scheduled for 534 | /// this module 535 | fn apply_breakpoints(&mut self, base: usize) { 536 | let filename = self.filename_from_module_base(base); 537 | 538 | // Bail if we don't have requested breakpoints for this module 539 | if !self.minmax_breakpoint.contains_key(&filename) { 540 | return; 541 | } 542 | 543 | // Save number of breakpoints at this function start 544 | let startbps = self.breakpoints.len(); 545 | 546 | let mut minmax = self.minmax_breakpoint[&filename]; 547 | 548 | // Convert this to a non-inclusive upper bound 549 | minmax.1 = minmax.1.checked_add(1).unwrap(); 550 | 551 | // Compute the size of all memory between the minimum and maximum 552 | // breakpoint offsets 553 | let region_size = minmax.1.checked_sub(minmax.0).unwrap() as usize; 554 | 555 | let mut contents = vec![0u8; region_size]; 556 | 557 | // Read all memory of this DLL that includes breakpoints 558 | // On partial reads that's fine, we'll only do a partial write later. 559 | let bread = self.read_mem(base + minmax.0, &mut contents); 560 | 561 | // Attempt to apply all requested breakpoints 562 | for breakpoint in self.target_breakpoints.get(&filename).unwrap() { 563 | let mut bp = breakpoint.clone(); 564 | bp.enabled = true; 565 | 566 | let bufoff = 567 | (breakpoint.offset as usize) 568 | .checked_sub(minmax.0 as usize).unwrap(); 569 | 570 | // Save the original byte 571 | bp.orig_byte = Some(contents[bufoff]); 572 | 573 | // Add in a breakpoint 574 | contents[bufoff] = 0xcc; 575 | 576 | if !self.breakpoints.contains_key(&(base + breakpoint.offset)) { 577 | self.breakpoints.insert(base + breakpoint.offset, bp); 578 | } else { 579 | // Silently ignore duplicate breakpoints 580 | } 581 | } 582 | 583 | // Write in all the breakpoints 584 | // If it partially writes then that's fine, we just apply the 585 | // breakpoints we can 586 | let _ = self.write_mem(base + minmax.0, &contents[..bread]); 587 | self.flush_instruction_caches(); 588 | 589 | mprint!(self, "Applied {:10} breakpoints ({:10} total breakpoints) {}\n", 590 | self.breakpoints.len() - startbps, 591 | self.breakpoints.len(), filename); 592 | 593 | return; 594 | } 595 | 596 | /// Remove all breakpoints, restoring the process to a clean state 597 | fn remove_breakpoints(&mut self) { 598 | for (module, base) in self.modules.iter() { 599 | if !self.minmax_breakpoint.contains_key(module) { 600 | // Ignore modules we have no applied breakpoints for 601 | continue; 602 | } 603 | 604 | // Get minimum and maximum offsets into the module where 605 | // breakpoints are applied 606 | let minmax = self.minmax_breakpoint[module]; 607 | 608 | // Compute the size of all memory between the minimum and maximum 609 | // breakpoint offsets 610 | let region_size = minmax.1.checked_add(1).unwrap() 611 | .checked_sub(minmax.0).unwrap() as usize; 612 | 613 | let mut contents = vec![0u8; region_size]; 614 | 615 | // Read all memory of this DLL that includes breakpoints 616 | // On partial reads that's fine, we'll only do a partial write 617 | // later. 618 | let bread = self.read_mem(base + minmax.0, &mut contents); 619 | 620 | let mut removed_bps = 0u64; 621 | 622 | // Restore all bytes that we applied breakpoints to 623 | for (_, bp) in self.breakpoints.iter_mut() { 624 | // Skip breakpoints not in this module 625 | if &*bp.modname != module { 626 | continue; 627 | } 628 | 629 | let bufoff = 630 | (bp.offset as usize) 631 | .checked_sub(minmax.0 as usize).unwrap(); 632 | 633 | // Restore original byte 634 | if let Some(byte) = bp.orig_byte { 635 | contents[bufoff] = byte; 636 | bp.enabled = false; 637 | removed_bps += 1; 638 | } 639 | } 640 | 641 | // Remove all the breakpoints 642 | // On partial removes it's fine. If we can't write to the memory 643 | // it's not mapped in, so we can just ignore partial writes here. 644 | let _ = self.write_mem(base + minmax.0, &contents[..bread]); 645 | self.flush_instruction_caches(); 646 | 647 | mprint!(self, "Removed {} breakpoints in {}\n", removed_bps, module); 648 | } 649 | 650 | // Sanity check that all breakpoints have been removed 651 | for (_, bp) in self.breakpoints.iter() { 652 | assert!(!bp.enabled, 653 | "Unexpected breakpoint left enabled \ 654 | after remove_breakpoints()") 655 | } 656 | } 657 | 658 | /// Handle a breakpoint 659 | fn handle_breakpoint(&mut self, tid: u32, addr: usize) -> bool { 660 | if let Some(ref mut bp) = self.breakpoints.get_mut(&addr).cloned() { 661 | // If we apply a breakpoint over an actual breakpoint just exit 662 | // out now because we don't want to handle it 663 | if bp.orig_byte == Some(0xcc) { 664 | return true; 665 | } 666 | 667 | // Update breakpoint hit frequency 668 | self.breakpoints.get_mut(&addr).unwrap().freq += 1; 669 | 670 | let mut orig_byte = [0u8; 1]; 671 | orig_byte[0] = bp.orig_byte.unwrap(); 672 | 673 | // Restore original byte 674 | assert!(self.write_mem(addr, &orig_byte) == 1); 675 | self.flush_instruction_caches(); 676 | 677 | // Create a new coverage record if one does not exist 678 | if !self.coverage.contains_key(&addr) { 679 | let funcoff = format!("{}+0x{:x}", bp.funcname, bp.funcoff); 680 | 681 | self.coverage.insert(addr, 682 | (bp.modname.clone(), bp.offset, funcoff.clone(), 0)); 683 | } 684 | 685 | // Update coverage frequencies 686 | let freq = { 687 | let bin = self.coverage.get_mut(&addr).unwrap(); 688 | bin.3 += 1; 689 | bin.3 690 | }; 691 | 692 | // Print coverage as we get it 693 | if self.bp_print { 694 | let funcoff = format!("{}+0x{:x}", bp.funcname, bp.funcoff); 695 | mprint!(self, "{:8} of {:8} hit | {:10} freq | 0x{:x} | \ 696 | {:>20}+0x{:08x} | {}\n", 697 | self.coverage.len(), self.breakpoints.len(), 698 | freq, 699 | addr, bp.modname, bp.offset, funcoff); 700 | } 701 | 702 | self.get_context(tid); 703 | 704 | // Back up so we re-execute where the breakpoint was 705 | 706 | #[cfg(target_pointer_width = "64")] 707 | { self.context.Rip = addr as u64; } 708 | 709 | #[cfg(target_pointer_width = "32")] 710 | { self.context.Eip = addr as u32; } 711 | 712 | // Single step if this is a frequency instruction 713 | if self.always_freq || bp.typ == BreakpointType::Freq { 714 | // Set the trap flag 715 | self.context.EFlags |= 1 << 8; 716 | self.single_step.insert(tid, addr); 717 | } else { 718 | // Breakpoint no longer enabled 719 | self.breakpoints.get_mut(&addr).unwrap().enabled = false; 720 | } 721 | 722 | self.set_context(tid); 723 | 724 | // Call the breakpoint callback if needed 725 | if let Some(callback) = bp.callback { 726 | if callback(self, tid, addr, bp.freq) == false { 727 | self.kill_requested = true; 728 | } 729 | } 730 | } else { 731 | // Hit unexpected breakpoint 732 | return false; 733 | } 734 | 735 | true 736 | } 737 | 738 | /// Gets a mutable reference to the internal context structure 739 | pub fn context(&mut self) -> &mut CONTEXT { 740 | &mut self.context 741 | } 742 | 743 | /// Loads `tid`'s context into the internal context structure 744 | pub fn get_context(&mut self, tid: u32) { 745 | unsafe { 746 | assert!(GetThreadContext( 747 | self.thread_handles[&tid], self.context) != 0); 748 | } 749 | } 750 | 751 | /// Sets `tid`'s context from the internal context structure 752 | pub fn set_context(&mut self, tid: u32) { 753 | unsafe { 754 | assert!(SetThreadContext( 755 | self.thread_handles[&tid], self.context) != 0); 756 | } 757 | } 758 | 759 | /// Get a filename to describe a given crash 760 | fn get_crash_filename(&self, context: &CONTEXT, 761 | exception: &EXCEPTION_RECORD) -> String { 762 | let pc = { 763 | #[cfg(target_pointer_width = "64")] 764 | { context.Rip as usize } 765 | 766 | #[cfg(target_pointer_width = "32")] 767 | { context.Eip as usize } 768 | }; 769 | 770 | // Search for the nearest module 771 | let mut nearest_module: Option<(&str, usize)> = None; 772 | for (module, base) in self.modules.iter() { 773 | if let Some(offset) = pc.checked_sub(*base) { 774 | if nearest_module.is_none() || 775 | nearest_module.unwrap().1 > offset { 776 | nearest_module = Some((module, offset)); 777 | } 778 | } 779 | } 780 | 781 | let code = exception.ExceptionCode; 782 | 783 | // Filename starts with exception code 784 | let mut filename = format!("crash_{:08x}_", code); 785 | 786 | // Filename then contains the module+offset, or if no suitable module 787 | // is detected then it just contains the absolute PC address 788 | if let Some((module, offset)) = nearest_module { 789 | filename += &format!("{}+0x{:x}", module, offset); 790 | } else { 791 | filename += &format!("0x{:x}", pc); 792 | } 793 | 794 | // If the crash is an access violation we also have the type of fault 795 | // (read or write) and whether it's a null deref, non-canon, or 796 | // other 797 | if code == 0xc0000005 { 798 | // This should never happen 799 | assert!(exception.NumberParameters == 2, 800 | "Invalid c0000005 parameters"); 801 | 802 | // Classify the type of exception 803 | if exception.ExceptionInformation[0] == 0 { 804 | filename += "_read"; 805 | } else if exception.ExceptionInformation[0] == 1 { 806 | filename += "_WRITE"; 807 | } else if exception.ExceptionInformation[0] == 8 { 808 | // DEP violation 809 | filename += "_DEP"; 810 | } 811 | 812 | let fault_addr = exception.ExceptionInformation[1] as u64; 813 | 814 | let noncanon_bits = fault_addr & 0xffff_0000_0000_0000; 815 | 816 | if noncanon_bits != 0 && noncanon_bits != 0xffff_0000_0000_0000 { 817 | // Address is non-canon, can only happen on 64-bits and is 818 | // typically a _really_ bad sign (fully controlled address) 819 | filename += "_NONCANON"; 820 | } else if (fault_addr as i64).abs() < 32 * 1024 { 821 | // Near-null, thus we consider it to be a null deref 822 | filename += "_null"; 823 | } else { 824 | // Address is canon, but also not null, seems bad 825 | filename += "_HIGH"; 826 | } 827 | } 828 | 829 | // All files are .dmp 830 | filename += ".dmp"; 831 | 832 | filename 833 | } 834 | 835 | /// Sync the coverage database to disk 836 | fn flush_coverage_database(&mut self) { 837 | mprint!(self, "Syncing code coverage database...\n"); 838 | 839 | let mut fd = BufWriter::with_capacity( 840 | 2 * 1024 * 1024, 841 | File::create("coverage.txt") 842 | .expect("Failed to open freq coverage file")); 843 | 844 | for (pc, (module, offset, symoff, freq)) in self.coverage.iter() { 845 | write!(fd, 846 | "{:016x} | Freq: {:10} | \ 847 | {:>20}+0x{:08x} | {}\n", 848 | pc, freq, module, offset, symoff) 849 | .expect("Failed to write coverage info"); 850 | } 851 | 852 | mprint!(self, "Sync complete ({} total unique coverage entries)\n", 853 | self.coverage.len()); 854 | } 855 | 856 | /// Run the process forever 857 | pub fn run(&mut self) -> i32 { 858 | let mut event = unsafe { std::mem::zeroed() }; 859 | 860 | let mut hit_initial_break = false; 861 | 862 | unsafe { loop { 863 | // Flush the coverage database on an intervals 864 | if Instant::now().duration_since(self.last_db_save) >= 865 | Duration::from_secs(5) { 866 | self.flush_coverage_database(); 867 | self.last_db_save = Instant::now(); 868 | } 869 | 870 | if self.kill_requested { 871 | assert!( 872 | TerminateProcess(self.process_handle.unwrap(), 123456) != 0, 873 | "Failed to kill target process"); 874 | self.kill_requested = false; 875 | } 876 | 877 | // Check if it's requested that we exit 878 | if EXIT_REQUESTED.load(Ordering::SeqCst) { 879 | // Exit out of the run loop 880 | return 0; 881 | } 882 | 883 | // Wait for a debug event :) 884 | let der = WaitForDebugEvent(&mut event, 10); 885 | if der == 0 { 886 | if GetLastError() == ERROR_SEM_TIMEOUT { 887 | // Just drop timeouts 888 | continue; 889 | } 890 | 891 | panic!("WaitForDebugEvent() returned error : {}", 892 | GetLastError()); 893 | } 894 | 895 | let decs = self.debug_event_callbacks.take() 896 | .expect("Event without callbacks present"); 897 | // Invoke callbacks 898 | for de in decs.iter() { 899 | de(self, &event); 900 | } 901 | self.debug_event_callbacks = Some(decs); 902 | 903 | // Get the PID and TID for the event 904 | let pid = event.dwProcessId; 905 | let tid = event.dwThreadId; 906 | 907 | match event.dwDebugEventCode { 908 | CREATE_PROCESS_DEBUG_EVENT => { 909 | // A new process was created under our debugger 910 | let create_process = event.u.CreateProcessInfo(); 911 | 912 | // Wrap up the hFile handle. We don't use it but this will 913 | // cause it to get dropped automatically for us 914 | let _ = Handle::new(create_process.hFile); 915 | 916 | // Make sure the hProcess and hThread are valid 917 | assert!( 918 | create_process.hProcess != std::ptr::null_mut() && 919 | create_process.hThread != std::ptr::null_mut(), 920 | "Passed null hProcess or hThread on create process \ 921 | event"); 922 | 923 | // Register this process and thread handles. Note we don't 924 | // wrap these in a `Handle`, that's because they are not 925 | // supposed to be closed by us. 926 | self.process_handle = Some(create_process.hProcess); 927 | self.thread_handles.insert(tid, create_process.hThread); 928 | 929 | let base = create_process.lpBaseOfImage as usize; 930 | 931 | // Register this module in our module list 932 | self.register_module(base); 933 | 934 | let mlcs = self.module_load_callbacks.take() 935 | .expect("Event without callbacks present"); 936 | 937 | // Invoke callbacks 938 | for mlc in mlcs.iter() { 939 | mlc(self, &self.filename_from_module_base(base), base); 940 | } 941 | 942 | self.module_load_callbacks = Some(mlcs); 943 | 944 | // Apply any pending breakpoints! 945 | self.apply_breakpoints(base); 946 | } 947 | CREATE_THREAD_DEBUG_EVENT => { 948 | // A thread was created in the target 949 | let create_thread = event.u.CreateThread(); 950 | 951 | // Insert this thread handle into our list of threads 952 | // We don't wrap this HANDLE in a `Handle` as we're not 953 | // supposed to call CloseHandle() on it according to the 954 | // API 955 | self.thread_handles.insert(tid, create_thread.hThread); 956 | } 957 | EXCEPTION_DEBUG_EVENT => { 958 | // An exception occurred in the target 959 | let exception = event.u.Exception_mut(); 960 | 961 | if exception.ExceptionRecord.ExceptionCode == 0x80000003 { 962 | // Exception was a breakpoint 963 | 964 | if !hit_initial_break { 965 | // If we're expecting an initial breakpoint, just 966 | // handle this exception 967 | hit_initial_break = true; 968 | assert!(ContinueDebugEvent(pid, tid, 969 | DBG_CONTINUE) != 0); 970 | continue; 971 | } 972 | 973 | // Attempt to handle the breakpoint based on our own 974 | // breakpoints we have applied to the target 975 | if !self.handle_breakpoint(tid, 976 | exception.ExceptionRecord 977 | .ExceptionAddress as usize) { 978 | mprint!(self, 979 | "Warning: Continuing unexpected 0x80000003\n"); 980 | } 981 | } else { 982 | if exception.ExceptionRecord.ExceptionCode == 983 | 0xc0000005 { 984 | // Target had an access violation 985 | 986 | self.get_context(tid); 987 | 988 | // Compute a filename for this crash 989 | let filename = self.get_crash_filename( 990 | &self.context, &mut exception.ExceptionRecord); 991 | 992 | mprint!(self, "Got crash: {}\n", filename); 993 | 994 | if !Path::new(&filename).is_file() { 995 | // Remove all breakpoints in the program 996 | // before minidumping 997 | self.remove_breakpoints(); 998 | 999 | // Take a full minidump of the process 1000 | dump(&filename, pid, tid, 1001 | self.process_handle.unwrap(), 1002 | &mut exception.ExceptionRecord, 1003 | &mut self.context); 1004 | } 1005 | 1006 | // Exit out 1007 | return 1008 | exception.ExceptionRecord.ExceptionCode as i32; 1009 | } else if exception.ExceptionRecord 1010 | .ExceptionCode == 0x80000004 { 1011 | // Single step exception 1012 | 1013 | // Check if we're expecting a single step on this 1014 | // thread. 1015 | if let Some(&pc) = self.single_step.get(&tid) { 1016 | // Disable trap flag 1017 | self.get_context(tid); 1018 | self.context.EFlags &= !(1 << 8); 1019 | self.set_context(tid); 1020 | 1021 | // Write breakpoint back in 1022 | assert!(self.write_mem(pc, b"\xcc") == 1); 1023 | self.flush_instruction_caches(); 1024 | 1025 | // Remove that we're single stepping this TID 1026 | self.single_step.remove(&tid); 1027 | } else { 1028 | // Uh oh, we didn't expect that 1029 | mprint!(self, "Unexpected single step, \ 1030 | continuing\n"); 1031 | } 1032 | 1033 | assert!(ContinueDebugEvent( 1034 | pid, tid, DBG_CONTINUE) != 0); 1035 | continue; 1036 | } else { 1037 | // Unhandled exception, pass it through as unhandled 1038 | print!("Unhandled exception {:x}\n", 1039 | exception.ExceptionRecord.ExceptionCode); 1040 | 1041 | assert!(ContinueDebugEvent( 1042 | pid, tid, DBG_EXCEPTION_NOT_HANDLED) != 0); 1043 | continue; 1044 | } 1045 | } 1046 | } 1047 | LOAD_DLL_DEBUG_EVENT => { 1048 | // A module was loaded in the target 1049 | let load_dll = event.u.LoadDll(); 1050 | 1051 | // Wrap up the handle so it gets dropped 1052 | let _ = Handle::new(load_dll.hFile); 1053 | 1054 | let base = load_dll.lpBaseOfDll as usize; 1055 | 1056 | // Register the module, attempt to load mesos, and apply 1057 | // all pending breakpoints 1058 | self.register_module(base); 1059 | 1060 | let mlcs = self.module_load_callbacks.take() 1061 | .expect("Event without callbacks present"); 1062 | 1063 | // Invoke callbacks 1064 | for mlc in mlcs.iter() { 1065 | mlc(self, &self.filename_from_module_base(base), base); 1066 | } 1067 | 1068 | self.module_load_callbacks = Some(mlcs); 1069 | 1070 | self.apply_breakpoints(base); 1071 | } 1072 | EXIT_THREAD_DEBUG_EVENT => { 1073 | // Remove the thread handle for this thread 1074 | assert!(self.thread_handles.remove(&tid).is_some(), 1075 | "Got exit thread event for nonexistant thread"); 1076 | } 1077 | EXIT_PROCESS_DEBUG_EVENT => { 1078 | // Target exited 1079 | mprint!(self, "Process exited, qutting!\n"); 1080 | return 0; 1081 | } 1082 | UNLOAD_DLL_DEBUG_EVENT => { 1083 | // Dll was unloaded in the target, unload it 1084 | let unload_dll = event.u.UnloadDll(); 1085 | self.unregister_module(unload_dll.lpBaseOfDll as usize); 1086 | } 1087 | OUTPUT_DEBUG_STRING_EVENT => { 1088 | // Target attempted to print a debug string, just ignore 1089 | // it 1090 | } 1091 | RIP_EVENT => { 1092 | } 1093 | _ => panic!("Unsupported event"), 1094 | } 1095 | 1096 | assert!(ContinueDebugEvent( 1097 | pid, tid, DBG_CONTINUE) != 0); 1098 | }} 1099 | } 1100 | } 1101 | 1102 | impl<'a> Drop for Debugger<'a> { 1103 | fn drop(&mut self) { 1104 | // Remove all breakpoints 1105 | self.remove_breakpoints(); 1106 | 1107 | // Flush coverage database one last time 1108 | self.flush_coverage_database(); 1109 | 1110 | // Detach from the process 1111 | unsafe { 1112 | assert!(DebugActiveProcessStop(self.pid) != 0, 1113 | "DebugActiveProcessStop() failed"); 1114 | } 1115 | 1116 | // All done, process is safely restored 1117 | print!("Detached from process {}\n", self.pid); 1118 | } 1119 | } 1120 | -------------------------------------------------------------------------------- /libs/debugger/src/ffi_helpers.rs: -------------------------------------------------------------------------------- 1 | /// Misc functions that help during various FFI activities 2 | 3 | use std::ffi::OsStr; 4 | use std::os::windows::ffi::OsStrExt; 5 | use std::iter::once; 6 | 7 | /// Convert rust string to null-terminated UTF-16 Windows API string 8 | pub fn win32_string(value: &str) -> Vec { 9 | OsStr::new(value).encode_wide().chain(once(0)).collect() 10 | } 11 | -------------------------------------------------------------------------------- /libs/debugger/src/handles.rs: -------------------------------------------------------------------------------- 1 | /// Crate to provide a Drop wrapper for HANDLEs 2 | 3 | use winapi::um::winnt::HANDLE; 4 | use winapi::um::handleapi::CloseHandle; 5 | 6 | /// Wrapper on a HANDLE to provide Drop support to clean up handles 7 | pub struct Handle(HANDLE); 8 | 9 | impl Handle { 10 | /// Wrap up a HANDLE 11 | pub fn new(handle: HANDLE) -> Option { 12 | // Return None if the handle is null 13 | if handle == std::ptr::null_mut() { return None; } 14 | 15 | Some(Handle(handle)) 16 | } 17 | 18 | /// Gets the raw HANDLE value this `Handle` represents 19 | pub fn raw(&self) -> HANDLE { 20 | self.0 21 | } 22 | } 23 | 24 | impl Drop for Handle { 25 | fn drop(&mut self) { 26 | unsafe { 27 | // Close that handle! 28 | assert!(CloseHandle(self.0) != 0, "Failed to drop HANDLE"); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /libs/debugger/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod debugger; 2 | mod minidump; 3 | mod sedebug; 4 | mod ffi_helpers; 5 | mod handles; 6 | 7 | // Make some things public 8 | pub use debugger::{Debugger, BreakpointType}; 9 | -------------------------------------------------------------------------------- /libs/debugger/src/minidump.rs: -------------------------------------------------------------------------------- 1 | /// Module containing utilities to create full minidumps of processes 2 | 3 | use winapi::um::winnt::EXCEPTION_POINTERS; 4 | use winapi::um::winnt::GENERIC_READ; 5 | use winapi::um::winnt::GENERIC_WRITE; 6 | use winapi::um::winnt::EXCEPTION_RECORD; 7 | use winapi::um::fileapi::CREATE_NEW; 8 | use winapi::um::winnt::HANDLE; 9 | use winapi::um::fileapi::CreateFileW; 10 | use winapi::um::handleapi::INVALID_HANDLE_VALUE; 11 | use winapi::um::errhandlingapi::GetLastError; 12 | use winapi::um::winnt::CONTEXT; 13 | 14 | use std::path::Path; 15 | use crate::handles::Handle; 16 | use crate::ffi_helpers::win32_string; 17 | 18 | #[repr(C)] 19 | #[allow(dead_code)] 20 | pub enum MinidumpType { 21 | MiniDumpNormal = 0x00000000, 22 | MiniDumpWithDataSegs = 0x00000001, 23 | MiniDumpWithFullMemory = 0x00000002, 24 | MiniDumpWithHandleData = 0x00000004, 25 | MiniDumpFilterMemory = 0x00000008, 26 | MiniDumpScanMemory = 0x00000010, 27 | MiniDumpWithUnloadedModules = 0x00000020, 28 | MiniDumpWithIndirectlyReferencedMemory = 0x00000040, 29 | MiniDumpFilterModulePaths = 0x00000080, 30 | MiniDumpWithProcessThreadData = 0x00000100, 31 | MiniDumpWithPrivateReadWriteMemory = 0x00000200, 32 | MiniDumpWithoutOptionalData = 0x00000400, 33 | MiniDumpWithFullMemoryInfo = 0x00000800, 34 | MiniDumpWithThreadInfo = 0x00001000, 35 | MiniDumpWithCodeSegs = 0x00002000, 36 | MiniDumpWithoutAuxiliaryState = 0x00004000, 37 | MiniDumpWithFullAuxiliaryState = 0x00008000, 38 | MiniDumpWithPrivateWriteCopyMemory = 0x00010000, 39 | MiniDumpIgnoreInaccessibleMemory = 0x00020000, 40 | MiniDumpWithTokenInformation = 0x00040000, 41 | MiniDumpWithModuleHeaders = 0x00080000, 42 | MiniDumpFilterTriage = 0x00100000, 43 | MiniDumpWithAvxXStateContext = 0x00200000, 44 | MiniDumpWithIptTrace = 0x00400000, 45 | MiniDumpValidTypeFlags = 0x007fffff, 46 | } 47 | 48 | #[link(name = "dbghelp")] 49 | extern "system" { 50 | pub fn MiniDumpWriteDump(hProcess: HANDLE, processId: u32, 51 | hFile: HANDLE, DumpType: u32, 52 | exception: *const MinidumpExceptionInformation, 53 | userstreamparam: usize, 54 | callbackParam: usize) -> i32; 55 | } 56 | 57 | #[repr(C, packed)] 58 | #[derive(Clone, Copy, Debug)] 59 | pub struct MinidumpExceptionInformation { 60 | thread_id: u32, 61 | exception: *const EXCEPTION_POINTERS, 62 | client_pointers: u32, 63 | } 64 | 65 | /// Create a full minidump of a given process 66 | pub fn dump(filename: &str, pid: u32, tid: u32, process: HANDLE, 67 | exception: &mut EXCEPTION_RECORD, context: &mut CONTEXT) { 68 | // Don't overwrite existing dumps 69 | if Path::new(filename).is_file() { 70 | print!("Ignoring duplicate crash {}\n", filename); 71 | return; 72 | } 73 | 74 | unsafe { 75 | let filename = win32_string(filename); 76 | 77 | let ep = EXCEPTION_POINTERS { 78 | ExceptionRecord: exception, 79 | ContextRecord: context, 80 | }; 81 | 82 | // Create the minidump file 83 | let fd = CreateFileW(filename.as_ptr(), 84 | GENERIC_READ | GENERIC_WRITE, 0, 85 | std::ptr::null_mut(), CREATE_NEW, 0, std::ptr::null_mut()); 86 | assert!(fd != INVALID_HANDLE_VALUE, "Failed to create dump file"); 87 | 88 | // Wrap up the HANDLE for drop tracking 89 | let fd = Handle::new(fd).expect("Failed to get handle to minidump"); 90 | 91 | let mei = MinidumpExceptionInformation { 92 | thread_id: tid, 93 | exception: &ep, 94 | client_pointers: 0, 95 | }; 96 | 97 | // Take a minidump! 98 | let res = MiniDumpWriteDump(process, pid, fd.raw(), 99 | MinidumpType::MiniDumpWithFullMemory as u32 | 100 | MinidumpType::MiniDumpWithHandleData as u32, 101 | &mei, 0, 0); 102 | assert!(res != 0, "MiniDumpWriteDump error: {}\n", GetLastError()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /libs/debugger/src/sedebug.rs: -------------------------------------------------------------------------------- 1 | use winapi::um::winnt::HANDLE; 2 | use winapi::um::winnt::TOKEN_PRIVILEGES; 3 | use winapi::um::winnt::TOKEN_ADJUST_PRIVILEGES; 4 | use winapi::um::winnt::TOKEN_QUERY; 5 | use winapi::um::winnt::SE_PRIVILEGE_ENABLED; 6 | use winapi::um::processthreadsapi::OpenProcessToken; 7 | use winapi::um::processthreadsapi::GetCurrentProcess; 8 | use winapi::um::securitybaseapi::AdjustTokenPrivileges; 9 | use winapi::um::winbase::LookupPrivilegeValueW; 10 | use crate::ffi_helpers::win32_string; 11 | use crate::handles::Handle; 12 | 13 | /// Enable SeDebugPrivilege so we can debug system services 14 | pub fn sedebug() { 15 | unsafe { 16 | let mut token: HANDLE = std::ptr::null_mut(); 17 | let mut tkp: TOKEN_PRIVILEGES = std::mem::zeroed(); 18 | 19 | // Get the token for the current process 20 | assert!(OpenProcessToken(GetCurrentProcess(), 21 | TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &mut token) != 0); 22 | 23 | // Wrap up the handle so it'll get Dropped correctly 24 | let token = Handle::new(token) 25 | .expect("Failed to get valid handle for token"); 26 | 27 | // Lookup SeDebugPrivilege 28 | let privname = win32_string("SeDebugPrivilege"); 29 | assert!(LookupPrivilegeValueW(std::ptr::null(), 30 | privname.as_ptr(), &mut tkp.Privileges[0].Luid) != 0); 31 | 32 | tkp.PrivilegeCount = 1; 33 | tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 34 | 35 | // Set the privilege 36 | assert!(AdjustTokenPrivileges(token.raw(), 0, &mut tkp, 0, 37 | std::ptr::null_mut(), std::ptr::null_mut()) != 0); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /mesogen_scripts/ghidra.py: -------------------------------------------------------------------------------- 1 | #Generates a Mesos file from the current program. 2 | #@author marpie (Markus Piéton - marpie@a12d404.net) 3 | #@category Mesos 4 | #@keybinding 5 | #@menupath 6 | #@toolbar 7 | 8 | import struct 9 | import ghidra.program.model.block.SimpleBlockModel as SimpleBlockModel 10 | 11 | def get_simple_blocks_by_function(image_base, listing): 12 | model = SimpleBlockModel(currentProgram) 13 | 14 | entries = {} 15 | block_iter = model.getCodeBlocks(monitor) 16 | while block_iter.hasNext() and (not monitor.isCancelled()): 17 | block = block_iter.next() 18 | for block_addr in block.getStartAddresses(): 19 | if monitor.isCancelled(): 20 | break 21 | block_offset = block_addr.getOffset() - image_base 22 | 23 | func_name = block.getName() 24 | func_offset = 0 25 | func_offset_rel = 0 26 | func_of_block = listing.getFunctionContaining(block_addr) 27 | if func_of_block: 28 | func_name = func_of_block.getName() 29 | func_offset = func_of_block.getEntryPoint().getOffset() 30 | func_offset_rel = func_offset - image_base 31 | block_offset = block_addr.getOffset() - func_offset 32 | 33 | try: 34 | entries["{}_{}".format(func_offset_rel,func_name)][2].append(block_offset) 35 | except KeyError: 36 | entries["{}_{}".format(func_offset_rel,func_name)] = [func_offset_rel, func_name, [block_offset]] 37 | 38 | return entries 39 | 40 | ghidra_file = askFile("Please select the Mesos Output-File", "Save To File") 41 | 42 | with open(ghidra_file.getAbsolutePath(), "wb") as fd: 43 | input_name = currentProgram.getName() 44 | image_base = currentProgram.getImageBase().getOffset() 45 | 46 | listing = currentProgram.getListing() 47 | 48 | # Write record type 0 (module) 49 | # unsigned 16-bit module name 50 | # And module name 51 | fd.write(struct.pack("= 4 and ARGV[1] == "cmdline": 15 | input_name = ARGV[3] 16 | 17 | filename = "%s/%s.meso" % (os.path.dirname(os.path.abspath(__file__)), input_name) 18 | if len(ARGV) >= 4 and ARGV[1] == "cmdline": 19 | filename = ARGV[2] 20 | filename += ".tmp" 21 | 22 | with open(filename, "wb") as fd: 23 | # Write record type 0 (module) 24 | # unsigned 16-bit module name 25 | # And module name 26 | fd.write(struct.pack("= 4 and ARGV[1] == "cmdline": 55 | idc.Exit(0) 56 | -------------------------------------------------------------------------------- /mesogen_scripts/ida_detect.py: -------------------------------------------------------------------------------- 1 | filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".idaprobe") 2 | 3 | with open(filename, "wb") as fd: 4 | fd.write(b"WOO") 5 | 6 | idc.Exit(-5) 7 | 8 | -------------------------------------------------------------------------------- /offline_meso.ps1: -------------------------------------------------------------------------------- 1 | Param ( 2 | [Parameter(Mandatory=$true)][string]$TargetPid, 3 | [string]$OutputZip = "meso_deps.zip" 4 | ) 5 | 6 | function is64bit($a) { 7 | try { 8 | Add-Type -MemberDefinition @' 9 | [DllImport("kernel32.dll", SetLastError = true, 10 | CallingConvention = CallingConvention.Winapi)] 11 | [return: MarshalAs(UnmanagedType.Bool)] 12 | public static extern bool IsWow64Process( 13 | [In] System.IntPtr hProcess, 14 | [Out, MarshalAs(UnmanagedType.Bool)] out bool wow64Process); 15 | '@ -Name NativeMethods -Namespace Kernel32 16 | } 17 | catch {} 18 | $is32Bit = [int]0 19 | if (!$a.Handle) { 20 | echo "Unable to open handle for process: Does the proceses exist? Do you have adequate permissions?" 21 | exit 22 | } 23 | if ([Kernel32.NativeMethods]::IsWow64Process($a.Handle, [ref]$is32Bit)) { 24 | $(if ($is32Bit) {$false} else {$true}) 25 | } else { 26 | "IsWow64Process call failed" 27 | exit 28 | } 29 | } 30 | 31 | # Create a new temp directory based on a random GUID 32 | function New-TemporaryDirectory { 33 | $parent = [System.IO.Path]::GetTempPath() 34 | [string] $name = [System.Guid]::NewGuid() 35 | $name = "mesotmp_" + $name 36 | New-Item -ItemType Directory -Path (Join-Path $parent $name) 37 | } 38 | 39 | $pshell_bitness = (is64bit(Get-Process -Id $PID)) 40 | echo "Powershell is 64-bit: $pshell_bitness" 41 | 42 | $target_bitness = (is64bit(Get-Process -Id $TargetPid)) 43 | echo "Target is 64-bit: $target_bitness" 44 | 45 | # Validate bitnesses match 46 | if ($pshell_bitness -ne $target_bitness) { 47 | echo "Your Powershell bitness does not match the target bitness" 48 | echo "Use 32-bit Powershell for 32-bit processes or 64-bit Powershell for 64-bit processes" 49 | echo "This is to get around pathing issues between things like C:\windows\system32 and C:\windows\syswow64" 50 | exit 51 | } 52 | 53 | # Get all the module/executable paths from a running process 54 | $paths = (Get-Process -Id $TargetPid -Module -FileVersionInfo).FileName 55 | 56 | # Create a new temporary directory 57 | $dirname = New-TemporaryDirectory 58 | 59 | ForEach ($path in $paths) { 60 | # Convert the path to not have a ":" in it by replacing it with a "_" and 61 | # then convert it to a lowercase string 62 | $lowerpath = ([string](Get-ChildItem -Path $path)).replace(":", "_").ToLower() 63 | 64 | # Prepend a root-level folder "cache" to all paths that will go in the zip 65 | $lowerpath = (Join-Path "cache" $lowerpath) 66 | 67 | # Compute this path in the temp folder 68 | $hirearchy = (Join-Path $dirname $lowerpath) 69 | 70 | # Get the parent directory from this filename 71 | $parent = (Split-Path $hirearchy -Parent) 72 | 73 | # Create the directory if it doesn't exist 74 | if (!(Test-Path -path $parent)) { New-Item -ItemType Directory -Path $parent | Out-Null } 75 | 76 | # Copy the file :) 77 | Copy-Item -Path $path -Destination $hirearchy 78 | } 79 | 80 | # Create zip from all the files in the temp folder 81 | Compress-Archive -Force -Path (Join-Path $dirname *) -DestinationPath $OutputZip 82 | 83 | # Remove temp directory 84 | Remove-Item -Recurse -Force $dirname 85 | 86 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate debugger; 2 | 3 | pub mod mesofile; 4 | 5 | use std::path::Path; 6 | use debugger::Debugger; 7 | 8 | /// Routine to invoke on module loads 9 | fn modload_handler(dbg: &mut Debugger, modname: &str, base: usize) { 10 | // Calculate what the filename for a cached meso would be for this module 11 | let path = mesofile::compute_cached_meso_name(dbg, modname, base); 12 | 13 | // Attempt to load breakpoints from the meso file 14 | mesofile::load_meso(dbg, &path); 15 | } 16 | 17 | fn main() { 18 | 19 | // mesos.exe -p pid mesos_file0 mesos_file1 mesos_file2 20 | // or mesos.exe mesos_file0 mesos_file1 mesos_file2 -- ./exe arg0 arg1 arg2 21 | 22 | // Usage and argument parsing 23 | let args: Vec = std::env::args().collect(); 24 | if args.len() < 2 { 25 | print!("Usage: mesos.exe -p \n"); 26 | print!(" or mesos.exe -- program.exe \n"); 27 | print!(" --freq - \ 28 | Treats all breakpoints as frequency breakpoints\n"); 29 | print!(" --verbose - \ 30 | Enables verbose prints for debugging\n"); 31 | print!(" --print - \ 32 | Prints breakpoint info on every single breakpoint\n"); 33 | print!(" --follow-fork - \ 34 | Capture coverage for child processes\n"); 35 | print!(" [explicit meso file] - \ 36 | Load a specific meso file regardless of loaded modules\n\n"); 37 | 38 | return; 39 | } 40 | 41 | 42 | let mut pid: Option = None; 43 | let mut frequency_mode_enabled = false; 44 | let mut verbose_mode_enabled = false; 45 | let mut follow_fork_enabled = false; 46 | let mut print_breakpoints_enabled = false; 47 | let mut mesos: Vec<&Path> = Vec::new(); 48 | 49 | let mut argv: Vec = Vec::new(); 50 | 51 | if args.len() > 2 { 52 | for (ii, arg) in args[1..].iter().enumerate() { 53 | if arg == "-p" { 54 | pid = Some(args.get(ii + 2) 55 | .expect("No PID specified with -p argument").parse().unwrap()); 56 | } 57 | else if arg == "--verbose" { 58 | verbose_mode_enabled = true; 59 | } 60 | else if arg == "--print" { 61 | print_breakpoints_enabled = true; 62 | } 63 | else if arg == "--freq" { 64 | frequency_mode_enabled = true; 65 | } 66 | else if arg == "--follow-fork" { 67 | follow_fork_enabled = true; 68 | } 69 | else if arg == "--" { 70 | argv.extend_from_slice(&args[ii + 2..]); 71 | break; 72 | } 73 | else { // Has to be a mesofile 74 | //mesofile::load_meso(&mut dbg, Path::new(arg)); 75 | mesos.push(Path::new(arg)); 76 | } 77 | } 78 | } 79 | 80 | let mut dbg:Debugger; 81 | if pid.is_none() && argv.len() > 0 { 82 | dbg = Debugger::spawn_proc(&argv, follow_fork_enabled); 83 | } 84 | else { 85 | dbg = Debugger::attach(pid.unwrap() as u32); 86 | } 87 | 88 | // Attach to process 89 | 90 | dbg.set_always_freq(frequency_mode_enabled); 91 | dbg.set_verbose(verbose_mode_enabled); 92 | dbg.set_bp_print(print_breakpoints_enabled); 93 | 94 | for mesofile in mesos { 95 | mesofile::load_meso(&mut dbg, mesofile); 96 | } 97 | 98 | // Register callback routine for module loads so we can attempt to apply 99 | // breakpoints to it from the meso file cache 100 | dbg.register_modload_callback(Box::new(modload_handler)); 101 | 102 | // Debug forever 103 | dbg.run(); 104 | } 105 | 106 | -------------------------------------------------------------------------------- /src/mesofile.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::sync::Arc; 3 | use debugger::{Debugger, BreakpointType}; 4 | 5 | /// Grab a native-endianness u32 from a slice of u8s 6 | fn u32_from_slice(val: &[u8]) -> u32 { 7 | let mut tmp = [0u8; 4]; 8 | tmp.copy_from_slice(&val[..4]); 9 | u32::from_ne_bytes(tmp) 10 | } 11 | 12 | /// Computes the path to the cached meso filename for a given module loaded 13 | /// at `base` 14 | pub fn compute_cached_meso_name(dbg: &mut Debugger, filename: &str, 15 | base: usize) -> PathBuf { 16 | let mut image_header = [0u8; 4096]; 17 | 18 | // Read the image header at `base` 19 | assert!(dbg.read_mem(base, &mut image_header) == 20 | std::mem::size_of_val(&image_header), 21 | "Failed to read PE image header from target"); 22 | 23 | // Validate this is a PE 24 | assert!(&image_header[0..2] == b"MZ", "File was not MZ"); 25 | let pe_ptr = u32_from_slice(&image_header[0x3c..0x40]) as usize; 26 | assert!(&image_header[pe_ptr..pe_ptr+4] == b"PE\0\0"); 27 | 28 | // Get TimeDateStamp and ImageSize from the PE header 29 | let timestamp = u32_from_slice(&image_header[pe_ptr+8..pe_ptr+0xc]); 30 | let imagesz = 31 | u32_from_slice(&image_header[pe_ptr+0x50..pe_ptr+0x54]); 32 | 33 | // Compute the meso name 34 | format!("cache\\{}_{:x}_{:x}.meso", filename, timestamp, imagesz).into() 35 | } 36 | 37 | /// Load a meso file based on `meso_path` and apply breakpoints as requested to 38 | /// the `Debugger` specified by `dbg` 39 | pub fn load_meso(dbg: &mut Debugger, meso_path: &Path) { 40 | // Do nothing if the file doesn't exist 41 | if !meso_path.is_file() { 42 | return; 43 | } 44 | 45 | // Read the file 46 | let meso: Vec = std::fs::read(meso_path).expect("Failed to read meso"); 47 | 48 | // Current module name we are processing 49 | let mut cur_modname: Option> = None; 50 | 51 | // Pointer to the remainder of the file 52 | let mut ptr = &meso[..]; 53 | 54 | // Read a `$ty` from the mesofile 55 | macro_rules! read { 56 | ($ty:ty) => {{ 57 | let mut array = [0; std::mem::size_of::<$ty>()]; 58 | array.copy_from_slice(&ptr[..std::mem::size_of::<$ty>()]); 59 | ptr = &ptr[std::mem::size_of::<$ty>()..]; 60 | <$ty>::from_le_bytes(array) 61 | }}; 62 | } 63 | 64 | while ptr.len() > 0 { 65 | // Get record type 66 | let record = read!(u8); 67 | 68 | if record == 0 { 69 | // Module record 70 | let modname_len = read!(u16); 71 | 72 | // Convert name to Rust str 73 | let modname = std::str::from_utf8(&ptr[..modname_len as usize]) 74 | .expect("Module name was not valid UTF-8"); 75 | ptr = &ptr[modname_len as usize..]; 76 | 77 | cur_modname = Some(Arc::new(modname.into())); 78 | } else if record == 1 { 79 | // Current module name state 80 | let module: &Arc = cur_modname.as_ref().unwrap(); 81 | 82 | // Function record 83 | let funcname_len = read!(u16); 84 | 85 | // Convert name to Rust str 86 | let funcname: Arc = 87 | Arc::new(std::str::from_utf8(&ptr[..funcname_len as usize]) 88 | .expect("Function name was not valid UTF-8") 89 | .to_string()); 90 | ptr = &ptr[funcname_len as usize..]; 91 | 92 | // Get function offset from module base 93 | let funcoff = read!(u64) as usize; 94 | 95 | // Get number of basic blocks 96 | let num_blocks = read!(u32) as usize; 97 | 98 | // Iterate over all block offsets 99 | for _ in 0..num_blocks { 100 | let blockoff = read!(i32) as isize as usize; 101 | 102 | // Add function offset from module base to offset 103 | let offset = funcoff.wrapping_add(blockoff); 104 | 105 | // Register this breakpoint 106 | dbg.register_breakpoint(module.clone(), offset, 107 | funcname.clone(), blockoff, BreakpointType::Single, None); 108 | } 109 | } else { 110 | panic!("Unhandled record"); 111 | } 112 | } 113 | } 114 | --------------------------------------------------------------------------------