From bc710768f12da4ec1cfa12b4336c3ce38484fd0e Mon Sep 17 00:00:00 2001 From: ReKreker Date: Sat, 15 Oct 2022 15:29:36 +0300 Subject: [PATCH] fix function call errors --- .gitignore | 1 + CHANGELOG.md | 11 +++ README.md | 24 ++--- docs/common errors.md | 8 ++ example/hooks.xml | 4 +- internal/asm.py | 38 +++++--- internal/cmpl.py | 26 +++++- internal/ftb.py | 2 + internal/inj.py | 7 +- internal/other.py | 5 + shooker | 206 ++++++++++++++++++++++-------------------- 11 files changed, 198 insertions(+), 134 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/common errors.md diff --git a/.gitignore b/.gitignore index 8cd1627..2835597 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ example/*.so example/leet_add example/*.o internal/__pycache__ +pypi/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d8eef37 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# 1.1 (15 Oct 2022) +## Changes +- add changelog +- fix multiple call imported function error +- add ability to get return from import function +- add debug logs +- fix section create bug +- add common errors note + +# 1.0 +Public release diff --git a/README.md b/README.md index 83ab0de..a09dbfe 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,14 @@ Tool for C-code injections in already compiled bins. ## Usage -Write hook config as described in the [instruction](docs/hooks%20xml.md). +Write hook config as described in the [instruction](https://github.com/ReKreker/shooker/blob/master/docs/hooks%20xml.md). ```shooker --xml config.xml target_dir/ output_dir/``` +## Install +```pip install shooker```
+*Please read about [common errors](https://github.com/ReKreker/shooker/blob/master/docs/common%20errors.md)* + ## Example \> ```cd example/```
\> ```make compile```
@@ -18,20 +22,18 @@ LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:./ ./leet_add
3713
\> ```make hook```
./../shooker ./ ./
-Patching libtarget.so...
-Compiling hook for add_n_print
-Patching the hook(s)...
-Hooking add_n_print
-Lib(s) patched
+INFO: Patching libtarget.so...
+INFO: Compiling hook for add_n_print
+INFO: Patching the hook(s)...
+INFO: Hooking add_n_print
+INFO: Lib(s) patched
\> ```make run```
LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:./ ./leet_add
-1337
- -## Install -```pip install shooker``` +Leet is 1337
## To improve - Add ability to inject to .exe/.dll - Try to avoid sub-instruction patching mechanism in the hook(s) - Add support of arm architecture -- Add support hooking raw binaries \ No newline at end of file +- Add support hooking raw binaries +- Develop true hook(not replace) diff --git a/docs/common errors.md b/docs/common errors.md new file mode 100644 index 0000000..ba761db --- /dev/null +++ b/docs/common errors.md @@ -0,0 +1,8 @@ +#### Multiple calls the same func at one line +```│translation.c:7:51: error: duplicate label ‘func_name_jmp_10’``` + +How to fix: +```printf(_s("Leet is %d\n"), arg1*100+arg2/100); printf(_s("Trigger error"));``` +to +```printf(_s("Leet is %d\n"), arg1*100+arg2/100); +printf(_s("Test"));``` diff --git a/example/hooks.xml b/example/hooks.xml index 5b38262..d020484 100644 --- a/example/hooks.xml +++ b/example/hooks.xml @@ -8,7 +8,7 @@ printf - printf(_s("%d\n"), arg1*100+arg2/100); + printf(_s("Leet is %d\n"), arg1*100+arg2/100); - \ No newline at end of file + diff --git a/internal/asm.py b/internal/asm.py index b002005..6429ff7 100644 --- a/internal/asm.py +++ b/internal/asm.py @@ -1,9 +1,9 @@ import capstone -from internal.other import * - from lief.ELF import ARCH +from internal.other import * + class Assembler: """Arch-depended stuff""" @@ -18,10 +18,15 @@ def jump(self, target_addr: int) -> bytes: """Create jmp-instruction to target_addr""" if self.arch == ARCH.x86_64: target_addr -= 5 # length of jmp instr - return b"\xE9" + target_addr.to_bytes(4, "little") + jump_inst = b"\xE9" + target_addr.to_bytes(4, "little") else: raise Unimplemented(f"Assemble jump for {arch}") + logging.debug( + f"Crafted jump-instruction is {jump_inst.hex()} for {hex(target_addr)}" + ) + return jump_inst + def brute_ptl(self, section: LiefSect, target_addr: int) -> int: """Find plt-stub for specific function from .got.plt""" cont = section.content.tobytes() @@ -36,6 +41,7 @@ def brute_ptl(self, section: LiefSect, target_addr: int) -> int: # offset from instruction + current entry plt position + len of jmp instruction if instr_offs + instr.address + instr.size == target_addr: + logging.debug(f"Found plt for {hex(target_addr)}") return instr.address else: raise Unimplemented(f"Enumerating plt for {self.arch}") @@ -47,7 +53,7 @@ def patch_sub_values(self, func_cont: list, func_offs: int) -> list: cont = bytearray(b"".join([i.to_bytes(1, "big") for i in func_cont])) register = None - lea_ip = None + lea_ip = 0 sub_instr = None call_ip = 0 @@ -57,6 +63,9 @@ def patch_sub_values(self, func_cont: list, func_offs: int) -> list: if instr.mnemonic == "lea" and "[rip - 7]" in instr.op_str: lea_ip = instr.address register = instr.op_str.replace(", [rip - 7]", "") + logging.debug( + f"Register of lea instr found {register} at {hex(func_offs+lea_ip)}" + ) if ( instr.mnemonic == "sub" @@ -64,6 +73,7 @@ def patch_sub_values(self, func_cont: list, func_offs: int) -> list: and instr.op_str.startswith(register) ): sub_instr = instr + logging.debug(f"Sub instr found at {hex(func_offs+instr.address)}") if ( instr.mnemonic == "call" @@ -71,20 +81,18 @@ def patch_sub_values(self, func_cont: list, func_offs: int) -> list: and 2 <= instr.size <= 3 ): call_ip = instr.address - break + logging.debug(f"Call instr found at {hex(func_offs+call_ip)}") - if lea_ip == 0: - raise NotFound(f"Get IP instruction") - elif sub_instr is None: - raise NotFound(f"Sub {register}, #const") - elif call_ip == 0: - raise NotFound(f"Register-jump instruction") + if lea_ip != 0 and sub_instr is not None and call_ip != 0: + sub_value = int.from_bytes(sub_instr.bytes[2:], "little") + relative_offs = func_offs + lea_ip - sub_value + sub_bytes = sub_instr.bytes[:2] + relative_offs.to_bytes(4, "little") - sub_value = int.from_bytes(sub_instr.bytes[2:], "little") - relative_offs = func_offs + lea_ip - sub_value - sub_bytes = sub_instr.bytes[:2] + relative_offs.to_bytes(4, "little") + cont[sub_instr.address : sub_instr.address + sub_instr.size] = sub_bytes - cont[sub_instr.address : sub_instr.address + sub_instr.size] = sub_bytes + # zeroing to continue parse function + lea_ip, call_ip = 0, 0 + register, sub_instr = None, None else: raise Unimplemented(f"Patch sub-instruction for {arch}") diff --git a/internal/cmpl.py b/internal/cmpl.py index 60f5567..e221c2a 100644 --- a/internal/cmpl.py +++ b/internal/cmpl.py @@ -16,6 +16,7 @@ def __init__(self, cc: str, path: Path) -> None: self.whitelist_funcs = [] def include_lib(self, inc: str) -> None: + logging.debug(f"Include lib {inc}") if not inc: raise NotFound("Include lib") @@ -25,17 +26,30 @@ def include_func(self, name: str, proto: str, addr: int) -> None: if not name or not proto or not addr: raise NotFound("Name/proto/addr for include func") - # use Labels as Values https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html - decl = f"#define {name} {name}_jmp: (({proto.replace('FUNC', '(*)')})((long)&&{name}_jmp-{addr}))" + # use Labels as Values https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html + # and Statements and Expressions https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html + label = f"UNIQ_LINE({name}_jmp)" + decl = ( + f"#define {name} (({proto.replace('FUNC', '(*)')})" + + "({" + + f"{label}:(long)&&{label}-{addr};" + + "}))" + ) + logging.debug(f"Include func {decl}") self.inc_fncs.append(decl) def assemble_transl(self) -> None: """Some asm-tricks""" # stuff like #include - self.code += "\n".join(self.inc_libs) + "\n" + self.code += "\n".join(self.inc_libs) # asm-trick to avoid broken xref for string from another segment - self.code += "#define _s(string) ((char *)(const char []){string})\n" + self.code += "\n#define _s(string) ((char *)(const char []){string})\n" + + # asm-trick for unique jump label for relative jump funcs + self.code += "\n#define CONCAT_(prefix, suffix) prefix##suffix" + self.code += "\n#define CONCAT(prefix, suffix) CONCAT_(prefix, suffix)" + self.code += "\n#define UNIQ_LINE(prefix) CONCAT(prefix##_, __LINE__)\n" # func declaration stuff self.code += "\n".join(self.inc_fncs) + "\n" @@ -58,6 +72,8 @@ def compile_transl(self, txt_addr: int) -> FuncsInfo: # uncomment to look translation.c # __import__("IPython").embed() + logging.debug("="*70 + "\n"+ self.code + "\n" + "="*70) + cmd = [ self.cc, "-fPIC", @@ -89,6 +105,7 @@ def compile_transl(self, txt_addr: int) -> FuncsInfo: start = txt.virtual_address end = start + txt.size offset = 0 + logging.debug(f"Translation offset {hex(start)}") # extract bytes of each function for sym in trs_bin.static_symbols: @@ -97,6 +114,7 @@ def compile_transl(self, txt_addr: int) -> FuncsInfo: and sym.value < end and sym.name in self.whitelist_funcs ): + logging.debug(f"Created {sym.name} with size {sym.size}") content = trs_bin.get_content_from_virtual_address(sym.value, sym.size) funcs_info[sym.name] = {"content": content, "offset": offset} offset += len(content) diff --git a/internal/ftb.py b/internal/ftb.py index d9c6d08..af8a3cf 100644 --- a/internal/ftb.py +++ b/internal/ftb.py @@ -20,6 +20,7 @@ def load_symbol(self, fnc_name: str) -> int: entry = FuncEntry(fnc.name, fnc.value) self.fncs.append(entry) + logging.debug(f"Static {entry.name} loaded at {hex(entry.addr)}") return fnc.value def load_import(self, fnc_name: str) -> int: @@ -32,6 +33,7 @@ def load_import(self, fnc_name: str) -> int: jump_addr = self.asm.brute_ptl(plt, fnc.address) entry = FuncEntry(fnc.symbol.name, jump_addr) self.fncs.append(entry) + logging.debug(f"Import {entry.name} loaded at {hex(entry.addr)}") return jump_addr diff --git a/internal/inj.py b/internal/inj.py index 58c8783..87503f3 100644 --- a/internal/inj.py +++ b/internal/inj.py @@ -1,7 +1,7 @@ -from internal.other import * - from lief.ELF import Section, SECTION_FLAGS, SECTION_TYPES +from internal.other import * + class Inject: """Create shook section and some hooking stuff""" @@ -14,11 +14,14 @@ def __init__(self, target: LiefBin, asm) -> None: def shook_sect_init(self) -> None: """Create section to find out base address for linker""" if self.bin.get_section(".shook") is not None: + logging.debug("Section .shook exists") return + logging.debug("Create .shook section") section = Section(".shook", SECTION_TYPES.PROGBITS) section += SECTION_FLAGS.EXECINSTR section += SECTION_FLAGS.WRITE + section.content = [0]*0x500 self.bin.add(section, loaded=True) def shook_sect_fill(self, content: list) -> None: diff --git a/internal/other.py b/internal/other.py index 46dbf00..8325f7f 100644 --- a/internal/other.py +++ b/internal/other.py @@ -1,4 +1,5 @@ import lief +import logging from pathlib import Path from typing import NewType, Dict, List @@ -12,6 +13,7 @@ "NotFound", "CompileFail", "Wrong", + "logging", ] LiefBin = NewType("liefBin", lief.ELF.Binary | lief.MachO.Binary | lief.PE.Binary) @@ -37,3 +39,6 @@ def __init__(self, cmd): class Wrong(Exception): def __init__(self, msg): pass + + +logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO) diff --git a/shooker b/shooker index b132f7b..7e6cc2e 100755 --- a/shooker +++ b/shooker @@ -10,103 +10,109 @@ from internal.inj import Inject from internal.cmpl import Compile from internal.other import * -parser = ArgumentParser() -parser.add_argument( - "--xml", - type=Path, - dest="config", - default="hooks.xml", - help="Hook config file (default: target_dir/hooks.xml)", -) -parser.add_argument("target_dir", type=Path, help="Directory with libs to hook") -parser.add_argument("output_dir", type=Path, help="Directory to save hooked libs") -args = parser.parse_args() - -args.output_dir.mkdir(parents=True, exist_ok=True) - -if not args.target_dir.exists(): - raise NotFound("Target dir") - -if args.config.name == "hooks.xml": - args.config = args.target_dir / "hooks.xml" -if not args.config.exists(): - raise NotFound("Hook config") - -with open(args.config) as xml_file: - parser = ET.parse(xml_file) - -shook = parser.getroot() -cc = shook.find("compiler").text - -for lib in shook.iterfind("lib_hook"): - lib_path = (args.target_dir / lib.attrib["path"]).name - print(f"Patching {lib_path}...") - - target = lief.parse(lib_path) - - cs_arch = lib.find("arch") - cs_mode = lib.find("mode") - if cs_arch is None or cs_mode is None: - raise NotFound("Arch or mode for hooked liblary") - asm = Assembler(target, cs_arch.text, cs_mode.text) - ftb = FuncTable(target, asm) - cmpl = Compile(cc, args.target_dir) - inj = Inject(target, asm) - - # parse included libs - for lib in lib.find("include").iterfind("lib"): - kind = inc.attrib.get("kind") - if kind == "system": - inc_value = f"#include <{inc}>" - elif kind == "local": - inc_value = f'#include "{inc}"' - else: - raise Wrong("Kind of include lib") - cmpl.include_lib(inc_value) - - # parse included funcs - for inc in lib.find("include").iterfind("func"): - kind = inc.attrib.get("kind") - name = inc.text - if kind == "import": - addr = ftb.load_import(name) - elif kind == "symbol": - addr = ftb.load_symbol(name) - else: - raise Wrong("Kind of include func") - proto = inc.attrib.get("proto") - cmpl.include_func(name, proto, addr) - - # define included libs&funcs - cmpl.assemble_transl() - - # compile hooks - for to_hook in lib.iterfind("hook"): - fnc_name = to_hook.attrib.get("name") - fnc_proto = to_hook.attrib.get("proto") - fnc_code = to_hook.text - - print(f"Compiling hook for {fnc_name}") - cmpl.add_func_to_transl(fnc_name, fnc_proto, fnc_code) - segm_addr = target.get_section(".shook").virtual_address - funcs_info = cmpl.compile_transl(segm_addr) - - print("Patching the hook(s)...") - content = [] - for func in funcs_info.values(): - arr = func["content"] - offset = func["offset"] + segm_addr - content += asm.patch_sub_values(arr, offset) - inj.shook_sect_fill(content) - - for to_hook in lib.iterfind("hook"): - fnc_name = to_hook.attrib["name"] - print(f"Hooking {fnc_name}") - - fnc_offset = target.get_static_symbol(fnc_name).value # address of func - # offset of func in created section - payl_offset = funcs_info[fnc_name]["offset"] - inj.hook(fnc_name, fnc_offset, payl_offset) - - target.write((args.output_dir / lib.attrib["path"]).name) -print("Lib(s) patched") + +def main(): + parser = ArgumentParser() + parser.add_argument( + "--xml", + type=Path, + dest="config", + default="hooks.xml", + help="Hook config file (default: target_dir/hooks.xml)", + ) + parser.add_argument("target_dir", type=Path, help="Directory with libs to hook") + parser.add_argument("output_dir", type=Path, help="Directory to save hooked libs") + args = parser.parse_args() + + args.output_dir.mkdir(parents=True, exist_ok=True) + + if not args.target_dir.exists(): + raise NotFound("Target dir") + + if args.config.name == "hooks.xml": + args.config = args.target_dir / "hooks.xml" + if not args.config.exists(): + raise NotFound("Hook config") + + with open(args.config) as xml_file: + parser = ET.parse(xml_file) + + shook = parser.getroot() + cc = shook.find("compiler").text + + for lib in shook.iterfind("lib_hook"): + lib_path = (args.target_dir / lib.attrib["path"]).name + logging.info(f"Patching {lib_path}...") + + target = lief.parse(lib_path) + + cs_arch = lib.find("arch") + cs_mode = lib.find("mode") + if cs_arch is None or cs_mode is None: + raise NotFound("Arch or mode for hooked liblary") + asm = Assembler(target, cs_arch.text, cs_mode.text) + ftb = FuncTable(target, asm) + cmpl = Compile(cc, args.target_dir) + inj = Inject(target, asm) + + # parse included libs + for lib in lib.find("include").iterfind("lib"): + kind = inc.attrib.get("kind") + if kind == "system": + inc_value = f"#include <{inc}>" + elif kind == "local": + inc_value = f'#include "{inc}"' + else: + raise Wrong("Kind of include lib") + cmpl.include_lib(inc_value) + + # parse included funcs + for inc in lib.find("include").iterfind("func"): + kind = inc.attrib.get("kind") + name = inc.text + if kind == "import": + addr = ftb.load_import(name) + elif kind == "symbol": + addr = ftb.load_symbol(name) + else: + raise Wrong("Kind of include func") + proto = inc.attrib.get("proto") + cmpl.include_func(name, proto, addr) + + # define included libs&funcs + cmpl.assemble_transl() + + # compile hooks + for to_hook in lib.iterfind("hook"): + fnc_name = to_hook.attrib.get("name") + fnc_proto = to_hook.attrib.get("proto") + fnc_code = to_hook.text + + logging.info(f"Compiling hook for {fnc_name}") + cmpl.add_func_to_transl(fnc_name, fnc_proto, fnc_code) + segm_addr = target.get_section(".shook").virtual_address + funcs_info = cmpl.compile_transl(segm_addr) + + logging.info("Patching the hook(s)...") + content = [] + for func in funcs_info.values(): + arr = func["content"] + offset = func["offset"] + segm_addr + content += asm.patch_sub_values(arr, offset) + inj.shook_sect_fill(content) + + for to_hook in lib.iterfind("hook"): + fnc_name = to_hook.attrib["name"] + logging.info(f"Hooking {fnc_name}") + + fnc_offset = target.get_static_symbol(fnc_name).value # address of func + # offset of func in created section + payl_offset = funcs_info[fnc_name]["offset"] + inj.hook(fnc_name, fnc_offset, payl_offset) + + target.write((args.output_dir / lib.attrib["path"]).name) + logging.info("Lib(s) patched") + + +if __name__ == "__main__": + main()