elfpeek 0.2.0__tar.gz

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-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TSVMV
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
elfpeek-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: elfpeek
3
+ Version: 0.2.0
4
+ Summary: Parse ELF/PE binaries and render them as a structural map: section layout, entry point, dependencies, symbols, hardening features, entropy and strings.
5
+ License-Expression: MIT
6
+ Keywords: elf,binary,visualization,reverse-engineering,blue-team,static-analysis
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Topic :: Software Development :: Disassemblers
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest; extra == "test"
17
+ Dynamic: license-file
18
+
19
+ # elfpeek
20
+
21
+ 把 ELF / PE 二进制解析成一张结构图:节区与段布局、内存映射、入口点、动态依赖与符号、加固特性、节区熵与字符串,一眼看清二进制骨架。
22
+
23
+ - 纯 Python 标准库,零第三方依赖
24
+ - 只读解析:不加载、不执行二进制中的任何代码
25
+ - ELF 32/64 位、大小端均支持;PE/COFF 32/64 位支持
26
+ - 输出面向逆向与蓝队:终端中文摘要 + 自包含 HTML(纯 SVG 布局图,无 JS,可直接截图存档)+ JSON
27
+
28
+ ## 安装
29
+
30
+ ```bash
31
+ pip install elfpeek
32
+ ```
33
+
34
+ ## 使用
35
+
36
+ ```bash
37
+ # 终端输出结构摘要(自动识别 ELF / PE)
38
+ elfpeek /bin/true
39
+ elfpeek sample.exe
40
+
41
+ # 导出自包含 HTML 结构图(含布局条带,可截图)
42
+ elfpeek /bin/true --html report.html
43
+
44
+ # 导出 JSON 供其他工具消费
45
+ elfpeek /bin/true --json report.json
46
+ ```
47
+
48
+ ## 输出内容
49
+
50
+ | 板块 | 内容 |
51
+ |------|------|
52
+ | 文件总览 | 格式、大小、位宽、端序、类型、机器架构、入口地址、SONAME;PE 另含映像基址与子系统 |
53
+ | 加固特性 | ELF:PIE / NX / RELRO(完整或部分)/ Canary / FORTIFY / RWX 段;PE:ASLR / DEP / CFG / 高熵 VA / SEHOP / RWX 节区 |
54
+ | 文件布局 | 水平条带,按各节区在文件中的大小比例着色 |
55
+ | 内存布局 | 按运行时虚拟地址排序的节区/段映射,标注地址区间与权限 |
56
+ | 节区表 | 名称、偏移、地址、大小、标志(WAX / CDXR),并附节区熵值(高熵标红提示加壳或压缩) |
57
+ | 动态依赖 | ELF:DT_NEEDED 共享库;PE:导入动态库 |
58
+ | 符号与函数 | ELF:导入/导出函数摘要;PE:导入函数与导出函数列表 |
59
+ | 字符串 | 可打印字符串提取,按 URL / IP / 路径 / 共享库分类 |
60
+
61
+ ## 开发
62
+
63
+ ```bash
64
+ # 运行测试(含 ELF 与 PE builder fixture,无需真实系统文件)
65
+ python3 -m pytest tests/ -q
66
+
67
+ # 静态检查
68
+ python3 -m ruff check .
69
+ ```
70
+
71
+ 架构说明:`parse.py` 用 `struct` 纯手工解析 ELF 头、程序头表、节区头表、动态段与符号表(32/64 位、大小端自适应);`pe.py` 以同样方式解析 PE 的 DOS/COFF/可选头、节区、导入表与导出表,并含 RVA 到文件偏移换算;`checks.py` 负责加固特性判定、节区熵计算与字符串提取分类;`model.py` 是数据模型与类型常量映射;`render/` 负责终端与 HTML 渲染;`cli.py` 按魔数分发到对应解析器。
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,57 @@
1
+ # elfpeek
2
+
3
+ 把 ELF / PE 二进制解析成一张结构图:节区与段布局、内存映射、入口点、动态依赖与符号、加固特性、节区熵与字符串,一眼看清二进制骨架。
4
+
5
+ - 纯 Python 标准库,零第三方依赖
6
+ - 只读解析:不加载、不执行二进制中的任何代码
7
+ - ELF 32/64 位、大小端均支持;PE/COFF 32/64 位支持
8
+ - 输出面向逆向与蓝队:终端中文摘要 + 自包含 HTML(纯 SVG 布局图,无 JS,可直接截图存档)+ JSON
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ pip install elfpeek
14
+ ```
15
+
16
+ ## 使用
17
+
18
+ ```bash
19
+ # 终端输出结构摘要(自动识别 ELF / PE)
20
+ elfpeek /bin/true
21
+ elfpeek sample.exe
22
+
23
+ # 导出自包含 HTML 结构图(含布局条带,可截图)
24
+ elfpeek /bin/true --html report.html
25
+
26
+ # 导出 JSON 供其他工具消费
27
+ elfpeek /bin/true --json report.json
28
+ ```
29
+
30
+ ## 输出内容
31
+
32
+ | 板块 | 内容 |
33
+ |------|------|
34
+ | 文件总览 | 格式、大小、位宽、端序、类型、机器架构、入口地址、SONAME;PE 另含映像基址与子系统 |
35
+ | 加固特性 | ELF:PIE / NX / RELRO(完整或部分)/ Canary / FORTIFY / RWX 段;PE:ASLR / DEP / CFG / 高熵 VA / SEHOP / RWX 节区 |
36
+ | 文件布局 | 水平条带,按各节区在文件中的大小比例着色 |
37
+ | 内存布局 | 按运行时虚拟地址排序的节区/段映射,标注地址区间与权限 |
38
+ | 节区表 | 名称、偏移、地址、大小、标志(WAX / CDXR),并附节区熵值(高熵标红提示加壳或压缩) |
39
+ | 动态依赖 | ELF:DT_NEEDED 共享库;PE:导入动态库 |
40
+ | 符号与函数 | ELF:导入/导出函数摘要;PE:导入函数与导出函数列表 |
41
+ | 字符串 | 可打印字符串提取,按 URL / IP / 路径 / 共享库分类 |
42
+
43
+ ## 开发
44
+
45
+ ```bash
46
+ # 运行测试(含 ELF 与 PE builder fixture,无需真实系统文件)
47
+ python3 -m pytest tests/ -q
48
+
49
+ # 静态检查
50
+ python3 -m ruff check .
51
+ ```
52
+
53
+ 架构说明:`parse.py` 用 `struct` 纯手工解析 ELF 头、程序头表、节区头表、动态段与符号表(32/64 位、大小端自适应);`pe.py` 以同样方式解析 PE 的 DOS/COFF/可选头、节区、导入表与导出表,并含 RVA 到文件偏移换算;`checks.py` 负责加固特性判定、节区熵计算与字符串提取分类;`model.py` 是数据模型与类型常量映射;`render/` 负责终端与 HTML 渲染;`cli.py` 按魔数分发到对应解析器。
54
+
55
+ ## License
56
+
57
+ MIT
@@ -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"]
@@ -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))
@@ -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())