├── .gitignore ├── ELFPatch ├── .gitignore ├── BasicELF │ ├── .gitignore │ ├── __init__.py │ ├── basicelf.py │ ├── constants.py │ ├── elfparse.py │ ├── elfstructs.py │ ├── segment.py │ ├── structs.py │ ├── types.py │ └── utils.py ├── __init__.py ├── chunk.py ├── chunk_manager.py ├── elfpatch.py ├── patch.py └── pwnasm.py ├── README.md ├── examples ├── ELFPatch ├── Makefile ├── test.c └── test.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/* 2 | *.pyc 3 | .vscode/* 4 | .gdb_history 5 | tests/* 6 | -------------------------------------------------------------------------------- /ELFPatch/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/* 2 | *.pyc 3 | -------------------------------------------------------------------------------- /ELFPatch/BasicELF/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/* 2 | *.pyc 3 | -------------------------------------------------------------------------------- /ELFPatch/BasicELF/__init__.py: -------------------------------------------------------------------------------- 1 | from .basicelf import BasicELF 2 | -------------------------------------------------------------------------------- /ELFPatch/BasicELF/basicelf.py: -------------------------------------------------------------------------------- 1 | from .elfstructs import * 2 | from . import structs as StructSkeletons 3 | from .elfparse import ELFParse 4 | from .constants import * 5 | from construct import * 6 | from .segment import * 7 | from .utils import page_start, page_end 8 | 9 | class BasicELF: 10 | def __init__(self, ELFFile): 11 | with open(ELFFile, "rb") as f: 12 | self.rawelf = bytearray(f.read()) 13 | 14 | self._init_structs() 15 | 16 | #Parse the ELF with the specific structs 17 | self.elf = ELFParse(self._structs, self.rawelf) 18 | 19 | self._added_segments = [] 20 | self._phdr_fixed = False 21 | # self._new_phdr_offset = None 22 | 23 | def write_file(self, filename): 24 | self._update_raw_elf() 25 | with open(filename, "wb") as f: 26 | f.write(self.rawelf) 27 | 28 | def new_segment(self, content=b"", flags=PT_R|PT_W|PT_X, type=PT_LOAD, size=None, align=0x1000, virtual_address=None, physical_off=None): 29 | if not self._phdr_fixed: 30 | self._phdr_fixed = True 31 | self._fix_phdr() 32 | 33 | if physical_off is not None and virtual_address is not None: 34 | physical_offset, virtual_addr = physical_off, virtual_address 35 | elif virtual_address is None and physical_off is not None: 36 | #TODO: Implement physical to virtual generation 37 | raise Exception("Cannot generate an segment with only physical offset") 38 | elif virtual_address is not None: 39 | physical_offset, virtual_addr = self._generate_physical_offset_for_virtual(virtual_address), virtual_address 40 | else: 41 | physical_offset, virtual_addr = self._generate_virtual_physical_offset_pair() 42 | 43 | if size is None: 44 | size = len(content)+0x10 45 | 46 | #Create a raw segment struct using the Container from construct 47 | segment_struct = Container(p_type=type, p_flags=flags, p_offset=physical_offset, p_vaddr=virtual_addr,p_paddr=virtual_addr, p_filesz=size, p_memsz=size, p_align=align) 48 | 49 | self.elf.phdr_table.append(segment_struct) 50 | self.elf.ehdr.e_phnum += 1 51 | 52 | #Create a new segment class, but this time for segment addition 53 | segment_to_add = Segment(physical_offset, virtual_addr, size, flags=flags, align=align, content=content) 54 | self._added_segments.append(segment_to_add) 55 | 56 | return segment_to_add 57 | 58 | #Helper function to translate a virtual address to physical address 59 | def virtual_to_physical(self, virtual_address): 60 | for segment in self.elf.phdr_table: 61 | if virtual_address >= segment.p_vaddr and virtual_address < segment.p_vaddr+segment.p_memsz: 62 | address_offset = virtual_address - segment.p_vaddr 63 | physical_offset = address_offset + segment.p_offset 64 | return physical_offset 65 | 66 | #Weird issue with WSL is that it won't load overlapping segments even if they overlap only on the alignment (Linux kernel doesn't give a fuck if segments overlap, it just overwrites them) 67 | #if you have one segment from 0x0000 to 0x2000 with a 0x4000 alignment, WSL won't load a 2nd segment at 0x3000 68 | #Easy fix for that is to just change the alignment to 0x1000 (PAGE_SIZE) for every segment since that's what the kernel loads it as default 69 | # def fix_WSL(self): 70 | # for phdr_entry in self.elf.phdr_table: 71 | # if phdr_entry.p_type == PT_LOAD: 72 | # phdr_entry.p_align = 0x1000 73 | 74 | # entry2 = self.elf.phdr_table[3] 75 | # entry_last = self.elf.phdr_table[-2] 76 | # self.elf.phdr_table[3] = entry_last 77 | # self.elf.phdr_table[-2] = entry2 78 | 79 | #Basically due to the weirdness of the loader and the kernel, the kernel believes the PHDR entry in memory would be at "FIRST_LOAD_SEGMENT + e_phoff", forwarding it to the loader, which is totally bizzare... Like what's the point of PHDR entry in the PHDR itself then? 80 | #Anyways, to cope with that, we try finding the smallest physical offset when loaded with the first segment itself would not conflict with any other segment's virtual addresses. It's a hacky approach but it works, so whatever... 81 | #Essentially we increase the size of the first loaded segment, so it loads the whole binary and then we change the PHDR and shit.... 82 | #Even though the next segments might overlap with the first one (and overwrite), we only care that they don't overlap at the PHDR entry (and end up overwriting that) 83 | 84 | #UPDATE: Figured out a better way to do it without the overlapping segments byjust creating another LOAD segment at the end and aligning the FIRST_LOAD_SEGMENT + e_phoff with the new load segment 85 | 86 | #Basically we are trying to make the physical offset and the virtual address of EHDR match up so that e_phoff - FIRST_SEG.paddr == vaddr_of_phoff - FIRST_SEG.vaddr 87 | 88 | def _fix_phdr(self): 89 | # Just an optimization, Try to find empty space between segments to hold the phdr and move it there 90 | size, physical_offset, virtual_addr, segment_ref = self._find_unused_space() 91 | if size >= 0x300: 92 | segment_ref.p_memsz += size 93 | segment_ref.p_filesz += size 94 | self._fix_pdhr_entry(physical_offset, virtual_addr, size=size) 95 | return 96 | 97 | #If it's not a dynamic binary, then we don't have the loader issue. We can just add a new segment for PHDR and load it there 98 | #Turns out LIBC_TLS also does the same thing as the kernel, which I explained above, so we can't do the special case for non dynamic libc 99 | # if not self._is_dynamic(): 100 | #The reason I'm adding a new segment so that other segments after it don't end up taking the mem of the phdr 101 | # new_phdr = self.new_segment(size=0x1000, flags=PT_R|PT_W) 102 | # self._fix_pdhr_entry(new_phdr.physical_offset, new_phdr.virtual_address) 103 | # return 104 | 105 | #If large empty not available, then create a new segment (described in the comment above the function) 106 | #The hack commented out, figured out a better way 107 | # size_for_load_segment = physical_offset + 0x500 108 | 109 | #fix size for the first segment 110 | # for seg in self.elf.phdr_table: 111 | # #only for the first segment 112 | # if seg.p_type == PT_LOAD: 113 | # seg.p_filesz = size_for_load_segment 114 | # seg.p_memsz = size_for_load_segment + 0x10 115 | # break 116 | 117 | physical_offset, virtual_addr = self._find_non_conflicting_address_pair_for_phdr() 118 | 119 | new_phdr = self.new_segment(size=0x1000, flags=PT_R|PT_W, virtual_address=virtual_addr, physical_off=physical_offset) 120 | 121 | self._fix_pdhr_entry(physical_offset, virtual_addr) 122 | 123 | # self._new_phdr_offset = physical_offset + 0x500 124 | 125 | #A function to find unused space between segments that can be used to hold the PHDR (should be > 0x500) 126 | def _find_unused_space(self): 127 | all_load_segs = [X for X in self.elf.phdr_table if X.p_type == PT_LOAD] 128 | first_seg = all_load_segs[0] 129 | all_load_segs.sort(key=(lambda X:X.p_offset)) 130 | 131 | largest_unused_space, poff, vaddr, segment_ref = -1, -1, -1, None 132 | #Essentially go through all the segments in sorted order and see if they have space in between (expects sane 133 | #non overlapping segments 134 | for idx in range(len(all_load_segs)-1): 135 | current_segment = all_load_segs[idx] 136 | current_segment_end_poff = current_segment.p_offset + current_segment.p_memsz 137 | current_segment_end_vaddr = current_segment.p_vaddr + current_segment.p_memsz 138 | 139 | next_segment = all_load_segs[idx+1] 140 | 141 | current_unused_space = next_segment.p_offset - current_segment_end_poff 142 | 143 | #Check if satisifes the check of being the correct offset away from the first load load segment 144 | #As explained in the comment above the _fix_phdr function 145 | is_viable_for_phdr = (current_segment_end_poff - first_seg.p_offset) == (current_segment_end_vaddr - first_seg.p_vaddr) 146 | 147 | if is_viable_for_phdr and (current_unused_space > largest_unused_space): 148 | largest_unused_space = current_unused_space 149 | poff = current_segment_end_poff 150 | vaddr = current_segment_end_vaddr 151 | segment_ref = current_segment 152 | return largest_unused_space, poff, vaddr, segment_ref 153 | 154 | #Basically look for the smallest no conflicting address pair which can all be loaded as a part of the first segment 155 | def _find_non_conflicting_address_pair_for_phdr(self): 156 | all_load_segs = [X for X in self.elf.phdr_table if X.p_type == PT_LOAD] 157 | 158 | #The closest physical offset we can use 159 | closest_phy_offset = self._generate_physical_offset() 160 | #The base of the first LOAD segment 161 | virtual_base = all_load_segs[0].p_vaddr 162 | 163 | #The minimum virtual address we would need to load upto to get the PHDR address in the first LOAD segment 164 | virtual_min_addr = virtual_base + closest_phy_offset 165 | 166 | while self._is_conflicting_for_phdr(virtual_min_addr) or self._is_conflicting_for_phdr(virtual_min_addr+0x1000): 167 | #Just go to the next page boundry 168 | virtual_min_addr = page_end(virtual_min_addr) 169 | 170 | return virtual_min_addr - virtual_base, virtual_min_addr 171 | 172 | def _generate_physical_offset_for_virtual(self, virtual_address): 173 | virtual_offset_needed = virtual_address & 0xfff 174 | 175 | closest_physical = self._generate_physical_offset() 176 | 177 | if closest_physical&0xfff > virtual_offset_needed: 178 | closest_physical = (closest_physical & -0x1000) + 0x1000 + virtual_offset_needed 179 | else: 180 | closest_physical = (closest_physical & -0x1000) + virtual_offset_needed 181 | 182 | return closest_physical 183 | 184 | def _is_conflicting_for_phdr(self, address): 185 | #all load segments except the first one 186 | all_load_segs = [X for X in self.elf.phdr_table if X.p_type == PT_LOAD][1:] 187 | address = page_start(address) 188 | for seg in all_load_segs: 189 | #Just check if it's conflicting 190 | #using page_start because the there can be case when p_vaddr can be > address in the middle of the page 191 | if address >= page_start(seg.p_vaddr) and address <= page_start(seg.p_vaddr + seg.p_memsz): 192 | return True 193 | 194 | return False 195 | 196 | 197 | def _is_dynamic(self): 198 | for seg in self.elf.phdr_table: 199 | if seg.p_type == PT_INTERP: 200 | return True 201 | return False 202 | 203 | #Basically move the phdr to the bottom to make more space 204 | def _fix_pdhr_entry(self, offset, virt_addr, size=0x1000): 205 | self.elf.ehdr.e_phoff = offset 206 | 207 | for entry in self.elf.phdr_table: 208 | if entry.p_type == PT_PHDR: 209 | #Set the binary offset 210 | entry.p_offset = self.elf.ehdr.e_phoff 211 | #Set the virtual addresses 212 | entry.p_vaddr = virt_addr 213 | entry.p_paddr = virt_addr 214 | #Set the size to a large-ish number 215 | entry.p_memsz = size 216 | entry.p_filesz = size 217 | 218 | break 219 | 220 | def _generate_virtual_physical_offset_pair(self): 221 | physical_offset = self._generate_physical_offset() 222 | virtual_offset = self._generate_virtual_offset() + (physical_offset & (0xfff)) 223 | #The binary is mapped in chunks of 0x1000 (the chunk which includes our physical offset will be mapped), so the LSBs of physical address and virtual offset should match. 224 | #So that out virt address falls in the right location when mapped as a whole chunk 225 | 226 | return physical_offset, virtual_offset 227 | 228 | 229 | def _generate_physical_offset(self): 230 | if len(self._added_segments) == 0: 231 | # if self._new_phdr_offset is not None: 232 | # return self._new_phdr_offset+0x10 233 | return (len(self.rawelf) & -0x10) + 0x10 234 | 235 | return ((self._added_segments[-1].physical_offset + self._added_segments[-1].size) & -0x10) + 0x10 236 | 237 | def _generate_virtual_offset(self): 238 | PAGE_SIZE = 0x1000 239 | 240 | #Get max virtual address mapped 241 | max_addr = max(self.elf.phdr_table, key=(lambda entry: (entry.p_vaddr + entry.p_memsz) if entry.p_type == PT_LOAD else 0)) 242 | 243 | return (((max_addr.p_vaddr + max_addr.p_memsz) & -PAGE_SIZE) + PAGE_SIZE) 244 | 245 | def _init_structs(self): 246 | if self.rawelf[4] == 0x1: #4th byte identifies the ELF type 247 | self._bits = 32 248 | elif self.rawelf[4] == 0x2: 249 | self._bits = 64 250 | else: 251 | raise Exception("Not a valid 32/64 bit ELF") 252 | 253 | self._structs = ELFStructs() 254 | #Initialize the structures used based on the bitsize so we don't have to look them up everytime 255 | if self._bits == 32: 256 | self._structs.Elf_ehdr = StructSkeletons.Elf32_Ehdr 257 | self._structs.Elf_phdr = StructSkeletons.Elf32_Phdr 258 | else: 259 | self._structs.Elf_ehdr = StructSkeletons.Elf64_Ehdr 260 | self._structs.Elf_phdr = StructSkeletons.Elf64_Phdr 261 | 262 | def _update_raw_elf(self): 263 | # write the ELF header 264 | self.rawelf[0:self._structs.Elf_ehdr.sizeof()] = self._structs.Elf_ehdr.build(self.elf.ehdr) 265 | 266 | #add the new updated LOAD segment's data in the ELF 267 | for segment in self._added_segments: 268 | segment_end = (segment.physical_offset + segment.size) 269 | if segment_end > len(self.rawelf): 270 | self.rawelf += b"\x00" * (segment_end - len(self.rawelf) + 1) 271 | self.rawelf[segment.physical_offset:segment.physical_offset+len(segment.content)] = segment.content 272 | 273 | # self._added_segments = [] 274 | 275 | #get location of Phdr table (segement table) and size 276 | phdr_offset = self.elf.ehdr.e_phoff 277 | phdr_size = self.elf.ehdr.e_phnum * self.elf.ehdr.e_phentsize 278 | phdr_end = phdr_offset + phdr_size 279 | 280 | #Check if Phdr table is after the end of the binary, and pad it with nulls if it is 281 | if phdr_end > len(self.rawelf): 282 | self.rawelf += b"\x00" * (phdr_end - len(self.rawelf)) 283 | 284 | #Due to the limitations of the construct library (Not dynamic arrays), I create a new phdr_table struct to finally serialize the phdr in raw bytes 285 | self.rawelf[phdr_offset:phdr_end] = Array(self.elf.ehdr.e_phnum, self._structs.Elf_phdr).build(self.elf.phdr_table) 286 | 287 | 288 | 289 | -------------------------------------------------------------------------------- /ELFPatch/BasicELF/constants.py: -------------------------------------------------------------------------------- 1 | PT_NULL = 0 2 | PT_LOAD = 1 3 | PT_DYNAMIC = 2 4 | PT_INTERP = 3 5 | PT_NOTE = 4 6 | PT_SHLIB = 5 7 | PT_PHDR = 6 8 | PT_TLS = 7 9 | PT_LOOS = 0x60000000 10 | PT_HIOS = 0x6fffffff 11 | PT_LOPROC = 0x70000000 12 | PT_HIPROC = 0x7fffffff 13 | PT_GNU_EH_FRAME = 0x6474e550 14 | PT_GNU_STACK = PT_LOOS + 0x474e551 15 | 16 | PT_X = 0x1 17 | PT_W = 0x2 18 | PT_R = 0x4 19 | -------------------------------------------------------------------------------- /ELFPatch/BasicELF/elfparse.py: -------------------------------------------------------------------------------- 1 | from construct import Array 2 | 3 | #The parsing of the ELF with the specific sized structs 4 | class ELFParse: 5 | def __init__(self, elf_structs, data): 6 | 7 | self.ehdr = elf_structs.Elf_ehdr.parse(data) 8 | self.phdr_table = Array(self.ehdr.e_phnum, elf_structs.Elf_phdr).parse(data[self.ehdr.e_phoff:]) 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ELFPatch/BasicELF/elfstructs.py: -------------------------------------------------------------------------------- 1 | 2 | class ELFStructs: 3 | def __init__(self, ehdr=None, phdr=None): 4 | self.Elf_ehdr = ehdr 5 | self.Elf_phdr = phdr 6 | -------------------------------------------------------------------------------- /ELFPatch/BasicELF/segment.py: -------------------------------------------------------------------------------- 1 | from .constants import * 2 | 3 | class Segment: 4 | def __init__(self, physical_offset, virtual_address, size, flags=PT_R|PT_W|PT_X, align=0x10, content=b""): 5 | if len(content) > size: 6 | raise Exception("Content is larger than size") 7 | 8 | self.physical_offset = physical_offset 9 | self.virtual_address = virtual_address 10 | self.flags = flags 11 | self.size = size 12 | self.align = align 13 | self.content = bytearray(content) 14 | 15 | def update_data(self, content, start_offset=None, end_offset=None): 16 | """ 17 | Update content between start and end offset 18 | if None are provided, update from 0 19 | """ 20 | if len(content) > self.size: 21 | raise Exception("Content is larger than size") 22 | 23 | if start_offset is None: 24 | start_offset = 0 25 | if end_offset is None: 26 | end_offset = start_offset+len(content) 27 | 28 | #Check if the end offset is greater than current content 29 | if end_offset > len(self.content): 30 | self.content += (end_offset - len(self.content)) * b"\x00" 31 | 32 | self.content[start_offset:end_offset] = content 33 | 34 | -------------------------------------------------------------------------------- /ELFPatch/BasicELF/structs.py: -------------------------------------------------------------------------------- 1 | from construct import * 2 | from .types import * 3 | 4 | #32 bit header struct 5 | 6 | Elf32_Ehdr = Struct( 7 | 'e_ident' / Struct( 8 | "MAGIC"/Const(b"\x7fELF"), #ELF Header 9 | 'EI_CLASS'/ Int8ul, 10 | "OTHER_STUFF"/ Array(11, Int8ul) 11 | ), 12 | 'e_type' / Elf32_Half, 13 | 'e_machine' / Elf32_Half, 14 | 'e_version' / Elf32_Word, 15 | 'e_entry' / Elf32_Addr, 16 | 'e_phoff' / Elf32_Off, 17 | 'e_shoff' / Elf32_Off, 18 | 'e_flags' / Elf32_Word, 19 | 'e_ehsize' / Elf32_Half, 20 | 'e_phentsize' / Elf32_Half, 21 | 'e_phnum' / Elf32_Half, 22 | 'e_shentsize' / Elf32_Half, 23 | 'e_shnum' / Elf32_Half, 24 | 'e_shstrndx' / Elf32_Half 25 | ) 26 | #32 bit Segment (program) header struct 27 | Elf32_Phdr = Struct( 28 | 'p_type' / Elf32_Word, 29 | 'p_offset' / Elf32_Off, 30 | 'p_vaddr' / Elf32_Addr, 31 | 'p_paddr' / Elf32_Addr, 32 | 'p_filesz' / Elf32_Word, 33 | 'p_memsz' / Elf32_Word, 34 | 'p_flags' / Elf32_Word, 35 | 'p_align' / Elf32_Word 36 | ) 37 | 38 | 39 | Elf64_Ehdr = Struct( 40 | 'e_ident' / Struct( 41 | "MAGIC"/Const(b"\x7fELF"), #ELF Header 42 | 'EI_CLASS'/ Int8ul, 43 | "OTHER_STUFF"/ Array(11, Int8ul) 44 | ), 45 | 'e_type' / Elf64_Half, 46 | 'e_machine' / Elf64_Half, 47 | 'e_version' / Elf64_Word, 48 | 'e_entry' / Elf64_Addr, 49 | 'e_phoff' / Elf64_Off, 50 | 'e_shoff' / Elf64_Off, 51 | 'e_flags' / Elf64_Word, 52 | 'e_ehsize' / Elf64_Half, 53 | 'e_phentsize' / Elf64_Half, 54 | 'e_phnum' / Elf64_Half, 55 | 'e_shentsize' / Elf64_Half, 56 | 'e_shnum' / Elf64_Half, 57 | 'e_shstrndx' / Elf64_Half 58 | ) 59 | 60 | 61 | Elf64_Phdr = Struct( 62 | 'p_type' / Elf64_Word, 63 | 'p_flags' / Elf64_Word, 64 | 'p_offset' / Elf64_Off, 65 | 'p_vaddr' / Elf64_Addr, 66 | 'p_paddr' / Elf64_Addr, 67 | 'p_filesz' / Elf64_Xword, 68 | 'p_memsz' / Elf64_Xword, 69 | 'p_align' / Elf64_Xword 70 | ) 71 | 72 | 73 | -------------------------------------------------------------------------------- /ELFPatch/BasicELF/types.py: -------------------------------------------------------------------------------- 1 | from construct import * 2 | 3 | #32 bit ELF struct type definitions 4 | Elf32_Addr = Int32ul 5 | Elf32_Half = Int16ul 6 | Elf32_Off = Int32ul 7 | Elf32_Sword = Int32sl 8 | Elf32_Word = Int32ul 9 | 10 | #64 bit ELF struct definitions 11 | Elf64_Addr = Int64ul 12 | Elf64_Half = Int16ul 13 | Elf64_SHalf = Int16sl 14 | Elf64_Off = Int64ul 15 | Elf64_Sword = Int32sl 16 | Elf64_Word = Int32ul 17 | Elf64_Xword = Int64ul 18 | Elf64_Sxword = Int64sl 19 | -------------------------------------------------------------------------------- /ELFPatch/BasicELF/utils.py: -------------------------------------------------------------------------------- 1 | 2 | def page_start(address): 3 | return (address & -0x1000) 4 | 5 | def page_end(address): 6 | return (address & -0x1000)+0x1000 7 | -------------------------------------------------------------------------------- /ELFPatch/__init__.py: -------------------------------------------------------------------------------- 1 | from .elfpatch import ELFPatch 2 | -------------------------------------------------------------------------------- /ELFPatch/chunk.py: -------------------------------------------------------------------------------- 1 | 2 | class Chunk: 3 | """ 4 | The chunk class 5 | Will be basically used act as proxy to points between segments 6 | So we can have a chunk in the middle of a segment, to preserve virtual address space as I don't want to create new segments for every patch 7 | """ 8 | def __init__(self, segment, start_offset, size): 9 | self._segment = segment 10 | self._start_offset = start_offset 11 | self._size = size 12 | self.flags = segment.flags 13 | 14 | self._virtual_address = segment.virtual_address + start_offset 15 | 16 | #@ property's just to stay up to date with new python features 17 | @property 18 | def size(self): 19 | return self._size 20 | 21 | @property 22 | def virtual_address(self): 23 | return self._virtual_address 24 | 25 | @property 26 | def content(self): 27 | return self._segment.content 28 | 29 | @content.setter 30 | def content(self, new_content): 31 | self.update_data(new_content) 32 | 33 | def update_data(self, content): 34 | if len(content) > self._size: 35 | raise Exception("Content larger than size") 36 | self._segment.update_data(content, start_offset=self._start_offset) 37 | 38 | -------------------------------------------------------------------------------- /ELFPatch/chunk_manager.py: -------------------------------------------------------------------------------- 1 | from .chunk import Chunk 2 | 3 | class ChunkManager: 4 | """ 5 | The class used to manage a Segment and dispatch chunks of appropriate size 6 | """ 7 | def __init__(self, segment): 8 | self.managed_segment = segment 9 | self.max_size = segment.size 10 | self.used_size = len(segment.content) 11 | self.left_size = self.max_size - self.used_size 12 | 13 | def new_chunk(self, size=None,flags=None, content=b""): 14 | if size is None: 15 | size = len(content) 16 | 17 | if size > self.left_size: 18 | raise Exception("Size unavailable") 19 | 20 | if flags is None: 21 | flags = self.managed_segment.flags 22 | 23 | if flags != self.managed_segment.flags: 24 | raise Exception("Flags mismatch") 25 | 26 | new_chunk = Chunk(self.managed_segment, start_offset=self.used_size, size=size) 27 | 28 | self.used_size += size 29 | self.left_size -= size 30 | 31 | return new_chunk 32 | 33 | -------------------------------------------------------------------------------- /ELFPatch/elfpatch.py: -------------------------------------------------------------------------------- 1 | from .BasicELF import BasicELF 2 | from .BasicELF.constants import * 3 | from .chunk_manager import ChunkManager 4 | from .patch import Patch 5 | from .pwnasm import PwnAssembler, PwnDisassembler 6 | from keystone import * 7 | from capstone import * 8 | 9 | class ELFPatch(BasicELF): 10 | 11 | def __init__(self, ELFFile): 12 | super().__init__(ELFFile) 13 | self._chunks = [] 14 | self.patches = [] 15 | 16 | #Super hacky, only supporting x86 or x64 for now 17 | #TODO: Make it modular and support more architectures 18 | if self._bits == 32: 19 | self.assembler = PwnAssembler(KS_ARCH_X86, KS_MODE_32) 20 | self.disassembler = PwnDisassembler(CS_ARCH_X86, CS_MODE_32) 21 | else: 22 | self.assembler = PwnAssembler(KS_ARCH_X86, KS_MODE_64) 23 | self.disassembler = PwnDisassembler(CS_ARCH_X86, CS_MODE_64) 24 | 25 | 26 | def new_chunk(self, size=None, content=b"", flags='rwx'): 27 | if size is None: 28 | size = len(content) 29 | 30 | flags = self._translate_flags(flags) 31 | 32 | for chunk in self._chunks: 33 | #Do a try catch because Chunk manager creates exceptions when sizes or flags don't match 34 | try: 35 | new_chunk = chunk.new_chunk(size=size, flags=flags, content=content) 36 | #If no exception, then succeded, return 37 | return new_chunk 38 | #errorn in adding chunk, pass 39 | except Exception as e: 40 | # print(e) 41 | pass 42 | 43 | #No chunks could satisfy the request 44 | 45 | page_aligned_size = (size & -0x1000) + 0x1000 46 | #Page aligned size so we can use it wisely for future chunks too and do not end up wasting virtual address space 47 | 48 | new_segment = self.new_segment(size=page_aligned_size, flags=flags) 49 | 50 | managed_chunk = ChunkManager(new_segment) 51 | self._chunks.append(managed_chunk) 52 | 53 | return managed_chunk.new_chunk(size=size, content=content, flags=flags) 54 | 55 | def new_patch(self, virtual_address, size=None, content=b"", append_jump_back=True, append_original_instructions=True): 56 | if size is None: 57 | size = len(content) 58 | 59 | physical_offset = self.virtual_to_physical(virtual_address) 60 | 61 | chunk_for_patch = self.new_chunk(size+0x10) #Extra size cuz we might need to append a jump back 62 | 63 | jump_to_chunk = self.assembler.assemble("jmp {}".format(chunk_for_patch.virtual_address), offset=virtual_address) 64 | 65 | size_of_jump = len(jump_to_chunk) 66 | 67 | overwritten_instructions = b"" 68 | #Basically start disasemling from the address where we have to patch to the point where we can overwrite it with jmp instruction and not fuck adjacent instructions 69 | #cuz x86 is not fixed length 70 | #Then we will pad the jmp with NOPs to take exactly as much as a full disassembled instruction so that we don't have broken instructions 71 | for instr in self.disassembler.disassemble(self.rawelf[physical_offset:], offset=virtual_address): 72 | overwritten_instructions += instr.bytes 73 | #If we have disassembled enough 74 | if len(overwritten_instructions) >= size_of_jump: 75 | break 76 | #Pad it 77 | jump_to_chunk += b"\x90"*(len(overwritten_instructions) - size_of_jump) 78 | 79 | new_patch = Patch(chunk_for_patch, virtual_address, patched_jump=jump_to_chunk, assembler=self.assembler, append_jump_back=append_jump_back, append_original_instructions=append_original_instructions, original_instructions=overwritten_instructions) 80 | 81 | self.patches.append(new_patch) 82 | return new_patch 83 | 84 | #Override the original _update_raw_elf to now add the patches in the rawelf 85 | 86 | def _translate_flags(self, flags): 87 | new_flags = 0 88 | if 'r' in flags or "R" in flags: 89 | new_flags |= PT_R 90 | if 'w' in flags or "W" in flags: 91 | new_flags |= PT_W 92 | if 'x' in flags or "X" in flags: 93 | new_flags |= PT_X 94 | 95 | return new_flags 96 | 97 | 98 | def _update_raw_elf(self): 99 | #Call the original update to get the segments added 100 | super()._update_raw_elf() 101 | 102 | for patch in self.patches: 103 | physical_offset_patch = self.virtual_to_physical(patch.virtual_address) 104 | self.rawelf[physical_offset_patch:physical_offset_patch+len(patch.patched_jump)] = patch.patched_jump 105 | 106 | -------------------------------------------------------------------------------- /ELFPatch/patch.py: -------------------------------------------------------------------------------- 1 | 2 | class Patch: 3 | """ 4 | The class which manages the patch 5 | By the time this is called, the jmp to the virtual address would already have been added in rawelf 6 | """ 7 | 8 | def __init__(self, chunk, virtual_address, patched_jump, assembler=None, append_jump_back=True, append_original_instructions=True, original_instructions=b""): 9 | self.chunk = chunk #The chunk for the patch 10 | self._virtual_address = virtual_address #The virtual address of where we are patching 11 | self.patched_jump = patched_jump #The jump instruction which will overwrite the instructions at the patch, Padded to NOPs to make sure we don't have invalid instructions 12 | self.append_jump_back = append_jump_back 13 | self.append_original_instructions = append_original_instructions 14 | self.original_instructions = original_instructions #The original instructions being overwritten 15 | self.assembler = assembler #The asembler for current architecture 16 | 17 | #@ property's just to stay up to date with new python features 18 | @property 19 | def content(self): 20 | return self.chunk.content 21 | 22 | @content.setter 23 | def content(self, new_content): 24 | self.update_patch(new_content) 25 | 26 | @property 27 | def virtual_address(self): 28 | return self._virtual_address 29 | 30 | def update_patch(self, content=b""): 31 | #If we have to append the original instructions we overwrote 32 | if self.append_original_instructions: 33 | content += self.original_instructions 34 | 35 | #If we have to append a jump back to the original code 36 | if self.append_jump_back: 37 | executing_virtual_address = self.chunk.virtual_address + len(content) 38 | #We want to jmp back to after the jump 39 | jump_back_address = self.virtual_address + len(self.patched_jump) 40 | content += self.assembler.assemble("jmp {}".format(jump_back_address), offset=executing_virtual_address) 41 | 42 | self.chunk.update_data(content) 43 | 44 | -------------------------------------------------------------------------------- /ELFPatch/pwnasm.py: -------------------------------------------------------------------------------- 1 | from keystone import * 2 | from capstone import * 3 | 4 | class PwnAssembler: 5 | def __init__(self, arch, mode): 6 | self.arch = arch 7 | self.mode = mode 8 | 9 | self.assembler = Ks(arch, mode) 10 | 11 | def assemble(self, data, offset=0): 12 | return bytes(self.assembler.asm(data, offset)[0]) 13 | 14 | class PwnDisassembler: 15 | def __init__(self, arch, mode): 16 | self.arch = arch 17 | self.mode = mode 18 | 19 | self.disassembler = Cs(arch, mode) 20 | 21 | def disassemble(self, data, offset=0x0): 22 | return self.disassembler.disasm(data, offset) 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ELFPatch 2 | 3 | A library to manipulate and patch ELFs with dynamically sized patches. 4 | 5 | # Why 6 | 7 | Mainly for CTFs and blackbox fuzzing. There have been times where I've wanted to patch ELFs but not enough space was available to do it inline, which is why this was created. 8 | 9 | I've tried using few other ELF patching programs, but none of them fit my needs/worked on my usecases. 10 | 11 | # How 12 | 13 | The process of adding a patch briefly boils down to the following: 14 | 15 | - New segments are added that hold a patch. 16 | - To add new segments, the segment table is first moved to the end of the binary. 17 | - The code at the patch address is replaced with a jump to the newly added segment. 18 | - At the end of the patch, it jumps back to the original address. 19 | 20 | ### Issues faced 21 | 22 | - Moving the segment table to the end was a huge hassle because of the diversity in ELF loaders. 23 | - Some binaries loaded with ld.so but broke with kernel's loader and vice versa. 24 | - It turns out some worked with overlapping segments which others absolutely hated it. 25 | - And a lot more weird quirks 26 | 27 | # Support 28 | 29 | Currently only supports x86/64, but it shouldn't be hard to extend it to other architectures (only need to modify the assembler directives). I'll add other architectures when I get time. 30 | 31 | # Bugs/issues 32 | 33 | It's still in beta, so any issues and bugs are welcome. 34 | 35 | # Documentation 36 | 37 | Sorry, there's no documentation available yet. You can read the API below or look at the examples directory. For a little more complicated example, look at the debugging section of this [blogpost](http://blog.perfect.blue/Hack-A-Sat-CTF-2020-Launch-Link). 38 | 39 | # API 40 | 41 | Credits to @LevitatingLion for this. 42 | 43 | ```python 44 | class ELFPatch: # The main patcher 45 | 46 | def __init__(self, file_or_path): 47 | ... 48 | 49 | def new_chunk(self, size, prot='rwx', align=0x1) -> Chunk: 50 | ... 51 | 52 | def new_patch(self, virtual_address, size=None, content=b"", append_jump_back=True, append_original_instructions=True) -> Patch: 53 | ... 54 | 55 | def write_file(self, filename): #writes patched ELF to file 56 | ... 57 | 58 | class Patch: # The actual patch object 59 | 60 | @property 61 | def chunk(self) -> Chunk: 62 | ... 63 | 64 | @property 65 | def size(self) -> int: 66 | ... 67 | 68 | @property 69 | def content(self) -> bytes: 70 | ... 71 | 72 | @content.setter 73 | def content(self, new_content): 74 | ... 75 | 76 | class Chunk: #raw memory chunk for anything 77 | 78 | @property 79 | def virtual_address(self) -> int: 80 | ... 81 | 82 | @property 83 | def size(self) -> int: 84 | ... 85 | 86 | @property 87 | def content(self) -> bytes: 88 | ... 89 | 90 | @content.setter 91 | def content(self, new_content): 92 | ... 93 | 94 | ``` 95 | -------------------------------------------------------------------------------- /examples/ELFPatch: -------------------------------------------------------------------------------- 1 | ../ELFPatch -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | CXX = gcc 2 | all: test 3 | 4 | test: test.c 5 | $(CXX) -o test test.c 6 | 7 | clean: 8 | rm test 9 | 10 | -------------------------------------------------------------------------------- /examples/test.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() 4 | { 5 | puts("It works"); 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /examples/test.py: -------------------------------------------------------------------------------- 1 | from ELFPatch import ELFPatch 2 | 3 | f = ELFPatch(b"./test") 4 | 5 | new_patch = f.new_patch(virtual_address=f.elf.ehdr.e_entry, size=0x100, append_original_instructions=True, append_jump_back=True) 6 | 7 | new_patch.update_patch(f.assembler.assemble(""" 8 | push rax 9 | push rdi 10 | push rsi 11 | push rdx 12 | jmp get_str 13 | 14 | make_syscall: 15 | pop rsi 16 | 17 | ;call 0x1030 (relative address for puts) 18 | 19 | mov rax, 0x1 20 | mov rdi, 0x1 21 | mov rdx, 12 22 | syscall 23 | jmp end 24 | 25 | get_str: 26 | call make_syscall 27 | .string "SICED PATCH\\n" 28 | 29 | end: 30 | pop rdx 31 | pop rsi 32 | pop rdi 33 | pop rax 34 | """,offset=new_patch.chunk.virtual_address)) 35 | 36 | 37 | print("New Patch at", hex(new_patch.chunk.virtual_address)) 38 | f.write_file("./out") 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | capstone 2 | keystone-engine 3 | construct 4 | --------------------------------------------------------------------------------