Package rekall :: Package plugins :: Package windows :: Package malware :: Module apihooks
[frames] | no frames]

Source Code for Module rekall.plugins.windows.malware.apihooks

  1  # Rekall Memory Forensics 
  2  # 
  3  # Based on original code by: 
  4  # Copyright (C) 2007-2013 Volatility Foundation 
  5  # 
  6  # Authors: 
  7  # Michael Hale Ligh <michael.ligh@mnin.org> 
  8  # 
  9  # This code: 
 10  # Copyright 2014 Google Inc. All Rights Reserved. 
 11  # 
 12  # Authors: 
 13  # Michael Cohen <scudette@google.com> 
 14  # 
 15  # This program is free software; you can redistribute it and/or modify 
 16  # it under the terms of the GNU General Public License as published by 
 17  # the Free Software Foundation; either version 2 of the License, or (at 
 18  # your option) any later version. 
 19  # 
 20  # This program is distributed in the hope that it will be useful, but 
 21  # WITHOUT ANY WARRANTY; without even the implied warranty of 
 22  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 23  # General Public License for more details. 
 24  # 
 25  # You should have received a copy of the GNU General Public License 
 26  # along with this program; if not, write to the Free Software 
 27  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 28  # 
 29   
 30  import struct 
 31   
 32  from rekall import testlib 
 33   
 34  from rekall.plugins.windows import common 
 35  from rekall.plugins.overlays.windows import pe_vtypes 
 36  from rekall_lib import utils 
 37   
 38   
