Skip to content

Commit b22e8e3

Browse files
authored
Merge pull request #65 from mrexodia/exception-hooks
Implement exception hooks
2 parents edc4ec6 + 3e2509b commit b22e8e3

File tree

4 files changed

+117
-66
lines changed

4 files changed

+117
-66
lines changed

src/dumpulator/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
from .dumpulator import Dumpulator
1+
from .dumpulator import Dumpulator, ExceptionType, MemoryViolation, ExceptionInfo
22
from .ntsyscalls import syscall

src/dumpulator/dumpulator.py

+105-62
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import sys
44
import traceback
55
from enum import Enum
6-
from typing import List, Union, NamedTuple
6+
from typing import List, Union, NamedTuple, Callable
77
import inspect
88
from collections import OrderedDict
99
from dataclasses import dataclass, field
@@ -39,22 +39,41 @@ class ExceptionType(Enum):
3939
ContextSwitch = 3
4040
Terminate = 4
4141

42+
class MemoryViolation(Enum):
43+
Unknown = 0
44+
ReadUnmapped = 1
45+
WriteUnmapped = 2
46+
ExecuteUnmapped = 3
47+
ReadProtect = 4
48+
WriteProtect = 5
49+
ExecuteProtect = 6
50+
ReadUnaligned = 7
51+
WriteUnaligned = 8
52+
ExecuteUnaligned = 9
53+
4254
@dataclass
4355
class ExceptionInfo:
4456
type: ExceptionType = ExceptionType.NoException
45-
memory_access: int = 0 # refers to UC_MEM_* values
57+
# type == ExceptionType.Memory
58+
memory_violation: MemoryViolation = MemoryViolation.Unknown
4659
memory_address: int = 0
4760
memory_size: int = 0
4861
memory_value: int = 0
62+
# type == ExceptionType.Interrupt
4963
interrupt_number: int = 0
64+
65+
# Internal state
66+
_handling: bool = False
67+
68+
@dataclass
69+
class UnicornExceptionInfo(ExceptionInfo):
70+
final: bool = False
5071
code_hook_h: Optional[int] = None # represents a `unicorn.uc_hook_h` value (from uc.hook_add)
5172
context: Optional[unicorn.UcContext] = None
5273
tb_start: int = 0
5374
tb_size: int = 0
5475
tb_icount: int = 0
5576
step_count: int = 0
56-
final: bool = False
57-
handling: bool = False
5877

