├── __init__.py ├── LICENSE ├── README.md ├── .gitignore ├── plugin.json ├── do_release.py └── gohelpers.py /__init__.py: -------------------------------------------------------------------------------- 1 | from binaryninja import PluginCommand 2 | 3 | from .gohelpers import rename_functions, rename_newproc_fptrs 4 | 5 | PluginCommand.register( 6 | "golang\\auto-rename functions", 7 | "Automatically rename go functions based on symbol table", 8 | rename_functions) 9 | 10 | PluginCommand.register("golang\\rename fptrs passed to newproc", "....", 11 | rename_newproc_fptrs) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Michael Rodler (contact@f0rki.at) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GO Loader Assist (v1.1) 2 | Author: **Michael Rodler** 3 | 4 | _Short script that parses go symbol table and renames/creates functions._ 5 | 6 | ## Description: 7 | 8 | go reversing helpers for binaryninja. 9 | 10 | Basically this is some stuff ported from the IDA pro script 11 | [golang_load_assist](https://github.com/strazzere/golang_loader_assist) 12 | 13 | Probably incomplete! 14 | 15 | ### go reversing blog posts 16 | 17 | * http://rednaga.io/2016/09/21/reversing_go_binaries_like_a_pro/ 18 | 19 | 20 | ## Installation Instructions 21 | 22 | ### Darwin 23 | 24 | no special instructions, package manager is recommended 25 | 26 | ### Linux 27 | 28 | no special instructions, package manager is recommended 29 | 30 | ### Windows 31 | 32 | no special instructions, package manager is recommended 33 | 34 | ## Minimum Version 35 | 36 | This plugin requires the following minimum version of Binary Ninja: 37 | 38 | * 1528 39 | 40 | 41 | 42 | ## Required Dependencies 43 | 44 | The following dependencies are required for this plugin: 45 | 46 | 47 | 48 | ## License 49 | 50 | This plugin is released under a MIT license. 51 | ## Metadata Version 52 | 53 | 2 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": [ 3 | "python2", 4 | "python3" 5 | ], 6 | "author": "Michael Rodler", 7 | "dependencies": {}, 8 | "description": "Short script that parses go symbol table and renames/creates functions.", 9 | "installinstructions": { 10 | "Darwin": "no special instructions, package manager is recommended", 11 | "Linux": "no special instructions, package manager is recommended", 12 | "Windows": "no special instructions, package manager is recommended" 13 | }, 14 | "license": { 15 | "name": "MIT", 16 | "text": "Copyright (c) 2017 Michael Rodler (contact@f0rki.at)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." 17 | }, 18 | "longdescription": "go reversing helpers for binaryninja.\n\nBasically this is some stuff ported from the IDA pro script\n[golang_load_assist](https://github.com/strazzere/golang_loader_assist)\n\n Probably incomplete!\n\n### go reversing blog posts\n\n * http://rednaga.io/2016/09/21/reversing_go_binaries_like_a_pro/", 19 | "minimumbinaryninjaversion": 1528, 20 | "name": "GO Loader Assist", 21 | "platforms": [ 22 | "Darwin", 23 | "Linux", 24 | "Windows" 25 | ], 26 | "pluginmetadataversion": 2, 27 | "type": [ 28 | "ui" 29 | ], 30 | "version": "1.1" 31 | } 32 | -------------------------------------------------------------------------------- /do_release.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | #Little utility to automatically do a new release. 3 | from git import Repo 4 | from json import load, dump 5 | from github_release import gh_release_create 6 | from sys import exit 7 | from os import path 8 | from argparse import ArgumentParser 9 | from subprocess import run 10 | '''WARNING: IF YOU DO NOT UPDATE YOUR README.md USING generate_plugininfo.py 11 | THIS PLUGIN WILL OVERWRITE IT!''' 12 | 13 | parser = ArgumentParser() 14 | parser.add_argument("-d", "--description", help="Description for the new release", action="store", dest="description", default="") 15 | parser.add_argument("-v", "--version", help="New version string", action="store", dest="new_version", default="") 16 | parser.add_argument("--force", help="Override the repository dirty check", action="store_true", dest="dirtyoverride", default=False) 17 | args = parser.parse_args() 18 | #TODO 19 | 20 | repo = Repo(".") 21 | reponame = list(repo.remotes.origin.urls)[0].split(':')[1].split('.')[0] 22 | if repo.is_dirty() and not args.dirtyoverride: 23 | print("Cowardly refusing to do anything as the plugin repository is currently dirty.") 24 | exit(-1) 25 | 26 | if not path.isfile("./generate_plugininfo.py"): 27 | print("Missing ./generate_plugininfo.py.") 28 | exit(-1) 29 | 30 | with open('plugin.json') as plugin: 31 | data = load(plugin) 32 | 33 | def update_version(data): 34 | print(f"Updating plugin with new version {data['version']}") 35 | with open('plugin.json', 'w') as plugin: 36 | dump(data, plugin) 37 | run(["./generate_plugininfo.py", "-r", "-f"], check=True) 38 | repo.index.add('plugin.json') 39 | repo.index.add('README.md') 40 | if args.description == "": 41 | repo.index.commit(f"Updating to {data['version']}") 42 | else: 43 | repo.index.commit(args.description) 44 | repo.git.push('origin') 45 | 46 | for tag in repo.tags: 47 | if tag.name == data['version']: 48 | if args.new_version == "": 49 | print(f"Current plugin version {data['version']} is already a tag. Shall I increment it for you?") 50 | yn = input("[y/n]: ") 51 | if yn == "Y" or yn == "y": 52 | digits = data['version'].split('.') 53 | newlast = str(int(digits[-1])+1) 54 | digits[-1] = newlast 55 | inc_version = '.'.join(digits) 56 | data['version'] = inc_version 57 | update_version(data) 58 | else: 59 | print("Stopping...") 60 | exit(-1) 61 | else: 62 | data['version'] = args.new_version 63 | update_version(data) 64 | 65 | # Create new tag 66 | new_tag = repo.create_tag(data['version']) 67 | # Push 68 | repo.remotes.origin.push(data['version']) 69 | # Create release 70 | gh_release_create(reponame, data['version'], publish=True, name="%s v%s" % (data['name'], data['version'])) 71 | -------------------------------------------------------------------------------- /gohelpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | A binaryninja plugin to assist loading go binaries 4 | 5 | Copyright (c) 2017 Michael Rodler 6 | Licensed under MIT License, see LICENCE. 7 | """ 8 | 9 | import re 10 | import struct 11 | 12 | import binaryninja as bn 13 | from binaryninja.log import log_alert, log_debug, log_info, log_warn 14 | 15 | _RE_REPLACE_UNDERSCORE = re.compile("[^a-zA-Z0-9\.]") 16 | _RE_COMPRESS_UNDERSCORE = re.compile("__+") 17 | 18 | GOFUNC_PREFIX = "go." 19 | 20 | # log_debug = log_info 21 | 22 | 23 | def santize_gofunc_name(name): 24 | name = name.replace(" ", "") 25 | # name = name.replace(";", "_") 26 | # name = name.replace(",", "_") 27 | return name 28 | 29 | 30 | def sanitize_var_name(name): 31 | varname = _RE_REPLACE_UNDERSCORE.sub("_", name) 32 | varname = _RE_COMPRESS_UNDERSCORE.sub("_", varname) 33 | return varname 34 | 35 | 36 | class GoHelper(bn.plugin.BackgroundTaskThread): 37 | def __init__(self, bv): 38 | bn.plugin.BackgroundTaskThread.__init__(self, "Go Loader Helper", True) 39 | self.bv = bv 40 | self.br = bn.binaryview.BinaryReader(bv) 41 | self.ptr_size = bv.platform.arch.address_size 42 | 43 | def get_section_by_name(self, section_name): 44 | if section_name in self.bv.sections: 45 | return self.bv.sections[section_name] 46 | else: 47 | return None 48 | 49 | def get_pointer_at_virt(self, addr, size=None): 50 | x = self.bv.read(addr, self.ptr_size) 51 | if len(x) == 8: 52 | return struct.unpack("Q", x)[0] 53 | elif len(x) == 4: 54 | return struct.unpack("I", x)[0] 55 | else: 56 | raise ValueError("Invalid size {} for pointer; data: {!r}" 57 | .format(len(x), x)) 58 | 59 | def get_pointer_at(self, at_addr, size=None): 60 | self.br.seek(at_addr) 61 | if size is None: 62 | size = self.ptr_size 63 | 64 | if size == 8: 65 | return self.br.read64() 66 | elif size == 4: 67 | return self.br.read32() 68 | else: 69 | raise ValueError("Unsupported ptr_size: {!r}".format(size)) 70 | 71 | def get_function_around(self, addr): 72 | bbl = self.bv.get_basic_blocks_at(addr) 73 | if not bbl: 74 | return None 75 | bb = bbl[0] 76 | if not bb: 77 | return None 78 | return bb.function 79 | 80 | 81 | class FunctionRenamer(GoHelper): 82 | def rename_functions(self): 83 | renamed = 0 84 | log_info("renaming functions based on .gopclntab section") 85 | 86 | gopclntab = self.get_section_by_name(".gopclntab") 87 | 88 | if gopclntab is None: 89 | pattern = b"\xfb\xff\xff\xff\x00\x00" 90 | base_addr = self.bv.find_next_data(0, pattern) 91 | 92 | if base_addr is None: 93 | log_alert("Failed to find section '.gopclntab'") 94 | return 95 | else: 96 | base_addr = gopclntab.start 97 | 98 | size_addr = base_addr + 8 99 | size = self.get_pointer_at(size_addr) 100 | 101 | log_info("found .gopclntab section at 0x{:x} with {} entries" 102 | .format(base_addr, size / (self.ptr_size * 2))) 103 | 104 | start_addr = size_addr + self.ptr_size 105 | end_addr = base_addr + (size * self.ptr_size * 2) 106 | 107 | for addr in range(start_addr, end_addr, (2 * self.ptr_size)): 108 | log_debug("analyzing at 0x{:x}".format(addr)) 109 | func_addr = self.get_pointer_at(addr) 110 | entry_offset = self.get_pointer_at(addr + self.ptr_size) 111 | 112 | log_debug("func_addr 0x{:x}, entry offset 0x{:x}" 113 | .format(func_addr, entry_offset)) 114 | 115 | name_str_offset = self.get_pointer_at( 116 | base_addr + entry_offset + self.ptr_size, 4) 117 | name_addr = base_addr + name_str_offset 118 | 119 | name = self.bv.get_ascii_string_at(name_addr) 120 | if not name: 121 | continue 122 | name=name.value 123 | 124 | log_debug("found name '{}' for address 0x{:x}" 125 | .format(name, func_addr)) 126 | 127 | func = self.bv.get_function_at(func_addr) 128 | if not func: 129 | func = self.bv.create_user_function(func_addr) 130 | 131 | if name and len(name) > 2: 132 | name = GOFUNC_PREFIX + santize_gofunc_name(name) 133 | sym = bn.types.Symbol('FunctionSymbol', func_addr, name, name) 134 | self.bv.define_user_symbol(sym) 135 | renamed += 1 136 | else: 137 | log_warn(("not using function name {!r} for function at 0x{:x}" 138 | " in .gopclntab addr 0x{:x} name addr 0x{:x}") 139 | .format(name, func_addr, addr, name_addr)) 140 | 141 | log_info("renamed {} go functions".format(renamed)) 142 | 143 | def run(self): 144 | self.rename_functions() 145 | 146 | 147 | def rename_functions(bv): 148 | helper = FunctionRenamer(bv) 149 | helper.start() 150 | 151 | 152 | # FIXME: this one doesn't work as expected... :S 153 | class NewProcRenamer(GoHelper): 154 | def rename(self): 155 | renamed = 0 156 | newprocfn = self.bv.get_symbol_by_raw_name("go.runtime.newproc") 157 | xrefs = self.bv.get_code_refs(newprocfn.address) 158 | for xref in xrefs: 159 | log_info("found xref at 0x{:x}".format(xref.address)) 160 | addr = xref.address 161 | fn = self.get_function_around(addr) 162 | callinst = fn.get_low_level_il_at(addr) 163 | if callinst.operation != bn.LowLevelILOperation.LLIL_CALL: 164 | log_debug("not a call instruction {!r}".format(callinst)) 165 | continue 166 | params = [] 167 | # FIXME: this is such a dirty hack 168 | # get the previous two LIL instruction 169 | j = 1 170 | while len(params) < 2: 171 | for i in range(1, 7): 172 | try: 173 | j += 1 174 | inst = fn.get_low_level_il_at(addr - j) 175 | log_debug("instruction: -{} {!r}".format(j, inst)) 176 | break 177 | except IndexError: 178 | continue 179 | params.append(inst) 180 | 181 | # FIXME: does this work on non-x86? 182 | # check if 2 push instructions 183 | skip = True 184 | for inst in params: 185 | if inst.operation != bn.LowLevelILOperation.LLIL_PUSH: 186 | skip = True 187 | if skip: 188 | continue 189 | 190 | # get the address of the function pointer, which should be the 191 | # second push instruction 192 | inst = params[1] 193 | fptr = inst.src.value.value 194 | log_info("found call to newproc {!r} with fptr {!r}" 195 | .format(callinst, fptr)) 196 | 197 | if fptr and not self.bv.get_symbol_at(fptr): 198 | a = self.get_pointer_at_virt(fptr) 199 | # target function 200 | tfn = self.bv.get_function_at(a) 201 | if tfn: 202 | varname = "fptr_" 203 | varname += sanitize_var_name(tfn.name) 204 | t = self.bv.parse_type_string("void*") 205 | self.bv.define_user_data_var(a, t[0]) 206 | sym = bn.types.Symbol('DataSymbol', a, varname, varname) 207 | self.bv.define_user_symbol(sym) 208 | renamed += 1 209 | 210 | log_info("renamed {} function pointers, passed to newproc" 211 | .format(renamed)) 212 | 213 | def run(self): 214 | self.rename() 215 | 216 | 217 | def rename_newproc_fptrs(bv): 218 | helper = NewProcRenamer(bv) 219 | helper.start() 220 | 221 | 222 | # def create_runtime_morestack(): 223 | # log.info("Attempting to find 'runtime.morestack' function") 224 | # text_seg = get_section_by_name('.text') 225 | # # text_vaddr = text_seg['vaddr'] 226 | 227 | # # This code string appears to work for ELF32 and ELF64 AFAIK 228 | # s = "mov qword [0x1003], 0" 229 | # res = cmdj("/aj " + s, text_seg) 230 | # if not res: 231 | # # let's search for the assembled variant 232 | # if ARCH == "x86" and BITS == 64: 233 | # h = "48c704250310.c3" 234 | # res = cmdj("/xj " + h, text_seg) 235 | 236 | # if not res: 237 | # log.warning("Couldn't find morestack signature") 238 | # return None 239 | 240 | # if len(res) > 1: 241 | # log.warning("more than one signature match... trying first") 242 | 243 | # res = res[0] 244 | # runtime_ms = cmdj("afij", res["offset"])[0] 245 | 246 | # if not runtime_ms: 247 | # log.warning("undefined function at morestack...") 248 | # return None 249 | 250 | # offset = runtime_ms['offset'] 251 | # log_debug("runtime.morestack begins at 0x{:x}" 252 | # .format(runtime_ms[offset])) 253 | 254 | # if "morestack" not in runtime_ms["name"]: 255 | # log_debug("renaming {} to 'runtime.morestack'" 256 | # .format(runtime_ms["name"])) 257 | # cmd("afn {} {}".format("runtime.morestack", runtime_ms['offset'])) 258 | 259 | # return runtime_ms 260 | --------------------------------------------------------------------------------