39 -class DecodingError(Exception):
40 """Raised when unable to decode an instruction."""
41 42
43 -class HookHeuristic(object):
44 """A Hook heuristic detects possible hooks. 45 46 This heuristic emulates some common CPU instructions to try and detect 47 control flow jumps within the first few instructions of a function. 48 49 These are essentially guesses based on the most common hook types. Be aware 50 that these are pretty easy to defeat which will cause the hook to be missed. 51 52 See rekall/src/hooks/amd64.asm and rekall/src/hooks/i386.asm For the test 53 cases which illustrate the type of hooks that we will detect. 54 """ 55
56 - def __init__(self, session=None):
57 self.session = session 58 self.Reset()
59
60 - def Reset(self):
61 # Keep track of registers, stack and main memory. 62 self.regs = {} 63 self.stack = [] 64 self.memory = {}
65
66 - def WriteToOperand(self, operand, value):
67 if operand["type"] == "REG": 68 self.regs[operand["reg"]] = value 69 70 elif operand["type"] == "IMM": 71 self.memory[operand["address"]] = value 72 73 elif operand["type"] == "MEM": 74 self.memory[self._get_mem_operand_target(operand)] = value 75 76 else: 77 raise DecodingError("Operand not supported")
78
79 - def ReadFromOperand(self, operand):
80 """Read the operand. 81 82 We support the following forms: 83 84 - Immediate (IMM): JMP 0x123456 85 - Absolute Memory Address (MEM): JMP [0x123456] 86 - Register (REG): JMP [EAX] 87 """ 88 # Read from register. 89 if operand["type"] == 'REG': 90 return self.regs.get(operand["reg"], 0) 91 92 # Immediate operand. 93 elif operand["type"] == 'IMM': 94 return operand["address"] 95 96 # Read the content of memory. 97 elif operand["type"] == "MEM": 98 return self._GetMemoryAddress( 99 self._get_mem_operand_target(operand), operand["size"]) 100 101 else: 102 raise DecodingError("Operand not supported")
103
104 - def _get_mem_operand_target(self, operand):
105 reg_base = operand["base"] 106 if reg_base == "RIP": 107 return operand["address"] 108 else: 109 # Register reference [base_reg + disp + index_reg * scale] 110 return (self.regs.get(reg_base, 0) + 111 operand["disp"] + 112 self.regs.get(operand["index"], 0) * operand["scale"])
113
114 - def _GetMemoryAddress(self, offset, size):
115 try: 116 # First check our local cache for a previously written value. 117 return self.memory[offset] 118 except KeyError: 119 data = self.address_space.read(offset, size) 120 format_string = {1: "b", 2: "H", 4: "I", 8: "Q"}[size] 121 122 return struct.unpack(format_string, data)[0]
123
124 - def process_lea(self, instruction):
125 """Copies the address from the second operand to the first.""" 126 operand = instruction.operands[1] 127 if operand["type"] == 'MEM': 128 self.WriteToOperand(instruction.operands[0], 129 self._get_mem_operand_target(operand)) 130 else: 131 raise DecodingError("Invalid LEA source.")
132
133 - def process_push(self, instruction):
134 value = self.ReadFromOperand(instruction.operands[0]) 135 136 self.stack.append(value)
137
138 - def process_pop(self, instruction):
139 try: 140 value = self.stack.pop(-1) 141 except IndexError: 142 value = 0 143 144 self.WriteToOperand(instruction.operands[0], value)
145
146 - def process_ret(self, _):
147 if self.stack: 148 return self.stack.pop(-1)
149
150 - def process_mov(self, instruction):
151 value = self.ReadFromOperand(instruction.operands[1]) 152 153 self.WriteToOperand(instruction.operands[0], value)
154
155 - def process_inc(self, instruction):
156 value = self.ReadFromOperand(instruction.operands[0]) 157 158 self.WriteToOperand(instruction.operands[0], value + 1)
159
160 - def process_dec(self, instruction):
161 value = self.ReadFromOperand(instruction.operands[0]) 162 163 self.WriteToOperand(instruction.operands[0], value - 1)
164
165 - def process_cmp(self, instruction):
166 """We dont do anything with the comparison since we dont test for it.""" 167 _ = instruction
168
169 - def process_test(self, instruction):
170 """We dont do anything with the comparison since we dont test for it.""" 171 _ = instruction
172
173 - def _Operate(self, instruction, operator):
174 value1 = self.ReadFromOperand(instruction.operands[0]) 175 value2 = self.ReadFromOperand(instruction.operands[1]) 176 177 self.WriteToOperand( 178 instruction.operands[0], operator(value1, value2))
179
180 - def process_xor(self, instruction):
181 return self._Operate(instruction, lambda x, y: x ^ y)
182
183 - def process_add(self, instruction):
184 return self._Operate(instruction, lambda x, y: x + y)
185
186 - def process_sub(self, instruction):
187 return self._Operate(instruction, lambda x, y: x - y)
188
189 - def process_and(self, instruction):
190 return self._Operate(instruction, lambda x, y: x & y)
191
192 - def process_or(self, instruction):
193 return self._Operate(instruction, lambda x, y: x | y)
194
195 - def process_shl(self, instruction):
196 return self._Operate(instruction, lambda x, y: x << (y % 0xFF))
197
198 - def process_shr(self, instruction):
199 return self._Operate(instruction, lambda x, y: x >> (y % 0xFF))
200
201 - def Inspect(self, function, instructions=10):
202 """The main entry point to the Hook processor. 203 204 We emulate the function instructions and try to determine the jump 205 destination. 206 207 Args: 208 function: A basic.Function() instance. 209 """ 210 self.Reset() 211 self.address_space = function.obj_vm 212 213 for instruction in function.disassemble(instructions=instructions): 214 if instruction.is_return(): 215 # RET Instruction terminates processing. 216 return self.process_ret(instruction) 217 218 elif instruction.mnemonic == "call": 219 return self.ReadFromOperand(instruction.operands[0]) 220 221 # A JMP instruction. 222 elif instruction.is_branch(): 223 return self.ReadFromOperand(instruction.operands[0]) 224 225 else: 226 try: 227 handler = getattr(self, "process_%s" % instruction.mnemonic) 228 except AttributeError: 229 continue 230 231 # Handle the instruction. 232 try: 233 handler(instruction) 234 except Exception: 235 self.session.logging.error( 236 "Unable to handle instruction %s", instruction.op_str) 237 return
238 239
240 -class CheckPEHooks(common.WindowsCommandPlugin):
241 """Checks a pe file mapped into memory for hooks.""" 242 243 name = "check_pehooks" 244 245 __args = [ 246 dict(name="image_base", default=0, 247 positional=True, type="SymbolAddress", 248 help="The base address of the pe image in memory."), 249 250 dict(name="type", default="all", 251 choices=["all", "iat", "inline", "eat"], 252 type="Choice", help="Type of hook to display."), 253 254 dict(name="thorough", default=False, type="Boolean", 255 help="By default we take some optimization. This flags forces " 256 "thorough but slower checks."), 257 ] 258 259 table_header = [ 260 dict(name="Type", width=10), 261 dict(name="source", width=20), 262 dict(name="target", width=20), 263 dict(name="source_func", width=60), 264 dict(name="target_func"), 265 ] 266
267 - def reported_access(self, address):
268 """Determines if the address should be reported. 269 270 This assesses the destination address for suspiciousness. For example if 271 the address resides in a VAD region which is not mapped by a dll then it 272 might be suspicious. 273 """ 274 destination_names = self.session.address_resolver.format_address( 275 address) 276 277 # For now very simple: If any of the destination_names start with vad_* 278 # it means that the address resolver cant determine which module they 279 # came from. 280 destination = hex(address) 281 for destination in destination_names: 282 if not destination.startswith("vad_"): 283 return False 284 285 return destination
286
287 - def detect_IAT_hooks(self):
288 """Detect Import Address Table hooks. 289 290 An IAT hook is where malware changes the IAT entry for a dll after its 291 loaded so that when it is called from within the DLL, flow control is 292 directed to the malware instead. 293 294 We determine the IAT entry is hooked if the address is outside the dll 295 which is imported. 296 """ 297 pe = pe_vtypes.PE(image_base=self.plugin_args.image_base, 298 session=self.session) 299 300 # First try to find all the names of the imported functions. 301 imports = [ 302 (dll, func_name) for dll, func_name, _ in pe.ImportDirectory()] 303 304 resolver = self.session.address_resolver 305 306 for idx, (dll, func_address, _) in enumerate(pe.IAT()): 307 308 try: 309 target_dll, target_func_name = imports[idx] 310 target_dll = self.session.address_resolver.NormalizeModuleName( 311 target_dll) 312 except IndexError: 313 # We can not retrieve these function's name from the 314 # OriginalFirstThunk array - possibly because it is not mapped 315 # in. 316 target_dll = dll 317 target_func_name = "" 318 319 self.session.report_progress( 320 "Checking function %s!%s", target_dll, target_func_name) 321 322 # We only want the containing module. 323 module = resolver.GetContainingModule(func_address) 324 if module and target_dll == module.name: 325 continue 326 327 # Use ordinal if function has no name 328 if not len(target_func_name): 329 target_func_name = "(%s)" % idx 330 331 function_name = "%s!%s" % (target_dll, target_func_name) 332 333 # Function_name is the name which the PE file want 334 yield function_name, func_address
335
336 - def collect_iat_hooks(self):
337 for function_name, func_address in self.detect_IAT_hooks(): 338 yield dict(Type="IAT", 339 source=function_name, 340 target=utils.FormattedAddress( 341 self.session.address_resolver, 342 func_address, max_count=1, hex_if_unknown=True), 343 target_func=self.session.profile.Function( 344 func_address))
345
346 - def detect_EAT_hooks(self, size=0):
347 """Detect Export Address Table hooks. 348 349 An EAT hook is where malware changes the EAT entry for a dll after its 350 loaded so that a new DLL wants to link against it, the new DLL will use 351 the malware's function instead of the exporting DLL's function. 352 353 We determine the EAT entry is hooked if the address lies outside the 354 exporting dll. 355 """ 356 address_space = self.session.GetParameter("default_address_space") 357 pe = pe_vtypes.PE(image_base=self.plugin_args.image_base, 358 session=self.session, 359 address_space=address_space) 360 start = self.plugin_args.image_base 361 end = self.plugin_args.image_base + size 362 363 # If the dll size is not provided we parse it from the PE header. 364 if not size: 365 for _, _, virtual_address, section_size in pe.Sections(): 366 # Only count executable sections. 367 section_end = (self.plugin_args.image_base + 368 virtual_address + section_size) 369 if section_end > end: 370 end = section_end 371 372 resolver = self.session.address_resolver 373 374 for dll, func, name, hint in pe.ExportDirectory(): 375 self.session.report_progress("Checking export %s!%s", dll, name) 376 377 # Skip zero or invalid addresses. 378 if address_space.read(func.v(), 10) == "\x00" * 10: 379 continue 380 381 # Report on exports which fall outside the dll. 382 if start < func.v() < end: 383 continue 384 385 function_name = "%s:%s (%s)" % ( 386 resolver.NormalizeModuleName(dll), name, hint) 387 388 yield function_name, func
389
390 - def collect_eat_hooks(self):
391 for function_name, func_address in self.detect_EAT_hooks(): 392 yield dict(Type="EAT", 393 source=function_name, 394 target=utils.FormattedAddress( 395 self.session.address_resolver, 396 func_address, max_count=1, hex_if_unknown=True), 397 target_func=self.session.profile.Function( 398 func_address))
399
400 - def detect_inline_hooks(self):
401 """A Generator of hooked exported functions from this PE file. 402 403 Yields: 404 A tuple of (function, name, jump_destination) 405 """ 406 # Inspect the export directory for inline hooks. 407 pe = pe_vtypes.PE(image_base=self.plugin_args.image_base, 408 address_space=self.session.GetParameter( 409 "default_address_space"), 410 session=self.session) 411 pfn_db = self.session.profile.get_constant_object("MmPfnDatabase") 412 heuristic = HookHeuristic(session=self.session) 413 414 ok_pages = set() 415 416 for _, function, name, _ in pe.ExportDirectory(): 417 # Dereference the function pointer. 418 function_address = function.deref().obj_offset 419 420 self.session.report_progress( 421 "Checking function %#x (%s)", function, name) 422 423 # Check if the page is private or a file mapping. Usually if a 424 # mapped page is modified it will be converted to a private page due 425 # to Windows copy on write semantics. We assume that hooks are only 426 # placed in memory, and therefore functions which are still mapped 427 # to disk files are not hooked and can be safely skipped. 428 if not self.plugin_args.thorough: 429 # We must do the vtop in the process address space. This is the 430 # physical page backing the function preamble. 431 phys_address = function.obj_vm.vtop(function_address) 432 433 # Page not mapped. 434 if phys_address == None: 435 continue 436 437 phys_page = phys_address >> 12 438 439 # We determined this page is ok before - we can skip it. 440 if phys_page in ok_pages: 441 continue 442 443 # Get the PFN DB record. 444 pfn_obj = pfn_db[phys_page] 445 446 # The page is controlled by a prototype PTE which means it is 447 # still a file mapping. It has not been changed. 448 if pfn_obj.IsPrototype: 449 ok_pages.add(phys_page) 450 continue 451 452 # Try to detect an inline hook. 453 destination = heuristic.Inspect(function, instructions=3) or "" 454 455 # If we did not detect a hook we skip this function. 456 if destination: 457 yield function, name, destination
458
459 - def collect_inline_hooks(self):
460 for function, _, destination in self.detect_inline_hooks(): 461 hook_detected = False 462 463 # Try to resolve the destination into a name. 464 destination_name = self.reported_access(destination) 465 466 # We know about it. We suppress the output for jumps that go into a 467 # known module. These should be visible using the regular vad 468 # module. 469 if destination_name: 470 hook_detected = True 471 472 # Skip non hooked results if verbosity is too low. 473 if self.plugin_args.verbosity < 10 and not hook_detected: 474 continue 475 476 # Only highlight results if verbosity is high. 477 highlight = "" 478 if hook_detected and self.plugin_args.verbosity > 1: 479 highlight = "important" 480 481 yield dict(Type="Inline", 482 source=utils.FormattedAddress( 483 self.session.address_resolver, 484 function.deref(), max_count=1), 485 target=utils.FormattedAddress( 486 self.session.address_resolver, 487 destination, max_count=1), 488 source_func=function.deref(), 489 target_func=self.session.profile.Function( 490 destination), 491 highlight=highlight)
492
493 - def collect(self):
494 if self.plugin_args.type in ["all", "inline"]: 495 for x in self.collect_inline_hooks(): 496 yield x 497 498 if self.plugin_args.type in ["all", "iat"]: 499 for x in self.collect_iat_hooks(): 500 yield x 501 502 if self.plugin_args.type in ["all", "eat"]: 503 for x in self.collect_eat_hooks(): 504 yield x
505 506
507 -class EATHooks(common.WinProcessFilter):
508 """Detect EAT hooks in process and kernel memory""" 509 510 name = "hooks_eat" 511 512 table_header = [ 513 dict(name="divider", type="Divider"), 514 dict(name="_EPROCESS", hidden=True), 515 dict(name="Type", hidden=True), 516 dict(name="source", width=20), 517 dict(name="target", width=20), 518 dict(name="target_func"), 519 ] 520 521 checker_method = CheckPEHooks.collect_eat_hooks 522
523 - def column_types(self):
524 return dict(_EPROCESS=self.session.profile._EPROCESS, 525 source="", 526 target="", 527 target_func=self.session.profile.Function())
528
529 - def collect_hooks(self, task, dll):
530 checker = self.session.plugins.check_pehooks( 531 image_base=dll.base, thorough=self.plugin_args.thorough) 532 533 for info in self.checker_method(checker): 534 info["_EPROCESS"] = task 535 yield info
536
537 - def collect(self):
538 cc = self.session.plugins.cc() 539 with cc: 540 for task in self.filter_processes(): 541 cc.SwitchProcessContext(task) 542 543 yield dict(divider="Process %s (%s)" % (task.name, task.pid)) 544 545 for dll in task.get_load_modules(): 546 for x in self.collect_hooks(task, dll): 547 yield x
548 549
550 -class TestEATHooks(testlib.SimpleTestCase):
551 PLUGIN = "hooks_eat" 552 553 PARAMETERS = dict( 554 commandline="hooks_eat %(pids)s" 555 )
556 557
558 -class IATHooks(EATHooks):
559 """Detect IAT/EAT hooks in process and kernel memory""" 560 561 name = "hooks_iat" 562 checker_method = CheckPEHooks.collect_iat_hooks
563 564
565 -class TestIATHooks(testlib.SimpleTestCase):
566 PLUGIN = "hooks_iat" 567 568 PARAMETERS = dict( 569 commandline="hooks_iat %(pids)s" 570 )
571 572
573 -class InlineHooks(EATHooks):
574 """Detect API hooks in process and kernel memory""" 575 576 name = "hooks_inline" 577 checker_method = CheckPEHooks.collect_inline_hooks 578 table_header = [ 579 dict(name="divider", type="Divider"), 580 dict(name="_EPROCESS", hidden=True), 581 dict(name="source", width=20), 582 dict(name="target", width=20), 583 dict(name="Type", hidden=True), 584 dict(name="source_func", width=60), 585 dict(name="target_func"), 586 ]
587 588
589 -class TestInlineHooks(testlib.SimpleTestCase):
590 PLUGIN = "hooks_inline" 591 592 PARAMETERS = dict( 593 commandline="hooks_inline %(pids)s" 594 )
595