5978
def __str__(self):
6079
return f"{self.type}, ({hex(self.tb_start)}, {hex(self.tb_size)}, {self.tb_icount})"
@@ -305,8 +324,9 @@ def __init__(self, minidump_file, *, trace=False, quiet=False, thread_id=None, d
305324
self.kill_me = None
306325
self.exit_code = None
307326
self.exports = self._all_exports()
308-
self.exception = ExceptionInfo()
309-
self.last_exception: Optional[ExceptionInfo] = None
327+
self._exception = UnicornExceptionInfo()
328+
self._last_exception: Optional[UnicornExceptionInfo] = None
329+
self._exception_hook: Optional[Callable[[ExceptionInfo], Optional[int]]] = None
310330
if not self._quiet:
311331
print("Memory map:")
312332
self.print_memory()
@@ -896,15 +916,28 @@ def allocate(self, size, page_align=False):
896916
self.memory.commit(self.memory.align_page(ptr), self.memory.align_page(size))
897917
return ptr
898918

899-
def handle_exception(self):
900-
assert not self.exception.handling
901-
self.exception.handling = True
919+
def set_exception_hook(self, exception_hook: Optional[Callable[[ExceptionInfo], Optional[int]]]):
920+
previous_hook = self._exception_hook
921+
self._exception_hook = exception_hook
922+
return previous_hook
902923

903-
if self.exception.type == ExceptionType.ContextSwitch:
924+
def handle_exception(self):
925+
assert not self._exception._handling
926+
self._exception._handling = True
927+
928+
if self._exception_hook is not None:
929+
hook_result = self._exception_hook(self._exception)
930+
if hook_result is not None:
931+
# Clear the pending exception
932+
self._last_exception = self._exception
933+
self._exception = UnicornExceptionInfo()
934+
return hook_result
935+
936+
if self._exception.type == ExceptionType.ContextSwitch:
904937
self.info(f"context switch, cip: {hex(self.regs.cip)}")
905938
# Clear the pending exception
906-
self.last_exception = self.exception
907-
self.exception = ExceptionInfo()
939+
self._last_exception = self._exception
940+
self._exception = UnicornExceptionInfo()
908941
# NOTE: the context has already been restored using context_restore in the caller
909942
return self.regs.cip
910943

@@ -961,22 +994,23 @@ def handle_exception(self):
961994
context_ex.XState.Offset = 0xF0 if self._x64 else 0x20
962995
context_ex.XState.Length = 0x160 if self._x64 else 0x140
963996
record = record_type()
964-
if self.exception.type == ExceptionType.Memory:
965-
record.ExceptionCode = 0xC0000005
997+
alignment_violations = [MemoryViolation.ReadUnaligned, MemoryViolation.WriteUnaligned, MemoryViolation.ExecuteUnaligned]
998+
if self._exception.type == ExceptionType.Memory and self._exception.memory_violation not in alignment_violations:
999+
record.ExceptionCode = STATUS_ACCESS_VIOLATION
9661000
record.ExceptionFlags = 0
9671001
record.ExceptionAddress = self.regs.cip
9681002
record.NumberParameters = 2
9691003
types = {
970-
UC_MEM_READ_UNMAPPED: EXCEPTION_READ_FAULT,
971-
UC_MEM_WRITE_UNMAPPED: EXCEPTION_WRITE_FAULT,
972-
UC_MEM_FETCH_UNMAPPED: EXCEPTION_READ_FAULT,
973-
UC_MEM_READ_PROT: EXCEPTION_READ_FAULT,
974-
UC_MEM_WRITE_PROT: EXCEPTION_WRITE_FAULT,
975-
UC_MEM_FETCH_PROT: EXCEPTION_EXECUTE_FAULT,
1004+
MemoryViolation.ReadUnmapped: EXCEPTION_READ_FAULT,
1005+
MemoryViolation.WriteUnmapped: EXCEPTION_WRITE_FAULT,
1006+
MemoryViolation.ExecuteUnmapped: EXCEPTION_READ_FAULT,
1007+
MemoryViolation.ReadProtect: EXCEPTION_READ_FAULT,
1008+
MemoryViolation.WriteProtect: EXCEPTION_WRITE_FAULT,
1009+
MemoryViolation.ExecuteProtect: EXCEPTION_EXECUTE_FAULT,
9761010
}
977-
record.ExceptionInformation[0] = types[self.exception.memory_access]
978-
record.ExceptionInformation[1] = self.exception.memory_address
979-
elif self.exception.type == ExceptionType.Interrupt and self.exception.interrupt_number == 3:
1011+
record.ExceptionInformation[0] = types[self._exception.memory_violation]
1012+
record.ExceptionInformation[1] = self._exception.memory_address
1013+
elif self._exception.type == ExceptionType.Interrupt and self._exception.interrupt_number == 3:
9801014
if self._x64:
9811015
context.Rip -= 1 # TODO: long int3 and prefixes
9821016
record.ExceptionCode = 0x80000003
@@ -990,11 +1024,11 @@ def handle_exception(self):
9901024
record.ExceptionAddress = context.Eip
9911025
record.NumberParameters = 1
9921026
else:
993-
raise NotImplementedError(f"{self.exception}") # TODO: implement
1027+
raise NotImplementedError(f"{self._exception}") # TODO: implement
9941028

9951029
# Clear the pending exception
996-
self.last_exception = self.exception
997-
self.exception = ExceptionInfo()
1030+
self._last_exception = self._exception
1031+
self._exception = UnicornExceptionInfo()
9981032

9991033
def write_stack(cur_ptr: int, data: bytes):
10001034
self.write(cur_ptr, data)
@@ -1024,19 +1058,19 @@ def write_stack(cur_ptr: int, data: bytes):
10241058

10251059
def start(self, begin, end=0xffffffffffffffff, count=0) -> None:
10261060
# Clear exceptions before starting
1027-
self.exception = ExceptionInfo()
1061+
self._exception = UnicornExceptionInfo()
10281062
emu_begin = begin
10291063
emu_until = end
10301064
emu_count = count
10311065
while True:
10321066
try:
1033-
if self.exception.type != ExceptionType.NoException:
1034-
if self.exception.final:
1067+
if self._exception.type != ExceptionType.NoException:
1068+
if self._exception.final:
10351069
# Restore the context (unicorn might mess with it before stopping)
1036-
if self.exception.context is not None:
1037-
self._uc.context_restore(self.exception.context)
1070+
if self._exception.context is not None:
1071+
self._uc.context_restore(self._exception.context)
10381072

1039-
if self.exception.type == ExceptionType.Terminate:
1073+
if self._exception.type == ExceptionType.Terminate:
10401074
if self.exit_code is not None:
10411075
self.info(f"exit code: {hex(self.exit_code)}")
10421076
break
@@ -1051,20 +1085,20 @@ def start(self, begin, end=0xffffffffffffffff, count=0) -> None:
10511085
emu_count = 0
10521086
else:
10531087
# If this happens there was an error restarting simulation
1054-
assert self.exception.step_count == 0
1088+
assert self._exception.step_count == 0
10551089

10561090
# Hook should be installed at this point
1057-
assert self.exception.code_hook_h is not None
1091+
assert self._exception.code_hook_h is not None
10581092

10591093
# Restore the context (unicorn might mess with it before stopping)
1060-
assert self.exception.context is not None
1061-
self._uc.context_restore(self.exception.context)
1094+
assert self._exception.context is not None
1095+
self._uc.context_restore(self._exception.context)
10621096

10631097
# Restart emulation
10641098
self.info(f"restarting emulation to handle exception...")
10651099
emu_begin = self.regs.cip
10661100
emu_until = 0xffffffffffffffff
1067-
emu_count = self.exception.tb_icount + 1
1101+
emu_count = self._exception.tb_icount + 1
10681102

10691103
self.info(f"emu_start({hex(emu_begin)}, {hex(emu_until)}, {emu_count})")
10701104
self.kill_me = None
@@ -1076,7 +1110,7 @@ def start(self, begin, end=0xffffffffffffffff, count=0) -> None:
10761110
except UcError as err:
10771111
if self.kill_me is not None and type(self.kill_me) is not UcError:
10781112
raise self.kill_me
1079-
if self.exception.type != ExceptionType.NoException:
1113+
if self._exception.type != ExceptionType.NoException:
10801114
# Handle the exception outside of the except handler
10811115
continue
10821116
else:
@@ -1232,7 +1266,7 @@ def load_dll(self, file_name: str, file_data: bytes):
12321266
def _hook_code_exception(uc: Uc, address, size, dp: Dumpulator):
12331267
try:
12341268
dp.info(f"exception step: {hex(address)}[{size}]")
1235-
ex = dp.exception
1269+
ex = dp._exception
12361270
ex.step_count += 1
12371271
if ex.step_count >= ex.tb_icount:
12381272
raise Exception("Stepped past the basic block without reaching exception")
@@ -1246,18 +1280,27 @@ def _hook_mem(uc: Uc, access, address, size, value, dp: Dumpulator):
12461280
return True
12471281

12481282
fetch_accesses = [UC_MEM_FETCH, UC_MEM_FETCH_PROT, UC_MEM_FETCH_UNMAPPED]
1249-
if access == UC_MEM_FETCH_UNMAPPED and address >= FORCE_KILL_ADDR - 0x10 and address <= FORCE_KILL_ADDR + 0x10 and dp.kill_me is not None:
1283+
if access == UC_MEM_FETCH_UNMAPPED and FORCE_KILL_ADDR - 0x10 <= address <= FORCE_KILL_ADDR + 0x10 and dp.kill_me is not None:
12501284
dp.error(f"forced exit memory operation {access} of {hex(address)}[{hex(size)}] = {hex(value)}")
12511285
return False
1252-
if dp.exception.final and access in fetch_accesses:
1286+
if dp._exception.final and access in fetch_accesses:
12531287
dp.info(f"fetch from {hex(address)}[{size}] already reported")
12541288
return False
12551289
# TODO: figure out why when you start executing at 0 this callback is triggered more than once
12561290
try:
1291+
violation = {
1292+
UC_MEM_READ_UNMAPPED: MemoryViolation.ReadUnmapped,
1293+
UC_MEM_WRITE_UNMAPPED: MemoryViolation.WriteUnmapped,
1294+
UC_MEM_FETCH_UNMAPPED: MemoryViolation.ExecuteUnmapped,
1295+
UC_MEM_READ_PROT: MemoryViolation.ReadProtect,
1296+
UC_MEM_WRITE_PROT: MemoryViolation.WriteProtect,
1297+
UC_MEM_FETCH_PROT: MemoryViolation.ExecuteProtect,
1298+
}.get(access, MemoryViolation.Unknown)
1299+
assert violation != MemoryViolation.Unknown, f"Unexpected memory access {access}"
12571300
# Extract exception information
1258-
exception = ExceptionInfo()
1301+
exception = UnicornExceptionInfo()
12591302
exception.type = ExceptionType.Memory
1260-
exception.memory_access = access
1303+
exception.memory_violation = violation
12611304
exception.memory_address = address
12621305
exception.memory_size = size
12631306
exception.memory_value = value
@@ -1269,7 +1312,7 @@ def _hook_mem(uc: Uc, access, address, size, value, dp: Dumpulator):
12691312
exception.tb_icount = tb.icount
12701313

12711314
# Print exception info
1272-
final = dp.trace or dp.exception.code_hook_h is not None
1315+
final = dp.trace or dp._exception.code_hook_h is not None
12731316
info = "final" if final else "initial"
12741317
if access == UC_MEM_READ_UNMAPPED:
12751318
dp.error(f"{info} unmapped read from {hex(address)}[{hex(size)}], cip = {hex(dp.regs.cip)}, exception: {exception}")
@@ -1295,25 +1338,25 @@ def _hook_mem(uc: Uc, access, address, size, value, dp: Dumpulator):
12951338
if final:
12961339
# Make sure this is the same exception we expect
12971340
if not dp.trace:
1298-
assert access == dp.exception.memory_access
1299-
assert address == dp.exception.memory_address
1300-
assert size == dp.exception.memory_size
1301-
assert value == dp.exception.memory_value
1341+
assert violation == dp._exception.memory_violation
1342+
assert address == dp._exception.memory_address
1343+
assert size == dp._exception.memory_size
1344+
assert value == dp._exception.memory_value
13021345

13031346
# Delete the code hook
1304-
uc.hook_del(dp.exception.code_hook_h)
1305-
dp.exception.code_hook_h = None
1347+
uc.hook_del(dp._exception.code_hook_h)
1348+
dp._exception.code_hook_h = None
13061349

13071350
# At this point we know for sure the context is correct so we can report the exception
1308-
dp.exception = exception
1309-
dp.exception.final = True
1351+
dp._exception = exception
1352+
dp._exception.final = True
13101353

13111354
# Stop emulation (we resume it on KiUserExceptionDispatcher later)
13121355
dp.stop()
13131356
return False
13141357

13151358
# There should not be an exception active
1316-
assert dp.exception.type == ExceptionType.NoException
1359+
assert dp._exception.type == ExceptionType.NoException
13171360

13181361
# Remove the translation block cache for this block
13191362
# Without doing this single stepping the block won't work
@@ -1325,7 +1368,7 @@ def _hook_mem(uc: Uc, access, address, size, value, dp: Dumpulator):
13251368
exception.code_hook_h = uc.hook_add(UC_HOOK_CODE, _hook_code_exception, user_data=dp)
13261369

13271370
# Store the exception info
1328-
dp.exception = exception
1371+
dp._exception = exception
13291372

13301373
# Stop emulation (we resume execution later)
13311374
dp.stop()
@@ -1452,7 +1495,7 @@ def _arg_type_string(arg):
14521495
def _hook_interrupt(uc: Uc, number, dp: Dumpulator):
14531496
try:
14541497
# Extract exception information
1455-
exception = ExceptionInfo()
1498+
exception = UnicornExceptionInfo()
14561499
exception.type = ExceptionType.Interrupt
14571500
exception.interrupt_number = number
14581501
exception.context = uc.context_save()
@@ -1470,11 +1513,11 @@ def _hook_interrupt(uc: Uc, number, dp: Dumpulator):
14701513
dp.error(f"interrupt {number} ({description}), cip = {hex(dp.regs.cip)}, cs = {hex(dp.regs.cs)}")
14711514

14721515
# There should not be an exception active
1473-
assert dp.exception.type == ExceptionType.NoException
1516+
assert dp._exception.type == ExceptionType.NoException
14741517

14751518
# At this point we know for sure the context is correct so we can report the exception
1476-
dp.exception = exception
1477-
dp.exception.final = True
1519+
dp._exception = exception
1520+
dp._exception.final = True
14781521
except AssertionError as err:
14791522
traceback.print_exc()
14801523
raise err
@@ -1560,7 +1603,7 @@ def syscall_arg(index):
15601603
status = syscall_impl(dp, *args)
15611604
if isinstance(status, ExceptionInfo):
15621605
print("context switch, stopping emulation")
1563-
dp.exception = status
1606+
dp._exception = status
15641607
raise dp.raise_kill(UcError(UC_ERR_EXCEPTION)) from None
15651608
else:
15661609
dp.info(f"status = {hex(status)}")
@@ -1610,11 +1653,11 @@ def _hook_invalid(uc: Uc, dp: Dumpulator):
16101653
instr = next(dp.cs.disasm(code, address, 1))
16111654
if _emulate_unsupported_instruction(dp, instr):
16121655
# Resume execution with a context switch
1613-
assert dp.exception.type == ExceptionType.NoException
1614-
exception = ExceptionInfo()
1656+
assert dp._exception.type == ExceptionType.NoException
1657+
exception = UnicornExceptionInfo()
16151658
exception.type = ExceptionType.ContextSwitch
16161659
exception.final = True
1617-
dp.exception = exception
1660+
dp._exception = exception
16181661
return False # NOTE: returning True would stop emulation
16191662
except StopIteration:
16201663
pass # Unsupported instruction

src/dumpulator/native.py

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# NTSTATUS
99
STATUS_SUCCESS = 0
1010
STATUS_NOT_IMPLEMENTED = 0xC0000002
11+
STATUS_ACCESS_VIOLATION = 0xC0000005
1112
STATUS_INVALID_HANDLE = 0xC0000008
1213
STATUS_NO_SUCH_FILE = 0xC000000F
1314
STATUS_ACCESS_DENIED = 0xC0000022

0 commit comments

Comments
 (0)