elfpeek 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.
- elfpeek/__init__.py +10 -0
- elfpeek/checks.py +179 -0
- elfpeek/cli.py +153 -0
- elfpeek/model.py +331 -0
- elfpeek/parse.py +280 -0
- elfpeek/pe.py +275 -0
- elfpeek/render/__init__.py +8 -0
- elfpeek/render/html_export.py +437 -0
- elfpeek/render/terminal.py +150 -0
- elfpeek-0.2.0.dist-info/METADATA +75 -0
- elfpeek-0.2.0.dist-info/RECORD +15 -0
- elfpeek-0.2.0.dist-info/WHEEL +5 -0
- elfpeek-0.2.0.dist-info/entry_points.txt +2 -0
- elfpeek-0.2.0.dist-info/licenses/LICENSE +21 -0
- elfpeek-0.2.0.dist-info/top_level.txt +1 -0
elfpeek/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""elfpeek: render an ELF/PE binary as a structural map."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .model import ElfFile, PEFile
|
|
6
|
+
from .parse import parse
|
|
7
|
+
from .pe import parse_pe
|
|
8
|
+
|
|
9
|
+
__version__ = "0.2.0"
|
|
10
|
+
__all__ = ["ElfFile", "PEFile", "__version__", "parse", "parse_pe"]
|
elfpeek/checks.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Security feature detection, entropy and string extraction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from .model import ElfFile
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"SecurityFeature",
|
|
12
|
+
"StringHit",
|
|
13
|
+
"extract_strings",
|
|
14
|
+
"section_entropy",
|
|
15
|
+
"security_features",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
ET_DYN = 3
|
|
19
|
+
PT_GNU_STACK = 0x6474E551
|
|
20
|
+
PT_GNU_RELRO = 0x6474E552
|
|
21
|
+
DF_BIND_NOW = 0x8
|
|
22
|
+
DF_1_NOW = 0x1
|
|
23
|
+
MIN_STRING_LEN = 4
|
|
24
|
+
|
|
25
|
+
CANARY_SYMBOL = "__stack_chk_fail"
|
|
26
|
+
FORTIFY_RE = re.compile(r"__(?:\w+)_chk$")
|
|
27
|
+
URL_RE = re.compile(r"https?://[\w./%\-?=&#:+]+")
|
|
28
|
+
PATH_RE = re.compile(r"(?:/[a-zA-Z0-9._\-]+){2,}")
|
|
29
|
+
IP_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SecurityFeature:
|
|
33
|
+
"""One checksec-style property."""
|
|
34
|
+
|
|
35
|
+
__slots__ = ("detail", "name", "status")
|
|
36
|
+
|
|
37
|
+
def __init__(self, name: str, status: str, detail: str = "") -> None:
|
|
38
|
+
# status: "yes" / "no" / "unknown"
|
|
39
|
+
self.name = name
|
|
40
|
+
self.status = status
|
|
41
|
+
self.detail = detail
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class StringHit:
|
|
45
|
+
"""One extracted printable string with a best-effort category."""
|
|
46
|
+
|
|
47
|
+
__slots__ = ("category", "offset", "value")
|
|
48
|
+
|
|
49
|
+
def __init__(self, value: str, category: str, offset: int) -> None:
|
|
50
|
+
self.value = value
|
|
51
|
+
self.category = category
|
|
52
|
+
self.offset = offset
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def security_features(elf: ElfFile) -> list[SecurityFeature]:
|
|
56
|
+
"""Return checksec-style hardening properties for an ELF binary."""
|
|
57
|
+
features: list[SecurityFeature] = []
|
|
58
|
+
|
|
59
|
+
is_pie = elf.type == ET_DYN
|
|
60
|
+
features.append(
|
|
61
|
+
SecurityFeature(
|
|
62
|
+
"PIE",
|
|
63
|
+
"yes" if is_pie else "no",
|
|
64
|
+
"ET_DYN,加载基址随机化" if is_pie else "ET_EXEC,固定基址",
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
stack = next((seg for seg in elf.segments if seg.type == PT_GNU_STACK), None)
|
|
69
|
+
if stack is None:
|
|
70
|
+
features.append(SecurityFeature("NX", "unknown", "无 GNU_STACK 段,栈权限未知"))
|
|
71
|
+
elif stack.flags & 0x1:
|
|
72
|
+
features.append(SecurityFeature("NX", "no", "GNU_STACK 含可执行位,栈可执行"))
|
|
73
|
+
else:
|
|
74
|
+
features.append(SecurityFeature("NX", "yes", "栈不可执行"))
|
|
75
|
+
|
|
76
|
+
has_relro = any(seg.type == PT_GNU_RELRO for seg in elf.segments)
|
|
77
|
+
bind_now = bool(elf.dt_flags & DF_BIND_NOW) or bool(elf.dt_flags_1 & DF_1_NOW)
|
|
78
|
+
if has_relro and bind_now:
|
|
79
|
+
features.append(SecurityFeature("RELRO", "full", "GNU_RELRO + BIND_NOW,完全只读重定位"))
|
|
80
|
+
elif has_relro:
|
|
81
|
+
features.append(SecurityFeature("RELRO", "partial", "GNU_RELRO,部分只读重定位"))
|
|
82
|
+
else:
|
|
83
|
+
features.append(SecurityFeature("RELRO", "no", "无 RELRO 段"))
|
|
84
|
+
|
|
85
|
+
names = {sym.name for sym in elf.symbols}
|
|
86
|
+
features.append(
|
|
87
|
+
SecurityFeature(
|
|
88
|
+
"Canary",
|
|
89
|
+
"yes" if CANARY_SYMBOL in names else "no",
|
|
90
|
+
"检测到栈溢出保护符号" if CANARY_SYMBOL in names else "未发现栈溢出保护符号",
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
fortified = [name for name in names if FORTIFY_RE.match(name)]
|
|
94
|
+
features.append(
|
|
95
|
+
SecurityFeature(
|
|
96
|
+
"FORTIFY",
|
|
97
|
+
"yes" if fortified else "no",
|
|
98
|
+
f"检测到 {len(fortified)} 个 _chk 加固函数" if fortified else "未发现 FORTIFY 加固符号",
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
rwx = [seg for seg in elf.segments if seg.flags & 0x7 == 0x7]
|
|
103
|
+
if rwx:
|
|
104
|
+
features.append(
|
|
105
|
+
SecurityFeature("RWX段", "warn", f"存在可读可写可执行段:{', '.join(seg.type_name for seg in rwx)}")
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
features.append(SecurityFeature("RWX段", "no", "无 RWX 段"))
|
|
109
|
+
return features
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def section_entropy(data: bytes, offset: int, size: int) -> float:
|
|
113
|
+
"""Shannon entropy of a section's bytes (0-8 bits)."""
|
|
114
|
+
if size <= 0 or offset < 0 or offset >= len(data):
|
|
115
|
+
return 0.0
|
|
116
|
+
end = min(offset + size, len(data))
|
|
117
|
+
chunk = data[offset:end]
|
|
118
|
+
if not chunk:
|
|
119
|
+
return 0.0
|
|
120
|
+
counts = [0] * 256
|
|
121
|
+
for byte in chunk:
|
|
122
|
+
counts[byte] += 1
|
|
123
|
+
total = len(chunk)
|
|
124
|
+
entropy = 0.0
|
|
125
|
+
for count in counts:
|
|
126
|
+
if count:
|
|
127
|
+
p = count / total
|
|
128
|
+
entropy -= p * math.log2(p)
|
|
129
|
+
return entropy
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def fill_entropy(obj, data: bytes) -> None:
|
|
133
|
+
"""Compute and store entropy on every section of a parsed binary."""
|
|
134
|
+
for sec in obj.sections:
|
|
135
|
+
offset = getattr(sec, "offset", None)
|
|
136
|
+
if offset is None:
|
|
137
|
+
offset = getattr(sec, "raw_offset", 0)
|
|
138
|
+
size = getattr(sec, "size", None)
|
|
139
|
+
if size is None:
|
|
140
|
+
size = getattr(sec, "raw_size", 0)
|
|
141
|
+
if getattr(sec, "is_nobits", False) or size <= 0:
|
|
142
|
+
sec.entropy = 0.0
|
|
143
|
+
continue
|
|
144
|
+
sec.entropy = section_entropy(data, offset, size)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def extract_strings(data: bytes, *, min_len: int = MIN_STRING_LEN, limit: int = 200) -> list[StringHit]:
|
|
148
|
+
"""Extract printable ASCII runs and classify URL/path/IP-like ones."""
|
|
149
|
+
hits: list[StringHit] = []
|
|
150
|
+
start = -1
|
|
151
|
+
n = len(data)
|
|
152
|
+
for i in range(n):
|
|
153
|
+
byte = data[i]
|
|
154
|
+
if 0x20 <= byte < 0x7F:
|
|
155
|
+
if start == -1:
|
|
156
|
+
start = i
|
|
157
|
+
else:
|
|
158
|
+
if start != -1 and i - start >= min_len:
|
|
159
|
+
_emit_string(hits, data[start:i], start)
|
|
160
|
+
if len(hits) >= limit:
|
|
161
|
+
return hits
|
|
162
|
+
start = -1
|
|
163
|
+
if start != -1 and n - start >= min_len:
|
|
164
|
+
_emit_string(hits, data[start:n], start)
|
|
165
|
+
return hits
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _emit_string(hits: list[StringHit], raw: bytes, offset: int) -> None:
|
|
169
|
+
value = raw.decode("ascii", "replace")
|
|
170
|
+
category = "other"
|
|
171
|
+
if URL_RE.search(value):
|
|
172
|
+
category = "URL"
|
|
173
|
+
elif IP_RE.search(value):
|
|
174
|
+
category = "IP"
|
|
175
|
+
elif PATH_RE.search(value):
|
|
176
|
+
category = "path"
|
|
177
|
+
elif value.strip().endswith(".so") or ".so." in value:
|
|
178
|
+
category = "library"
|
|
179
|
+
hits.append(StringHit(value, category, offset))
|
elfpeek/cli.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Command line entry point for elfpeek."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .checks import extract_strings, fill_entropy
|
|
12
|
+
from .model import PEFile
|
|
13
|
+
from .parse import ParseError
|
|
14
|
+
from .parse import parse as parse_elf
|
|
15
|
+
from .pe import ParseError as PEParseError
|
|
16
|
+
from .pe import parse_pe
|
|
17
|
+
from .render import render_html, render_terminal
|
|
18
|
+
|
|
19
|
+
__all__ = ["main"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load(path: Path):
|
|
23
|
+
"""Dispatch parsing by file magic and inject entropy / strings."""
|
|
24
|
+
data = path.read_bytes()
|
|
25
|
+
if len(data) >= 4 and data[:4] == b"\x7fELF":
|
|
26
|
+
obj = parse_elf(data, str(path))
|
|
27
|
+
elif len(data) >= 2 and data[:2] == b"MZ":
|
|
28
|
+
obj = parse_pe(data, str(path))
|
|
29
|
+
else:
|
|
30
|
+
raise ParseError("不是 ELF 或 PE 文件(魔数不匹配)")
|
|
31
|
+
fill_entropy(obj, data)
|
|
32
|
+
obj.strings = extract_strings(data)
|
|
33
|
+
return obj
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _to_dict(obj) -> dict:
|
|
37
|
+
base = {
|
|
38
|
+
"tool": "elfpeek",
|
|
39
|
+
"path": obj.path,
|
|
40
|
+
"size": obj.size,
|
|
41
|
+
"is_64": obj.is_64,
|
|
42
|
+
"type_name": obj.type_name,
|
|
43
|
+
"machine_name": obj.machine_name,
|
|
44
|
+
"entry": obj.entry,
|
|
45
|
+
"needed": list(obj.needed),
|
|
46
|
+
"strings": [
|
|
47
|
+
{"value": s.value, "category": s.category, "offset": s.offset} for s in obj.strings
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
if isinstance(obj, PEFile):
|
|
51
|
+
base.update(
|
|
52
|
+
{
|
|
53
|
+
"format": "PE",
|
|
54
|
+
"image_base": obj.image_base,
|
|
55
|
+
"subsystem": obj.subsystem_name,
|
|
56
|
+
"exports": list(obj.exports),
|
|
57
|
+
"imports": list(obj.imports),
|
|
58
|
+
"sections": [
|
|
59
|
+
{
|
|
60
|
+
"name": sec.name,
|
|
61
|
+
"vaddr": sec.virtual_address,
|
|
62
|
+
"vsize": sec.virtual_size,
|
|
63
|
+
"raw_offset": sec.raw_offset,
|
|
64
|
+
"raw_size": sec.raw_size,
|
|
65
|
+
"flags_text": sec.flags_text,
|
|
66
|
+
"entropy": round(sec.entropy, 2),
|
|
67
|
+
}
|
|
68
|
+
for sec in obj.sections
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
base.update(
|
|
74
|
+
{
|
|
75
|
+
"format": "ELF",
|
|
76
|
+
"little_endian": obj.little_endian,
|
|
77
|
+
"soname": obj.soname,
|
|
78
|
+
"segments": [
|
|
79
|
+
{
|
|
80
|
+
"type_name": seg.type_name,
|
|
81
|
+
"offset": seg.offset,
|
|
82
|
+
"vaddr": seg.vaddr,
|
|
83
|
+
"filesz": seg.filesz,
|
|
84
|
+
"memsz": seg.memsz,
|
|
85
|
+
"flags_text": seg.flags_text,
|
|
86
|
+
}
|
|
87
|
+
for seg in obj.segments
|
|
88
|
+
],
|
|
89
|
+
"sections": [
|
|
90
|
+
{
|
|
91
|
+
"name": sec.name,
|
|
92
|
+
"type_name": sec.type_name,
|
|
93
|
+
"offset": sec.offset,
|
|
94
|
+
"addr": sec.addr,
|
|
95
|
+
"size": sec.size,
|
|
96
|
+
"flags_text": sec.flags_text,
|
|
97
|
+
"entropy": round(sec.entropy, 2),
|
|
98
|
+
}
|
|
99
|
+
for sec in obj.sections
|
|
100
|
+
],
|
|
101
|
+
"symbols": [
|
|
102
|
+
{"name": sym.name, "bind": sym.bind, "type": sym.type}
|
|
103
|
+
for sym in obj.symbols
|
|
104
|
+
],
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return base
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main(argv: list[str] | None = None) -> int:
|
|
111
|
+
parser = argparse.ArgumentParser(
|
|
112
|
+
prog="elfpeek",
|
|
113
|
+
description="把 ELF/PE 二进制解析成结构图:节区/段布局、入口、依赖、符号、安全特性与熵。",
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument("file", metavar="FILE", help="待解析的 ELF 或 PE 文件路径")
|
|
116
|
+
parser.add_argument("--html", metavar="PATH", help="将结构报告导出为自包含 HTML")
|
|
117
|
+
parser.add_argument("--json", metavar="PATH", help="将解析结果导出为 JSON")
|
|
118
|
+
parser.add_argument("--version", action="version", version=f"elfpeek {__version__}")
|
|
119
|
+
args = parser.parse_args(argv)
|
|
120
|
+
|
|
121
|
+
path = Path(args.file)
|
|
122
|
+
try:
|
|
123
|
+
obj = _load(path)
|
|
124
|
+
except OSError as exc:
|
|
125
|
+
print(f"错误:无法读取文件 {path}:{exc}", file=sys.stderr)
|
|
126
|
+
return 2
|
|
127
|
+
except (ParseError, PEParseError) as exc:
|
|
128
|
+
print(f"错误:{exc}", file=sys.stderr)
|
|
129
|
+
return 1
|
|
130
|
+
|
|
131
|
+
print(render_terminal(obj))
|
|
132
|
+
|
|
133
|
+
status = 0
|
|
134
|
+
if args.html:
|
|
135
|
+
try:
|
|
136
|
+
Path(args.html).write_text(render_html(obj), encoding="utf-8")
|
|
137
|
+
print(f"HTML 报告已写入 {args.html}")
|
|
138
|
+
except OSError as exc:
|
|
139
|
+
print(f"错误:无法写入 HTML 文件 {args.html}:{exc}", file=sys.stderr)
|
|
140
|
+
status = 2
|
|
141
|
+
if args.json:
|
|
142
|
+
payload = json.dumps(_to_dict(obj), ensure_ascii=False, indent=2)
|
|
143
|
+
try:
|
|
144
|
+
Path(args.json).write_text(payload + "\n", encoding="utf-8")
|
|
145
|
+
print(f"JSON 报告已写入 {args.json}")
|
|
146
|
+
except OSError as exc:
|
|
147
|
+
print(f"错误:无法写入 JSON 文件 {args.json}:{exc}", file=sys.stderr)
|
|
148
|
+
status = 2
|
|
149
|
+
return status
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
raise SystemExit(main())
|
elfpeek/model.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Data model for a parsed ELF binary."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DT_NAMES",
|
|
9
|
+
"MACHINE_NAMES",
|
|
10
|
+
"SECTION_TYPE_NAMES",
|
|
11
|
+
"SEGMENT_TYPE_NAMES",
|
|
12
|
+
"SYMBOL_BIND_NAMES",
|
|
13
|
+
"SYMBOL_TYPE_NAMES",
|
|
14
|
+
"TYPE_NAMES",
|
|
15
|
+
"ElfFile",
|
|
16
|
+
"PEFile",
|
|
17
|
+
"PESection",
|
|
18
|
+
"Section",
|
|
19
|
+
"Segment",
|
|
20
|
+
"Symbol",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
TYPE_NAMES: dict[int, str] = {
|
|
24
|
+
1: "可重定位",
|
|
25
|
+
2: "可执行",
|
|
26
|
+
3: "共享库",
|
|
27
|
+
4: "核心转储",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
SEGMENT_TYPE_NAMES: dict[int, str] = {
|
|
31
|
+
0: "NULL",
|
|
32
|
+
1: "LOAD",
|
|
33
|
+
2: "DYNAMIC",
|
|
34
|
+
3: "INTERP",
|
|
35
|
+
4: "NOTE",
|
|
36
|
+
5: "SHLIB",
|
|
37
|
+
6: "PHDR",
|
|
38
|
+
7: "TLS",
|
|
39
|
+
0x6474e550: "GNU_EH_FRAME",
|
|
40
|
+
0x6474e551: "GNU_STACK",
|
|
41
|
+
0x6474e552: "GNU_RELRO",
|
|
42
|
+
0x6474e553: "GNU_PROPERTY",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
SECTION_TYPE_NAMES: dict[int, str] = {
|
|
46
|
+
0: "NULL",
|
|
47
|
+
1: "PROGBITS",
|
|
48
|
+
2: "SYMTAB",
|
|
49
|
+
3: "STRTAB",
|
|
50
|
+
4: "RELA",
|
|
51
|
+
5: "HASH",
|
|
52
|
+
6: "DYNAMIC",
|
|
53
|
+
7: "NOTE",
|
|
54
|
+
8: "NOBITS",
|
|
55
|
+
9: "REL",
|
|
56
|
+
10: "SHLIB",
|
|
57
|
+
11: "DYNSYM",
|
|
58
|
+
14: "INIT_ARRAY",
|
|
59
|
+
15: "FINI_ARRAY",
|
|
60
|
+
16: "PREINIT_ARRAY",
|
|
61
|
+
17: "GROUP",
|
|
62
|
+
18: "SYMTAB_SHNDX",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
DT_NAMES: dict[int, str] = {
|
|
66
|
+
0: "NULL",
|
|
67
|
+
1: "NEEDED",
|
|
68
|
+
2: "PLTRELSZ",
|
|
69
|
+
3: "PLTGOT",
|
|
70
|
+
4: "HASH",
|
|
71
|
+
5: "STRTAB",
|
|
72
|
+
6: "SYMTAB",
|
|
73
|
+
7: "RELA",
|
|
74
|
+
8: "RELASZ",
|
|
75
|
+
9: "RELAENT",
|
|
76
|
+
10: "STRSZ",
|
|
77
|
+
11: "SYMENT",
|
|
78
|
+
12: "INIT",
|
|
79
|
+
13: "FINI",
|
|
80
|
+
14: "SONAME",
|
|
81
|
+
15: "RPATH",
|
|
82
|
+
16: "SYMBOLIC",
|
|
83
|
+
17: "REL",
|
|
84
|
+
20: "PLTREL",
|
|
85
|
+
21: "DEBUG",
|
|
86
|
+
23: "JMPREL",
|
|
87
|
+
24: "BIND_NOW",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
SYMBOL_BIND_NAMES: dict[int, str] = {
|
|
91
|
+
0: "局部",
|
|
92
|
+
1: "全局",
|
|
93
|
+
2: "弱",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
SYMBOL_TYPE_NAMES: dict[int, str] = {
|
|
97
|
+
0: "未类型",
|
|
98
|
+
1: "对象",
|
|
99
|
+
2: "函数",
|
|
100
|
+
3: "节区",
|
|
101
|
+
4: "文件",
|
|
102
|
+
5: "COMMON",
|
|
103
|
+
6: "TLS",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
MACHINE_NAMES: dict[int, str] = {
|
|
107
|
+
0: "未指定",
|
|
108
|
+
3: "x86",
|
|
109
|
+
40: "ARM",
|
|
110
|
+
62: "x86_64",
|
|
111
|
+
183: "AArch64",
|
|
112
|
+
243: "RISC-V",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
PE_MACHINE_NAMES: dict[int, str] = {
|
|
116
|
+
0x14C: "x86",
|
|
117
|
+
0x8664: "x86_64",
|
|
118
|
+
0xAA64: "AArch64",
|
|
119
|
+
0x1C0: "ARM Thumb",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
PE_SUBSYSTEM_NAMES: dict[int, str] = {
|
|
123
|
+
1: "原生",
|
|
124
|
+
2: "Windows 图形",
|
|
125
|
+
3: "Windows 控制台",
|
|
126
|
+
7: "POSIX",
|
|
127
|
+
9: "Windows CE",
|
|
128
|
+
10: "EFI 应用",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
PE_SECTION_FLAG_TEXT = {
|
|
132
|
+
0x00000020: "C", # CNT_CODE
|
|
133
|
+
0x00000040: "D", # CNT_INITIALIZED_DATA
|
|
134
|
+
0x00000080: "U", # CNT_UNINITIALIZED_DATA
|
|
135
|
+
0x20000000: "X", # MEM_EXECUTE
|
|
136
|
+
0x40000000: "R", # MEM_READ
|
|
137
|
+
0x80000000: "W", # MEM_WRITE
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class Section:
|
|
143
|
+
"""One ELF section header."""
|
|
144
|
+
|
|
145
|
+
name: str
|
|
146
|
+
type: int
|
|
147
|
+
type_name: str
|
|
148
|
+
flags: int
|
|
149
|
+
addr: int
|
|
150
|
+
offset: int
|
|
151
|
+
size: int
|
|
152
|
+
link: int
|
|
153
|
+
info: int
|
|
154
|
+
addralign: int
|
|
155
|
+
entsize: int
|
|
156
|
+
entropy: float = 0.0
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def flags_text(self) -> str:
|
|
160
|
+
return "".join(
|
|
161
|
+
char
|
|
162
|
+
for bit, char in (
|
|
163
|
+
(0x1, "W"),
|
|
164
|
+
(0x2, "A"),
|
|
165
|
+
(0x4, "X"),
|
|
166
|
+
(0x10, "M"),
|
|
167
|
+
(0x20, "S"),
|
|
168
|
+
(0x40, "I"),
|
|
169
|
+
(0x80, "L"),
|
|
170
|
+
)
|
|
171
|
+
if self.flags & bit
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def is_nobits(self) -> bool:
|
|
176
|
+
return self.type == 8
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class Segment:
|
|
181
|
+
"""One ELF program header."""
|
|
182
|
+
|
|
183
|
+
type: int
|
|
184
|
+
type_name: str
|
|
185
|
+
flags: int
|
|
186
|
+
offset: int
|
|
187
|
+
vaddr: int
|
|
188
|
+
paddr: int
|
|
189
|
+
filesz: int
|
|
190
|
+
memsz: int
|
|
191
|
+
align: int
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def flags_text(self) -> str:
|
|
195
|
+
return "".join(
|
|
196
|
+
char
|
|
197
|
+
for bit, char in ((0x4, "R"), (0x2, "W"), (0x1, "X"))
|
|
198
|
+
if self.flags & bit
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class Symbol:
|
|
204
|
+
"""One symbol from .symtab or .dynsym."""
|
|
205
|
+
|
|
206
|
+
name: str
|
|
207
|
+
bind: int
|
|
208
|
+
type: int
|
|
209
|
+
shndx: int
|
|
210
|
+
value: int
|
|
211
|
+
size: int
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def bind_name(self) -> str:
|
|
215
|
+
return SYMBOL_BIND_NAMES.get(self.bind, str(self.bind))
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def type_name(self) -> str:
|
|
219
|
+
return SYMBOL_TYPE_NAMES.get(self.type, str(self.type))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class ElfFile:
|
|
224
|
+
"""Parsed view of an ELF binary."""
|
|
225
|
+
|
|
226
|
+
path: str
|
|
227
|
+
size: int
|
|
228
|
+
is_64: bool
|
|
229
|
+
little_endian: bool
|
|
230
|
+
type: int
|
|
231
|
+
machine: int
|
|
232
|
+
entry: int
|
|
233
|
+
flags: int
|
|
234
|
+
segments: list[Segment] = field(default_factory=list)
|
|
235
|
+
sections: list[Section] = field(default_factory=list)
|
|
236
|
+
needed: list[str] = field(default_factory=list)
|
|
237
|
+
symbols: list[Symbol] = field(default_factory=list)
|
|
238
|
+
soname: str = ""
|
|
239
|
+
dt_flags: int = 0
|
|
240
|
+
dt_flags_1: int = 0
|
|
241
|
+
strings: list = field(default_factory=list)
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def type_name(self) -> str:
|
|
245
|
+
return TYPE_NAMES.get(self.type, f"0x{self.type:x}")
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def machine_name(self) -> str:
|
|
249
|
+
return MACHINE_NAMES.get(self.machine, f"0x{self.machine:x}")
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def dynamic_symbols(self) -> list[Symbol]:
|
|
253
|
+
# Symbols parsed from .dynsym carry non-empty names from .dynstr.
|
|
254
|
+
return [sym for sym in self.symbols if sym.name]
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def functions(self) -> list[Symbol]:
|
|
258
|
+
return [sym for sym in self.dynamic_symbols if sym.type == 2]
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def imported_functions(self) -> list[Symbol]:
|
|
262
|
+
# Undefined dynamic symbols (shndx == 0 / UNDEF) are imports.
|
|
263
|
+
return [sym for sym in self.functions if sym.shndx == 0]
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def exported_functions(self) -> list[Symbol]:
|
|
267
|
+
return [sym for sym in self.functions if sym.shndx != 0]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class PESection:
|
|
272
|
+
"""One PE/COFF section header."""
|
|
273
|
+
|
|
274
|
+
name: str
|
|
275
|
+
virtual_address: int
|
|
276
|
+
virtual_size: int
|
|
277
|
+
raw_offset: int
|
|
278
|
+
raw_size: int
|
|
279
|
+
flags: int
|
|
280
|
+
entropy: float = 0.0
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def flags_text(self) -> str:
|
|
284
|
+
return "".join(
|
|
285
|
+
char for bit, char in PE_SECTION_FLAG_TEXT.items() if self.flags & bit
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def is_code(self) -> bool:
|
|
290
|
+
return bool(self.flags & 0x00000020)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass
|
|
294
|
+
class PEFile:
|
|
295
|
+
"""Parsed view of a PE/COFF binary."""
|
|
296
|
+
|
|
297
|
+
path: str
|
|
298
|
+
size: int
|
|
299
|
+
is_64: bool
|
|
300
|
+
type: int # 0x2000 DLL, 0x0002 EXEC
|
|
301
|
+
machine: int
|
|
302
|
+
entry: int
|
|
303
|
+
image_base: int
|
|
304
|
+
subsystem: int
|
|
305
|
+
dll_characteristics: int
|
|
306
|
+
characteristics: int
|
|
307
|
+
sections: list[PESection] = field(default_factory=list)
|
|
308
|
+
needed: list[str] = field(default_factory=list)
|
|
309
|
+
exports: list[str] = field(default_factory=list)
|
|
310
|
+
imports: list[str] = field(default_factory=list)
|
|
311
|
+
strings: list = field(default_factory=list)
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def type_name(self) -> str:
|
|
315
|
+
if self.type & 0x2000:
|
|
316
|
+
return "动态链接库"
|
|
317
|
+
if self.type & 0x0002:
|
|
318
|
+
return "可执行"
|
|
319
|
+
return "未知"
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def machine_name(self) -> str:
|
|
323
|
+
return PE_MACHINE_NAMES.get(self.machine, f"0x{self.machine:x}")
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def subsystem_name(self) -> str:
|
|
327
|
+
return PE_SUBSYSTEM_NAMES.get(self.subsystem, str(self.subsystem))
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def little_endian(self) -> bool:
|
|
331
|
+
return True
|