├── .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 |
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 | #include2 | #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 --------------------------------------------------------------------------------