1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
| """Convert an ELF64 PIE OEP memory dump back to file-time state."""
import argparse import os import struct from pathlib import Path
PT_LOAD = 1 PT_DYNAMIC = 2
DT_NULL = 0 DT_DEBUG = 21 DT_RELA = 7 DT_RELASZ = 8 DT_RELAENT = 9 DT_JMPREL = 23 DT_PLTRELSZ = 2
R_X86_64_GLOB_DAT = 6 R_X86_64_JUMP_SLOT = 7 R_X86_64_RELATIVE = 8
DYNAMIC_POINTER_TAGS = { 3, 4, 5, 6, 7, 12, 13, 17, 23, 25, 26, 32, 0x6FFFFEF5, 0x6FFFFFF0, 0x6FFFFFFC, 0x6FFFFFFE, }
def parse_program_headers(data): if data[:6] != b"\x7fELF\x02\x01": raise ValueError("input is not a little-endian ELF64 file")
phoff = struct.unpack_from("<Q", data, 0x20)[0] phentsize = struct.unpack_from("<H", data, 0x36)[0] phnum = struct.unpack_from("<H", data, 0x38)[0] if phentsize < 0x38 or not 0 < phnum < 0x1000: raise ValueError("invalid program header table")
return [ struct.unpack_from("<IIQQQQQQ", data, phoff + i * phentsize) for i in range(phnum) ]
def vaddr_to_offset(loads, vaddr, size=1): for ph in loads: file_offset, segment_vaddr, filesz = ph[2], ph[3], ph[5] if segment_vaddr <= vaddr and vaddr + size <= segment_vaddr + filesz: return file_offset + vaddr - segment_vaddr raise ValueError("virtual address 0x%x is not file-backed" % vaddr)
def repair_dynamic(data, phdrs, old_base): loads = [ph for ph in phdrs if ph[0] == PT_LOAD] image_min = min(ph[3] for ph in loads) image_max = max(ph[3] + ph[6] for ph in loads) runtime_min = old_base + image_min runtime_max = old_base + image_max
dynamic = next((ph for ph in phdrs if ph[0] == PT_DYNAMIC), None) if dynamic is None: raise ValueError("ELF has no PT_DYNAMIC")
dynamic_offset = vaddr_to_offset(loads, dynamic[3], dynamic[5]) values = {} changes = []
for relative in range(0, dynamic[5], 16): offset = dynamic_offset + relative tag, value = struct.unpack_from("<QQ", data, offset) if tag == DT_NULL: break
repaired = value reason = None if tag == DT_DEBUG and value != 0: repaired = 0 reason = "clear DT_DEBUG" elif tag in DYNAMIC_POINTER_TAGS and runtime_min <= value < runtime_max: repaired = value - old_base reason = "remove old load bias from dynamic pointer"
if repaired != value: struct.pack_into("<Q", data, offset + 8, repaired) changes.append((offset + 8, value, repaired, reason)) values[tag] = repaired
return loads, values, changes
def iter_rela(data, loads, vaddr, size, entry_size): if entry_size < 24 or size % entry_size: raise ValueError("invalid ELF64 RELA table") offset = vaddr_to_offset(loads, vaddr, size) for relative in range(0, size, entry_size): yield struct.unpack_from("<QQq", data, offset + relative)
def repair_relocation_targets(data, loads, dynamic): rela_vaddr = dynamic.get(DT_RELA) rela_size = dynamic.get(DT_RELASZ, 0) rela_entry = dynamic.get(DT_RELAENT, 24) if rela_vaddr is None: raise ValueError("ELF has no DT_RELA")
relocations = list(iter_rela(data, loads, rela_vaddr, rela_size, rela_entry))
jmprel_vaddr = dynamic.get(DT_JMPREL) jmprel_size = dynamic.get(DT_PLTRELSZ, 0) if jmprel_vaddr is not None and jmprel_size: relocations.extend( iter_rela(data, loads, jmprel_vaddr, jmprel_size, rela_entry) )
changes = [] for r_offset, r_info, r_addend in relocations: relocation_type = r_info & 0xFFFFFFFF try: target = vaddr_to_offset(loads, r_offset, 8) except ValueError: continue
current = struct.unpack_from("<Q", data, target)[0] if relocation_type == R_X86_64_RELATIVE: repaired = r_addend & 0xFFFFFFFFFFFFFFFF reason = "restore R_X86_64_RELATIVE addend" elif relocation_type in (R_X86_64_GLOB_DAT, R_X86_64_JUMP_SLOT): repaired = 0 reason = "clear resolved symbol slot" else: continue
if repaired != current: struct.pack_into("<Q", data, target, repaired) changes.append((target, current, repaired, reason)) return changes
def repair(input_path, output_path, old_base): data = bytearray(Path(input_path).read_bytes()) phdrs = parse_program_headers(data) loads, dynamic, dynamic_changes = repair_dynamic(data, phdrs, old_base) relocation_changes = repair_relocation_targets(data, loads, dynamic)
Path(output_path).write_bytes(data) os.chmod(output_path, 0o755)
for offset, old, new, reason in dynamic_changes + relocation_changes: print("0x%04x: 0x%x -> 0x%x (%s)" % (offset, old, new, reason)) print( "wrote %s: %d dynamic fixes, %d relocation-target fixes" % (output_path, len(dynamic_changes), len(relocation_changes)) )
def main(): parser = argparse.ArgumentParser() parser.add_argument("input") parser.add_argument("output") parser.add_argument( "old_base", type=lambda value: int(value, 0), help="load bias of the process from which the OEP dump was taken", ) args = parser.parse_args() repair(args.input, args.output, args.old_base)
if __name__ == "__main__": main()
|