vb6decompiler 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vb6decompiler/__init__.py +11 -0
- vb6decompiler/blob.py +1067 -0
- vb6decompiler/cli.py +292 -0
- vb6decompiler/datareport.py +886 -0
- vb6decompiler/frm.py +1107 -0
- vb6decompiler/literals.py +275 -0
- vb6decompiler/mask.py +192 -0
- vb6decompiler/pe.py +387 -0
- vb6decompiler/procs.py +444 -0
- vb6decompiler/sqlstitch.py +227 -0
- vb6decompiler-0.2.0.dist-info/METADATA +279 -0
- vb6decompiler-0.2.0.dist-info/RECORD +16 -0
- vb6decompiler-0.2.0.dist-info/WHEEL +5 -0
- vb6decompiler-0.2.0.dist-info/entry_points.txt +2 -0
- vb6decompiler-0.2.0.dist-info/licenses/LICENSE +21 -0
- vb6decompiler-0.2.0.dist-info/top_level.txt +1 -0
vb6decompiler/pe.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""PE surface of a VB6 install: what a program imports, exports and drags in.
|
|
2
|
+
|
|
3
|
+
Why this exists
|
|
4
|
+
---------------
|
|
5
|
+
A VB6 application is almost never one file. The executable imports only
|
|
6
|
+
``MSVBVM60.DLL`` and reaches everything else (ODBC, DAO, RDO, XML, mail, sockets)
|
|
7
|
+
through COM created at run time. Reading the import table therefore tells you
|
|
8
|
+
what the *runtime* needs, not what the program does — and the components it really
|
|
9
|
+
uses show up only as sibling DLLs/OCXs, ActiveX registrations and type libraries.
|
|
10
|
+
|
|
11
|
+
Two measured traps are encoded here:
|
|
12
|
+
|
|
13
|
+
* **Both thunk arrays must be walked.** In an import descriptor
|
|
14
|
+
``OriginalFirstThunk`` and ``FirstThunk`` sit in different address ranges; naming
|
|
15
|
+
only ``OriginalFirstThunk`` leaves every ``call dword ptr [0x40xxxx]`` target
|
|
16
|
+
anonymous in the caller's disassembly. Walking both raised named call sites from
|
|
17
|
+
~135k to ~267k on the reference target.
|
|
18
|
+
* **Do not hand-roll the export table.** A hand-written export reader produced
|
|
19
|
+
``NumberOfFunctions ≈ 761,000`` for a 1 MB OCX; ``pefile`` read the same file as
|
|
20
|
+
four standard COM registration exports. Use :func:`exports` here (bounded and
|
|
21
|
+
validated) or ``pefile`` — never a quick struct.unpack.
|
|
22
|
+
|
|
23
|
+
Nothing in this module needs a third-party package.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import os
|
|
29
|
+
import struct
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Iterable
|
|
32
|
+
|
|
33
|
+
#: Imports that identify a VB6-built binary. ``MSVBVM60`` = VB6, ``MSVBVM50`` = VB5.
|
|
34
|
+
VB6_RUNTIMES = {"MSVBVM60.DLL", "MSVBVM50.DLL"}
|
|
35
|
+
|
|
36
|
+
#: The four exports every self-registering COM component carries.
|
|
37
|
+
COM_REGISTRATION_EXPORTS = {
|
|
38
|
+
"DllRegisterServer",
|
|
39
|
+
"DllUnregisterServer",
|
|
40
|
+
"DllGetClassObject",
|
|
41
|
+
"DllCanUnloadNow",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#: Data directory indices used below (PE spec order).
|
|
45
|
+
_DIR_EXPORT = 0
|
|
46
|
+
_DIR_IMPORT = 1
|
|
47
|
+
_DIR_RESOURCE = 2
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PEError(Exception):
|
|
51
|
+
"""The file is not a PE image this module can read."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Section:
|
|
56
|
+
name: str
|
|
57
|
+
virtual_address: int
|
|
58
|
+
virtual_size: int
|
|
59
|
+
raw_offset: int
|
|
60
|
+
raw_size: int
|
|
61
|
+
characteristics: int
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def contains(self) -> bool:
|
|
65
|
+
return bool(self.characteristics & 0x80000000) # IMAGE_SCN_MEM_WRITE
|
|
66
|
+
|
|
67
|
+
def va_to_offset(self, va: int) -> int | None:
|
|
68
|
+
delta = va - self.virtual_address
|
|
69
|
+
if 0 <= delta < max(self.raw_size, self.virtual_size):
|
|
70
|
+
return self.raw_offset + delta
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Import:
|
|
76
|
+
dll: str
|
|
77
|
+
functions: list[str] = field(default_factory=list)
|
|
78
|
+
unnamed: int = 0
|
|
79
|
+
is_ordinal_only: int = 0
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def is_vb6_runtime(self) -> bool:
|
|
83
|
+
return self.dll.upper() in VB6_RUNTIMES
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class PEImage:
|
|
88
|
+
path: str
|
|
89
|
+
machine: int
|
|
90
|
+
image_base: int
|
|
91
|
+
entry_point_rva: int
|
|
92
|
+
sections: list[Section]
|
|
93
|
+
imports: list[Import]
|
|
94
|
+
exports: list[tuple[str | None, int]] # (name or None, ordinal)
|
|
95
|
+
size: int
|
|
96
|
+
|
|
97
|
+
# -- addressing ---------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def bits(self) -> int:
|
|
101
|
+
return 64 if self.machine in (0x8664, 0xAA64) else 32
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def is_vb6(self) -> bool:
|
|
105
|
+
return any(i.is_vb6_runtime for i in self.imports)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def entry_point(self) -> int:
|
|
109
|
+
"""Absolute virtual address of the entry point (ImageBase + RVA)."""
|
|
110
|
+
return self.image_base + self.entry_point_rva
|
|
111
|
+
|
|
112
|
+
def va_to_offset(self, va: int) -> int | None:
|
|
113
|
+
"""Absolute VA -> file offset.
|
|
114
|
+
|
|
115
|
+
VB6 native builds use a fixed ``ImageBase`` of 0x400000, so
|
|
116
|
+
``offset = VA - ImageBase`` works for the common case; this function is
|
|
117
|
+
the honest version: it walks the section table and returns ``None`` for
|
|
118
|
+
addresses that live outside every raw section (BSS, headers).
|
|
119
|
+
"""
|
|
120
|
+
if va < self.image_base:
|
|
121
|
+
return None
|
|
122
|
+
rva = va - self.image_base
|
|
123
|
+
for sec in self.sections:
|
|
124
|
+
off = sec.va_to_offset(rva)
|
|
125
|
+
if off is not None:
|
|
126
|
+
return off
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def section_of(self, rva: int) -> Section | None:
|
|
130
|
+
for sec in self.sections:
|
|
131
|
+
if sec.virtual_address <= rva < sec.virtual_address + max(
|
|
132
|
+
sec.virtual_size, sec.raw_size
|
|
133
|
+
):
|
|
134
|
+
return sec
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# -- convenience --------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def import_dlls(self) -> list[str]:
|
|
141
|
+
return [i.dll for i in self.imports]
|
|
142
|
+
|
|
143
|
+
def summary(self) -> dict:
|
|
144
|
+
return {
|
|
145
|
+
"path": self.path,
|
|
146
|
+
"bits": self.bits,
|
|
147
|
+
"image_base": hex(self.image_base),
|
|
148
|
+
"entry_point": hex(self.entry_point),
|
|
149
|
+
"vb6": self.is_vb6,
|
|
150
|
+
"import_dlls": len(self.imports),
|
|
151
|
+
"import_functions": sum(len(i.functions) + i.unnamed for i in self.imports),
|
|
152
|
+
"exports": len(self.exports),
|
|
153
|
+
"com_registration": sorted(
|
|
154
|
+
n for n, _ in self.exports if n in COM_REGISTRATION_EXPORTS
|
|
155
|
+
),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# parsing
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _u16(b: bytes, off: int) -> int:
|
|
165
|
+
return struct.unpack_from("<H", b, off)[0]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _u32(b: bytes, off: int) -> int:
|
|
169
|
+
return struct.unpack_from("<I", b, off)[0]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def read(path: str) -> PEImage:
|
|
173
|
+
"""Parse a PE file. Raises :class:`PEError` for anything else."""
|
|
174
|
+
with open(path, "rb") as fh:
|
|
175
|
+
data = fh.read()
|
|
176
|
+
|
|
177
|
+
if data[:2] != b"MZ":
|
|
178
|
+
raise PEError(f"{os.path.basename(path)}: not an MZ image")
|
|
179
|
+
pe_off = _u32(data, 0x3C)
|
|
180
|
+
if data[pe_off : pe_off + 4] != b"PE\0\0":
|
|
181
|
+
raise PEError(f"{os.path.basename(path)}: no PE signature")
|
|
182
|
+
|
|
183
|
+
coff = pe_off + 4
|
|
184
|
+
machine = _u16(data, coff)
|
|
185
|
+
n_sections = _u16(data, coff + 2)
|
|
186
|
+
opt_size = _u16(data, coff + 16)
|
|
187
|
+
opt = coff + 20
|
|
188
|
+
magic = _u16(data, opt)
|
|
189
|
+
if magic == 0x10B:
|
|
190
|
+
bits = 32
|
|
191
|
+
image_base = _u32(data, opt + 28)
|
|
192
|
+
directory_count = _u32(data, opt + 92)
|
|
193
|
+
directory_off = opt + 96
|
|
194
|
+
elif magic == 0x20B:
|
|
195
|
+
bits = 64
|
|
196
|
+
image_base = struct.unpack_from("<Q", data, opt + 24)[0]
|
|
197
|
+
directory_count = _u32(data, opt + 108)
|
|
198
|
+
directory_off = opt + 112
|
|
199
|
+
else:
|
|
200
|
+
raise PEError(f"{os.path.basename(path)}: unknown optional header magic 0x{magic:X}")
|
|
201
|
+
entry_point_rva = _u32(data, opt + 16)
|
|
202
|
+
|
|
203
|
+
sections: list[Section] = []
|
|
204
|
+
sec_off = opt + opt_size
|
|
205
|
+
for i in range(n_sections):
|
|
206
|
+
base = sec_off + i * 40
|
|
207
|
+
name = data[base : base + 8].split(b"\0")[0].decode("latin1", "replace")
|
|
208
|
+
sections.append(
|
|
209
|
+
Section(
|
|
210
|
+
name=name,
|
|
211
|
+
virtual_size=_u32(data, base + 8),
|
|
212
|
+
virtual_address=_u32(data, base + 12),
|
|
213
|
+
raw_size=_u32(data, base + 16),
|
|
214
|
+
raw_offset=_u32(data, base + 20),
|
|
215
|
+
characteristics=_u32(data, base + 36),
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def directory(index: int) -> tuple[int, int]:
|
|
220
|
+
if index >= directory_count:
|
|
221
|
+
return (0, 0)
|
|
222
|
+
off = directory_off + index * 8
|
|
223
|
+
return (_u32(data, off), _u32(data, off + 4))
|
|
224
|
+
|
|
225
|
+
def rva_to_off(rva: int) -> int | None:
|
|
226
|
+
for sec in sections:
|
|
227
|
+
if sec.virtual_address <= rva < sec.virtual_address + max(
|
|
228
|
+
sec.virtual_size, sec.raw_size
|
|
229
|
+
):
|
|
230
|
+
delta = rva - sec.virtual_address
|
|
231
|
+
if delta < sec.raw_size:
|
|
232
|
+
return sec.raw_offset + delta
|
|
233
|
+
return None # inside .bss: no file bytes
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
imports = _read_imports(data, directory(_DIR_IMPORT), rva_to_off, bits)
|
|
237
|
+
exports = _read_exports(data, directory(_DIR_EXPORT), rva_to_off)
|
|
238
|
+
|
|
239
|
+
return PEImage(
|
|
240
|
+
path=path,
|
|
241
|
+
machine=machine,
|
|
242
|
+
image_base=image_base,
|
|
243
|
+
entry_point_rva=entry_point_rva,
|
|
244
|
+
sections=sections,
|
|
245
|
+
imports=imports,
|
|
246
|
+
exports=exports,
|
|
247
|
+
size=len(data),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _read_imports(data: bytes, directory: tuple[int, int], rva_to_off, bits: int):
|
|
252
|
+
rva, size = directory
|
|
253
|
+
if not rva:
|
|
254
|
+
return []
|
|
255
|
+
off = rva_to_off(rva)
|
|
256
|
+
if off is None:
|
|
257
|
+
return []
|
|
258
|
+
out: list[Import] = []
|
|
259
|
+
# 5 dwords per descriptor; a zeroed descriptor terminates the array.
|
|
260
|
+
while off + 20 <= len(data):
|
|
261
|
+
oft, _ts, _fwd, name_rva, ft = struct.unpack_from("<IIIII", data, off)
|
|
262
|
+
if not (oft or name_rva or ft):
|
|
263
|
+
break
|
|
264
|
+
name_off = rva_to_off(name_rva)
|
|
265
|
+
dll = ""
|
|
266
|
+
if name_off is not None:
|
|
267
|
+
end = data.find(b"\0", name_off)
|
|
268
|
+
dll = data[name_off:end].decode("latin1", "replace")
|
|
269
|
+
entry = Import(dll=dll)
|
|
270
|
+
|
|
271
|
+
# Both thunk arrays: OriginalFirstThunk holds the name RVA table,
|
|
272
|
+
# FirstThunk holds the loaded addresses. Some linkers leave OFT empty, so
|
|
273
|
+
# the walk is done on each and functions are de-duplicated.
|
|
274
|
+
seen: set[int] = set()
|
|
275
|
+
for thunk_rva in (oft, ft):
|
|
276
|
+
if not thunk_rva:
|
|
277
|
+
continue
|
|
278
|
+
t_off = rva_to_off(thunk_rva)
|
|
279
|
+
if t_off is None:
|
|
280
|
+
continue
|
|
281
|
+
width = 4 if bits == 32 else 8
|
|
282
|
+
step = width
|
|
283
|
+
for n in range(4096): # bounded: a corrupt table must not loop
|
|
284
|
+
pos = t_off + n * step
|
|
285
|
+
if pos + step > len(data):
|
|
286
|
+
break
|
|
287
|
+
value = struct.unpack_from("<I" if width == 4 else "<Q", data, pos)[0]
|
|
288
|
+
if value == 0:
|
|
289
|
+
break
|
|
290
|
+
if value & (1 << (31 if width == 4 else 63)):
|
|
291
|
+
ordinal = value & 0xFFFF
|
|
292
|
+
marker = 0x80000000 | ordinal
|
|
293
|
+
if marker in seen:
|
|
294
|
+
continue
|
|
295
|
+
seen.add(marker)
|
|
296
|
+
entry.unnamed += 1
|
|
297
|
+
entry.is_ordinal_only += 1
|
|
298
|
+
continue
|
|
299
|
+
hint_off = rva_to_off(value)
|
|
300
|
+
if hint_off is None:
|
|
301
|
+
continue
|
|
302
|
+
if hint_off in seen:
|
|
303
|
+
continue
|
|
304
|
+
seen.add(hint_off)
|
|
305
|
+
end = data.find(b"\0", hint_off + 2)
|
|
306
|
+
fname = data[hint_off + 2 : end].decode("latin1", "replace")
|
|
307
|
+
if fname:
|
|
308
|
+
entry.functions.append(fname)
|
|
309
|
+
out.append(entry)
|
|
310
|
+
off += 20
|
|
311
|
+
if len(out) > 512:
|
|
312
|
+
break
|
|
313
|
+
return out
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _is_x86(data: bytes) -> bool:
|
|
317
|
+
pe_off = _u32(data, 0x3C)
|
|
318
|
+
return _u16(data, pe_off + 4) in (0x14C,)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _read_exports(data: bytes, directory: tuple[int, int], rva_to_off):
|
|
322
|
+
rva, size = directory
|
|
323
|
+
if not rva:
|
|
324
|
+
return []
|
|
325
|
+
off = rva_to_off(rva)
|
|
326
|
+
if off is None or off + 40 > len(data):
|
|
327
|
+
return []
|
|
328
|
+
n_functions = _u32(data, off + 20)
|
|
329
|
+
n_names = _u32(data, off + 24)
|
|
330
|
+
addr_funcs = _u32(data, off + 28)
|
|
331
|
+
addr_names = _u32(data, off + 32)
|
|
332
|
+
addr_ordinals = _u32(data, off + 36)
|
|
333
|
+
# Bound both tables by the export directory's own size: a corrupt count is
|
|
334
|
+
# what produced the 761,000-export reading in the hand-written reader.
|
|
335
|
+
if n_names > 65535 or n_functions > 65535:
|
|
336
|
+
raise PEError(
|
|
337
|
+
f"export table claims {n_names} names / {n_functions} functions; refusing to read"
|
|
338
|
+
)
|
|
339
|
+
out: list[tuple[str | None, int]] = []
|
|
340
|
+
names_by_ordinal: dict[int, str] = {}
|
|
341
|
+
names_off = rva_to_off(addr_names)
|
|
342
|
+
ords_off = rva_to_off(addr_ordinals)
|
|
343
|
+
if names_off is not None and ords_off is not None:
|
|
344
|
+
for i in range(min(n_names, 8192)):
|
|
345
|
+
name_rva = _u32(data, names_off + i * 4)
|
|
346
|
+
ordinal_index = _u16(data, ords_off + i * 2)
|
|
347
|
+
name_off = rva_to_off(name_rva)
|
|
348
|
+
if name_off is None:
|
|
349
|
+
continue
|
|
350
|
+
end = data.find(b"\0", name_off)
|
|
351
|
+
names_by_ordinal[ordinal_index] = data[name_off:end].decode("latin1", "replace")
|
|
352
|
+
funcs_off = rva_to_off(addr_funcs)
|
|
353
|
+
count = min(n_functions, 8192) if funcs_off is not None else n_names
|
|
354
|
+
for i in range(count):
|
|
355
|
+
out.append((names_by_ordinal.get(i), i + 1))
|
|
356
|
+
return out
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
# directory-level helpers
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def scan_directory(path: str, recursive: bool = False) -> list[dict]:
|
|
365
|
+
"""PE summary for every ``.exe/.dll/.ocx`` under a path (or one file)."""
|
|
366
|
+
if os.path.isfile(path):
|
|
367
|
+
files = [path]
|
|
368
|
+
else:
|
|
369
|
+
files = []
|
|
370
|
+
for root, _dirs, names in os.walk(path):
|
|
371
|
+
for n in names:
|
|
372
|
+
if n.lower().endswith((".exe", ".dll", ".ocx")):
|
|
373
|
+
files.append(os.path.join(root, n))
|
|
374
|
+
if not recursive:
|
|
375
|
+
break
|
|
376
|
+
out = []
|
|
377
|
+
for f in sorted(files):
|
|
378
|
+
try:
|
|
379
|
+
out.append(read(f).summary())
|
|
380
|
+
except (PEError, OSError, struct.error) as exc:
|
|
381
|
+
out.append({"path": f, "error": str(exc)})
|
|
382
|
+
return out
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def vb6_binaries(images: Iterable[dict]) -> list[str]:
|
|
386
|
+
"""Which of these images import a VB6 runtime (i.e. carry VB6 code)."""
|
|
387
|
+
return [i["path"] for i in images if i.get("vb6")]
|