pypackerdetect 1.2.0__tar.gz → 1.2.1__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.
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/.github/workflows/python-package.yml +29 -17
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/.gitignore +3 -0
- pypackerdetect-1.2.1/.readthedocs.yaml +13 -0
- {pypackerdetect-1.2.0/src/pypackerdetect.egg-info → pypackerdetect-1.2.1}/PKG-INFO +7 -6
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/README.md +4 -3
- pypackerdetect-1.2.1/docs/mkdocs.yml +60 -0
- pypackerdetect-1.2.1/docs/pages/api.md +101 -0
- pypackerdetect-1.2.1/docs/pages/cli/pypackerdetect.md +100 -0
- pypackerdetect-1.2.1/docs/pages/contributing.md +56 -0
- pypackerdetect-1.2.1/docs/pages/img/icon.png +0 -0
- pypackerdetect-1.2.1/docs/pages/index.md +72 -0
- pypackerdetect-1.2.1/docs/pages/installation.md +56 -0
- pypackerdetect-1.2.1/docs/pages/internals.md +141 -0
- pypackerdetect-1.2.1/docs/requirements.txt +6 -0
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/pyproject.toml +2 -2
- pypackerdetect-1.2.1/requirements.txt +2 -0
- pypackerdetect-1.2.1/src/pypackerdetect/VERSION.txt +1 -0
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect/__info__.py +1 -1
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect/__init__.py +39 -16
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect/__main__.py +28 -23
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect/db/sections.json +2 -2
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1/src/pypackerdetect.egg-info}/PKG-INFO +7 -6
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect.egg-info/SOURCES.txt +11 -1
- pypackerdetect-1.2.1/src/pypackerdetect.egg-info/requires.txt +2 -0
- pypackerdetect-1.2.0/requirements.txt +0 -2
- pypackerdetect-1.2.0/src/pypackerdetect/VERSION.txt +0 -1
- pypackerdetect-1.2.0/src/pypackerdetect.egg-info/requires.txt +0 -2
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/LICENSE +0 -0
- {pypackerdetect-1.2.0/docs → pypackerdetect-1.2.1/docs/pages/img}/logo.png +0 -0
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/setup.cfg +0 -0
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect/db/packers.json +0 -0
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect/db/sigs_long.txt +0 -0
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect/db/sigs_short.txt +0 -0
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect.egg-info/dependency_links.txt +0 -0
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect.egg-info/entry_points.txt +0 -0
- {pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect.egg-info/top_level.txt +0 -0
|
@@ -10,7 +10,6 @@ on:
|
|
|
10
10
|
branches: [ "main" ]
|
|
11
11
|
|
|
12
12
|
permissions:
|
|
13
|
-
id-token: write # for OIDC
|
|
14
13
|
contents: read
|
|
15
14
|
|
|
16
15
|
jobs:
|
|
@@ -20,7 +19,7 @@ jobs:
|
|
|
20
19
|
fail-fast: false
|
|
21
20
|
matrix:
|
|
22
21
|
os: [ubuntu-latest]
|
|
23
|
-
python-version: ["3.
|
|
22
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
24
23
|
steps:
|
|
25
24
|
- uses: actions/checkout@v5
|
|
26
25
|
- name: Set up Python ${{ matrix.python-version }}
|
|
@@ -40,31 +39,44 @@ jobs:
|
|
|
40
39
|
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
|
41
40
|
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
|
42
41
|
deploy:
|
|
42
|
+
# only publish from pushes to main, never from pull requests
|
|
43
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
43
44
|
environment:
|
|
44
45
|
name: pypi
|
|
45
|
-
url:
|
|
46
|
+
url: https://pypi.org/p/pypackerdetect
|
|
47
|
+
permissions:
|
|
48
|
+
# required for PyPI trusted publishing (OIDC)
|
|
49
|
+
id-token: write
|
|
50
|
+
contents: read
|
|
46
51
|
runs-on: ubuntu-latest
|
|
47
52
|
needs: build
|
|
48
53
|
steps:
|
|
49
54
|
- uses: actions/checkout@v5
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
version
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
- name: Check whether the version is already on PyPI
|
|
56
|
+
# comparing with PyPI (not with the previous commit) also releases a version whose push did not trigger a deploy
|
|
57
|
+
id: version
|
|
58
|
+
run: |
|
|
59
|
+
version=$(tr -d '[:space:]' < src/pypackerdetect/VERSION.txt)
|
|
60
|
+
status=$(curl -s -o /dev/null -w '%{http_code}' --retry 3 "https://pypi.org/pypi/pypackerdetect/$version/json")
|
|
61
|
+
case "$status" in
|
|
62
|
+
200) echo "Version $version is already on PyPI" ; echo "new=false" >> "$GITHUB_OUTPUT" ;;
|
|
63
|
+
404) echo "Version $version is not on PyPI yet" ; echo "new=true" >> "$GITHUB_OUTPUT" ;;
|
|
64
|
+
*) echo "::error::Unexpected HTTP status $status from PyPI" ; exit 1 ;;
|
|
65
|
+
esac
|
|
66
|
+
- if: steps.version.outputs.new == 'true'
|
|
60
67
|
name: Cleanup README
|
|
61
68
|
run: |
|
|
62
69
|
sed -ri 's/^(##*)\s*:.*:\s*/\1 /g' README.md
|
|
63
70
|
awk '{if (match($0,"## Supporters")) exit; print}' README.md > README
|
|
64
71
|
mv -f README README.md
|
|
65
|
-
- if: steps.
|
|
66
|
-
name:
|
|
72
|
+
- if: steps.version.outputs.new == 'true'
|
|
73
|
+
name: Set up Python
|
|
74
|
+
uses: actions/setup-python@v6
|
|
75
|
+
with:
|
|
76
|
+
python-version: "3.13"
|
|
77
|
+
- if: steps.version.outputs.new == 'true'
|
|
78
|
+
name: Build pypackerdetect package
|
|
67
79
|
run: python3 -m pip install --upgrade build && python3 -m build
|
|
68
|
-
- if: steps.
|
|
69
|
-
name:
|
|
80
|
+
- if: steps.version.outputs.new == 'true'
|
|
81
|
+
name: Publish to PyPI
|
|
70
82
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypackerdetect
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Re-implementation and packaging of PyPackerDetect
|
|
5
5
|
Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -691,14 +691,14 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
691
691
|
Classifier: Programming Language :: Python :: 3
|
|
692
692
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
693
693
|
Classifier: Topic :: Security
|
|
694
|
-
Requires-Python: <4,>=3.
|
|
694
|
+
Requires-Python: <4,>=3.10
|
|
695
695
|
Description-Content-Type: text/markdown
|
|
696
696
|
License-File: LICENSE
|
|
697
697
|
Requires-Dist: pefile
|
|
698
|
-
Requires-Dist: peid>=2.
|
|
698
|
+
Requires-Dist: peid>=2.4.0
|
|
699
699
|
Dynamic: license-file
|
|
700
700
|
|
|
701
|
-
<p align="center"><img src="https://github.com/packing-box/PyPackerDetect/raw/main/docs/logo.png"></p>
|
|
701
|
+
<p align="center"><img src="https://github.com/packing-box/PyPackerDetect/raw/main/docs/pages/img/logo.png"></p>
|
|
702
702
|
<h1 align="center">PyPackerDetect <a href="https://twitter.com/intent/tweet?text=PyPackerDetect%20-%20Packer%20detector%20using%20multiple%20heuristics%20and%20PEiD's%20signatures%20database.%0D%0Ahttps%3a%2f%2fgithub%2ecom%2fpacking-box%2fPyPackerDetect%0D%0A&hashtags=python,pe,peid,packer,packingdetection"><img src="https://img.shields.io/badge/Tweet--lightgrey?logo=twitter&style=social" alt="Tweet" height="20"/></a></h1>
|
|
703
703
|
<h3 align="center">Detect packers on PE files using heuristics and signatures.</h3>
|
|
704
704
|
|
|
@@ -723,8 +723,9 @@ $ pypackerdetect --help
|
|
|
723
723
|
usage examples:
|
|
724
724
|
- pypackerdetect program.exe
|
|
725
725
|
- pypackerdetect program.exe -b
|
|
726
|
-
- pypackerdetect program.exe --low-imports --unknown-sections
|
|
726
|
+
- pypackerdetect program.exe --low-imports --unknown-sections # disable these two checks
|
|
727
727
|
- pypackerdetect program.exe --imports-threshold 5 --bad-sections-threshold 5
|
|
728
|
+
- pypackerdetect program.exe --peid-db-path user_db.txt --sections-json sections.json
|
|
728
729
|
```
|
|
729
730
|
|
|
730
731
|
## Detection Mechanisms
|
|
@@ -732,7 +733,7 @@ usage examples:
|
|
|
732
733
|
- PEID signatures
|
|
733
734
|
- Known packer section names
|
|
734
735
|
- Entrypoint in non-standard section
|
|
735
|
-
-
|
|
736
|
+
- Threshold of non-standard sections reached
|
|
736
737
|
- Low number of imports
|
|
737
738
|
- Overlapping entrypoint sections
|
|
738
739
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<p align="center"><img src="https://github.com/packing-box/PyPackerDetect/raw/main/docs/logo.png"></p>
|
|
1
|
+
<p align="center"><img src="https://github.com/packing-box/PyPackerDetect/raw/main/docs/pages/img/logo.png"></p>
|
|
2
2
|
<h1 align="center">PyPackerDetect <a href="https://twitter.com/intent/tweet?text=PyPackerDetect%20-%20Packer%20detector%20using%20multiple%20heuristics%20and%20PEiD's%20signatures%20database.%0D%0Ahttps%3a%2f%2fgithub%2ecom%2fpacking-box%2fPyPackerDetect%0D%0A&hashtags=python,pe,peid,packer,packingdetection"><img src="https://img.shields.io/badge/Tweet--lightgrey?logo=twitter&style=social" alt="Tweet" height="20"/></a></h1>
|
|
3
3
|
<h3 align="center">Detect packers on PE files using heuristics and signatures.</h3>
|
|
4
4
|
|
|
@@ -23,8 +23,9 @@ $ pypackerdetect --help
|
|
|
23
23
|
usage examples:
|
|
24
24
|
- pypackerdetect program.exe
|
|
25
25
|
- pypackerdetect program.exe -b
|
|
26
|
-
- pypackerdetect program.exe --low-imports --unknown-sections
|
|
26
|
+
- pypackerdetect program.exe --low-imports --unknown-sections # disable these two checks
|
|
27
27
|
- pypackerdetect program.exe --imports-threshold 5 --bad-sections-threshold 5
|
|
28
|
+
- pypackerdetect program.exe --peid-db-path user_db.txt --sections-json sections.json
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
## Detection Mechanisms
|
|
@@ -32,7 +33,7 @@ usage examples:
|
|
|
32
33
|
- PEID signatures
|
|
33
34
|
- Known packer section names
|
|
34
35
|
- Entrypoint in non-standard section
|
|
35
|
-
-
|
|
36
|
+
- Threshold of non-standard sections reached
|
|
36
37
|
- Low number of imports
|
|
37
38
|
- Overlapping entrypoint sections
|
|
38
39
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
site_author: dhondta
|
|
2
|
+
site_name: PyPackerDetect
|
|
3
|
+
site_description: Detect packers on PE files using heuristics and signatures
|
|
4
|
+
site_url: https://pypackerdetect.readthedocs.io
|
|
5
|
+
repo_url: https://github.com/packing-box/pypackerdetect
|
|
6
|
+
edit_uri: edit/main/docs/pages/
|
|
7
|
+
copyright: Copyright © 2021 Alexandre D'Hondt
|
|
8
|
+
docs_dir: pages
|
|
9
|
+
nav:
|
|
10
|
+
- Home: index.md
|
|
11
|
+
- Installation: installation.md
|
|
12
|
+
- Command-line tool: cli/pypackerdetect.md
|
|
13
|
+
- Python API: api.md
|
|
14
|
+
- How it works: internals.md
|
|
15
|
+
- Contributing: contributing.md
|
|
16
|
+
extra:
|
|
17
|
+
generator: false
|
|
18
|
+
social:
|
|
19
|
+
- icon: fontawesome/solid/paper-plane
|
|
20
|
+
link: mailto:alexandre.dhondt@gmail.com
|
|
21
|
+
name: Contact Alex
|
|
22
|
+
- icon: fontawesome/brands/github
|
|
23
|
+
link: https://github.com/dhondta
|
|
24
|
+
name: Alex on GitHub
|
|
25
|
+
- icon: fontawesome/brands/linkedin
|
|
26
|
+
link: https://www.linkedin.com/in/alexandre-d-2ab2aa14/
|
|
27
|
+
name: Alex on LinkedIn
|
|
28
|
+
- icon: fontawesome/brands/twitter
|
|
29
|
+
link: https://twitter.com/alex_dhondt
|
|
30
|
+
name: Alex on Twitter
|
|
31
|
+
theme:
|
|
32
|
+
name: material
|
|
33
|
+
logo: img/logo.png
|
|
34
|
+
favicon: img/icon.png
|
|
35
|
+
palette:
|
|
36
|
+
- scheme: default
|
|
37
|
+
toggle:
|
|
38
|
+
icon: material/brightness-7
|
|
39
|
+
name: Switch to dark mode
|
|
40
|
+
- scheme: slate
|
|
41
|
+
toggle:
|
|
42
|
+
icon: material/brightness-4
|
|
43
|
+
name: Switch to light mode
|
|
44
|
+
features:
|
|
45
|
+
- content.code.copy
|
|
46
|
+
- navigation.sections
|
|
47
|
+
- navigation.top
|
|
48
|
+
- toc.follow
|
|
49
|
+
use_directory_urls: false
|
|
50
|
+
markdown_extensions:
|
|
51
|
+
- admonition
|
|
52
|
+
- attr_list
|
|
53
|
+
- def_list
|
|
54
|
+
- pymdownx.details
|
|
55
|
+
- pymdownx.highlight
|
|
56
|
+
- pymdownx.inlinehilite
|
|
57
|
+
- pymdownx.superfences
|
|
58
|
+
- tables
|
|
59
|
+
- toc:
|
|
60
|
+
permalink: true
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Python API
|
|
2
|
+
|
|
3
|
+
The `pypackerdetect` package exposes one class:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from pypackerdetect import PyPackerDetect
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## `PyPackerDetect`
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
PyPackerDetect(peid=True, peid_large_db=False, peid_ep_only=True, peid_db_path=None,
|
|
15
|
+
bad_ep_sections=True, low_imports=True, packer_sections=True,
|
|
16
|
+
unknown_sections=True, import_threshold=10, unknown_sections_threshold=3,
|
|
17
|
+
bad_sections_threshold=2, logger=None, packers_json=None,
|
|
18
|
+
sections_json=None, **kwargs)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Configures a detector. The JSON databases are loaded once, here, so one
|
|
22
|
+
instance can analyze many files.
|
|
23
|
+
|
|
24
|
+
| Parameter | Type | Description |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `peid` | `bool` | Match PEiD signatures. |
|
|
27
|
+
| `peid_large_db` | `bool` | Use the large embedded PEiD database instead of the short one. |
|
|
28
|
+
| `peid_ep_only` | `bool` | Match PEiD signatures at the entry point only. `False` scans the whole file. |
|
|
29
|
+
| `peid_db_path` | `str` or `None` | Path to a custom PEiD database. It takes precedence over `peid_large_db`. |
|
|
30
|
+
| `bad_ep_sections` | `bool` | Check the section(s) holding the entry point. |
|
|
31
|
+
| `low_imports` | `bool` | Check the number of imports. |
|
|
32
|
+
| `packer_sections` | `bool` | Check for known packer section names. |
|
|
33
|
+
| `unknown_sections` | `bool` | Check the number of non-standard and invalid section names. |
|
|
34
|
+
| `import_threshold` | `int` | Report a detection when there are this many imports or fewer. |
|
|
35
|
+
| `unknown_sections_threshold` | `int` | Report a detection from this many non-standard section names. |
|
|
36
|
+
| `bad_sections_threshold` | `int` | Report a detection from this many section names with invalid characters. |
|
|
37
|
+
| `logger` | `logging.Logger` | Optional logger for debug output. |
|
|
38
|
+
| `packers_json` | `str` or `None` | Path to a custom packer section names JSON. `None` uses the embedded one. |
|
|
39
|
+
| `sections_json` | `str` or `None` | Path to a custom known section names JSON. `None` uses the embedded one. |
|
|
40
|
+
| `**kwargs` | | Ignored. It lets you pass `vars(args)` from an argument parser. |
|
|
41
|
+
|
|
42
|
+
See [Databases](internals.md#databases) for the JSON formats.
|
|
43
|
+
|
|
44
|
+
### `detect`
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
PyPackerDetect.detect(pe)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Analyzes one executable.
|
|
51
|
+
|
|
52
|
+
| Parameter | Type | Description |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `pe` | `str` or `pefile.PE` | Path to the executable, or an already parsed `pefile.PE` object. |
|
|
55
|
+
|
|
56
|
+
**Returns** a dictionary with two lists of messages:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
{'detections': [...], 'suspicions': [...]}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See [Findings](internals.md#findings) for the possible messages.
|
|
63
|
+
|
|
64
|
+
**Raises** `pefile.PEFormatError` if the file is not a valid PE file.
|
|
65
|
+
|
|
66
|
+
!!! note "Passing a `pefile.PE` object"
|
|
67
|
+
PEiD and `objdump` work on files. If the `PE` object has a `path`
|
|
68
|
+
attribute, that file is used. Otherwise (for instance with
|
|
69
|
+
`PE(data=...)`), the data is copied to a temporary file for the duration
|
|
70
|
+
of the analysis.
|
|
71
|
+
|
|
72
|
+
### `report`
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
PyPackerDetect.report(pe, findings) # static method
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Prints the findings returned by [`detect`](#detect) in the format of the
|
|
79
|
+
original project (see [Output](cli/pypackerdetect.md#output)). `pe` is the
|
|
80
|
+
path or the `pefile.PE` object, and is only used for the report header.
|
|
81
|
+
|
|
82
|
+
## Example
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import logging
|
|
86
|
+
from pefile import PE
|
|
87
|
+
from pypackerdetect import PyPackerDetect
|
|
88
|
+
|
|
89
|
+
detector = PyPackerDetect(peid_large_db=True, import_threshold=5,
|
|
90
|
+
logger=logging.getLogger("pypackerdetect"))
|
|
91
|
+
|
|
92
|
+
for path in ["a.exe", "b.exe"]:
|
|
93
|
+
findings = detector.detect(path)
|
|
94
|
+
if findings["detections"]:
|
|
95
|
+
print(f"{path} looks packed")
|
|
96
|
+
PyPackerDetect.report(path, findings)
|
|
97
|
+
|
|
98
|
+
# with a PE object parsed from memory
|
|
99
|
+
with open("c.exe", "rb") as f:
|
|
100
|
+
findings = detector.detect(PE(data=f.read()))
|
|
101
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# `pypackerdetect`: detect packing
|
|
2
|
+
|
|
3
|
+
`pypackerdetect` runs all the [detection mechanisms](../internals.md) on a PE
|
|
4
|
+
file and reports the findings, either as **detections** or **suspicions**.
|
|
5
|
+
|
|
6
|
+
```console
|
|
7
|
+
$ pypackerdetect [path arguments] [optional arguments] [threshold arguments] [-b] [-v] [-h] path
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Arguments
|
|
11
|
+
|
|
12
|
+
| Argument | Description |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `path` | The PE file to analyze. It must exist. |
|
|
15
|
+
|
|
16
|
+
### Path arguments
|
|
17
|
+
|
|
18
|
+
| Option | Default | Description |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `--packers-json JSON` | embedded | JSON file mapping known packer section names to packer descriptions (see [Databases](../internals.md#databases)). |
|
|
21
|
+
| `--peid-db-path TXT_DB` | embedded | PEiD signatures database to use instead of the embedded one. It takes precedence over `--peid-large-db`. |
|
|
22
|
+
| `--sections-json JSON` | embedded | JSON file with the lists of known section names. |
|
|
23
|
+
|
|
24
|
+
### Optional arguments
|
|
25
|
+
|
|
26
|
+
All checks are **enabled by default**. Each of these flags **turns one off**.
|
|
27
|
+
|
|
28
|
+
| Option | Turns off |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `--bad-ep-sections` | The checks on the entry point section: irregular section, overlapping sections, no section, null entry point. |
|
|
31
|
+
| `--low-imports` | The check for a low number of imports. |
|
|
32
|
+
| `--packer-sections` | The check for known packer section names. |
|
|
33
|
+
| `--peid` | Detection with PEiD signatures. |
|
|
34
|
+
| `--peid-ep-only` | Entry point-only PEiD matching. PEiD then scans **every offset of the file** with the signatures that are not tied to the entry point, which is much slower. |
|
|
35
|
+
| `--unknown-sections` | The checks on the number of non-standard and invalid section names. |
|
|
36
|
+
|
|
37
|
+
| Option | Default | Description |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `--peid-large-db` | off | Use the large embedded PEiD database (4,287 signatures) instead of the short one (1,832 signatures). |
|
|
40
|
+
|
|
41
|
+
!!! warning "Flags turn checks off"
|
|
42
|
+
The option names describe the checks, not the action. For instance,
|
|
43
|
+
`pypackerdetect program.exe --low-imports` **disables** the low imports
|
|
44
|
+
check.
|
|
45
|
+
|
|
46
|
+
### Threshold arguments
|
|
47
|
+
|
|
48
|
+
| Option | Default | Description |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `--bad-sections-threshold N` | 2 | Report a detection when at least `N` section names contain invalid characters. |
|
|
51
|
+
| `--imports-threshold N` | 10 | Report a detection when the executable has `N` imports or fewer. |
|
|
52
|
+
| `--unknown-sections-threshold N` | 3 | Report a detection when at least `N` section names are non-standard. |
|
|
53
|
+
|
|
54
|
+
`N` must be a non-negative integer.
|
|
55
|
+
|
|
56
|
+
### Extra arguments
|
|
57
|
+
|
|
58
|
+
| Option | Description |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `-b`, `--benchmark` | Print the elapsed time, in seconds, after the report. |
|
|
61
|
+
| `-v`, `--verbose` | Show debug information, including each finding as it is found. |
|
|
62
|
+
| `-h`, `--help` | Show the help message and exit. |
|
|
63
|
+
|
|
64
|
+
## Output
|
|
65
|
+
|
|
66
|
+
```console
|
|
67
|
+
$ pypackerdetect calc_packed.exe
|
|
68
|
+
Packer report for: calc_packed.exe
|
|
69
|
+
Detections: 3
|
|
70
|
+
Suspicions: 0
|
|
71
|
+
Log:
|
|
72
|
+
[DETECTION] Entry point 0xc4f80 in irregular section(s): 'UPX1'
|
|
73
|
+
[DETECTION] Section name 'UPX0' matches known packer: [UPX packer]
|
|
74
|
+
[DETECTION] Section name 'UPX1' matches known packer: [UPX packer]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The `Log:` section only appears when there is at least one finding. See
|
|
78
|
+
[Findings](../internals.md#findings) for the list of messages.
|
|
79
|
+
|
|
80
|
+
## Exit code
|
|
81
|
+
|
|
82
|
+
| Code | Meaning |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `0` | The analysis ran, whatever the findings. |
|
|
85
|
+
| `1` | The analysis failed, for instance because the input is not a valid PE file. |
|
|
86
|
+
| `2` | Invalid arguments. |
|
|
87
|
+
|
|
88
|
+
!!! note
|
|
89
|
+
The exit code does not tell whether the file is packed. Parse the report,
|
|
90
|
+
or use the [Python API](../api.md), for that.
|
|
91
|
+
|
|
92
|
+
## Examples
|
|
93
|
+
|
|
94
|
+
```console
|
|
95
|
+
$ pypackerdetect program.exe
|
|
96
|
+
$ pypackerdetect program.exe -b
|
|
97
|
+
$ pypackerdetect program.exe --low-imports --unknown-sections # disable these two checks
|
|
98
|
+
$ pypackerdetect program.exe --imports-threshold 5 --bad-sections-threshold 5
|
|
99
|
+
$ pypackerdetect program.exe --peid-db-path user_db.txt --sections-json sections.json
|
|
100
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Contributions are welcome, especially **new packer section names** and
|
|
4
|
+
**false positive reports**.
|
|
5
|
+
|
|
6
|
+
## Reporting issues
|
|
7
|
+
|
|
8
|
+
Open an issue on [GitHub](https://github.com/packing-box/pypackerdetect/issues)
|
|
9
|
+
and include:
|
|
10
|
+
|
|
11
|
+
- the PyPackerDetect version (`pypackerdetect --help` prints it);
|
|
12
|
+
- the exact command and its output, ideally with `-v`;
|
|
13
|
+
- if possible, a link to (or the hash of) the sample.
|
|
14
|
+
|
|
15
|
+
## Adding section names
|
|
16
|
+
|
|
17
|
+
- **Packer section names** go in `src/pypackerdetect/db/packers.json`, with a
|
|
18
|
+
short description of the packer. Avoid names that are also standard
|
|
19
|
+
section names: they are ignored anyway (see [Section names](internals.md#section-names)).
|
|
20
|
+
- **Standard section names** that cause false positives go in the `known` list
|
|
21
|
+
of `src/pypackerdetect/db/sections.json`.
|
|
22
|
+
|
|
23
|
+
Test your change on packed and unpacked samples. The
|
|
24
|
+
[packed PE dataset](https://github.com/packing-box/dataset-packed-pe) and
|
|
25
|
+
[Docker Packing Box](https://github.com/packing-box/docker-packing-box) help
|
|
26
|
+
with this. Section names are compared after stripping null bytes and spaces,
|
|
27
|
+
so do not add trailing spaces to the names.
|
|
28
|
+
|
|
29
|
+
## Development setup
|
|
30
|
+
|
|
31
|
+
```console
|
|
32
|
+
$ git clone https://github.com/packing-box/pypackerdetect
|
|
33
|
+
$ cd pypackerdetect
|
|
34
|
+
$ pip install -e .
|
|
35
|
+
$ pip install flake8
|
|
36
|
+
$ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
CI (`.github/workflows/python-package.yml`) installs the package on Python
|
|
40
|
+
3.9 to 3.13 and runs this flake8 check.
|
|
41
|
+
|
|
42
|
+
## Releasing
|
|
43
|
+
|
|
44
|
+
The version is stored in `src/pypackerdetect/VERSION.txt`. When a push to
|
|
45
|
+
`main` changes that file, CI builds the package and publishes it to PyPI.
|
|
46
|
+
|
|
47
|
+
## Building this documentation
|
|
48
|
+
|
|
49
|
+
```console
|
|
50
|
+
$ pip install -r docs/requirements.txt
|
|
51
|
+
$ mkdocs serve -f docs/mkdocs.yml # live preview at http://127.0.0.1:8000
|
|
52
|
+
$ mkdocs build --strict -f docs/mkdocs.yml # what Read the Docs runs
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Read the Docs builds the site from `.readthedocs.yaml` at the root of the
|
|
56
|
+
repository.
|
|
Binary file
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# PyPackerDetect
|
|
2
|
+
|
|
3
|
+
<p align="center"><img src="img/logo.png" alt="PyPackerDetect logo" width="200"></p>
|
|
4
|
+
|
|
5
|
+
**PyPackerDetect** detects whether a Windows PE executable is packed. It
|
|
6
|
+
combines several heuristics on the PE structure with
|
|
7
|
+
[PEiD](https://github.com/packing-box/peid) signatures. It is a complete
|
|
8
|
+
refactoring of [Cylance's PyPackerDetect](https://github.com/cylance/PyPackerDetect)
|
|
9
|
+
into a Python package with a console script.
|
|
10
|
+
|
|
11
|
+
[](https://pypi.python.org/pypi/pypackerdetect/)
|
|
12
|
+
[](https://pypi.python.org/pypi/pypackerdetect/)
|
|
13
|
+
[](https://github.com/packing-box/pypackerdetect/actions/workflows/python-package.yml)
|
|
14
|
+
[](https://doi.org/10.5281/zenodo.5732866)
|
|
15
|
+
[](https://pypi.python.org/pypi/pypackerdetect)
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **Six detection mechanisms** (see [How it works](internals.md)):
|
|
20
|
+
- PEiD signatures;
|
|
21
|
+
- known packer section names;
|
|
22
|
+
- entry point in a non-standard section;
|
|
23
|
+
- entry point in overlapping sections, or outside every section;
|
|
24
|
+
- too many non-standard (or invalid) section names;
|
|
25
|
+
- a low number of imports.
|
|
26
|
+
- **Two levels of findings**: *detections* are strong indicators of packing,
|
|
27
|
+
*suspicions* are weaker hints.
|
|
28
|
+
- **Configurable**: each check can be turned off, thresholds can be tuned and
|
|
29
|
+
the section-name and signature databases can be replaced with your own.
|
|
30
|
+
- **A command-line tool**, [`pypackerdetect`](cli/pypackerdetect.md), and a
|
|
31
|
+
[Python API](api.md).
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```console
|
|
36
|
+
$ pip install pypackerdetect
|
|
37
|
+
$ pypackerdetect calc_packed.exe
|
|
38
|
+
Packer report for: calc_packed.exe
|
|
39
|
+
Detections: 3
|
|
40
|
+
Suspicions: 0
|
|
41
|
+
Log:
|
|
42
|
+
[DETECTION] Entry point 0xc4f80 in irregular section(s): 'UPX1'
|
|
43
|
+
[DETECTION] Section name 'UPX0' matches known packer: [UPX packer]
|
|
44
|
+
[DETECTION] Section name 'UPX1' matches known packer: [UPX packer]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
From Python:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from pypackerdetect import PyPackerDetect
|
|
51
|
+
|
|
52
|
+
findings = PyPackerDetect().detect("program.exe")
|
|
53
|
+
print(findings["detections"], findings["suspicions"])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Related projects
|
|
57
|
+
|
|
58
|
+
PyPackerDetect is part of the [packing-box](https://github.com/packing-box) ecosystem:
|
|
59
|
+
|
|
60
|
+
- [Awesome Executable Packing](https://github.com/packing-box/awesome-executable-packing): a curated list of resources about executable packing.
|
|
61
|
+
- [Bintropy](https://github.com/packing-box/bintropy): estimates how likely a binary is to contain compressed or encrypted bytes.
|
|
62
|
+
- [Dataset of packed ELF files](https://github.com/packing-box/dataset-packed-elf) and [Dataset of packed PE files](https://github.com/packing-box/dataset-packed-pe).
|
|
63
|
+
- [Docker Packing Box](https://github.com/packing-box/docker-packing-box): a Docker image of packers and tools for building datasets of packed executables.
|
|
64
|
+
- [DSFF](https://github.com/packing-box/python-dsff): the DataSet File Format library.
|
|
65
|
+
- [PEiD](https://github.com/packing-box/peid): a Python implementation of the Packed Executable iDentifier, used here for signature matching.
|
|
66
|
+
- [REMINDer](https://github.com/packing-box/reminder): a packing detector based on a simple entropy heuristic.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
PyPackerDetect is released under the
|
|
71
|
+
[GNU General Public License v3 or later](https://www.gnu.org/licenses/gpl-3.0.html).
|
|
72
|
+
The original project is © Cylance (author: Nick Cano).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
- Python **3.9 or later**. CI runs on 3.9 to 3.13.
|
|
6
|
+
- [`pefile`](https://pypi.org/project/pefile/) and
|
|
7
|
+
[`peid`](https://pypi.org/project/peid/) (2.4.0 or later), which pip
|
|
8
|
+
installs automatically.
|
|
9
|
+
- *Optional:* `objdump` (from GNU binutils). It is only used to resolve long
|
|
10
|
+
section names; see [Long section names](internals.md#long-section-names).
|
|
11
|
+
|
|
12
|
+
## From PyPI
|
|
13
|
+
|
|
14
|
+
```console
|
|
15
|
+
$ pip install pypackerdetect
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This installs the `pypackerdetect` Python package and the `pypackerdetect`
|
|
19
|
+
console script.
|
|
20
|
+
|
|
21
|
+
## From source
|
|
22
|
+
|
|
23
|
+
```console
|
|
24
|
+
$ git clone https://github.com/packing-box/pypackerdetect
|
|
25
|
+
$ cd pypackerdetect
|
|
26
|
+
$ pip install .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For development, use an editable install:
|
|
30
|
+
|
|
31
|
+
```console
|
|
32
|
+
$ pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Checking the installation
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
$ pypackerdetect --help
|
|
39
|
+
[...]
|
|
40
|
+
PyPackerDetect 1.2.1
|
|
41
|
+
[...]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
>>> from pypackerdetect.__info__ import __version__
|
|
46
|
+
>>> __version__
|
|
47
|
+
'1.2.1'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Write access to the database folder
|
|
51
|
+
|
|
52
|
+
The first time a PEiD database is used, [peid](https://github.com/packing-box/peid)
|
|
53
|
+
writes a JSON cache of its search tree **next to the database file**. For the
|
|
54
|
+
embedded databases, that folder is the installed `pypackerdetect/db/` package
|
|
55
|
+
directory. If the folder is read-only, detection still works without the
|
|
56
|
+
cache, but each run rebuilds the search tree, which is slower.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# How it works
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
┌── section names ──▶ known/unknown/packer/invalid names
|
|
7
|
+
program.exe ─pefile─▶├── entry point ────▶ section(s) holding it
|
|
8
|
+
├── import table ───▶ number of imports
|
|
9
|
+
└── file ──── peid ─▶ PEiD signatures
|
|
10
|
+
│
|
|
11
|
+
▼
|
|
12
|
+
{'detections': [...], 'suspicions': [...]}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
[pefile](https://github.com/erocarrera/pefile) parses the executable, then
|
|
16
|
+
each enabled check adds its findings. A **detection** is a strong indicator
|
|
17
|
+
of packing. A **suspicion** is a weaker hint.
|
|
18
|
+
|
|
19
|
+
## Section names
|
|
20
|
+
|
|
21
|
+
Each section name is decoded as ASCII, then stripped of padding (null bytes
|
|
22
|
+
and spaces). A name that is not valid ASCII is an **invalid name**: it
|
|
23
|
+
produces a suspicion, and it is counted for the
|
|
24
|
+
[bad sections threshold](cli/pypackerdetect.md#threshold-arguments).
|
|
25
|
+
|
|
26
|
+
A valid name is then:
|
|
27
|
+
|
|
28
|
+
- **unknown** if it is not in the `known` list of `sections.json`;
|
|
29
|
+
- a **packer section** if it is a key of `packers.json` and is not a known
|
|
30
|
+
name, so that a packer entry cannot clash with a standard name.
|
|
31
|
+
|
|
32
|
+
### Long section names
|
|
33
|
+
|
|
34
|
+
Section names are limited to 8 bytes. Some compilers (for instance MinGW with
|
|
35
|
+
debug information) write longer names in the COFF string table and store
|
|
36
|
+
`/N` in the section header, where `N` is the offset of the name in that
|
|
37
|
+
table. For such names, PyPackerDetect runs `objdump -h` on the file and uses
|
|
38
|
+
the real names. If `objdump` is not installed, fails or times out (after 10
|
|
39
|
+
seconds), the `/N` name is kept.
|
|
40
|
+
|
|
41
|
+
## Entry point section
|
|
42
|
+
|
|
43
|
+
The entry point is the `AddressOfEntryPoint` field of the optional header. A
|
|
44
|
+
section holds it when:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
VirtualAddress <= entry point < VirtualAddress + VirtualSize
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`SizeOfRawData` is used instead of `VirtualSize` when the latter is zero, as
|
|
51
|
+
the Windows loader does.
|
|
52
|
+
|
|
53
|
+
Then:
|
|
54
|
+
|
|
55
|
+
1. A null entry point is a suspicion.
|
|
56
|
+
2. If no section holds the entry point, it is a detection.
|
|
57
|
+
3. If several sections hold it, they overlap, which is a detection.
|
|
58
|
+
4. If none of the entry point sections is `acceptable` (`.text`), the
|
|
59
|
+
section is irregular, unless one of these exceptions applies:
|
|
60
|
+
- the file has a Delphi BSS section (`delphi-bss`) and the entry point is
|
|
61
|
+
in a Delphi code section (`delphi`: `.itext`, `CODE`);
|
|
62
|
+
- the file has no `.text` section at all and the entry point is in an
|
|
63
|
+
`alternative` section (`.code`, `text`, `.text0`, ...);
|
|
64
|
+
- the file has a `.text` section and the entry point is in a `driver`
|
|
65
|
+
section (`INIT`).
|
|
66
|
+
|
|
67
|
+
## Imports
|
|
68
|
+
|
|
69
|
+
The imports of all the DLLs in the import table are counted. A count lower
|
|
70
|
+
than or equal to the threshold (10 by default) is a detection, as packers
|
|
71
|
+
usually only import what they need to unpack the payload (often
|
|
72
|
+
`LoadLibrary` and `GetProcAddress`). A file with no import table counts as 0
|
|
73
|
+
imports.
|
|
74
|
+
|
|
75
|
+
.NET assemblies (importing `mscoree.dll`) are skipped, as they always have
|
|
76
|
+
very few native imports.
|
|
77
|
+
|
|
78
|
+
## PEiD signatures
|
|
79
|
+
|
|
80
|
+
The file is passed to
|
|
81
|
+
[`peid.identify_packer`](https://peid.readthedocs.io/en/latest/api.html#identify_packer)
|
|
82
|
+
with the chosen database. By default, only the signatures that apply at the
|
|
83
|
+
entry point are matched, using the short embedded database. Every match is
|
|
84
|
+
reported in one detection.
|
|
85
|
+
|
|
86
|
+
## Findings
|
|
87
|
+
|
|
88
|
+
| Level | Message | Check |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| Suspicion | `Section name with invalid characters` | `unknown_sections` (always collected) |
|
|
91
|
+
| Suspicion | `Null entry point` | `bad_ep_sections` |
|
|
92
|
+
| Detection | `Entry point 0x… does not fall in any section` | `bad_ep_sections` |
|
|
93
|
+
| Detection | `Entry point 0x… falls in overlapping sections: '…'` | `bad_ep_sections` |
|
|
94
|
+
| Detection | `Entry point 0x… in irregular section(s): '…'` | `bad_ep_sections` |
|
|
95
|
+
| Detection | `Too few imports (total: N)` | `low_imports` |
|
|
96
|
+
| Detection | `Detected N non-standard sections: '…'` | `unknown_sections` |
|
|
97
|
+
| Detection | `Detected N sections with invalid names` | `unknown_sections` |
|
|
98
|
+
| Detection | `Section name '…' matches known packer: […]` | `packer_sections` |
|
|
99
|
+
| Detection | `Found PEID signature: …` | `peid` |
|
|
100
|
+
|
|
101
|
+
## Databases
|
|
102
|
+
|
|
103
|
+
The embedded databases are in the `pypackerdetect/db/` package folder.
|
|
104
|
+
|
|
105
|
+
### `packers.json`
|
|
106
|
+
|
|
107
|
+
Known packer section names, mostly from
|
|
108
|
+
[Hexacorn](https://www.hexacorn.com/blog/2016/12/15/pe-section-names-re-visited/).
|
|
109
|
+
The `packers` key maps each section name to a description:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"source": "https://www.hexacorn.com/blog/2016/12/15/pe-section-names-re-visited/",
|
|
114
|
+
"last_update": "Jul 11 2021",
|
|
115
|
+
"packers": {
|
|
116
|
+
"UPX0": "UPX packer",
|
|
117
|
+
".MPRESS1": "Mpress Packer"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `sections.json`
|
|
123
|
+
|
|
124
|
+
Lists of standard section names, under the `sections` key:
|
|
125
|
+
|
|
126
|
+
| Key | Use |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `known` | Standard section names; any other name is non-standard. |
|
|
129
|
+
| `acceptable` | Regular entry point sections. |
|
|
130
|
+
| `alternative` | Entry point sections accepted when there is no `.text` section. |
|
|
131
|
+
| `delphi-bss` | BSS sections that identify a Delphi executable. |
|
|
132
|
+
| `delphi` | Entry point sections accepted for Delphi executables. |
|
|
133
|
+
| `driver` | Entry point sections accepted for drivers. |
|
|
134
|
+
|
|
135
|
+
A custom file passed with `--sections-json` must contain all of these keys.
|
|
136
|
+
|
|
137
|
+
### PEiD databases
|
|
138
|
+
|
|
139
|
+
`sigs_short.txt` (1,832 signatures, the default) and `sigs_long.txt` (4,287
|
|
140
|
+
signatures, `--peid-large-db`) are in the PEiD text format, described in the
|
|
141
|
+
[PEiD documentation](https://peid.readthedocs.io/en/latest/signatures.html).
|
|
@@ -19,7 +19,7 @@ authors = [
|
|
|
19
19
|
description = "Re-implementation and packaging of PyPackerDetect"
|
|
20
20
|
license = {file = "LICENSE"}
|
|
21
21
|
keywords = ["packing", "packed-executable", "packing-detection", "pe"]
|
|
22
|
-
requires-python = ">=3.
|
|
22
|
+
requires-python = ">=3.10,<4"
|
|
23
23
|
classifiers = [
|
|
24
24
|
"Development Status :: 5 - Production/Stable",
|
|
25
25
|
"Environment :: Console",
|
|
@@ -33,7 +33,7 @@ classifiers = [
|
|
|
33
33
|
]
|
|
34
34
|
dependencies = [
|
|
35
35
|
"pefile",
|
|
36
|
-
"peid>=2.
|
|
36
|
+
"peid>=2.4.0",
|
|
37
37
|
]
|
|
38
38
|
dynamic = ["version"]
|
|
39
39
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.2.1
|
|
@@ -13,7 +13,7 @@ __credits__ = "Alexandre D'Hondt"
|
|
|
13
13
|
__copyright__ = "© {} Cylance".format([__y, __s + "-" + __y][__y != __s])
|
|
14
14
|
__license__ = "GPLv3 (https://www.gnu.org/licenses/gpl-3.0.fr.html)"
|
|
15
15
|
__reference__ = "https://github.com/cylance/PyPackerDetect"
|
|
16
|
-
__source__ = "https://github.com/
|
|
16
|
+
__source__ = "https://github.com/packing-box/pypackerdetect"
|
|
17
17
|
|
|
18
18
|
with open(os.path.join(os.path.dirname(__file__), "VERSION.txt")) as f:
|
|
19
19
|
__version__ = f.read().strip()
|
|
@@ -13,10 +13,17 @@ __all__ = ["PyPackerDetect"]
|
|
|
13
13
|
@lru_cache
|
|
14
14
|
def _real_section_names(path, logger=None, timeout=10):
|
|
15
15
|
from subprocess import Popen, PIPE, TimeoutExpired
|
|
16
|
-
|
|
16
|
+
try:
|
|
17
|
+
p, names = Popen(["objdump", "-h", path], stdout=PIPE, stderr=PIPE), []
|
|
18
|
+
except OSError as e: # e.g. objdump is not installed
|
|
19
|
+
if logger:
|
|
20
|
+
logger.debug(f"objdump: {e}")
|
|
21
|
+
return
|
|
17
22
|
try:
|
|
18
23
|
out, err = p.communicate(timeout=timeout)
|
|
19
24
|
except TimeoutExpired:
|
|
25
|
+
p.kill()
|
|
26
|
+
p.communicate()
|
|
20
27
|
if logger:
|
|
21
28
|
logger.debug(f"objdump: timeout ({timeout} seconds)")
|
|
22
29
|
return
|
|
@@ -47,10 +54,12 @@ class PyPackerDetect:
|
|
|
47
54
|
'packers_json': os.path.abspath(packers_json) if packers_json is not None else _p("packers.json"),
|
|
48
55
|
'sections_json': os.path.abspath(sections_json) if sections_json is not None else _p("sections.json"),
|
|
49
56
|
}
|
|
50
|
-
logger
|
|
57
|
+
if logger:
|
|
58
|
+
logger.debug(f"JSON with common packer section names: {self.__config['packers_json']}")
|
|
51
59
|
with open(self.__config['packers_json']) as f:
|
|
52
60
|
self.__config['packers_json'] = json.load(f)['packers']
|
|
53
|
-
logger
|
|
61
|
+
if logger:
|
|
62
|
+
logger.debug(f"JSON with known section names: {self.__config['sections_json']}")
|
|
54
63
|
with open(self.__config['sections_json']) as f:
|
|
55
64
|
self.__config['sections_json'] = json.load(f)['sections']
|
|
56
65
|
self.__thresholds = {
|
|
@@ -61,30 +70,44 @@ class PyPackerDetect:
|
|
|
61
70
|
self.logger = logger
|
|
62
71
|
|
|
63
72
|
def detect(self, pe):
|
|
64
|
-
""" Analyze the input PE file for suspicions and/or detections of packers. """
|
|
65
|
-
r, s = {'detections': [], 'suspicions': []}, .0
|
|
66
|
-
def __add(i, msg):
|
|
67
|
-
if self.logger:
|
|
68
|
-
self.logger.debug("%s:%s" % (['DETECTION', 'SUSPICION'][i], msg))
|
|
69
|
-
r[['detections', 'suspicions'][i]].append(msg)
|
|
73
|
+
""" Analyze the input PE file (path or pefile.PE instance) for suspicions and/or detections of packers. """
|
|
70
74
|
if not isinstance(pe, PE):
|
|
71
75
|
path = pe
|
|
72
76
|
pe = PE(path)
|
|
73
77
|
pe.path = path
|
|
78
|
+
if getattr(pe, "path", None) is None:
|
|
79
|
+
# a PE instance parsed from a buffer has no path while PEiD and objdump work on files ; use a temporary copy
|
|
80
|
+
from tempfile import NamedTemporaryFile
|
|
81
|
+
with NamedTemporaryFile(suffix=".exe", delete=False) as f:
|
|
82
|
+
f.write(pe.__data__)
|
|
83
|
+
pe.path = f.name
|
|
84
|
+
try:
|
|
85
|
+
return self.detect(pe)
|
|
86
|
+
finally:
|
|
87
|
+
pe.path = None
|
|
88
|
+
os.remove(f.name)
|
|
89
|
+
r = {'detections': [], 'suspicions': []}
|
|
90
|
+
def __add(i, msg):
|
|
91
|
+
if self.logger:
|
|
92
|
+
self.logger.debug("%s:%s" % (['DETECTION', 'SUSPICION'][i], msg))
|
|
93
|
+
r[['detections', 'suspicions'][i]].append(msg)
|
|
74
94
|
ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
|
|
75
95
|
# parse the input PE file
|
|
76
96
|
d = {'all': [], 'bad': [], 'ep': [], 'packer': [], 'unknown': []}
|
|
77
97
|
for i, s in enumerate(pe.sections):
|
|
78
98
|
try:
|
|
79
|
-
n = s.Name.decode("ascii").strip().rstrip("\0")
|
|
99
|
+
n = s.Name.decode("ascii").strip().rstrip("\0 ")
|
|
80
100
|
# special case: for some executables compiled with debug information, some sections may be named with
|
|
81
101
|
# the format "/[N]" where N is the offset in the string table ; in this case, we compute
|
|
82
102
|
# the real section names and map them to the shortened ones to compare the relevant names
|
|
83
103
|
# with the database of known section names
|
|
84
|
-
if re.match(r"^\/\d+$", n) and (rsn := _real_section_names(pe.path, logger=self.logger))
|
|
104
|
+
if re.match(r"^\/\d+$", n) and (rsn := _real_section_names(pe.path, logger=self.logger)) and \
|
|
105
|
+
i < len(rsn):
|
|
85
106
|
n = rsn[i]
|
|
86
107
|
d['all'].append(n)
|
|
87
|
-
|
|
108
|
+
# the loader uses SizeOfRawData when VirtualSize is null ; the upper bound is excluded as it is the start
|
|
109
|
+
# of the next section for contiguous sections
|
|
110
|
+
if s.VirtualAddress <= ep < s.VirtualAddress + (s.Misc_VirtualSize or s.SizeOfRawData):
|
|
88
111
|
d['ep'].append(n)
|
|
89
112
|
if n not in self.__config['sections_json']['known']:
|
|
90
113
|
d['unknown'].append(n)
|
|
@@ -93,7 +116,7 @@ class PyPackerDetect:
|
|
|
93
116
|
if p is not None and n not in self.__config['sections_json']['known']:
|
|
94
117
|
d['packer'].append(n)
|
|
95
118
|
except UnicodeDecodeError:
|
|
96
|
-
d['bad'].append(s.Name.decode('latin1').strip().rstrip("\0"))
|
|
119
|
+
d['bad'].append(s.Name.decode('latin1').strip().rstrip("\0 "))
|
|
97
120
|
__add(1, "Section name with invalid characters")
|
|
98
121
|
# bad entry point sections
|
|
99
122
|
__intersect = lambda l1, l2: len(set(l1).intersection(l2)) > 0
|
|
@@ -103,7 +126,7 @@ class PyPackerDetect:
|
|
|
103
126
|
__add(1, "Null entry point")
|
|
104
127
|
l_ep = len(d['ep'])
|
|
105
128
|
if l_ep == 0:
|
|
106
|
-
__add(0, "Entry point 0x%x
|
|
129
|
+
__add(0, "Entry point 0x%x does not fall in any section" % ep)
|
|
107
130
|
else:
|
|
108
131
|
if l_ep > 1:
|
|
109
132
|
__add(0, "Entry point 0x%x falls in overlapping sections: '%s'" % (ep, ep_sec_list))
|
|
@@ -126,11 +149,11 @@ class PyPackerDetect:
|
|
|
126
149
|
c = 0
|
|
127
150
|
try:
|
|
128
151
|
for lib in pe.DIRECTORY_ENTRY_IMPORT:
|
|
129
|
-
if lib.dll.decode("
|
|
152
|
+
if lib.dll.decode("latin-1").strip().rstrip("\0").lower() == "mscoree.dll":
|
|
130
153
|
c = -1 # .NET assembly, counting imports is misleading as they will have a low number
|
|
131
154
|
break
|
|
132
155
|
c += len(lib.imports)
|
|
133
|
-
except AttributeError
|
|
156
|
+
except AttributeError: # no import directory
|
|
134
157
|
pass
|
|
135
158
|
if (0 <= c <= self.__thresholds['imports']):
|
|
136
159
|
__add(0, "Too few imports (total: %d)" % c)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
|
2
2
|
import logging
|
|
3
|
-
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
3
|
+
from argparse import ArgumentParser, ArgumentTypeError, RawTextHelpFormatter
|
|
4
4
|
from ast import literal_eval
|
|
5
|
-
from os.path import
|
|
5
|
+
from os.path import isfile
|
|
6
|
+
from pefile import PEFormatError
|
|
6
7
|
from time import perf_counter
|
|
7
8
|
|
|
8
9
|
from .__info__ import __author__, __credits__, __copyright__, __license__, __reference__, __source__, __version__
|
|
@@ -10,8 +11,8 @@ from .__init__ import PyPackerDetect
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def valid_file(path):
|
|
13
|
-
if not
|
|
14
|
-
raise
|
|
14
|
+
if not isfile(path):
|
|
15
|
+
raise ArgumentTypeError(f"input file does not exist: {path}")
|
|
15
16
|
return path
|
|
16
17
|
|
|
17
18
|
|
|
@@ -19,10 +20,10 @@ class PositiveInt:
|
|
|
19
20
|
def __call__(self, string):
|
|
20
21
|
try:
|
|
21
22
|
n = literal_eval(string)
|
|
22
|
-
except ValueError:
|
|
23
|
-
raise
|
|
23
|
+
except (SyntaxError, ValueError):
|
|
24
|
+
raise ArgumentTypeError(f"invalid positive int value: '{string}'")
|
|
24
25
|
if not isinstance(n, int) or n < 0:
|
|
25
|
-
raise
|
|
26
|
+
raise ArgumentTypeError(f"invalid positive int value: '{string}'")
|
|
26
27
|
return int(n)
|
|
27
28
|
|
|
28
29
|
def __repr__(self):
|
|
@@ -38,7 +39,7 @@ def main():
|
|
|
38
39
|
examples = "usage examples:\n- " + "\n- ".join([
|
|
39
40
|
"pypackerdetect program.exe",
|
|
40
41
|
"pypackerdetect program.exe -b",
|
|
41
|
-
"pypackerdetect program.exe --low-imports --unknown-sections",
|
|
42
|
+
"pypackerdetect program.exe --low-imports --unknown-sections # disable these two checks",
|
|
42
43
|
"pypackerdetect program.exe --imports-threshold 5 --bad-sections-threshold 5",
|
|
43
44
|
"pypackerdetect program.exe --peid-db-path user_db.txt --sections-json sections.json",
|
|
44
45
|
])
|
|
@@ -53,20 +54,24 @@ def main():
|
|
|
53
54
|
"(default: package-embedded JSON)")
|
|
54
55
|
opt = parser.add_argument_group("optional arguments")
|
|
55
56
|
opt.add_argument("--bad-ep-sections", action="store_false",
|
|
56
|
-
help="check for bad entry point sections (default
|
|
57
|
-
opt.add_argument("--low-imports", action="store_false",
|
|
58
|
-
|
|
59
|
-
opt.add_argument("--
|
|
60
|
-
|
|
57
|
+
help="disable the check for bad entry point sections (enabled by default)")
|
|
58
|
+
opt.add_argument("--low-imports", action="store_false",
|
|
59
|
+
help="disable the check for a low number of imports (enabled by default)")
|
|
60
|
+
opt.add_argument("--packer-sections", action="store_false",
|
|
61
|
+
help="disable the check for known packer section names (enabled by default)")
|
|
62
|
+
opt.add_argument("--peid", action="store_false", help="disable detection with PEiD (enabled by default)")
|
|
63
|
+
opt.add_argument("--peid-ep-only", action="store_false",
|
|
64
|
+
help="disable entry point-only matching for PEiD, scanning the whole file instead (default: False)")
|
|
61
65
|
opt.add_argument("--peid-large-db", action="store_true", help="use the large database for PEiD (default: False)")
|
|
62
|
-
opt.add_argument("--unknown-sections", action="store_false",
|
|
66
|
+
opt.add_argument("--unknown-sections", action="store_false",
|
|
67
|
+
help="disable the check for unknown section names (enabled by default)")
|
|
63
68
|
thrs = parser.add_argument_group("threshold arguments")
|
|
64
|
-
thrs.add_argument("--bad-sections-threshold", dest="
|
|
65
|
-
help="threshold for the number of bad sections (default: 2)")
|
|
66
|
-
thrs.add_argument("--imports-threshold", dest="
|
|
69
|
+
thrs.add_argument("--bad-sections-threshold", dest="bad_sections_threshold", metavar="N", type=PositiveInt(),
|
|
70
|
+
default=2, help="threshold for the number of bad sections (default: 2)")
|
|
71
|
+
thrs.add_argument("--imports-threshold", dest="import_threshold", metavar="N", type=PositiveInt(), default=10,
|
|
67
72
|
help="threshold for the minimum number of imports (default: 10)")
|
|
68
|
-
thrs.add_argument("--unknown-sections-threshold", dest="
|
|
69
|
-
help="threshold for the number of unknown sections (default: 3)")
|
|
73
|
+
thrs.add_argument("--unknown-sections-threshold", dest="unknown_sections_threshold", metavar="N",
|
|
74
|
+
type=PositiveInt(), default=3, help="threshold for the number of unknown sections (default: 3)")
|
|
70
75
|
extra = parser.add_argument_group("extra arguments")
|
|
71
76
|
extra.add_argument("-b", "--benchmark", action="store_true",
|
|
72
77
|
help="enable benchmarking, output in seconds (default: False)")
|
|
@@ -86,11 +91,11 @@ def main():
|
|
|
86
91
|
PyPackerDetect.report(args.path, r)
|
|
87
92
|
if dt != "":
|
|
88
93
|
print(dt)
|
|
94
|
+
except PEFormatError as e:
|
|
95
|
+
args.logger.error(f"Not a valid PE file ({e.value})")
|
|
96
|
+
code = 1
|
|
89
97
|
except Exception as e:
|
|
90
|
-
|
|
91
|
-
e.value = "Not a PE file"
|
|
92
|
-
if str(e) != "no result":
|
|
93
|
-
args.logger.exception(e)
|
|
98
|
+
args.logger.exception(e)
|
|
94
99
|
code = 1
|
|
95
100
|
return code
|
|
96
101
|
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
".debug$P",
|
|
51
51
|
".debug$S",
|
|
52
52
|
".debug$T",
|
|
53
|
-
".drectve
|
|
53
|
+
".drectve",
|
|
54
54
|
".didat",
|
|
55
55
|
".didata",
|
|
56
56
|
".edata",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
".vsdata",
|
|
95
95
|
".xdata",
|
|
96
96
|
".wixburn",
|
|
97
|
-
".wpp_sf
|
|
97
|
+
".wpp_sf",
|
|
98
98
|
"BSS",
|
|
99
99
|
"CODE",
|
|
100
100
|
"DATA",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypackerdetect
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Re-implementation and packaging of PyPackerDetect
|
|
5
5
|
Author-email: Alexandre D'Hondt <alexandre.dhondt@gmail.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -691,14 +691,14 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
691
691
|
Classifier: Programming Language :: Python :: 3
|
|
692
692
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
693
693
|
Classifier: Topic :: Security
|
|
694
|
-
Requires-Python: <4,>=3.
|
|
694
|
+
Requires-Python: <4,>=3.10
|
|
695
695
|
Description-Content-Type: text/markdown
|
|
696
696
|
License-File: LICENSE
|
|
697
697
|
Requires-Dist: pefile
|
|
698
|
-
Requires-Dist: peid>=2.
|
|
698
|
+
Requires-Dist: peid>=2.4.0
|
|
699
699
|
Dynamic: license-file
|
|
700
700
|
|
|
701
|
-
<p align="center"><img src="https://github.com/packing-box/PyPackerDetect/raw/main/docs/logo.png"></p>
|
|
701
|
+
<p align="center"><img src="https://github.com/packing-box/PyPackerDetect/raw/main/docs/pages/img/logo.png"></p>
|
|
702
702
|
<h1 align="center">PyPackerDetect <a href="https://twitter.com/intent/tweet?text=PyPackerDetect%20-%20Packer%20detector%20using%20multiple%20heuristics%20and%20PEiD's%20signatures%20database.%0D%0Ahttps%3a%2f%2fgithub%2ecom%2fpacking-box%2fPyPackerDetect%0D%0A&hashtags=python,pe,peid,packer,packingdetection"><img src="https://img.shields.io/badge/Tweet--lightgrey?logo=twitter&style=social" alt="Tweet" height="20"/></a></h1>
|
|
703
703
|
<h3 align="center">Detect packers on PE files using heuristics and signatures.</h3>
|
|
704
704
|
|
|
@@ -723,8 +723,9 @@ $ pypackerdetect --help
|
|
|
723
723
|
usage examples:
|
|
724
724
|
- pypackerdetect program.exe
|
|
725
725
|
- pypackerdetect program.exe -b
|
|
726
|
-
- pypackerdetect program.exe --low-imports --unknown-sections
|
|
726
|
+
- pypackerdetect program.exe --low-imports --unknown-sections # disable these two checks
|
|
727
727
|
- pypackerdetect program.exe --imports-threshold 5 --bad-sections-threshold 5
|
|
728
|
+
- pypackerdetect program.exe --peid-db-path user_db.txt --sections-json sections.json
|
|
728
729
|
```
|
|
729
730
|
|
|
730
731
|
## Detection Mechanisms
|
|
@@ -732,7 +733,7 @@ usage examples:
|
|
|
732
733
|
- PEID signatures
|
|
733
734
|
- Known packer section names
|
|
734
735
|
- Entrypoint in non-standard section
|
|
735
|
-
-
|
|
736
|
+
- Threshold of non-standard sections reached
|
|
736
737
|
- Low number of imports
|
|
737
738
|
- Overlapping entrypoint sections
|
|
738
739
|
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
.gitignore
|
|
2
|
+
.readthedocs.yaml
|
|
2
3
|
LICENSE
|
|
3
4
|
README.md
|
|
4
5
|
pyproject.toml
|
|
5
6
|
requirements.txt
|
|
6
7
|
.github/workflows/python-package.yml
|
|
7
|
-
docs/
|
|
8
|
+
docs/mkdocs.yml
|
|
9
|
+
docs/requirements.txt
|
|
10
|
+
docs/pages/api.md
|
|
11
|
+
docs/pages/contributing.md
|
|
12
|
+
docs/pages/index.md
|
|
13
|
+
docs/pages/installation.md
|
|
14
|
+
docs/pages/internals.md
|
|
15
|
+
docs/pages/cli/pypackerdetect.md
|
|
16
|
+
docs/pages/img/icon.png
|
|
17
|
+
docs/pages/img/logo.png
|
|
8
18
|
src/pypackerdetect/VERSION.txt
|
|
9
19
|
src/pypackerdetect/__info__.py
|
|
10
20
|
src/pypackerdetect/__init__.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.2.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pypackerdetect-1.2.0 → pypackerdetect-1.2.1}/src/pypackerdetect.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|