richprint-pe 1.0.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.
- richprint/__init__.py +62 -0
- richprint/__main__.py +7 -0
- richprint/cli.py +112 -0
- richprint/constants.py +47 -0
- richprint/data/__init__.py +1 -0
- richprint/data/comp_id.txt +4566 -0
- richprint/database.py +94 -0
- richprint/exceptions.py +41 -0
- richprint/models.py +72 -0
- richprint/parser.py +338 -0
- richprint_pe-1.0.0.dist-info/METADATA +119 -0
- richprint_pe-1.0.0.dist-info/RECORD +15 -0
- richprint_pe-1.0.0.dist-info/WHEEL +4 -0
- richprint_pe-1.0.0.dist-info/entry_points.txt +2 -0
- richprint_pe-1.0.0.dist-info/licenses/LICENSE +22 -0
richprint/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
richprint - Decode and print Rich headers from Windows PE executables.
|
|
3
|
+
|
|
4
|
+
The Rich header is metadata embedded by Microsoft's linker containing
|
|
5
|
+
compiler version information (@comp.id records).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .parser import parse_file, parse_bytes
|
|
9
|
+
from .database import load_database, lookup_description, CompilerDatabase
|
|
10
|
+
from .models import CompilerEntry, RichHeader, PEInfo, ParseResult
|
|
11
|
+
from .constants import (
|
|
12
|
+
MZ_SIGNATURE,
|
|
13
|
+
PE_SIGNATURE,
|
|
14
|
+
RICH_SIGNATURE,
|
|
15
|
+
DANS_SIGNATURE,
|
|
16
|
+
MACHINE_TYPES,
|
|
17
|
+
get_machine_type,
|
|
18
|
+
)
|
|
19
|
+
from .exceptions import (
|
|
20
|
+
RichPrintError,
|
|
21
|
+
FileOpenError,
|
|
22
|
+
NoMZHeaderError,
|
|
23
|
+
NoPEHeaderError,
|
|
24
|
+
InvalidDOSHeaderError,
|
|
25
|
+
NoRichHeaderError,
|
|
26
|
+
NoDanSTokenError,
|
|
27
|
+
InvalidRichHeaderError,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = "1.0.0"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Main API
|
|
34
|
+
"parse_file",
|
|
35
|
+
"parse_bytes",
|
|
36
|
+
"load_database",
|
|
37
|
+
"lookup_description",
|
|
38
|
+
# Models
|
|
39
|
+
"CompilerEntry",
|
|
40
|
+
"RichHeader",
|
|
41
|
+
"PEInfo",
|
|
42
|
+
"ParseResult",
|
|
43
|
+
"CompilerDatabase",
|
|
44
|
+
# Constants
|
|
45
|
+
"MZ_SIGNATURE",
|
|
46
|
+
"PE_SIGNATURE",
|
|
47
|
+
"RICH_SIGNATURE",
|
|
48
|
+
"DANS_SIGNATURE",
|
|
49
|
+
"MACHINE_TYPES",
|
|
50
|
+
"get_machine_type",
|
|
51
|
+
# Exceptions
|
|
52
|
+
"RichPrintError",
|
|
53
|
+
"FileOpenError",
|
|
54
|
+
"NoMZHeaderError",
|
|
55
|
+
"NoPEHeaderError",
|
|
56
|
+
"InvalidDOSHeaderError",
|
|
57
|
+
"NoRichHeaderError",
|
|
58
|
+
"NoDanSTokenError",
|
|
59
|
+
"InvalidRichHeaderError",
|
|
60
|
+
# Version
|
|
61
|
+
"__version__",
|
|
62
|
+
]
|
richprint/__main__.py
ADDED
richprint/cli.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Command-line interface for richprint."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from .database import load_database
|
|
9
|
+
from .parser import parse_file
|
|
10
|
+
from .models import ParseResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def format_entry_line(entry) -> str:
|
|
14
|
+
"""Format a single Rich header entry for display."""
|
|
15
|
+
return (
|
|
16
|
+
f"{entry.comp_id:08x} {entry.product_id:4x} {entry.build_version:6d} "
|
|
17
|
+
f"{entry.count:5d}"
|
|
18
|
+
+ (f" {entry.description}" if entry.description else "")
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def print_result(result: ParseResult) -> None:
|
|
23
|
+
"""Print parse result in human-readable format."""
|
|
24
|
+
print(f"Processing {result.filename}")
|
|
25
|
+
|
|
26
|
+
if not result.success:
|
|
27
|
+
print(result.error, file=sys.stderr)
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
if result.pe_info:
|
|
31
|
+
print(f"Target machine: {result.pe_info.machine_name}")
|
|
32
|
+
|
|
33
|
+
if result.rich_header and result.rich_header.entries:
|
|
34
|
+
print("@comp.id id version count description")
|
|
35
|
+
for entry in result.rich_header.entries:
|
|
36
|
+
print(format_entry_line(entry))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
40
|
+
"""Create argument parser."""
|
|
41
|
+
parser = argparse.ArgumentParser(
|
|
42
|
+
prog="richprint",
|
|
43
|
+
description="Decode and print Rich headers from Windows PE executables.",
|
|
44
|
+
epilog=(
|
|
45
|
+
"Rich headers contain compiler version information embedded by "
|
|
46
|
+
"Microsoft's linker."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"files",
|
|
52
|
+
nargs="*",
|
|
53
|
+
metavar="FILE",
|
|
54
|
+
help="PE executable file(s) to analyze",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--json",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Output results as JSON",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--database", "-d",
|
|
65
|
+
metavar="PATH",
|
|
66
|
+
help="Path to custom comp_id.txt database file",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--version", "-V",
|
|
71
|
+
action="version",
|
|
72
|
+
version="%(prog)s 1.0.0",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return parser
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
79
|
+
"""Main entry point for CLI."""
|
|
80
|
+
parser = create_parser()
|
|
81
|
+
args = parser.parse_args(argv)
|
|
82
|
+
|
|
83
|
+
if not args.files:
|
|
84
|
+
print(
|
|
85
|
+
"Rich header decoder. Usage:\n\n"
|
|
86
|
+
" richprint file ...\n\n"
|
|
87
|
+
"Rich headers can be found in executable files, DLLs, "
|
|
88
|
+
"and other binary files\ncreated by Microsoft linker."
|
|
89
|
+
)
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
# Load database
|
|
93
|
+
db = load_database(args.database)
|
|
94
|
+
|
|
95
|
+
results = []
|
|
96
|
+
for filename in args.files:
|
|
97
|
+
result = parse_file(filename, db)
|
|
98
|
+
results.append(result)
|
|
99
|
+
|
|
100
|
+
if args.json:
|
|
101
|
+
output = [r.to_dict() for r in results]
|
|
102
|
+
print(json.dumps(output, indent=2))
|
|
103
|
+
else:
|
|
104
|
+
for result in results:
|
|
105
|
+
print_result(result)
|
|
106
|
+
|
|
107
|
+
# Return non-zero if any file failed
|
|
108
|
+
return 0 if all(r.success for r in results) else 1
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
sys.exit(main())
|
richprint/constants.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Magic numbers and constants for PE/Rich header parsing."""
|
|
2
|
+
|
|
3
|
+
# Signature magic values
|
|
4
|
+
MZ_SIGNATURE = 0x5A4D # "MZ" - DOS executable signature
|
|
5
|
+
PE_SIGNATURE = 0x4550 # "PE\0\0" - PE header signature
|
|
6
|
+
RICH_SIGNATURE = 0x68636952 # "Rich" (little-endian)
|
|
7
|
+
DANS_SIGNATURE = 0x536E6144 # "DanS" (little-endian)
|
|
8
|
+
|
|
9
|
+
# DOS header offsets
|
|
10
|
+
DOS_NUM_RELOCS_OFFSET = 0x06 # Number of relocations
|
|
11
|
+
DOS_HEADER_PARA_OFFSET = 0x08 # Size of header in paragraphs
|
|
12
|
+
DOS_RELOC_OFFSET = 0x18 # File address of relocation table
|
|
13
|
+
DOS_PE_OFFSET = 0x3C # File address of PE header
|
|
14
|
+
|
|
15
|
+
# PE header offsets (relative to PE signature)
|
|
16
|
+
PE_MACHINE_OFFSET = 4 # Machine type field
|
|
17
|
+
|
|
18
|
+
# Machine type mapping
|
|
19
|
+
# From https://msdn.microsoft.com/en-us/windows/hardware/gg463119.aspx
|
|
20
|
+
MACHINE_TYPES = {
|
|
21
|
+
0x8664: "x64",
|
|
22
|
+
0x14C: "x32",
|
|
23
|
+
0x1D3: "Matsushita AM33",
|
|
24
|
+
0x1C0: "ARM LE",
|
|
25
|
+
0x1C4: "ARMv7+ Thumb",
|
|
26
|
+
0xAA64: "ARMv8 64bit",
|
|
27
|
+
0xEBC: "EFI bytecode",
|
|
28
|
+
0x200: "Intel Itanium",
|
|
29
|
+
0x9041: "Mitsubishi M32R LE",
|
|
30
|
+
0x266: "MIPS16",
|
|
31
|
+
0x366: "MIPS w/FPU",
|
|
32
|
+
0x466: "MIPS16 w/FPU",
|
|
33
|
+
0x1F0: "PowerPC LE",
|
|
34
|
+
0x1F1: "PowerPC w/FPU",
|
|
35
|
+
0x166: "MIPS LE",
|
|
36
|
+
0x1A2: "Hitachi SH3",
|
|
37
|
+
0x1A3: "Hitachi SH3 DSP",
|
|
38
|
+
0x1A6: "Hitachi SH4",
|
|
39
|
+
0x1A8: "Hitachi SH5",
|
|
40
|
+
0x1C2: "ARM or Thumb",
|
|
41
|
+
0x169: "MIPS LE WCE v2",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_machine_type(machine_id: int) -> str:
|
|
46
|
+
"""Get human-readable machine type name."""
|
|
47
|
+
return MACHINE_TYPES.get(machine_id, "Unknown")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Package marker for bundled data files.
|