Skip to content

Commit cda5db3

Browse files
authored
Merge pull request #26 from mrexodia/module-manager
Module manager
2 parents 89721ef + f0f591b commit cda5db3

File tree

7 files changed

+417
-140
lines changed

7 files changed

+417
-140
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ You can get the syscall parameters from [ntsyscalls.py](https://github.com/mrexo
103103

104104
## Collecting the dump
105105

106-
There is a simple [x64dbg](https://github.com/x64dbg/x64dbg) plugin available called [MiniDumpPlugin](https://github.com/mrexodia/MiniDumpPlugin/releases). To create a dump, pause execution and execute the command `MiniDump my.dmp`.
106+
~~There is a simple [x64dbg](https://github.com/x64dbg/x64dbg) plugin available called [MiniDumpPlugin](https://github.com/mrexodia/MiniDumpPlugin/releases)~~ The [minidump](https://help.x64dbg.com/en/latest/commands/memory-operations/minidump.html) command has been integrated into x64dbg since 2022-10-10. To create a dump, pause execution and execute the command `MiniDump my.dmp`.
107107

108108
## Installation
109109

src/dumpulator/details.py

+19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import struct
22
from collections import namedtuple
3+
from typing import List
34

45
from unicorn import *
56
from unicorn.x86_const import *
@@ -563,3 +564,21 @@ def __setitem__(self, index, value):
563564
"Reserved"
564565
]
565566
assert len(interrupt_names) == 32
567+
568+
def format_table(table: List[List[str]]):
569+
result = ""
570+
header = table[0]
571+
lengths = [0] * len(header)
572+
for row in table:
573+
for index, col in enumerate(row):
574+
lengths[index] = max(lengths[index], len(col))
575+
for row in table:
576+
if len(result) > 0:
577+
result += "\n"
578+
line = ""
579+
for index, col in enumerate(row):
580+
if index > 0:
581+
line += " "
582+
line += f"{col:>{lengths[index]}}"
583+
result += line.rstrip()
584+
return result

src/dumpulator/dumpulator.py

+184-76
Large diffs are not rendered by default.

src/dumpulator/memory.py

+68-44
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,15 @@ class MemoryState(Enum):
3030
MEM_RESERVE = 0x2000
3131
MEM_FREE = 0x10000
3232

33-
class MemoryBasicInformation:
34-
def __init__(self, base: int, allocation_base: int, allocation_protect: MemoryProtect):
35-
self.base = base
36-
self.allocation_base = allocation_base
37-
self.allocation_protect = allocation_protect
38-
self.region_size: int = PAGE_SIZE
39-
self.state: MemoryState = None
40-
self.protect: MemoryProtect = None
41-
self.type: MemoryType = None
42-
43-
def __str__(self):
44-
return f"MemoryBasicInformation(base: {hex(self.base)}, allocation_base: {hex(self.allocation_base)}, region_size: {hex(self.region_size)}, state: {self.state}, protect: {self.protect}, type: {self.type})"
45-
4633
class MemoryRegion:
47-
def __init__(self, start: int, size: int, protect: MemoryProtect = MemoryProtect.PAGE_NOACCESS, type: MemoryType = MemoryType.MEM_PRIVATE, comment: str = ""):
34+
def __init__(self, start: int, size: int, protect: MemoryProtect = MemoryProtect.PAGE_NOACCESS, type: MemoryType = MemoryType.MEM_PRIVATE, info: Any = None):
4835
assert start & 0xFFF == 0
4936
assert size & 0xFFF == 0
5037
self.start = start
5138
self.size = size
5239
self.protect = protect
5340
self.type = type
54-
self.comment = comment
41+
self.info = info
5542

5643
@property
5744
def end(self):
@@ -84,12 +71,12 @@ def overlaps(self, other):
8471

8572
def __str__(self):
8673
result = f"{hex(self.start)}[{hex(self.size)}]"
87-
if len(self.comment) > 0:
88-
result += f" ({self.comment})"
74+
if self.info is not None:
75+
result += f" ({self.info})"
8976
return result
9077

9178
def __repr__(self) -> str:
92-
return f"MemoryRegion({hex(self.start)}, {hex(self.size)}, {self.protect}, {self.type}, {self.comment}"
79+
return f"MemoryRegion({hex(self.start)}, {hex(self.size)}, {self.protect}, {self.type}, {repr(self.info)})"
9380

9481
def pages(self):
9582
for page in range(self.start, self.end, PAGE_SIZE):
@@ -105,6 +92,20 @@ def decommit(self, addr: int, size: int) -> None:
10592
def protect(self, addr: int, size: int, protect: MemoryProtect) -> None:
10693
raise NotImplementedError()
10794

95+
class MemoryBasicInformation:
96+
def __init__(self, base: int, allocation_base: int, allocation_protect: MemoryProtect):
97+
self.base = base
98+
self.allocation_base = allocation_base
99+
self.allocation_protect = allocation_protect
100+
self.region_size: int = PAGE_SIZE
101+
self.state: MemoryState = None
102+
self.protect: MemoryProtect = None
103+
self.type: MemoryType = None
104+
self.info: Any = None
105+
106+
def __str__(self):
107+
return f"MemoryBasicInformation(base: {hex(self.base)}, allocation_base: {hex(self.allocation_base)}, region_size: {hex(self.region_size)}, state: {self.state}, protect: {self.protect}, type: {self.type})"
108+
108109
class MemoryManager:
109110
def __init__(self, page_manager: PageManager, minimum = 0x10000, maximum = 0x7fffffff0000, granularity = 0x10000):
110111
self._page_manager = page_manager
@@ -114,9 +115,9 @@ def __init__(self, page_manager: PageManager, minimum = 0x10000, maximum = 0x7ff
114115
self._regions: List[MemoryRegion] = []
115116
self._committed: Dict[int, MemoryRegion] = {}
116117

117-
def find_parent(self, region: Union[MemoryRegion, int]):
118+
def find_region(self, region: Union[MemoryRegion, int]):
118119
if isinstance(region, int):
119-
region = MemoryRegion(self.page_align(region), 0)
120+
region = MemoryRegion(self.align_page(region), 0)
120121
index = bisect.bisect_right(self._regions, region)
121122
if index == 0:
122123
return None
@@ -127,31 +128,35 @@ def find_parent(self, region: Union[MemoryRegion, int]):
127128
else:
128129
return None
129130

130-
def page_align(self, addr: int):
131+
def find_commit(self, addr: int):
132+
addr = self.align_page(addr)
133+
return self._committed.get(addr, None)
134+
135+
def align_page(self, addr: int):
131136
mask = PAGE_SIZE - 1
132137
return (addr + mask) & ~mask
133138

134-
def allocation_align(self, addr: int):
139+
def align_allocation(self, addr: int):
135140
mask = self._granularity - 1
136141
return (addr + mask) & ~mask
137142

138143
def find_free(self, size: int):
139-
assert size > 0 and self.page_align(size) == size
144+
assert size > 0 and self.align_page(size) == size
140145
base = self._minimum
141146
while base < self._maximum:
142147
info = self.query(base)
143148
assert info.base == base
144-
if info.state == MemoryState.MEM_FREE and info.region_size >= size and self.allocation_align(base) == base:
149+
if info.state == MemoryState.MEM_FREE and info.region_size >= size and self.align_allocation(base) == base:
145150
return info.base
146151
base += info.region_size
147152
return None
148153

149-
def reserve(self, start: int, size: int, protect: MemoryProtect, type: MemoryType = MemoryType.MEM_PRIVATE, comment: str = ""):
154+
def reserve(self, start: int, size: int, protect: MemoryProtect, type: MemoryType = MemoryType.MEM_PRIVATE, info: Any = None):
150155
assert isinstance(protect, MemoryProtect)
151156
assert isinstance(type, MemoryType)
152-
assert size > 0 and self.page_align(size) == size
153-
assert self.allocation_align(start) == start
154-
region = MemoryRegion(start, size, protect, type, comment)
157+
assert size > 0 and self.align_page(size) == size
158+
assert self.align_allocation(start) == start
159+
region = MemoryRegion(start, size, protect, type, info)
155160
if region.start < self._minimum or region.end > self._maximum:
156161
raise KeyError(f"Requested region {region} is out of bounds")
157162

@@ -170,9 +175,9 @@ def check_overlaps(index):
170175
self._regions.insert(index, region)
171176

172177
def release(self, start: int):
173-
assert self.allocation_align(start) == start
178+
assert self.align_allocation(start) == start
174179

175-
parent_region = self.find_parent(start)
180+
parent_region = self.find_region(start)
176181
if parent_region is None:
177182
raise KeyError(f"Could not find parent for {hex(start)}")
178183
if parent_region.start != start:
@@ -191,10 +196,10 @@ def release(self, start: int):
191196

192197
def commit(self, start: int, size: int, protect: MemoryProtect = MemoryProtect.UNDEFINED):
193198
assert isinstance(protect, MemoryProtect)
194-
assert size > 0 and self.page_align(size) == size
195-
assert self.page_align(start) == start
199+
assert size > 0 and self.align_page(size) == size
200+
assert self.align_page(start) == start
196201
region = MemoryRegion(start, size)
197-
parent_region = self.find_parent(region)
202+
parent_region = self.find_region(region)
198203
if parent_region is None:
199204
raise KeyError(f"Could not find parent for {region}")
200205

@@ -208,17 +213,19 @@ def commit(self, start: int, size: int, protect: MemoryProtect = MemoryProtect.U
208213
else:
209214
for page in region.pages():
210215
if page in self._committed:
211-
self._page_manager.protect(page, PAGE_SIZE, protect)
212-
self._committed[page].protect = protect
216+
page_region = self._committed[page]
217+
if page_region.protect != protect:
218+
self._page_manager.protect(page, PAGE_SIZE, protect)
219+
page_region.protect = protect
213220
else:
214221
self._page_manager.commit(page, PAGE_SIZE, protect)
215222
self._committed[page] = MemoryRegion(page, PAGE_SIZE, protect, parent_region.type)
216223

217224
def decommit(self, start: int, size: int):
218-
assert size > 0 and self.page_align(size) == size
219-
assert self.page_align(start) == start
225+
assert size > 0 and self.align_page(size) == size
226+
assert self.align_page(start) == start
220227
region = MemoryRegion(start, size)
221-
parent_region = self.find_parent(region)
228+
parent_region = self.find_region(region)
222229
if parent_region is None:
223230
raise KeyError(f"Could not find parent for {region}")
224231

@@ -234,10 +241,10 @@ def decommit(self, start: int, size: int):
234241

235242
def protect(self, start: int, size: int, protect: MemoryProtect):
236243
assert isinstance(protect, MemoryProtect)
237-
assert size > 0 and self.page_align(size) == size
238-
assert self.page_align(start) == start
244+
assert size > 0 and self.align_page(size) == size
245+
assert self.align_page(start) == start
239246
region = MemoryRegion(start, size)
240-
parent_region = self.find_parent(region)
247+
parent_region = self.find_region(region)
241248
if parent_region is None:
242249
raise KeyError(f"Could not find parent for {region}")
243250

@@ -250,15 +257,17 @@ def protect(self, start: int, size: int, protect: MemoryProtect):
250257
old_protect = self._committed[region.start].protect
251258
self._page_manager.protect(region.start, region.size, protect)
252259
for page in region.pages():
253-
self._committed[page].protect = protect
260+
page_region = self._committed[page]
261+
if page_region.protect != protect:
262+
page_region.protect = protect
254263

255264
return old_protect
256265

257266
def query(self, start: int):
258-
assert self.page_align(start) == start
267+
start = self.align_page(start)
259268

260269
region = MemoryRegion(start, 0)
261-
parent_region = self.find_parent(region)
270+
parent_region = self.find_region(region)
262271
if parent_region is None:
263272
index = bisect.bisect_right(self._regions, region)
264273
next_start = self._maximum
@@ -279,11 +288,15 @@ def query(self, start: int):
279288
continue
280289
elif result is None:
281290
result = MemoryBasicInformation(page, parent_region.start, parent_region.protect)
291+
if page == parent_region.start:
292+
result.info = parent_region.info
282293
if page in self._committed:
283294
result.state = MemoryState.MEM_COMMIT
284295
commited_page = self._committed[page]
285296
result.protect = commited_page.protect
286297
result.type = commited_page.type
298+
if commited_page.info:
299+
result.info = commited_page.info
287300
assert commited_page.type == parent_region.type
288301
else:
289302
result.state = MemoryState.MEM_RESERVE
@@ -292,6 +305,8 @@ def query(self, start: int):
292305
else:
293306
commited_page = self._committed.get(page, None)
294307
if result.state == MemoryState.MEM_RESERVE:
308+
if commited_page is not None:
309+
break
295310
result.region_size += PAGE_SIZE
296311
elif result.state == MemoryState.MEM_COMMIT:
297312
if commited_page is not None and commited_page.type == result.type and commited_page.protect == result.protect:
@@ -301,3 +316,12 @@ def query(self, start: int):
301316
else:
302317
assert False # unreachable
303318
return result
319+
320+
def map(self):
321+
addr = self._minimum
322+
regions: List[MemoryBasicInformation] = []
323+
while addr < self._maximum:
324+
info = self.query(addr)
325+
regions.append(info)
326+
addr += info.region_size
327+
return regions

src/dumpulator/modules.py

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from typing import Dict, Optional, Type, Union, List
2+
3+
import pefile
4+
from .memory import MemoryManager
5+
6+
# TODO: support forwards
7+
class ModuleExport:
8+
def __init__(self, address: int, ordinal: int, name: str):
9+
self.address = address
10+
self.ordinal = ordinal
11+
self.name = name
12+
13+
class Module:
14+
def __init__(self, pe: pefile.PE, path: str):
15+
self.pe = pe
16+
self.path = path
17+
self.name = path.split("\\")[-1]
18+
self._exports_by_address: Dict[int, int] = {}
19+
self._exports_by_ordinal: Dict[int, int] = {}
20+
self._exports_by_name: Dict[str, int] = {}
21+
self.exports: List[ModuleExport] = []
22+
self._parse_pe()
23+
24+
def _parse_pe(self):
25+
self.base: int = self.pe.OPTIONAL_HEADER.ImageBase
26+
self.size: int = self.pe.OPTIONAL_HEADER.SizeOfImage
27+
self.entry: int = self.base + self.pe.OPTIONAL_HEADER.AddressOfEntryPoint
28+
self.pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]])
29+
pe_exports = self.pe.DIRECTORY_ENTRY_EXPORT.symbols if hasattr(self.pe, "DIRECTORY_ENTRY_EXPORT") else []
30+
for pe_export in pe_exports:
31+
va = self.base + pe_export.address
32+
if pe_export.name:
33+
name = pe_export.name.decode("ascii")
34+
else:
35+
name = None
36+
export = ModuleExport(va, pe_export.ordinal, name)
37+
self._exports_by_address[export.address] = len(self.exports)
38+
self._exports_by_ordinal[export.ordinal] = len(self.exports)
39+
if name is not None:
40+
self._exports_by_name[name] = len(self.exports)
41+
self.exports.append(export)
42+
43+
def find_export(self, key: Union[str, int]):
44+
if isinstance(key, int):
45+
index = self._exports_by_ordinal.get(key, None)
46+
if index is None:
47+
index = self._exports_by_address.get(key, None)
48+
if index is None:
49+
return None
50+
return self.exports[index]
51+
elif isinstance(key, str):
52+
index = self._exports_by_name.get(key)
53+
if index is None:
54+
return None
55+
return self.exports[index]
56+
raise TypeError()
57+
58+
def __repr__(self):
59+
return f"Module({hex(self.base)}, {hex(self.size)}, {repr(self.path)})"
60+
61+
def __contains__(self, addr: int):
62+
return addr >= self.base and addr < self.base + self.size
63+
64+
class ModuleManager:
65+
def __init__(self, memory: MemoryManager):
66+
self._memory = memory
67+
self._name_lookup: Dict[str, int] = {}
68+
self._modules: Dict[int, Module] = {}
69+
70+
def add(self, pe: pefile.PE, path: str):
71+
module = Module(pe, path)
72+
self._modules[module.base] = module
73+
region = self._memory.find_region(module.base)
74+
assert region.start == module.base
75+
assert region is not None
76+
region.info = module
77+
self._name_lookup[module.name] = module.base
78+
self._name_lookup[module.name.lower()] = module.base
79+
self._name_lookup[module.path] = module.base
80+
return module
81+
82+
def find(self, key: Union[str, int]) -> Optional[Module]:
83+
if isinstance(key, int):
84+
region = self._memory.find_region(key)
85+
if region.info:
86+
assert isinstance(region.info, Module)
87+
return region.info
88+
return None
89+
if isinstance(key, str):
90+
base = self._name_lookup.get(key, None)
91+
if base is None:
92+
base = self._name_lookup.get(key.lower(), None)
93+
if base is None:
94+
return None
95+
return self.find(base)
96+
raise TypeError()
97+
98+
def __getitem__(self, key: Union[str, int]) -> Module:
99+
module = self.find(key)
100+
if module is None:
101+
raise KeyError()
102+
return module
103+
104+
def __contains__(self, key: Union[str, int]):
105+
return self.find(key) is not None
106+
107+
def __iter__(self):
108+
for base in self._modules:
109+
yield self._modules[base]

0 commit comments

Comments
 (0)