def go(f, daddr, tags): e = ELFFile(f) for seg in e.iter_segments(): dat = seg.data() print seg.header for c in range(len(dat)): daddr[seg['p_vaddr']+c] = ord(dat[c]) for sec in e.iter_sections(): print sec.name, sec.header if sec['sh_flags'] & 4: dat = sec.data() for op in distorm3.DecomposeGenerator(sec['sh_addr'], dat, distorm3.Decode32Bits): p = "\\t{\\o{"+op.mnemonic+"}}" f = False for i in op.operands: if f == False: f = True else: p += ", " if i.type == "Immediate": p += "\\i{"+str(i.value)+"}" else: p += str(i) print i.type, dir(i) tags[op.address]['len'] = str(op.size) tags[op.address]['parsed'] = str(p) tags[op.address]['endian'] = 'little' print op.mnemonic, op.flowControl, op.size print dir(op) return hex(e.header['e_entry'])
def disasm_around(self, trace, starting_addr, size): ''' returns the disassembly starting at addr for size instructions. @type trace: vtrace @param trace: instance of vtrace @type addr: int @param addr: address where to begin disassembly @type size: int @param size: number of instructions to disassemble ''' disasm = [] try: code = trace.readMemory(starting_addr, size) except: raise Exception("unable to read memory for disasm") if self.arch == '32': asm_arch = distorm3.Decode32Bits elif self.arch == '64': asm_arch = distorm3.Decode64Bits elif self.arch == '16': asm_arch = distorm3.Decode16Bits for inst in distorm3.DecomposeGenerator(starting_addr, code, asm_arch): if not inst.valid: return disasm else: disasm.append(inst) return disasm
def print_valid_code(code): offset = 0 while offset < len(code): for insn in distorm3.DecomposeGenerator( offset, code[offset:], distorm3.Decode64Bits, distorm3.DF_STOP_ON_FLOW_CONTROL): print offset, " : ", insn offset += insn.size
def call_scan(data_vr_address, data, start_limit=None, end_limit=None): """Disassemble a block of data and yield possible calls to imported functions. We're looking for instructions such as these: x86: CALL DWORD [0x1000400] JMP DWORD [0x1000400] We also capture indirect call where the API address is moved into a register and the regsiter is called: MOV ECX [0x9400c] CALL ECX Register """ # Call scan has two options based on the limit settings # if there are no limits then only potential IAT pointers # to inside the scanned data will be kept. This is useful # if you are scanning a data blob and are not able to resolve # outside the blob. # # If the limits are set then any potential IAT pointers to # inside the limits are kept. This is useful if you are # scanning the code segment of a PE file but your IAT may # be located in another segment in the PE file. Set the # limits to be the start and end of the mapped PE. # if start_limit == None: start_limit = data_vr_address if end_limit ==None: end_limit = data_vr_address + len(data) iat_ptrs=[] reg_redirect = {"EAX":0x0, "EBX":0x0, "ECX":0x0, "EDX":0x0} mode = distorm3.Decode32Bits for op in distorm3.DecomposeGenerator(data_vr_address, data, mode): if not op.valid: continue iat_loc = None if (_call_or_unc_jmp(op) and op.operands[0].type == 'AbsoluteMemoryAddress'): iat_loc = (op.operands[0].disp) & 0xffffffff if op.mnemonic == "MOV" and op.operands[0].type == 'Register' and op.operands[1].type == 'AbsoluteMemory': #print "MOV %s %s %s" % (op.operands[0], op.operands[1], op.operands[1].type) reg_redirect[str(op.operands[0])] = op.address + op.operands[1].disp if op.mnemonic == "MOV" and op.operands[0].type == 'Register' and op.operands[1].type == 'AbsoluteMemoryAddress': #print "MOV %s %s %s" % (op.operands[0], op.operands[1], op.operands[1].type) reg_redirect[str(op.operands[0])] =op.operands[1].disp if op.mnemonic == "CALL" and op.operands[0].type == 'Register': #print "CALL %s %s" % (op.operands[0], op.operands[0].type) iat_loc = reg_redirect[str(op.operands[0])] if (not iat_loc or (iat_loc < start_limit) or (iat_loc > end_limit)): continue # resolve iat_loc to API #print iat_loc if iat_loc not in iat_ptrs: iat_ptrs.append(iat_loc) return iat_ptrs
def build_code_dict(code, section, dwarfinfo): # Credit: https://github.com/HexHive/SMoTherSpectre s_off = section['sh_offset'] ## Create a dictionary of valid instructions at all offsets for offset in tqdm(range(len(code))): if code_dict.has_key(offset): continue for insn in distorm3.DecomposeGenerator( s_off + offset, code[offset:], distorm3.Decode64Bits, distorm3.DF_STOP_ON_FLOW_CONTROL): if insn.valid: code_dict[offset] = insn offset += insn.size
def build_code_dict(code, section, dwarfinfo): s_off = section['sh_offset'] # print_valid_code(code) print hex(len(code)) ## Create a dictionary of valid instructions at all offsets for offset in range(len(code)): if offset & 0xfff == 0: print hex(offset) if code_dict.has_key(offset): continue for insn in distorm3.DecomposeGenerator( s_off + offset, code[offset:], distorm3.Decode64Bits, distorm3.DF_STOP_ON_FLOW_CONTROL): if insn.valid: code_dict[offset] = insn offset += insn.size
def check(self, addr): max_mem_addr = (2**32 if self.mode == distorm3.Decode32Bits else 2**64) - 1 max_gadget_size = min(MAX_GADGET_SIZE, max_mem_addr - addr) if max_gadget_size == 0: return False code = self.addrspace.zread(addr, max_gadget_size) for op in distorm3.DecomposeGenerator( addr, code, self.mode, features=distorm3.DF_RETURN_FC_ONLY): if not op.valid: continue if self.rop_size > 0 and op.flowControl == 'FC_RET': self.rop_size = addr - op.address return True else: return False return False
def call_scan(data_vr_address, data, start_limit=None, end_limit=None): """Disassemble a block of data and yield possible calls to imported functions. We're looking for instructions such as these: x86: CALL DWORD [0x1000400] JMP DWORD [0x1000400] """ # Call scan has two options based on the limit settings # if there are no limits then only potential IAT pointers # to inside the scanned data will be kept. This is useful # if you are scanning a data blob and are not able to resolve # outside the blob. # # If the limits are set then any potential IAT pointers to # inside the limits are kept. This is useful if you are # scanning the code segment of a PE file but your IAT may # be located in another segment in the PE file. Set the # limits to be the start and end of the mapped PE. # if start_limit == None: start_limit = data_vr_address if end_limit == None: end_limit = data_vr_address + len(data) iat_ptrs = [] mode = distorm3.Decode32Bits for op in distorm3.DecomposeGenerator(data_vr_address, data, mode): if not op.valid: continue iat_loc = None if (_call_or_unc_jmp(op) and op.operands[0].type == 'AbsoluteMemoryAddress'): iat_loc = (op.operands[0].disp) & 0xffffffff if (not iat_loc or (iat_loc < start_limit) or (iat_loc > end_limit)): continue # resolve iat_loc to API #print iat_loc if iat_loc not in iat_ptrs: iat_ptrs.append(iat_loc) return iat_ptrs
def _parseCodeAddress(self, rva, raw_offset, queue): for instr in distorm3.DecomposeGenerator(rva, self.raw[raw_offset:], distorm3.Decode32Bits): # we have already analyzed this address if self.parsed[raw_offset]: break self.parsed[raw_offset] = True # this is not executable memory if not self.rva_is_executable(rva): break print hex(instr.address)[:-1], instr, instr.size raw_offset += instr.size rva += instr.size # jump to or call an address? if instr.flowControl in ['FC_CALL', 'FC_UNC_BRANCH', 'FC_CND_BRANCH'] and \ instr.operands[0].type == distorm3.OPERAND_IMMEDIATE: queue.append(instr.operands[0].value) # stop disassembling.. if instr.mnemonic.lower() in ['retn', 'jmp']: break
def call_scan(self, addr_space, base_address, data): """Disassemble a block of data and yield possible calls to imported functions. We're looking for instructions such as these: x86: CALL DWORD [0x1000400] JMP DWORD [0x1000400] x64: CALL QWORD [RIP+0x989d] On x86, the 0x1000400 address is an entry in the IAT or call table. It stores a DWORD which is the location of the API function being called. On x64, the 0x989d is a relative offset from the current instruction (RIP). @param addr_space: an AS to scan with @param base_address: memory base address @param data: buffer of data found at base_address """ end_address = base_address + len(data) memory_model = addr_space.profile.metadata.get('memory_model', '32bit') if memory_model == '32bit': mode = distorm3.Decode32Bits else: mode = distorm3.Decode64Bits for op in distorm3.DecomposeGenerator(base_address, data, mode): if not op.valid: continue iat_loc = None if memory_model == '32bit': if (self._call_or_unc_jmp(op) and op.operands[0].type == 'AbsoluteMemoryAddress'): iat_loc = (op.operands[0].disp) & 0xffffffff else: if (self._call_or_unc_jmp(op) and 'FLAG_RIP_RELATIVE' in op.flags and op.operands[0].type == 'AbsoluteMemory'): iat_loc = op.address + op.size + op.operands[0].disp if (not iat_loc or (iat_loc < base_address) or (iat_loc > end_address)): continue # This is the address being called call_dest = obj.Object("address", offset=iat_loc, vm=addr_space) if call_dest == None: continue yield op.address, iat_loc, int(call_dest)
def find_tables(start_addr, vm): """ This function finds the RVAs to KeServiceDescriptorTable and KeServiceDescriptorTableShadow in the NT module. @param start_addr: virtual address of KeAddSystemServiceTable @param vm: kernel address space We're looking for two instructions like this: //if (KeServiceDescriptorTable[i].Base) 4B 83 BC 1A 40 88 2A 00 00 cmp qword ptr [r10+r11+2A8840h], 0 //if (KeServiceDescriptorTableShadow[i].Base) 4B 83 BC 1A 80 88 2A 00 00 cmp qword ptr [r10+r11+2A8880h], 0 In the example, 2A8840h is the RVA of KeServiceDescriptorTable and 2A8880h is the RVA of KeServiceDescriptorTableShadow. The exported KeAddSystemServiceTable is a very small function (about 120 bytes at the most) and the two instructions appear very early, which reduces the possibility of false positives. If distorm3 is installed, we use it to decompose instructions in x64 format. If distorm3 is not available, we use Volatility's object model as a very simple and generic instruction parser. """ service_tables = [] try: import distorm3 use_distorm = True except ImportError: use_distorm = False function_size = 120 if use_distorm: data = vm.zread(start_addr, function_size) for op in distorm3.DecomposeGenerator(start_addr, data, distorm3.Decode64Bits): # Stop decomposing if we reach the function end if op.flowControl == 'FC_RET': break # Looking for a 9-byte CMP instruction whose first operand # has a 32-bit displacement and second operand is zero if op.mnemonic == 'CMP' and op.size == 9 and op.operands[ 0].dispSize == 32 and op.operands[0].value == 0: # The displacement is the RVA we want service_tables.append(op.operands[0].disp) elif op.mnemonic == 'LEA' and op.size == 7 and op.operands[ 1].dispSize == 32 and op.operands[1].disp > 0: service_tables.append(op.operands[1].disp) else: vm.profile.add_types({ '_INSTRUCTION': [ 9, { 'opcode': [0, ['String', dict(length=4)]], 'disp': [4, ['int']], 'value': [8, ['unsigned char']], } ] }) # The variations assume (which happens to be correct on all OS) # that volatile registers are used in the CMP QWORD instruction. # All combinations of volatile registers (rax, rcx, rdx, r8-r11) # will result in one of the variations in this list. ops_list = [ "\x4B\x83\xBC", # r10, r11 "\x48\x83\xBC", # rax, rcx "\x4A\x83\xBC", # rax, r8 "\x48\x8D\x8B", # win8x64 LEA RCX, [EBX+??????] ] for i in range(function_size): op = obj.Object("_INSTRUCTION", offset=start_addr + i, vm=vm) if op.value == 0: for s in ops_list: if op.opcode.v().startswith(s): service_tables.append(op.disp) return service_tables
class ApiHooks(procdump.ProcExeDump): """Detect API hooks in process and kernel memory""" def __init__(self, config, *args, **kwargs): procdump.ProcExeDump.__init__(self, config, *args, **kwargs) config.remove_option("DUMP-DIR") config.add_option("NO-WHITELIST", short_option='N', default=False, action='store_true', help='No whitelist (show all hooks, can be verbose)') config.add_option("SKIP-KERNEL", short_option='R', default=False, action='store_true', help='Skip kernel mode checks') config.add_option("SKIP-PROCESS", short_option='P', default=False, action='store_true', help='Skip process checks') config.add_option( "QUICK", short_option='Q', default=False, action='store_true', help='Work faster by only analyzing critical processes and dlls') self.compiled_rules = self.compile() # When the --quick option is set, we only scan the processes # and dlls in these lists. Feel free to adjust them for # your own purposes. self.critical_process = [ "explorer.exe", "svchost.exe", "lsass.exe", "services.exe", "winlogon.exe", "csrss.exe", "smss.exe", "wininit.exe", "iexplore.exe", "firefox.exe", "spoolsv.exe" ] self.critical_dlls = [ "ntdll.dll", "kernel32.dll", "ws2_32.dll", "advapi32.dll", "secur32.dll", "crypt32.dll", "user32.dll", "gdi32.dll", "shell32.dll", "shlwapi.dll", "lsasrv.dll", "cryptdll.dll", "wsock32.dll", "mswsock.dll", "urlmon.dll", "csrsrv.dll", "winsrv.dll", "wininet.dll" ] # When scanning for calls to unknown code pages (UCP), only # analyze the following drivers. This is based on an analysis of # the modules rootkits are most likely to infect, but feel free # to adjust it for your own purposes. self.ucpscan_modules = [ "tcpip.sys", "ntfs.sys", "fastfast.sys", "wanarp.sys", "ndis.sys", "atapi.sys", "ntoskrnl.exe", "ntkrnlpa.exe", "ntkrnlmp.exe" ] @staticmethod def is_valid_profile(profile): return (profile.metadata.get('os', 'unknown') == 'windows' and profile.metadata.get('memory_model', '32bit') == '32bit') def compile(self): """ Precompile the regular expression rules. Its quicker if we do this once per plugin run, rather than once per API hook that needs checking. """ ret = dict() for key, rules in whitelist_rules.items(): for rule in rules: ruleset = (( re.compile(rule[0], re.I), # Process name re.compile(rule[1], re.I), # Source module re.compile(rule[2], re.I), # Destination module re.compile(rule[3], re.I), # Function name )) if ret.has_key(key): ret[key].append(ruleset) else: ret[key] = [ruleset] return ret def whitelist(self, rule_key, process, src_mod, dst_mod, function): """Check if an API hook should be ignored due to whitelisting. @param rule_key: a key from the whitelist_rules dictionary which describes the type of hook (i.e. Usermode IAT or Kernel Inline). @param process: name of the suspected victim process. @param src_mod: name of the source module whose function has been hooked. this varies depending on whether we're dealing with IAT EAT, inline, etc. @param dst_mod: name of the module that is the destination of the hook pointer. this is usually the rootkit dll, exe, or sys, however, in many cases there is no module name since the rootkit is trying to be stealthy. @param function: name of the function that has been hooked. """ # There are no whitelist rules for this hook type if rule_key not in self.compiled_rules: return False for rule in self.compiled_rules[rule_key]: if (rule[0].search(process) != None and rule[1].search(src_mod) != None and rule[2].search(dst_mod) != None and rule[3].search(function) != None): return True return False @staticmethod def check_syscall(addr_space, module, module_group): """ Enumerate syscall hooks in ntdll.dll. A syscall hook is one that modifies the function prologue of an NT API function (i.e. ntdll!NtCreateFile) or swaps the location of the sysenter with a malicious address. @param addr_space: a process AS for the process containing the ntdll.dll module. @param module: the _LDR_DATA_TABLE_ENTRY for ntdll.dll @param module_group: a ModuleGroup instance for the process. """ # Resolve the real location of KiFastSystem Call for comparison KiFastSystemCall = module.getprocaddress("KiFastSystemCall") KiIntSystemCall = module.getprocaddress("KiIntSystemCall") if not KiFastSystemCall or not KiIntSystemCall: #debug.debug("Abort check_syscall, can't find KiFastSystemCall") return # Add the RVA to make it absolute KiFastSystemCall += module.DllBase KiIntSystemCall += module.DllBase # Check each exported function if its an NT syscall for _, f, n in module.exports(): # Ignore forwarded exports if not f: #debug.debug("Skipping forwarded export {0}".format(n or '')) continue function_address = module.DllBase + f if not addr_space.is_valid_address(function_address): #debug.debug("Function address {0:#x} for {1} is paged".format( # function_address, n or '')) continue # Read enough of the function prologue for two instructions data = addr_space.zread(function_address, 24) instructions = [] for op in distorm3.Decompose(function_address, data, distorm3.Decode32Bits): if not op.valid: break if len(instructions) == 3: break instructions.append(op) i0 = instructions[0] i1 = instructions[1] i2 = instructions[2] # They both must be properly decomposed and have two operands if (not i0 or not i0.valid or len(i0.operands) != 2 or not i1 or not i1.valid or len(i1.operands) != 2): #debug.debug("Error decomposing prologue for {0} at {1:#x}".format( # n or '', function_address)) continue # Now check the instruction and operand types if (i0.mnemonic == "MOV" and i0.operands[0].type == 'Register' and i0.operands[0].name == 'EAX' and i0.operands[1].type == 'Immediate' and i1.mnemonic == "MOV" and i1.operands[0].type == 'Register' and i1.operands[0].name == 'EDX' and i0.operands[1].type == 'Immediate'): if i2.operands[0].type == "Register": # KiFastSystemCall is already in the register syscall_address = i1.operands[1].value else: # Pointer to where KiFastSystemCall is stored syscall_address = obj.Object('address', offset=i1.operands[1].value, vm=addr_space) if syscall_address not in [KiFastSystemCall, KiIntSystemCall]: hook_module = module_group.find_module(syscall_address) hook = Hook( hook_type=HOOKTYPE_NT_SYSCALL, hook_mode=HOOK_MODE_USER, function_name=n or '', function_address=function_address, hook_address=syscall_address, hook_module=hook_module, victim_module=module, ) # Add the bytes that will later be disassembled in the # output to show exactly how the hook works. The first # hop is the ntdll!Nt* API and the next hop is the rootkit. hook.add_hop_chunk(function_address, data) hook.add_hop_chunk(syscall_address, addr_space.zread(syscall_address, 24)) yield hook def check_ucpcall(self, addr_space, module, module_group): """Scan for calls to unknown code pages. @param addr_space: a kernel AS @param module: the _LDR_DATA_TABLE_ENTRY to scan @param module_group: a ModuleGroup instance for the process. """ try: dos_header = obj.Object("_IMAGE_DOS_HEADER", offset=module.DllBase, vm=addr_space) nt_header = dos_header.get_nt_header() except (ValueError, exceptions.SanityCheckException), _why: #debug.debug('get_nt_header() failed: {0}'.format(why)) return # Parse the PE sections for this driver for sec in nt_header.get_sections(self._config.UNSAFE): # Only check executable sections if not sec.Characteristics & 0x20000000: continue # Calculate the virtual address of this PE section in memory sec_va = module.DllBase + sec.VirtualAddress # Extract the section's data and make sure its not all zeros data = addr_space.zread(sec_va, sec.Misc.VirtualSize) if data == "\x00" * len(data): continue # Disassemble instructions in the section for op in distorm3.DecomposeGenerator(sec_va, data, distorm3.Decode32Bits): if (op.valid and ((op.flowControl == 'FC_CALL' and op.mnemonic == "CALL") or (op.flowControl == 'FC_UNC_BRANCH' and op.mnemonic == "JMP")) and op.operands[0].type == 'AbsoluteMemoryAddress'): # This is ADDR, which is the IAT location const = op.operands[0].disp & 0xFFFFFFFF # Abort if ADDR is not a valid address if not addr_space.is_valid_address(const): continue # This is what [ADDR] points to - the absolute destination call_dest = obj.Object("address", offset=const, vm=addr_space) # Abort if [ADDR] is not a valid address if not addr_space.is_valid_address(call_dest): continue check1 = module_group.find_module(const) check2 = module_group.find_module(call_dest) # If ADDR or [ADDR] point to an unknown code page if not check1 or not check2: hook = Hook( hook_type=HOOKTYPE_CODEPAGE_KERNEL, hook_mode=HOOK_MODE_KERNEL, function_name="", function_address=op.address, hook_address=call_dest, ) # Add the location we found the call hook.add_hop_chunk( op.address, data[op.address - sec_va:op.address - sec_va + 24]) # Add the rootkit stub hook.add_hop_chunk(call_dest, addr_space.zread(call_dest, 24)) yield hook
def rebuild_iat(pid, pe_data, base_address, oep, newimpdir="newimpdir", newiat="newiat", loadfrommem=True): """Rebuild the import address table for the pe_data that was passed. @param pid: process ID for winappdbg to attach to and dump IAT offsets @param pe_data: full PE file read in as a binary string @param base_address: base address of PE (this override the base addres set in the pe_data) @param oep: original entry point of the PE (this override the base addres set in the pe_data) @param newimpdir: name for new section that will contain imports @param newiat: name for new section that will contain new IAT @param loadfrommem: pe data is mapped or unmapped (default mapped) """ pf = pe_init.PE(loadfrommem=loadfrommem, pestr=pe_data) pf.NThdr.ImageBase = base_address # get offset to oep rva_oep = oep - base_address pf.Opthdr.AddressOfEntryPoint = rva_oep # clear the existing import table # there are two different versions of elfesteem one that uses an extra .l[] object # and one that does not. If we get the wrong one, just catch the error and continue. try: pf.DirImport.impdesc.l=[] except: pf.DirImport.impdesc =[] ###################################################################### pdata = None data_vr_addr = None data_rva = None # locate section that contains OEP for tmp_sec in pf.SHList: tmp_start = tmp_sec.addr tmp_end = tmp_start + tmp_sec.size if (rva_oep >= tmp_start) and (rva_oep <= tmp_end): try: pdata = pf._rva.get(tmp_start,tmp_end) except AttributeError as e: raise AttributeError("You are using the wrong version of elfesteem, don't use pip instead install from https://github.com/serpilliere/elfesteem") data_vr_addr = base_address + tmp_start data_rva = tmp_start break # make sure we have found the correct section # WARNING! if the pdata section is not the same as the one loaded in memory the IAT extraction will # fail because the addresses and offsets will be wrong. assert pdata != None, "Unable to locate rva_oep: 0x%x in sections: %s" % (rva_oep, pf.SHList) # find all call/jmp to possible IAT function pointers # iat_ptrs is a list of all the addresses that are potential IAT pointers iat_ptrs = call_scan(data_vr_addr, pdata, start_limit=base_address, end_limit=base_address+len(pe_data)) assert len(iat_ptrs) != 0, "Unable to find IAT pointer candidates in code!" imp_table = reslove_iat_pointers(pid, iat_ptrs) # Create a table with module names as the keys and all the functions assigned accordingly #[module]:[func1, func2, ...] mod_table = {} for iat_ptr in imp_table.keys(): # TODO: it is possbile some module names may end with somethin other than .dll tmp_mod = imp_table[iat_ptr][0]+".dll" tmp_fn = imp_table[iat_ptr][1] if tmp_mod in mod_table.keys(): f_arr = mod_table[tmp_mod] # Only add function name if it isn't already assigned if tmp_fn not in f_arr: f_arr.append(tmp_fn) mod_table[tmp_mod] = f_arr else: mod_table[tmp_mod] = [tmp_fn] newiat_rawsize = (( (len(imp_table.keys()) * 4 ) / 0x1000) + 1) * 0x1000 # Create new section to hold new IAT s_newiat = pf.SHList.add_section(name=newiat, rawsize=newiat_rawsize) ###################################################################### # # elfesteem.PE has a special format for describing the IAT directory # We are converting the mod_table into that format... # ###################################################################### new_dll=[({"name": mod_table.keys()[0],"firstthunk": s_newiat.addr}, mod_table[mod_table.keys()[0]])] for mod in mod_table.keys()[1:]: tmp_entry = ({"name": mod,"firstthunk": None}, mod_table[mod]) new_dll.append(tmp_entry) ###################################################################### # # Add the new imports table directory to PE file # ###################################################################### pf.DirImport.add_dlldesc(new_dll) newimpdir_rawsize = ((len(pf.DirImport) / 0x1000) + 1) * 0x1000 s_newimpdir = pf.SHList.add_section(name=newimpdir, rawsize=newimpdir_rawsize) pf.SHList.align_sections(0x1000, 0x1000) pf.DirImport.set_rva(s_newimpdir.addr) ###################################################################### # # Create a mapping from the old IAT pointers to the new ones # iat_map[ <old_pointer> ] = <new_pointer> # # TODO: handle import by ordinal # ###################################################################### iat_map ={} for iat_ptr in imp_table.keys(): if imp_table[iat_ptr][1] == None: continue tmp_mod = imp_table[iat_ptr][0]+".dll" tmp_fn = imp_table[iat_ptr][1] try: iat_map[iat_ptr] = pf.DirImport.get_funcvirt(tmp_mod, tmp_fn) except: continue # If there is some error building the module map stop! assert iat_map != {}, "The iat_map is empty, we are unable to find the new IAT pointers for the old pointers in imp_table." ###################################################################### # # Patch the code section in the PE and replace all references to the # old IAT pointers with references to the new pointers. # ###################################################################### # Make a copy of the code data to work on odata = pdata mode = distorm3.Decode32Bits for op in distorm3.DecomposeGenerator(data_vr_addr, pdata, mode): if not op.valid: continue iat_loc = None if _iat_candidate(op): for operand in op.operands: test_operand = operand.disp & 0xffffffff if test_operand in iat_map.keys(): #print "Fixing IAT pointer for: %s" % op #op_ptr = op.address - base_address #TODO: is this right?? op_ptr = op.address - data_vr_addr op_size = op.size orig_op = odata[op_ptr:op_ptr+op_size] orig_operand = struct.pack('<I',test_operand) new_operand = struct.pack('<I',iat_map[test_operand]) new_op = orig_op.replace(orig_operand, new_operand) odata = odata[:op_ptr] + new_op + odata[op_ptr+op_size:] #stop testing operands break # Copy the patched section back to the PE pf._rva.set(data_rva,odata) # Disable rebase, since addresses are absolute any rebase will make this explode pf.NThdr.dllcharacteristics = 0x0 return str(pf)
def check_ucpcall(self, addr_space, module, module_group): """Scan for calls to unknown code pages. @param addr_space: a kernel AS @param module: the _LDR_DATA_TABLE_ENTRY to scan @param module_group: a ModuleGroup instance for the process. """ try: dos_header = obj.Object("_IMAGE_DOS_HEADER", offset=module.DllBase, vm=addr_space) nt_header = dos_header.get_nt_header() except (ValueError, exceptions.SanityCheckException) as _why: # debug.debug('get_nt_header() failed: {0}'.format(why)) return # Parse the PE sections for this driver for sec in nt_header.get_sections(self._config.UNSAFE): # Only check executable sections if not sec.Characteristics & 0x20000000: continue # Calculate the virtual address of this PE section in memory sec_va = module.DllBase + sec.VirtualAddress # Extract the section's data and make sure its not all zeros data = addr_space.zread(sec_va, sec.Misc.VirtualSize) if data == b'\x00' * len(data): continue # Disassemble instructions in the section for op in distorm3.DecomposeGenerator(sec_va, data, distorm3.Decode32Bits): if (op.valid and ((op.flowControl == 'FC_CALL' and op.mnemonic == "CALL") or (op.flowControl == 'FC_UNC_BRANCH' and op.mnemonic == "JMP")) and op.operands[0].type == 'AbsoluteMemoryAddress'): # This is ADDR, which is the IAT location const = op.operands[0].disp & 0xFFFFFFFF # Abort if ADDR is not a valid address if not addr_space.is_valid_address(const): continue # This is what [ADDR] points to - the absolute destination call_dest = obj.Object("address", offset=const, vm=addr_space) # Abort if [ADDR] is not a valid address if not addr_space.is_valid_address(call_dest): continue check1 = module_group.find_module(const) check2 = module_group.find_module(call_dest) # If ADDR or [ADDR] point to an unknown code page if not check1 or not check2: hook = Hook( hook_type=HOOKTYPE_CODEPAGE_KERNEL, hook_mode=HOOK_MODE_KERNEL, function_name="", function_address=op.address, hook_address=call_dest, ) # Add the location we found the call hook.add_hop_chunk( op.address, data[op.address - sec_va:op.address - sec_va + 24], ) # Add the rootkit stub hook.add_hop_chunk(call_dest, addr_space.zread(call_dest, 24)) yield hook
def ssexy_win32(fname): import pefile # load the pe file pe = pefile.PE(fname) # make a addr: value dictionary for all imports imports = dict((x.address, x.name or (e.dll.lower(), x.ordinal)) for e in pe.DIRECTORY_ENTRY_IMPORT for x in e.imports) # apply config to the imports, if its not in the configuration, just # prepend the api with an underscore, the way gcc likes it. imports = dict( (k, config.apis[v] if v in config.apis else config.Api('_' + v)) for k, v in imports.items()) # dictionary with addr: value where addr is the address of the # `jmp dword [thunk address]' and value the name of this import. iat_label = {} # a set of all relocations relocs = set([(pe.OPTIONAL_HEADER.ImageBase + y.rva) for x in pe.DIRECTORY_ENTRY_BASERELOC for y in x.entries]) # a list of addresses that were used. addresses = [] # a list of all m128 values we use m128s = [] # a list of all dword values we use m32s = [] instructions = pyasm2.block() # walk each section, find those that are executable and disassemble those for section in filter(lambda x: x.IMAGE_SCN_MEM_EXECUTE, pe.sections): g = distorm3.DecomposeGenerator( pe.OPTIONAL_HEADER.ImageBase + section.VirtualAddress, section.get_data(), distorm3.Decode32Bits) for instr in g: # useless instruction? if str(instr) in ('NOP', 'ADD [EAX], AL', 'LEA ESI, [ESI]', 'INT 3') or str(instr)[:2] == 'DB': continue # a jump to one of the imports? #if instr.mnemonic == 'JMP' and instr.operands[0].type == \ # distorm3.OPERAND_ABSOLUTE_ADDRESS and \ # instr.operands[0].disp in imports: # iat_label[instr.address] = imports[instr.operands[0].disp] # continue # quite hackery, but when the jumps with thunk address have been # processed, we can be fairly sure that there will be no (legit) # code anymore. #if len(iat_label): # break #print str(instr) #print str(instr) # convert the instruction from distorm3 format to pyasm2 format. instr = distorm3_to_pyasm2(instr) # we create the block already here, otherwise our `labelnr' is # not defined. #block = pyasm2.block(pyasm2.Label('%08x' % instr.address), instr) offset_flat = None addr = instr.address # now we check if this instruction has a relocation inside it # not a very efficient way, but oke. reloc = instr.length > 4 and relocs.intersection( range(instr.address, instr.address + instr.length - 3)) if reloc: # make an immediate with `addr' set to True enable_addr = lambda x: Immediate(int(x), addr=True) # TODO support for two relocations in one instruction # (displacement *and* immediate) reloc = reloc.pop() # there is only one operand, that's easy if not instr.op2: #sys.stderr.write('reloc in op1 %s??\n' % instr.op1) if isinstance(instr.op1, pyasm2.MemoryAddress): # special occassion, this memory addres is an import if instr.op1.reg1 is None and \ instr.op1.reg2 is None and \ int(instr.op1.disp) in imports: instr.op1 = imports[int(instr.op1.disp)] else: addresses.append(int(instr.op1.disp)) # change the displacement to a label #instr.op1 = str(instr.op1).replace('0x', # '__lbl_00') instr.op1 = enable_addr(instr.op1) elif isinstance(instr.op1, pyasm2.Immediate): addresses.append(int(instr.op1)) offset_flat = int(instr.op1) #instr.op1 = str(instr.op1).replace('0x', # 'offset flat:__lbl_00') # if the second operand is an immediate and the relocation is # in the last four bytes of the instruction, then this # immediate is the reloc. Otherwise, if the second operand is # a memory address, then it's the displacement. elif isinstance(instr.op2, pyasm2.Immediate) and reloc == \ instr.address + instr.length - 4: # keep this address addresses.append(int(instr.op2)) # make a label from this address # TODO: fix this horrible hack offset_flat = int(instr.op2) #instr.op2 = pyasm2.Label('offset flat:__lbl_%08x' % # int(instr.op2), prepend=False) elif isinstance(instr.op2, pyasm2.MemoryAddress) and \ reloc == instr.address + instr.length - 4: addresses.append(int(instr.op2.disp)) # change the displacement to a label instr.op2 = enable_addr(instr.op2) #instr.op2 = str(instr.op2).replace('0x', '__lbl_00') #sys.stderr.write('reloc in op2 memaddr %s\n' % # str(instr.op2)) # the relocation is not inside the second operand, it must be # inside the first operand after all. elif isinstance(instr.op1, pyasm2.MemoryAddress): addresses.append(int(instr.op1.disp)) instr.op1 = enable_addr(instr.op1) #instr.op1 = str(instr.op1).replace('0x', '__lbl_00') #sys.stderr.write('reloc in op1 memaddr %s\n' % # str(instr.op1)) elif isinstance(instr.op1, pyasm2.Immediate): addresses.append(int(instr.op1)) instr.op1 = enable_addr(instr.op1) #instr.op1 = '__lbl_%08x' % int(instr.op1) #sys.stderr.write('reloc in op1 imm %s\n' % instr.op1) else: sys.stderr.write('Invalid Relocation!\n') instr = translate.Translater(instr, m128s, m32s).translate() if offset_flat: encode_offset_flat = lambda x: str(x).replace( '0x', 'offset flat:__lbl_') if isinstance( x, (int, long, pyasm2.imm )) and int(x) == offset_flat or isinstance( x, pyasm2.mem) and x.disp == offset_flat else x if isinstance(instr, pyasm2.block): for x in instr.instructions: x.op1 = encode_offset_flat(x.op1) x.op2 = encode_offset_flat(x.op2) else: x.op1 = encode_offset_flat(x.op1) x.op2 = encode_offset_flat(x.op2) instructions += pyasm2.block(pyasm2.Label('%08x' % addr), instr) # remove any addresses that are from within the current section newlist = addresses[:] for i in xrange(len(addresses)): if addresses[i] >= pe.OPTIONAL_HEADER.ImageBase + \ section.VirtualAddress and addresses[i] < \ pe.OPTIONAL_HEADER.ImageBase + section.VirtualAddress + \ len(section.get_data()): newlist[i] = None addresses = filter(lambda x: x is not None, newlist) # walk over each instruction, if it has references, we update them for instr in instructions.instructions: # we can skip labels if isinstance(instr, pyasm2.Label): continue # check for references to imports if isinstance(instr, pyasm2.RelativeJump): # not very good, but for now (instead of checking relocs) we check # if the index is in the iat tabel.. if int(instr.lbl.index, 16) in iat_label: instr.lbl.index = iat_label[int(instr.lbl.index, 16)] instr.lbl.prepend = False continue program = ['.file "ssexy.c"', '.intel_syntax noprefix'] # we walk over each section, if a reference to this section has been found # then we will dump the entire section as bytecode.. with matching labels for section in pe.sections: base = pe.OPTIONAL_HEADER.ImageBase + section.VirtualAddress data = section.get_data() addr = set(range(base, base + len(data))).intersection(addresses) if addr: # create a header for this section program.append('.section %s,"dr"' % section.Name.strip('\x00')) # for now we do it the easy way.. one line and label per byte, lol for addr in xrange(len(data)): program.append('__lbl_%08x: .byte 0x%02x' % (base + addr, ord(data[addr]))) # empty line.. program.append('') # if there is memory left for left in xrange(section.Misc_VirtualSize - len(data)): program.append( '.lcomm __lbl_%08x, 1, 32' % (pe.OPTIONAL_HEADER.ImageBase + section.VirtualAddress + left)) # now we define all xmm's etc we gathered program.append('.align 4') program += m32s program.append('.align 16') program += m128s # time to define 'main' program.append('.globl _main') OEP = pe.OPTIONAL_HEADER.ImageBase + pe.OPTIONAL_HEADER.AddressOfEntryPoint # append each instruction for instr in instructions.instructions: # if this is an label, we want a colon as postfix if isinstance(instr, pyasm2.Label): program.append(str(instr) + ':') # if OEP is at this address, we will also add the `_main' label if str(instr) == '__lbl_%08x' % OEP: program.append('_main:') # we have to initialize the stack register, so.. # for now we assume esp gpr is stored as first gpr in xmm7 program.append('movd xmm7, esp') else: # TODO: fix this terrible hack as well program.append( str(instr).replace('byte', 'byte ptr').replace( 'word', 'word ptr').replace('retn', 'ret').replace( '__lbl_00400000', '0x400000').replace('oword ptr', '')) print '\n'.join(program)
def ssexy_linux(fname, *eips): import elf32 from construct import Struct, ULInt32, ULInt16, ULInt8, Array, CString from construct import OptionalGreedyRange # assume low-endian binary elf32_rel = Struct('elf32_rel', ULInt32('r_offset'), ULInt32('r_info')) ELF32_R_SYM = lambda x: x.r_info >> 8 ELF32_R_TYPE = lambda x: x.r_info & 0xff R_386_PC32 = 2 elf32_sym = Struct('elf32_sym', ULInt32('st_name'), ULInt32('st_value'), ULInt32('st_size'), ULInt8('st_info'), ULInt8('st_other'), ULInt16('st_shndx')) elf = elf32.elf32_file.parse_stream(file(fname, 'rb')) # retrieve section by name elf32_section = lambda elf, name: [ x for x in elf.sections if x.name == name ][0] # for now we assume that all code is in the .text section code_section = [x for x in elf.sections if x.name == '.text'] if not len(code_section): raise Exception('your binary doesn\'t have a .text section..') relocs = [x.data.value for x in elf.sections if x.name == '.rel.dyn'] if not len(relocs): raise Exception('no relocs available, compile with -pie') # read all relocations relocs = Array(len(relocs[0]) / elf32_rel.sizeof(), elf32_rel).parse(relocs[0]) # now get the offsets of the relocations relocs = set([x.r_offset for x in relocs]) imports = {} # a list of addresses that were used. addresses = [] # a list of all m128 values we use m128s = [] # a list of all dword values we use m32s = [] instructions = pyasm2.block() # get string at offset dynstr = lambda x: CString(None).parse( elf32_section(elf, '.dynstr').data.value[x:]) # read the symbol table imports = OptionalGreedyRange(elf32_sym).parse( elf32_section(elf, '.dynsym').data.value) # resolve relocations section = elf32_section(elf, '.rel.dyn') relocates = {} for x in xrange(0, section.size, elf32_rel.sizeof()): x = elf32_rel.parse(section.data.value[x:x + elf32_rel.sizeof()]) # relocation to fixup addresses to imports if ELF32_R_TYPE(x) == R_386_PC32: relocates[x.r_offset] = dynstr(imports[ELF32_R_SYM(x)].st_name) # walk each section, find those that are executable and disassemble those section = elf32_section(elf, '.text') g = distorm3.DecomposeGenerator(section.addr, section.data.value, distorm3.Decode32Bits) for instr in g: # useless instruction? if str(instr) in ('NOP', 'ADD [EAX], AL', 'LEA ESI, [ESI]', 'INT 3') or str(instr)[:2] == 'DB': continue # a jump to one of the imports? #if instr.mnemonic == 'JMP' and instr.operands[0].type == \ # distorm3.OPERAND_ABSOLUTE_ADDRESS and \ # instr.operands[0].disp in imports: # iat_label[instr.address] = imports[instr.operands[0].disp] # continue # quite hackery, but when the jumps with thunk address have been # processed, we can be fairly sure that there will be no (legit) # code anymore. #if len(iat_label): # break #print str(instr) #print str(instr) # convert the instruction from distorm3 format to pyasm2 format. instr = distorm3_to_pyasm2(instr) # we create the block already here, otherwise our `labelnr' is # not defined. #block = pyasm2.block(pyasm2.Label('%08x' % instr.address), instr) offset_flat = None addr = instr.address # now we check if this instruction has a relocation inside it # not a very efficient way, but oke. reloc = instr.length > 4 and relocs.intersection( range(instr.address, instr.address + instr.length - 3)) if reloc: # make an immediate with `addr' set to True enable_addr = lambda x: Immediate(int(x), addr=True) # TODO support for two relocations in one instruction # (displacement *and* immediate) reloc = reloc.pop() if not hasattr(instr, 'op1'): instr.op1, instr.op2 = None, None # there is only one operand, that's easy if not instr.op2: #sys.stderr.write('reloc in op1 %s??\n' % instr.op1) if isinstance(instr.op1, pyasm2.MemoryAddress): # special occassion, this memory addres is an import if instr.op1.reg1 is None and \ instr.op1.reg2 is None and \ int(instr.op1.disp) in imports: instr.op1 = imports[int(instr.op1.disp)] else: addresses.append(int(instr.op1.disp)) # change the displacement to a label #instr.op1 = str(instr.op1).replace('0x', # '__lbl_00') instr.op1 = enable_addr(instr.op1) elif isinstance(instr.op1, pyasm2.Immediate): addresses.append(int(instr.op1)) offset_flat = int(instr.op1) #instr.op1 = str(instr.op1).replace('0x', # 'offset flat:__lbl_00') # if the second operand is an immediate and the relocation is # in the last four bytes of the instruction, then this # immediate is the reloc. Otherwise, if the second operand is # a memory address, then it's the displacement. elif isinstance(instr.op2, pyasm2.Immediate) and reloc == \ instr.address + instr.length - 4: # keep this address addresses.append(int(instr.op2)) # make a label from this address # TODO: fix this horrible hack offset_flat = int(instr.op2) #instr.op2 = pyasm2.Label('offset flat:__lbl_%08x' % # int(instr.op2), prepend=False) elif isinstance(instr.op2, pyasm2.MemoryAddress) and \ reloc == instr.address + instr.length - 4: addresses.append(int(instr.op2.disp)) # change the displacement to a label instr.op2 = enable_addr(instr.op2) #instr.op2 = str(instr.op2).replace('0x', '__lbl_00') #sys.stderr.write('reloc in op2 memaddr %s\n' % # str(instr.op2)) # the relocation is not inside the second operand, it must be # inside the first operand after all. elif isinstance(instr.op1, pyasm2.MemoryAddress): addresses.append(int(instr.op1.disp)) instr.op1 = enable_addr(instr.op1) #instr.op1 = str(instr.op1).replace('0x', '__lbl_00') #sys.stderr.write('reloc in op1 memaddr %s\n' % # str(instr.op1)) elif isinstance(instr.op1, pyasm2.Immediate): addresses.append(int(instr.op1)) instr.op1 = enable_addr(instr.op1) #instr.op1 = '__lbl_%08x' % int(instr.op1) #sys.stderr.write('reloc in op1 imm %s\n' % instr.op1) else: sys.stderr.write('Invalid Relocation!\n') #print instr m32len = len(m32s) instr = translate.Translater(instr, m128s, m32s).translate() if offset_flat: encode_offset_flat = lambda x: str(x).replace( '0x', 'offset flat:__lbl_') if isinstance( x, (int, long, pyasm2.imm )) and int(x) == offset_flat or isinstance( x, pyasm2.mem) and x.disp == offset_flat else x if isinstance(instr, pyasm2.block): for x in instr.instructions: x.op1 = encode_offset_flat(x.op1) x.op2 = encode_offset_flat(x.op2) else: x.op1 = encode_offset_flat(x.op1) x.op2 = encode_offset_flat(x.op2) # update stuff m32s = m32s[:m32len] + [ x.replace('0x%08x' % offset_flat, 'offset flat:__lbl_%08x' % offset_flat) for x in m32s[m32len:] ] instructions += pyasm2.block(pyasm2.Label('%08x' % addr), instr) # remove any addresses that are from within the current section newlist = addresses[:] for i in xrange(len(addresses)): if addresses[i] >= code_section[0].addr and addresses[i] < \ code_section[0].addr + code_section[0].size: newlist[i] = None addresses = filter(lambda x: x is not None, newlist) # walk over each instruction, if it has references, we update them for instr in instructions.instructions: # we can skip labels if isinstance(instr, pyasm2.Label): continue # check for references to imports if isinstance(instr, pyasm2.RelativeJump): # not very good, but for now (instead of checking relocs) we check # if the index is in the iat tabel.. #if int(instr.lbl.index, 16) in iat_label: #instr.lbl.index = iat_label[int(instr.lbl.index, 16)] #instr.lbl.prepend = False continue program = ['.file "ssexy.c"', '.intel_syntax noprefix'] # we walk over each section, if a reference to this section has been found # then we will dump the entire section as bytecode.. with matching labels for section in elf.sections: base = section.addr data = section.data.value addr = set(range(base, base + section.size)).intersection(addresses) if addr: # create a header for this section program.append('.section %s' % section.name) # for now we do it the easy way.. one line and label per byte, lol for addr in xrange(section.size): program.append('__lbl_%08x: .byte 0x%02x' % (base + addr, ord(data[addr]))) # empty line.. program.append('') # now we define all xmm's etc we gathered program.append('.align 4') program += m32s program.append('.align 16') program += m128s # time to define 'main' program.append('.text') program.append('.globl Main') program.append('.type Main, @function') OEP = elf.entry # f****d up shit relocates = dict( ('jmp __lbl_%08x' % k, 'jmp ' + v) for k, v in relocates.items()) eips = ['__lbl_%08x' % x for x in eips] # append each instruction for instr in instructions.instructions: # if this is an label, we want a colon as postfix if isinstance(instr, pyasm2.Label): program.append(str(instr) + ':') # if OEP is at this address, we will also add the `_main' label if str(instr) == '__lbl_%08x' % OEP: program.append('Main:') # we have to initialize the stack register, so.. # for now we assume esp gpr is stored as first gpr in xmm7 program.append('movd xmm7, esp') # if the label is in the list of addresses to which we have to add # an "movd xmm7, esp" instruction, then add it (e.g. callback # function for pthread_create) if str(instr) in eips: program.append('movd xmm7, esp') else: # TODO: fix this terrible hack as well program.append( str(instr).replace('byte', 'byte ptr').replace( 'word', 'word ptr').replace('retn', 'ret').replace( '__lbl_00400000', '0x400000').replace('oword ptr', '')) if program[-1] in relocates: program[-1] = relocates[program[-1]] print '\n'.join(program)