├── LICENSE ├── README.md ├── requirements.txt └── windows_memory_patches.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 theresponder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Memory Patch Detector 2 | Detects code differentials between executables in disk and the corresponding processes/modules in memory 3 | 4 | ## Requirements 5 | pip install -r requirements.txt 6 | 7 | ## Usage 8 | python windows_memory_patches.py 9 | 10 | ## Notes 11 | The script needs Administrator/SYSTEM privileges in order to analyze all the processes in memory. 12 | At the moment, it doesn't check WoW64 processes at all. 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pefile==2019.4.18 2 | capstone==4.0.1 3 | psutil==5.6.6 4 | winappdbg==1.5 -------------------------------------------------------------------------------- /windows_memory_patches.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | import capstone 4 | import pefile 5 | import psutil 6 | import winappdbg 7 | 8 | NTDLL = ctypes.windll.ntdll 9 | KERNEL32 = ctypes.windll.kernel32 10 | PSAPI = ctypes.windll.psapi 11 | IMAGE_SCN_MEM_EXECUTE = 0x20000000 12 | 13 | # Change address size by system architecture 14 | if winappdbg.System.bits == 64: 15 | PTR = ctypes.c_uint64 16 | else: 17 | PTR = ctypes.c_void_p 18 | 19 | 20 | def list_relocations(relocations, size, virtual_address): 21 | relocations_list = [False] * size 22 | for relocation in relocations: 23 | for relocation_entry in relocation.entries: 24 | addr2 = relocation_entry.rva - virtual_address 25 | if addr2 >= 0: 26 | for i in range(addr2, addr2 + ctypes.sizeof(PTR) + 1): 27 | if (i + 1) < size: 28 | relocations_list[i + 1] = True 29 | return relocations_list 30 | 31 | 32 | def parse_relocations(proc, module_base_address, pe, data_rva, rva, size): 33 | data = proc.read(module_base_address + data_rva, size) 34 | file_offset = pe.get_offset_from_rva(data_rva) 35 | 36 | entries = [] 37 | for idx in range(len(data) / 2): 38 | 39 | entry = pe.__unpack_data__( 40 | pe.__IMAGE_BASE_RELOCATION_ENTRY_format__, 41 | data[idx * 2:(idx + 1) * 2], 42 | file_offset=file_offset) 43 | 44 | if not entry: 45 | break 46 | word = entry.Data 47 | 48 | relocation_type = (word >> 12) 49 | relocation_offset = (word & 0x0fff) 50 | relocation_data = pefile.RelocationData( 51 | struct=entry, 52 | type=relocation_type, 53 | base_rva=rva, 54 | rva=relocation_offset + rva) 55 | 56 | if relocation_data.struct.Data > 0 and \ 57 | (relocation_data.type == pefile.RELOCATION_TYPE['IMAGE_REL_BASED_HIGHLOW'] or 58 | relocation_data.type == pefile.RELOCATION_TYPE['IMAGE_REL_BASED_DIR64']): 59 | entries.append(relocation_data) 60 | file_offset += entry.sizeof() 61 | 62 | return entries 63 | 64 | 65 | def get_relocations(pe, proc, module_base_address): 66 | try: 67 | relocations = [] 68 | relocation_table = pe.NT_HEADERS.OPTIONAL_HEADER.DATA_DIRECTORY[ 69 | pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_BASERELOC']] 70 | rva = relocation_table.VirtualAddress 71 | size = relocation_table.Size 72 | 73 | if size == 0: 74 | return [] 75 | 76 | rlc_size = pefile.Structure(pe.__IMAGE_BASE_RELOCATION_format__).sizeof() 77 | end = rva + size 78 | while rva < end: 79 | try: 80 | rlc = pe.__unpack_data__( 81 | pe.__IMAGE_BASE_RELOCATION_format__, 82 | proc.read(module_base_address + rva, rlc_size), 83 | file_offset=pe.get_offset_from_rva(rva)) 84 | except pefile.PEFormatError: 85 | rlc = None 86 | 87 | if not rlc: 88 | break 89 | relocation_entries = parse_relocations(proc, module_base_address, pe, rva + rlc_size, rlc.VirtualAddress, 90 | rlc.SizeOfBlock - rlc_size) 91 | 92 | relocations.append( 93 | pefile.BaseRelocationData( 94 | struct=rlc, 95 | entries=relocation_entries)) 96 | 97 | if not rlc.SizeOfBlock: 98 | break 99 | rva += rlc.SizeOfBlock 100 | 101 | return relocations 102 | except Exception as ex: 103 | print(str(ex)) 104 | 105 | 106 | def analyze_process(pid): 107 | proc = winappdbg.Process(pid) 108 | process_patches = {'pid': pid, 'file': proc.get_filename(), 'modules': []} 109 | 110 | if proc.get_bits() != winappdbg.System.bits: 111 | return 112 | 113 | # Initialize disassembler 114 | md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32) 115 | 116 | for module_base_addr in proc.get_module_bases(): 117 | module = proc.get_module_at_address(module_base_addr) 118 | module_obj = {'file': module.get_filename(), 119 | 'base_address': module_base_addr, 120 | 'patches': [], 121 | 'additional_sections': []} 122 | 123 | try: 124 | module_data = proc.read(module_base_addr, module.get_size()) 125 | pe_mem = pefile.PE(data=module_data, fast_load=True) 126 | pe_disk = pefile.PE(name=module.get_filename(), fast_load=True) 127 | 128 | # We assume that the section Characteristics field could have been modified at runtime, 129 | # so we trust each section's Characteristics from disk, even if it's not marked as executable in memory - 130 | # this is since a section can be marked not executable but the pages in it marked as executable. 131 | disk_exec_sections = [section for section in pe_disk.sections if 132 | section.Characteristics & IMAGE_SCN_MEM_EXECUTE] 133 | disk_section_names = [section.Name for section in disk_exec_sections] 134 | mem_exec_sections = [section for section in pe_mem.sections if 135 | section.Characteristics & IMAGE_SCN_MEM_EXECUTE 136 | or section.Name in disk_section_names] 137 | 138 | # Sort the section lists by name for sanity checking and easier looping later on 139 | mem_exec_sections.sort(key=lambda s: s.Name) 140 | disk_exec_sections.sort(key=lambda s: s.Name) 141 | 142 | if not len(disk_exec_sections): 143 | # Module has no executable sections on disk 144 | continue 145 | elif len(mem_exec_sections) != len(disk_exec_sections) or \ 146 | any(mem_exec_sections[idx].Name != disk_exec_sections[idx].Name for idx in 147 | range(len(mem_exec_sections))): 148 | # Incompatible number of executable sections, or mismatching section names. 149 | additional_sections = [section.Name for section in mem_exec_sections if 150 | section.Name not in disk_section_names] 151 | module_obj['additional_sections'].append(additional_sections) 152 | continue 153 | 154 | for idx in range(0, len(mem_exec_sections)): 155 | mem_section_data = proc.read(module_base_addr + mem_exec_sections[idx].VirtualAddress, 156 | mem_exec_sections[idx].Misc_VirtualSize) 157 | disk_section_data = disk_exec_sections[idx].get_data()[:mem_exec_sections[idx].Misc_VirtualSize] 158 | 159 | # Compare text sections between disk and memory 160 | if mem_section_data == disk_section_data: 161 | continue 162 | 163 | # Handle a case where there is no data in disk section 164 | if disk_section_data == '': 165 | module_obj['patches'].append({'offset': mem_exec_sections[idx].VirtualAddress, 166 | 'mem_bytes': mem_section_data, 167 | 'disk_bytes': disk_section_data}) 168 | else: 169 | relocations = get_relocations(pe_mem, proc, module_base_addr) 170 | relocations_list = list_relocations(relocations, mem_exec_sections[idx].Misc_VirtualSize, 171 | mem_exec_sections[idx].VirtualAddress) 172 | last_patch_position = None 173 | current_patch = None 174 | 175 | for i in range(mem_exec_sections[idx].Misc_VirtualSize): 176 | # Check if there's a differential between memory and disk, taking to account base relocations 177 | if not relocations_list[i] and ( 178 | i > len(disk_section_data) - 1 or disk_section_data[i] != mem_section_data[i]): 179 | curr_disk_section_byte = '' 180 | 181 | if i < len(disk_section_data): 182 | curr_disk_section_byte = disk_section_data[i] 183 | 184 | if last_patch_position is not None and i == last_patch_position + 1: 185 | current_patch['mem_bytes'] += mem_section_data[i] 186 | current_patch['disk_bytes'] += curr_disk_section_byte 187 | else: 188 | current_patch = {'offset': mem_exec_sections[idx].VirtualAddress + i, 189 | 'mem_bytes': mem_section_data[i], 190 | 'disk_bytes': curr_disk_section_byte} 191 | module_obj['patches'].append(current_patch) 192 | last_patch_position = i 193 | 194 | # If there are patches, convert bytes to REIL 195 | if module_obj['patches']: 196 | for patch in module_obj['patches']: 197 | patch['mem_code'] = "" 198 | patch['disk_code'] = "" 199 | for (address, size, mnemonic, op_str) in md.disasm_lite(patch['mem_bytes'], patch['offset']): 200 | patch['mem_code'] += "{0:#x}:\t{1}\t{2}\n".format(address, mnemonic, op_str) 201 | for (address, size, mnemonic, op_str) in md.disasm_lite(patch['disk_bytes'], patch['offset']): 202 | patch['disk_code'] += "{0:#x}:\t{1}\t{2}\n".format(address, mnemonic, op_str) 203 | process_patches['modules'].append(module_obj) 204 | elif module_obj['additional_sections']: 205 | process_patches['modules'].append(module_obj) 206 | except OSError as ex: 207 | if ex.winerror != 299: 208 | print(str(ex)) 209 | return process_patches 210 | 211 | 212 | def print_process_patches(process_patches): 213 | print("Patches in PID {0}, File {1}".format(process_patches['pid'], process_patches['file'])) 214 | for module in process_patches['modules']: 215 | print("Module {}".format(module['file'])) 216 | for patch in module['patches']: 217 | if patch['disk_code'] != '' and patch['mem_code'] != '': 218 | print("Disk Code: ") 219 | print("{}".format(patch['disk_code'])) 220 | print("Memory Code: ") 221 | print("{}".format(patch['mem_code'])) 222 | for section in module['additional_sections']: 223 | print("Additional executable section: ") 224 | print("{}".format(section.Name)) 225 | 226 | 227 | def get_process_patches(process_ids=None): 228 | processes_patches = [] 229 | if not process_ids: 230 | process_ids = [pid for pid in psutil.pids() if pid != 0] 231 | 232 | for pid in process_ids: 233 | try: 234 | process_patches = analyze_process(pid) 235 | if process_patches is not None and len(process_patches['modules']) > 0: 236 | print_process_patches(process_patches) 237 | processes_patches.append(process_patches) 238 | else: 239 | print("No patches in process ID: {}".format(pid)) 240 | except Exception as ex: 241 | print("Error analyzing process ID: {}".format(pid)) 242 | return processes_patches 243 | 244 | 245 | if __name__ == "__main__": 246 | system = winappdbg.System() 247 | system.request_debug_privileges() 248 | system.scan_processes() 249 | 250 | patches = get_process_patches() 251 | --------------------------------------------------------------------------------