├── .gitignore ├── README.md ├── example.sh ├── glory-penguin.png ├── glory.py ├── hook.c ├── requirements.txt └── usage.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GLORYHook 2 | The first Linux hooking framework to allow merging two binary files into one! 3 | 4 |

5 | 6 |

7 | 8 | ## How is this different? 9 | Other hooking methods do not allow calling libraries from within the hook, so you must resort to writing shellcode or your own implementation for libc APIs. This is not the case with GLORYHook. Check out hook.c, you can call any libc API you want! 10 | 11 | ## Use cases 12 | 1. Debugging - Can't use LD_PRELOAD? Don't want to mess with injecting dependency shared objects and can't bother installing dependency libraries on the system each time? Just hook your file instantly and ship it with zero extra steps. 13 | 2. File Infection/Backdoor - Can be used as an alternative for an LD_PRELOAD rootkit but with **extra stealth sauce**. Defenders contact me for how to detect. 14 | 15 | ## Important Notes 16 | GLORYHook supports only x64. 17 | Currently hooking is only supported on imports (e.g. libc functions). 18 | Currently interacting with globals in your hook is unsupported but will be added soon. 19 | 20 | ## Installation 21 | 1. Install my custom LIEF (I customized LIEF to make ELF manipulations easier): 22 | ``` 23 | git clone https://github.com/tsarpaul/LIEF 24 | cd LIEF 25 | python3 ./setup.py install 26 | ``` 27 | 2. ```pip3 install -r requirements.txt``` 28 | 29 | ## Usage 30 | 31 | ![usage](https://raw.githubusercontent.com/tsarpaul/GLORYHook/master/usage.png) 32 | 33 | 1. Define gloryhook_ in your hook file 34 | 2. `gcc -shared -zrelro -znow hook.c -o hook` 35 | 3. `python3 glory.py ./file-to-hook ./hook -o ./hooked-file` 36 | 37 | Check hook.c and example.sh. 38 | 39 | ## GLORY TO YOU 40 | -------------------------------------------------------------------------------- /example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gcc -shared -zrelro -znow hook.c -o hook 4 | python3 glory.py /bin/ls ./hook -o ./hooked-ls 5 | 6 | -------------------------------------------------------------------------------- /glory-penguin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsarpaul/GLORYHook/109c90d0be4c2bba9f4e3a03e2e41cb406bc57c1/glory-penguin.png -------------------------------------------------------------------------------- /glory.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | import shutil 3 | import struct 4 | import argparse 5 | 6 | import lief 7 | from capstone import * 8 | from keystone import * 9 | 10 | md = None 11 | ks = None 12 | PLT_STUB_SIZE = 0x10 13 | MEMORY_OPERAND = 2 14 | INT_SZ = 8 15 | 16 | PLT_INDEX_OFFSET = 0x6 17 | PLT_JMP_OFFSET = 0xb 18 | 19 | def isexec(segment): 20 | return segment.has(lief.ELF.SEGMENT_FLAGS.X) and segment.type == lief.ELF.SEGMENT_TYPES.LOAD 21 | 22 | def isread(segment): 23 | return segment.has(lief.ELF.SEGMENT_FLAGS.R) and not segment.has(lief.ELF.SEGMENT_FLAGS.X) and segment.type == lief.ELF.SEGMENT_TYPES.LOAD 24 | 25 | def pc_in_instr(i): 26 | return 'rip' in i.op_str or 'eip' in i.op_str 27 | 28 | class Binary64(): 29 | def _fix_got_after_injection(self, injected_offset, injected_size): 30 | with open(self.path, 'rb+') as f: 31 | # Fix header - it should point at its own VA 32 | f.seek(self.got_section.file_offset) 33 | got_header_va = self.got_section.virtual_address 34 | f.write(struct.pack(" 0 and instruction_va < injected_va < instruction_va + operand_delta 92 | 93 | def injection_in_negative_delta(injected_va, injected_size, instruction_va, operand_delta): 94 | return operand_delta < 0 and instruction_va > injected_va + injected_size and injected_va > instruction_va + operand_delta 95 | 96 | x_sections = [] 97 | for s in self.binary.sections: 98 | if s.has(lief.ELF.SECTION_FLAGS.EXECINSTR): 99 | x_sections.append(s) 100 | 101 | code_changes = [] 102 | for s in x_sections: 103 | code_start = s.file_offset 104 | code_va = s.virtual_address 105 | code_size = s.size 106 | 107 | # Fix relative addressings where the injection is located in the middle 108 | # Collect relative instructions 109 | with open(self.path, 'rb') as f: 110 | f.seek(code_start) 111 | injected_code = f.read(code_size) 112 | 113 | # Disassemble for the relatives 114 | injected_disassembly = md.disasm(injected_code, code_va) 115 | disassembly_index = 0 116 | disassembly_va = code_va 117 | 118 | for i in injected_disassembly: 119 | offset = code_start + disassembly_index 120 | #if offset == 0x3FED00: 121 | # import pdb; pdb.set_trace() 122 | 123 | # Relative call 124 | if i.mnemonic in ['call', 'jmp'] and i.operands[0].type == MEMORY_OPERAND: 125 | # Capstone resolves imm to the appropriate address, instead of a delta 126 | imm = i.operands[0].imm 127 | new_imm = None 128 | 129 | if self.va(offset) > self.va(injected_offset) + injected_size > imm: 130 | new_imm = imm - injected_size 131 | elif self.va(offset) < self.va(injected_offset) < imm: 132 | new_imm = imm + injected_size 133 | 134 | if new_imm is not None: 135 | # Keystone does not resolve imm to appropriate address, so we have to recalculate the offset 136 | new_imm = new_imm - disassembly_va 137 | new_asm_str = f'{i.mnemonic} {hex(new_imm)};' 138 | new_asm, count = ks.asm(new_asm_str) 139 | assert(len(new_asm) == i.size) # TODO: If this happens we overflowed, we'll need to inject extra code to add up the relative call 140 | code_changes.append((offset, new_asm)) 141 | 142 | elif pc_in_instr(i): 143 | rip_operand_index = 0 144 | try: 145 | if i.operands[1].mem.disp != 0: 146 | rip_operand_index = 1 147 | except IndexError: 148 | pass 149 | 150 | disp = i.operands[rip_operand_index].mem.disp 151 | new_disp = None 152 | 153 | if(injection_in_positive_delta(self.va(injected_offset), self.va(offset), disp + i.size)): 154 | new_disp = disp + injected_size 155 | elif(injection_in_negative_delta(self.va(injected_offset), injected_size, self.va(offset), disp + i.size)): 156 | new_disp = disp - injected_size 157 | 158 | if new_disp is not None: 159 | new_asm_str = f'{i.mnemonic} {i.op_str};' 160 | new_asm_str = new_asm_str.replace(str(hex(disp)), hex(new_disp)) 161 | new_asm, count = ks.asm(new_asm_str) 162 | assert(len(new_asm) == i.size) 163 | code_changes.append((offset, new_asm)) 164 | 165 | disassembly_index += i.size 166 | disassembly_va += i.size 167 | 168 | self.apply_code_changes(code_changes) 169 | 170 | 171 | def __init__(self, path): 172 | self.path = path 173 | self.reload() 174 | 175 | self.arch = self.binary.header.machine_type 176 | if self.arch not in [lief.ELF.ARCH.x86_64, lief.ELF.ARCH.i386]: 177 | raise("Unsupported architectures!") 178 | 179 | exec_segs = [seg for seg in self.binary.segments if isexec(seg)] 180 | read_segs = [seg for seg in self.binary.segments if isread(seg)] 181 | assert(len(exec_segs) == 1) 182 | assert(len(read_segs) == 1) 183 | 184 | def lief_write(self): 185 | self.binary.write(self.path) 186 | self.reload() 187 | 188 | def reload(self): 189 | """Using old LIEF primitives MAY RESULT IN UNDEFINED BEHAVIOR after reloading!""" 190 | self.binary = lief.parse(self.path) 191 | self.plt_section = self.binary.get_section('.plt') 192 | self.text_section = self.binary.get_section('.text') 193 | self.got_section = self.binary.get_section('.got') 194 | self.load_segs = [seg for seg in self.binary.segments if seg.type == lief.ELF.SEGMENT_TYPES.LOAD] 195 | self.exec_seg = self.load_segs[0] 196 | self.read_seg = self.load_segs[1] 197 | 198 | def inject(self, content, offset, end_of_section=True, extend_backwards=False): 199 | size = len(content) 200 | 201 | # Adjust the binary before injecting 202 | self.update_injection_metadata(offset, size, end_of_section, extend_backwards) 203 | 204 | with open(self.path, 'rb+') as f: 205 | f.seek(offset) 206 | f.write(content) 207 | self.reload() 208 | 209 | self.update_injection_code(offset, size) 210 | 211 | def fix_new_plt_entries(self, injection_start, injection_size, got_start, got_sz): 212 | # Minus header and adjust to index 0 213 | max_got_index = got_sz//0x8 - 1 - 3 214 | #max_got_index = (self.plt_section.size-injection_size)//0x10 - 1 215 | injection_end = injection_start + injection_size 216 | 217 | got_end = got_start + got_sz 218 | got_end_va = self.va(got_end) 219 | 220 | code_changes = [] 221 | # Fix our new PLT entries 222 | for i, plt_entry in enumerate(range(injection_start, injection_end, 0x10)): 223 | delta_from_plt_start = plt_entry - self.plt_section.file_offset 224 | plt_entry_va = self.plt_section.virtual_address + delta_from_plt_start 225 | got_entry = got_end + i*INT_SZ 226 | got_entry_va = got_end_va + i*INT_SZ 227 | 228 | # Fix GOT entry, should hold (PLT_entry + 0x6) initially 229 | plt_entry_stub_va = plt_entry_va + 0x6 230 | code_changes.append((got_entry, struct.pack("PLT addresses 388 | imports = {v:k for (k, v) in imports.items()} # Flip addr:name to name:addr 389 | self.replace_named_calls(merged_code_base_addr, merged_code_base_addr + len(new_code), new_import_callers, imports) 390 | 391 | # Replace PLT callers with functions from new binary 392 | import_callers = self.get_import_callers() 393 | exports = binary.get_hook_exports() 394 | exports = {v:k for (k, v) in exports.items()} # Flip addr:name to name:addr 395 | for name, addr in exports.items(): 396 | # Adjust address for new binary 397 | exports[name] += merged_code_base_addr 398 | 399 | self.replace_named_calls(self.text_section.file_offset, self.text_section.file_offset + self.text_section.size, import_callers, exports) 400 | 401 | class Merger: 402 | # TODO: Add support for multiple executable LOAD segments 403 | def __init__(self, paths, out_path): 404 | self.binaries = [Binary64(p) for p in paths] 405 | 406 | arch = self.binaries[0].arch 407 | if not all([binary.arch == arch for binary in self.binaries]): 408 | raise("Inconsistent arch in binaries!") 409 | 410 | # Setup the new merged file 411 | shutil.copy(self.binaries[0].path, out_path) 412 | self.new_binary = Binary64(out_path) 413 | 414 | def merge(self): 415 | # Merge loadable segments 416 | for binary in self.binaries[1:]: 417 | try: 418 | binary.binary.get_section('.got.plt') 419 | except: 420 | pass 421 | else: 422 | print("[!] Currently we do not support injecting binaries with .got.plt. Recompile it with -zrelro -znow") 423 | exit(1) 424 | 425 | offset = binary.plt_section.offset + PLT_STUB_SIZE # Skip PLT stub 426 | size = binary.plt_section.size - PLT_STUB_SIZE 427 | with open(binary.path, 'rb') as f: 428 | f.seek(offset) 429 | plt_code = f.read(size) 430 | 431 | self.new_binary.merge_symbols(binary) 432 | self.new_binary.merge_plt(plt_code, binary) 433 | self.new_binary.merge_binary_code(binary) 434 | 435 | print('[+] Done!') 436 | 437 | if __name__ == "__main__": 438 | # TODO: Add a check that we're on a good LIEF release 439 | # TODO: Add support for more than 2 binaries(?) 440 | 441 | parser = argparse.ArgumentParser(description='GLORYHook') 442 | parser.add_argument('file1', help='path to file to install hooks on') 443 | parser.add_argument('file2', help='file with gloryhooks') 444 | parser.add_argument('-o', '--output', help='output path', required=True) 445 | 446 | args = parser.parse_args() 447 | 448 | path1 = args.file1 449 | path2 = args.file2 450 | out_path = args.output 451 | 452 | if not (path.exists(path1) and path.exists(path2)): 453 | print("[!] ERR: One of the supplied input paths does not exist!") 454 | exit(1) 455 | 456 | print('[+] Beginning merge!') 457 | merger = Merger([path1, path2], out_path) 458 | md_arch = CS_MODE_64 if merger.binaries[0].arch == lief.ELF.ARCH.x86_64 else CS_MODE_32 459 | ks_arch = KS_MODE_64 if md_arch == CS_MODE_64 else KS_MODE_32 460 | md = Cs(CS_ARCH_X86, md_arch) 461 | ks = Ks(KS_ARCH_X86, ks_arch) 462 | md.detail = True 463 | merger.merge() 464 | 465 | -------------------------------------------------------------------------------- /hook.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | char *gloryhook_strrchr(const char *s, int c){ 6 | printf("STRRCHR HOOKED!\n"); 7 | return strrchr(s, c); 8 | } 9 | 10 | char *gloryhook_getenv(const char *name) { 11 | printf("GETENV HOOKED!\n"); 12 | return getenv(name); 13 | } 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | capstone==4.0.2 2 | keystone-engine==0.9.2 3 | -------------------------------------------------------------------------------- /usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsarpaul/GLORYHook/109c90d0be4c2bba9f4e3a03e2e41cb406bc57c1/usage.png --------------------------------------------------------------------------------