├── .github
└── workflows
│ └── lint.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── imgs
├── config.png
├── example_results.gif
├── results.png
├── search_example.png
├── search_preview.png
└── string_search_example.png
├── requirements.txt
└── src
└── unpacme_search.py
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: Python linting
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | lint:
13 |
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - name: Set up Python 3.8
20 | uses: actions/setup-python@v2
21 | with:
22 | python-version: 3.8
23 |
24 | - name: Install dependencies
25 | run: |
26 | python -m pip install --upgrade pip
27 | pip install flake8 pytest
28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
29 |
30 | - name: Lint with flake8
31 | run: |
32 | # stop the build if there are Python syntax errors or undefined names
33 |
34 | flake8 . --count --ignore=E501,E127,E128,F405,F403,E265,E266,E303,W292,W291,W293,E231,F401,W605,E126,E226 --show-source --statistics
35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | .idea/
161 |
162 | # local dev scripts
163 | scripts/
164 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com for more information
2 | # See https://pre-commit.com/hooks.html for more hooks
3 | repos:
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v3.2.0
6 | hooks:
7 | - id: trailing-whitespace
8 | - id: check-yaml
9 | - id: check-added-large-files
10 | args: ['--maxkb=3000']
11 | - repo: https://github.com/pycqa/flake8
12 | rev: '6.0.0'
13 | hooks:
14 | - id: flake8
15 | args: ["--ignore=E501,E127,E128,F405,F403,E265,E266,E303,W292,W291,W293,E231,F401,W605,E501,E203,W504"]
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.1.1 - 2023-11-19
4 | - Bugfix: Fix issue with results that do not have a malware_family or classification property
5 |
6 | ## 1.1.0 - 2023-10-09
7 |
8 | - Added support for String Searching
9 | - Updated to use new `bytes` search prefix instead of `string.hex`
10 | - Fixed error logging in several places
11 | - Tested with IDA 8.3
12 | - Minor fixes and updates
13 |
14 | ## 1.0.2 - 2023-07-11
15 |
16 | - Copy results from the results window
17 | - Code cleanup
18 |
19 | ## 1.0.1 - 2023-07-02
20 |
21 | - Updated version
22 |
23 | ## 1.01 - 2023-06-29
24 |
25 | - Fixed a bug with x64 sample search
26 | - Fixed a bug with no malware family in results
27 |
28 | ## 1.0 - 2023-06-23
29 |
30 | - Initial Release
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2023, OALABS
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UnpacMe IDA Byte Search
2 | [](https://www.unpac.me/) [](https://discord.gg/cw4U3WHvpn)
3 |
4 | A search plugin for [UnpacMe](https://unpac.me/) to quickly find related malware samples and determine if a code block is a good
5 | candidate for a detection rule. The plugin searches both malicious files and our goodware corpus. This allows an analyst to quickly determine
6 | if the block of code belongs to a single known family, multiple families or if it is a common pattern found in goodware.
7 |
8 | **The plugin requires a valid API key for [UnpacMe](https://www.unpac.me/).**
9 |
10 | ## Installation
11 | Before using the plugin you must install the following python modules your IDA environment.
12 |
13 | - [requests](https://pypi.org/project/requests/)
14 | - [keyring](https://pypi.org/project/keyring/)
15 |
16 | Using pip:
17 | ```
18 | pip install requests keyring
19 | ```
20 |
21 | ## Searching
22 |
23 | Select the instructions you would like to search for and right click. Then select `UnpacMe Byte Search`.
24 |
25 |
26 |
27 |
28 |
29 | ### Search Preview
30 |
31 | When the `Search Preview` option is enabled, the plugin will display a preview of the search bytes that can be customized before searching.
32 |
33 |
34 |
35 |
36 |
37 | ### String Searching
38 |
39 | To search for a specific string, you can either select the string within the _Strings subview_ or the address where the
40 | string is referenced and right click to search.
41 |
42 |
43 |
44 |
45 |
46 | You can also search for a specific string by selecting the address where the string is referenced and searching.
47 |
48 |
49 | ### Results
50 |
51 | The results window shows a summary of the search results, followed by a table of the raw results. If the pattern is a
52 | good candidate for a rule, you can quickly copy it use the `Copy Pattern` button. To view the analysis of a file simply
53 | click on the SHA256 hash within the table to open a new browser tab to the analysis on [UnpacMe](https://www.unpac.me).
54 |
55 | To copy results simply select any of the desired cells and click the `Copy Selected Results` button.
56 |
57 |
58 |
59 |
60 |
61 | ## Configuration
62 |
63 | The plugin has the following configuration options that can be set via the plugin menu.
64 |
65 |
66 |
67 |
68 |
69 | - **API Key** - Your Unpac.me API key. This can be found in your account settings on [Unpac.me](https://www.unpac.me/account#/). We use the keyring module
70 | to store the API token within the system keyring.
71 | - **Log Level** - Set the log verbosity.
72 | - **Search Preview** - When enabled, the plugin will display a preview of the search bytes that can be edited before searching.
73 | - **Auto Wildcard** - The plugin will wildcard `??` bytes likely to change between samples. The following types are wildcarded by
74 | the plugin when set.
75 | - Memory References
76 | - Direct Memory References
77 | - Memory References with Displacement
78 | - Immediate Far Address
79 | - Immediate Near Address
80 | - **Search Goodware** - When set the plugin will also search the UnpacMe Goodware corpus.
81 |
82 | ## Troubleshooting and Support
83 |
84 | If you run into issues using the plugin, please let us know either via [Discord](https://discord.gg/cw4U3WHvpn) or by opening an issue on this repo.
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/imgs/config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OALabs/UnpacMe-IDA-Byte-Search/99742d1c45f092974b77a0fc6787301e484df9c9/imgs/config.png
--------------------------------------------------------------------------------
/imgs/example_results.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OALabs/UnpacMe-IDA-Byte-Search/99742d1c45f092974b77a0fc6787301e484df9c9/imgs/example_results.gif
--------------------------------------------------------------------------------
/imgs/results.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OALabs/UnpacMe-IDA-Byte-Search/99742d1c45f092974b77a0fc6787301e484df9c9/imgs/results.png
--------------------------------------------------------------------------------
/imgs/search_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OALabs/UnpacMe-IDA-Byte-Search/99742d1c45f092974b77a0fc6787301e484df9c9/imgs/search_example.png
--------------------------------------------------------------------------------
/imgs/search_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OALabs/UnpacMe-IDA-Byte-Search/99742d1c45f092974b77a0fc6787301e484df9c9/imgs/search_preview.png
--------------------------------------------------------------------------------
/imgs/string_search_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OALabs/UnpacMe-IDA-Byte-Search/99742d1c45f092974b77a0fc6787301e484df9c9/imgs/string_search_example.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 | keyring
--------------------------------------------------------------------------------
/src/unpacme_search.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import binascii
3 |
4 | import ida_idaapi
5 | import ida_kernwin
6 | import idaapi
7 | import ida_diskio
8 | import ida_bytes
9 | import idc
10 | import ida_ua
11 | import ida_nalt
12 | import idautils
13 | import json
14 | import logging
15 | import requests
16 | import keyring
17 | from datetime import datetime
18 | import webbrowser
19 | import os
20 | from typing import Dict, Any, Tuple
21 | import time
22 |
23 | from PyQt5.QtCore import Qt, QByteArray
24 | from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout, QGridLayout, QFormLayout, \
25 | QLineEdit, QTextEdit, QTableWidget, QTableWidgetItem, QHeaderView, QComboBox, QCheckBox, QFrame, QApplication, QShortcut
26 | from PyQt5.QtGui import QColor, QPixmap, QPainter, QIcon, QFontMetrics, QGuiApplication, QKeySequence
27 |
28 |
29 | logger = logging.getLogger("UnpacMeSearch")
30 | logger.setLevel(logging.INFO)
31 |
32 | UPMS_ICON_32_ENCODED = b'iVBORw0KGgoAAAANSUhEUgAAACAAAABICAYAAACX3ffDAAAKtmlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU+kSgP9700N' \
33 | b'CC4QiJfTeWwApIbQASq82QhIglBATgoLYkMUVWFFERLAs6KqIgo0qNkSxLYoF7AuyiKjrYkFUVN4FDmF333nvnTfnTO' \
34 | b'bL3Pnnn/nP/c+ZCwBZgy0UpsHyAKQLMkVhfl60mNg4Gm4YYIEKgIEy0GdzxEJGSEgQQGTW/l0+9gJoyt6xmMr178//q' \
35 | b'yhweWIOAFAIwglcMScd4VOIfuQIRZkAoI4gfr0VmcIpvoWwkggpEOHfpzhphj9NccI0o0nTMRFhTIRpAOBJbLYoCQCS' \
36 | b'OeKnZXGSkDykqR6sBVy+AOFchN3T0zO4CLcjbIzECBGeyk9P+EuepL/lTJDmZLOTpDzTy7TgvfliYRo7+/88jv8t6Wm' \
37 | b'S2T0MESUli/zDEKuFnNn91IxAKQsSFgbPMp87HT/NyRL/yFnmiJlxs8xlewdK16YtDJrlRL4vS5onkxUxyzyxT/gsiz' \
38 | b'LCpHslipiMWWaL5vaVpEZK/ck8ljR/TnJE9Cxn8aMWzrI4NTxwLoYp9YskYdL6eQI/r7l9faW9p4v/0i+fJV2bmRzhL' \
39 | b'+2dPVc/T8CYyymOkdbG5Xn7zMVESuOFmV7SvYRpIdJ4Xpqf1C/OCpeuzUReyLm1IdIzTGEHhMwyCAEikAFowBvwgRgI' \
40 | b'QRpgg2zkfwziQVZn8lZmTjXFzBBmi/hJyZk0BnLbeDSWgGNpTrO1trUDYOruzrwa76nTdxKiXpvzbagFwK11cnLy9Jw' \
41 | b'vALlTx+MBIDbO+YyXACA/DMCVdo5ElDXjQ0/9YAARyAEloIZUrAeMgQWwBY7AFXgCHxAAgkEEiAVLAQckg3SklxUgF6' \
42 | b'wHBaAIbAHbQSXYC/aBQ+AoOAGaQTu4AC6D6+AWuAcegX4wBF6BUfARTEAQhIPIEAVSg7QhA8gMsoXokDvkAwVBYVAsF' \
43 | b'A8lQQJIAuVCG6AiqBSqhKqhWug41ApdgK5CPdADaAAagd5BX2AUTIKVYE3YELaC6TADDoQj4CVwErwczoHz4c1wBVwD' \
44 | b'H4Gb4Avwdfge3A+/gsdQACWDoqJ0UBYoOoqJCkbFoRJRItQaVCGqHFWDqke1obpQd1D9qNeoz2gsmoKmoS3Qrmh/dCS' \
45 | b'ag16OXoMuRleiD6Gb0J3oO+gB9Cj6O4aM0cCYYVwwLEwMJgmzAlOAKcccwDRiLmHuYYYwH7FYLBVrhHXC+mNjsSnYVd' \
46 | b'hi7G5sA/Y8tgc7iB3D4XBqODOcGy4Yx8Zl4gpwO3FHcOdwt3FDuE94Gbw23hbvi4/DC/B5+HL8YfxZ/G38MH6CIE8wI' \
47 | b'LgQgglcQjahhLCf0Ea4SRgiTBAViEZEN2IEMYW4nlhBrCdeIj4mvpeRkdGVcZYJleHLrJOpkDkmc0VmQOYzSZFkSmKS' \
48 | b'FpMkpM2kg6TzpAek92Qy2ZDsSY4jZ5I3k2vJF8lPyZ9kKbKWsixZruxa2SrZJtnbsm/kCHIGcgy5pXI5cuVyJ+Vuyr2' \
49 | b'WJ8gbyjPl2fJr5KvkW+X75McUKAo2CsEK6QrFCocVriq8UMQpGir6KHIV8xX3KV5UHKSgKHoUJoVD2UDZT7lEGVLCKh' \
50 | b'kpsZRSlIqUjip1K40qKyrbK0cpr1SuUj6j3E9FUQ2pLGoatYR6gtpL/aKiqcJQ4alsUqlXua0yrjpP1VOVp1qo2qB6T' \
51 | b'/WLGk3NRy1Vbatas9oTdbS6qXqo+gr1PeqX1F/PU5rnOo8zr3DeiXkPNWANU40wjVUa+zRuaIxpamn6aQo1d2pe1Hyt' \
52 | b'RdXy1ErRKtM6qzWiTdF21+Zrl2mf035JU6YxaGm0ClonbVRHQ8dfR6JTrdOtM6FrpBupm6fboPtEj6hH10vUK9Pr0Bv' \
53 | b'V19ZfoJ+rX6f/0IBgQDdINthh0GUwbmhkGG240bDZ8IWRqhHLKMeozuixMdnYw3i5cY3xXROsCd0k1WS3yS1T2NTBNN' \
54 | b'm0yvSmGWzmaMY3223WY44xdzYXmNeY91mQLBgWWRZ1FgOWVMsgyzzLZss3VvpWcVZbrbqsvls7WKdZ77d+ZKNoE2CTZ' \
55 | b'9Nm887W1JZjW2V7145s52u31q7F7q29mT3Pfo/9fQeKwwKHjQ4dDt8cnRxFjvWOI076TvFOu5z66Er0EHox/YozxtnL' \
56 | b'ea1zu/NnF0eXTJcTLn+6Wrimuh52fTHfaD5v/v75g266bmy3ard+d5p7vPvP7v0eOh5sjxqPZ556nlzPA57DDBNGCuM' \
57 | b'I442XtZfIq9FrnOnCXM08743y9vMu9O72UfSJ9Kn0eeqr65vkW+c76ufgt8rvvD/GP9B/q38fS5PFYdWyRgOcAlYHdA' \
58 | b'aSAsMDKwOfBZkGiYLaFsALAhZsW/B4ocFCwcLmYBDMCt4W/CTEKGR5yOlQbGhIaFXo8zCbsNywrnBK+LLww+EfI7wiS' \
59 | b'iIeRRpHSiI7ouSiFkfVRo1He0eXRvfHWMWsjrkeqx7Lj22Jw8VFxR2IG1vks2j7oqHFDosLFvcuMVqycsnVpepL05ae' \
60 | b'WSa3jL3sZDwmPjr+cPxXdjC7hj2WwErYlTDKYXJ2cF5xPbll3BGeG6+UN5zollia+CLJLWlb0kiyR3J58ms+k1/Jf5v' \
61 | b'in7I3ZTw1OPVg6mRadFpDOj49Pr1VoChIFXRmaGWszOgRmgkLhP3LXZZvXz4qChQdEEPiJeKWTCVkSLohMZb8IBnIcs' \
62 | b'+qyvq0ImrFyZUKKwUrb2SbZm/KHs7xzfllFXoVZ1VHrk7u+tyB1YzV1WugNQlrOtbqrc1fO7TOb92h9cT1qet/zbPOK' \
63 | b'837sCF6Q1u+Zv66/MEf/H6oK5AtEBX0bXTduPdH9I/8H7s32W3auel7IbfwWpF1UXnR12JO8bWfbH6q+Glyc+Lm7hLH' \
64 | b'kj1bsFsEW3q3emw9VKpQmlM6uG3BtqYyWllh2Yfty7ZfLbcv37uDuEOyo78iqKJlp/7OLTu/ViZX3qvyqmrYpbFr067' \
65 | b'x3dzdt/d47qnfq7m3aO+Xn/k/36/2q26qMawp34fdl7Xv+f6o/V2/0H+pPaB+oOjAt4OCg/2Hwg511jrV1h7WOFxSB9' \
66 | b'dJ6kaOLD5y66j30ZZ6i/rqBmpD0TFwTHLs5fH4470nAk90nKSfrD9lcGpXI6WxsAlqym4abU5u7m+JbelpDWjtaHNta' \
67 | b'zxtefpgu0571RnlMyVniWfzz06eyzk3dl54/vWFpAuDHcs6Hl2MuXi3M7Sz+1LgpSuXfS9f7GJ0nbvidqX9qsvV1mv0' \
68 | b'a83XHa833XC40firw6+N3Y7dTTedbrbccr7V1jO/5+xtj9sX7njfuXyXdff6vYX3enoje+/3Le7rv8+9/+JB2oO3D7M' \
69 | b'eTjxa9xjzuPCJ/JPypxpPa34z+a2h37H/zID3wI1n4c8eDXIGX/0u/v3rUP5z8vPyYe3h2he2L9pHfEduvVz0cuiV8N' \
70 | b'XE64I/FP7Y9cb4zak/Pf+8MRozOvRW9HbyXfF7tfcHP9h/6BgLGXv6Mf3jxHjhJ7VPhz7TP3d9if4yPLHiK+5rxTeTb' \
71 | b'23fA78/nkyfnBSyRezpUQCFKJyYCMC7gwCQYwGgIDMEcdHMbD0t0Mz3wDSB/8Qz8/e0OAJQj5ipEYl5HoBjiBquA0DO' \
72 | b'E4Cp8SjCE8B2dlKdnYOnZ/YpwSJfL/VWmvJy2PshS8E/ZWae/0vd/7RgKqs9+Kf9F1uNDCpTAs42AAAAlmVYSWZNTQA' \
73 | b'qAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAA' \
74 | b'AAAAAAAJAAAAABAAAAkAAAAAEAA5KGAAcAAAASAAAAhKACAAQAAAABAAAAIKADAAQAAAABAAAASAAAAABBU0NJSQAAA' \
75 | b'FNjcmVlbnNob3Tx3tJyAAAACXBIWXMAABYlAAAWJQFJUiTwAAAC1WlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6' \
76 | b'eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJ' \
77 | b'ERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cm' \
78 | b'RmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY' \
79 | b'29tL2V4aWYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8i' \
80 | b'PgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+MzI8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA' \
81 | b'8ZXhpZjpVc2VyQ29tbWVudD5TY3JlZW5zaG90PC9leGlmOlVzZXJDb21tZW50PgogICAgICAgICA8ZXhpZjpQaXhlbF' \
82 | b'lEaW1lbnNpb24+NzI8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yP' \
83 | b'C90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj4xNDQ8L3RpZmY6WVJlc29sdXRp' \
84 | b'b24+CiAgICAgICAgIDx0aWZmOlhSZXNvbHV0aW9uPjE0NDwvdGlmZjpYUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY' \
85 | b'6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOl' \
86 | b'JERj4KPC94OnhtcG1ldGE+CibWx0MAAAKaSURBVGgF7Zm9bhNBEMdn9s5OzkAaCp4AUfMIiIYnoADT8dUgpFAhUaRGg' \
87 | b'vAOfIiSGCQIFQ2iQUpHlUegQNjEZ93eDjNH1lycu83eGXGFdwuftd6Z/29mZ2dPCT4c7t+BVR64eeWAVjkBEDLQeQbU' \
88 | b'ShegBB+2INRAqIHOa6BzgM4bYSjCzmsgAIRTEGogZKDzDIRj2PkWBICQgZCB0IhCDXSegRg7LsOlMqBK9DlBqz/5tga' \
89 | b'I2JIVv/LHREAihBKOf1pbAfQiAGPo7WmYXgbCuyyXlrPhLw/QGGB9AJDlNFL9wfWt92d/PvmYvMwM3m4L0QhAIk8PaB' \
90 | b'T1B8PHIxzbSJ/tJs9zwlttILwBRFznsLMobiGefkheFBAI0ybb4QVQiGvaUf3kRjlyK26fAmE0Z6IBxIkAIp5reKMmA' \
91 | b'6e4hZCa0Dne9IVwAoh4xuKnZhz55797bsXqntu7yStfiFoA2Ufe89fj78nVrU84qROrmxcIY2CISD9cDQIfHP7TSjqZ' \
92 | b'bSYiboC+AdEmRWpMGiMkymmc7G1/wWmdqMzfv5ReiBM6xw6MVpT1DFzj6XvctCo54ipnhlcT4Xlubu+MhijGos/+oo3' \
93 | b'ZRV6/X2Vj56I184idDmdsgjn7QWkbRae0S448KwFkBSL05MllIN9Bc9NiDp+Bst5mkw36/L12zGugZHBsMUkE/peNH+' \
94 | b'ahyhzgmOp/mmgEEGftrlxXLHMA131e5JRrIlvXZ1zO2vxmAfi0uaNjwF6WRhttRFw29hQgyztq9Y8LI6v+8bAZ8HKrP' \
95 | b'CBPcrS41d4AckxTk/PryHJjcau9AURWkbSYJQcdfQtrBLCkdKV5APDOgGw+F6LcTY3GYtUvGsfy1uMzCgCFxQ3pWs8R' \
96 | b'rZV9soGzcH8DwbrYx3Lr9SEAAAAASUVORK5CYII='
97 |
98 | UPMS_MENU_ICON_ENCODED = b'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAP' \
99 | b'oAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABK' \
100 | b'ARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAA' \
101 | b'EAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAABfvA/wAAAACXBIWXMAAAsTAAALEwEAmpwYAAACyGlUWHRYTUw6' \
102 | b'Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIE' \
103 | b'NvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRm' \
104 | b'LXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxucz' \
105 | b'p0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDov' \
106 | b'L25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2' \
107 | b'x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAg' \
108 | b'ICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW' \
109 | b'9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4zMjwvZXhpZjpQaXhl' \
110 | b'bFhEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOkNvbG9yU3BhY2U+MTwvZXhpZjpDb2xvclNwYWNlPgogICAgICAgIC' \
111 | b'A8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MzI8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICA8L3JkZjpEZXNjcmlw' \
112 | b'dGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4Ko+jingAAAlJJREFUWAntlrtu1UAQhv/xhSRIpOUJEDWPEE' \
113 | b'W8AgUcOkA0NBQ0UKRJkNLwEFxESQ5IFAGJBtEg0VHxCEhEJMQkttf8c8IcG8uXtUGJkLLF2fWemfm/nd0dW+5fLgqc' \
114 | b'YAtOUHsmfQrwf2cgkPIE8SiPOsyjMxDS0xX4SNk9BRFBBacE6xuNAohDIHd4GUdYJcBtivysZqNPtPr/YIDFs8Bhjm' \
115 | b'm6hGtrr+X7+lt5mhW4NRZiEICuPNnHNFvCZHMqu7aSh9vyOC9wcwyEN4CKpw5bdXGD2NiWJ78hkiHb4QUwE8+wlS7i' \
116 | b'enXlJm69Qrh8lglviF4AFc8yvEiTbnGD0DPBA3qDz14QnQAqfkjxKKP4+3LPTayt33gjz3whWgF0H7Mcz79+w5W1d7' \
117 | b'LXJtY2rxCZw4RhdroKhNjrWCuZFRMVZ5H5zLm7EmCX+xoGBfL9BJ8efZCkTVTn760UF+MY5+HgXICUflcZ7g7LZCNH' \
118 | b'1BSM4qD4BXq8KhxCrXp8/rGwjEu0/9LkY3NRhAeRYJLSRRxnhZdH1RvlgUYADUaHeNYfjbXQt26X2lkjqKiYZZPzZ9' \
119 | b'rE1WcetOJgseY9g2oG+NvfGMfLziLNAWziuPtBAFE6bHU+i5kDdKVYc8rUxgchzvkEHWJjAHoFO/eOgLE7wPKQ4D62' \
120 | b'dgv0krRclDKM64EsLf1HlgEvDxaVXsi+QPWt9gbQa5o48HPk71p9q70BVPZfZEDDVJfwx0P1j+ManwJ4Z0CPfyjgJ8' \
121 | b'qwVj/1de9Iv3p8mgIE4dEbssuedgu1mJ1X9xeol7VOGchwfwAAAABJRU5ErkJggg=='
122 |
123 | UPMS_ICON_32 = ida_kernwin.load_custom_icon(data=base64.b64decode(UPMS_ICON_32_ENCODED), format="png")
124 | UPMS_MENU_ICON = ida_kernwin.load_custom_icon(data=base64.b64decode(UPMS_MENU_ICON_ENCODED), format="png")
125 |
126 | if len(logger.handlers) > 0:
127 | logger.handlers = []
128 |
129 | log_stream = logging.StreamHandler()
130 | formatter = logging.Formatter('UnpacMeSearch:%(levelname)s:%(message)s')
131 | log_stream.setFormatter(formatter)
132 |
133 | # probably bug here?
134 | logger.addHandler(log_stream)
135 |
136 | BAD_OFFSETS = [0xffffffff, 0xffffffffffffffff]
137 |
138 |
139 | class SearchPreview(QDialog):
140 | def __init__(self, search_list, code_block, parent=None):
141 | super(SearchPreview, self).__init__(parent)
142 |
143 | self.setWindowTitle("Search Preview")
144 | self.resize(640, 480)
145 | layout = QVBoxLayout()
146 | self.edit_search = QTextEdit(self)
147 | self.edit_search.setMinimumWidth(300)
148 | self.edit_codeblock = QTextEdit(self)
149 | self.edit_codeblock.setReadOnly(True)
150 | self.edit_codeblock.setText(code_block)
151 | self.edit_codeblock.setLineWrapMode(QTextEdit.NoWrap)
152 |
153 | self.edit_codeblock.setFixedWidth(320)
154 |
155 | self.btn_search = QPushButton("Search", self)
156 | self.btn_search.clicked.connect(self.accept)
157 | self.btn_cancel = QPushButton("Cancel", self)
158 | self.btn_cancel.clicked.connect(self.reject)
159 |
160 | search_query = "\n".join(search_list)
161 |
162 | self.edit_search.setText(search_query)
163 |
164 | layout_view = QHBoxLayout()
165 |
166 | layout_view.setAlignment(Qt.AlignTop)
167 | layout_view.addWidget(self.edit_search)
168 | layout_view.addWidget(self.edit_codeblock)
169 | layout_view.addStretch(1)
170 |
171 | layout_buttons = QHBoxLayout()
172 | layout_buttons.addWidget(self.btn_cancel)
173 | layout_buttons.addWidget(self.btn_search)
174 |
175 | layout.addLayout(layout_view)
176 | layout.addLayout(layout_buttons)
177 | self.setLayout(layout)
178 |
179 | def get_search_pattern(self):
180 | search_pattern = self.edit_search.toPlainText().replace("\n", "").replace(" ", "")
181 | search_pattern = ' '.join([search_pattern[i:i + 2] for i in range(0, len(search_pattern), 2)])
182 | return search_pattern
183 |
184 |
185 | class GoodwareView(QDialog):
186 | def __init__(self, metadata, parent=None):
187 | super(GoodwareView, self).__init__(parent)
188 |
189 | self.setWindowTitle("Goodware Details")
190 | self.resize(600, 300)
191 | layout = QVBoxLayout()
192 | form_layout = QFormLayout()
193 |
194 | self.lbl_sha256 = QLabel("SHA256:")
195 | self.lbl_sha256_val = QLabel(metadata['sha256'])
196 |
197 | self.lbl_name = QLabel("Name:")
198 | self.lbl_name_val = QLabel(metadata['name'])
199 |
200 | self.lbl_size = QLabel("Size:")
201 | self.lbl_size_val = QLabel(str(metadata['size']))
202 |
203 | self.lbl_type = QLabel("Type:")
204 | self.lbl_type_val = QLabel(metadata['type'])
205 |
206 | self.lbl_subsystem = QLabel("Subsystem:")
207 | self.lbl_subsystem_val = QLabel(metadata['subsytem'])
208 |
209 | self.lbl_machine_type = QLabel("Machine Type:")
210 | self.lbl_machine_type_val = QLabel(metadata['machine_type'])
211 |
212 | self.lbl_linker_version = QLabel("Linker Version:")
213 | self.lbl_linker_version_val = QLabel(metadata['linker_version'])
214 |
215 | form_layout.addRow(self.lbl_sha256, self.lbl_sha256_val)
216 | form_layout.addRow(self.lbl_name, self.lbl_name_val)
217 | form_layout.addRow(self.lbl_size, self.lbl_size_val)
218 | form_layout.addRow(self.lbl_type, self.lbl_type_val)
219 | form_layout.addRow(self.lbl_subsystem, self.lbl_subsystem_val)
220 | form_layout.addRow(self.lbl_machine_type, self.lbl_machine_type_val)
221 | form_layout.addRow(self.lbl_linker_version, self.lbl_linker_version_val)
222 |
223 | if 'metadata' in metadata.keys():
224 | if 'StringInfo' in metadata['metadata'].keys():
225 | for prop, prop_val in metadata['metadata']['StringInfo'].items():
226 | lbl = QLabel(prop)
227 | lbl_val = QLabel(prop_val)
228 | form_layout.addRow(lbl, lbl_val)
229 |
230 | self.btn_search = QPushButton("Ok", self)
231 | self.btn_search.clicked.connect(self.accept)
232 |
233 | layout_buttons = QHBoxLayout()
234 | layout_buttons.addWidget(self.btn_search)
235 |
236 | layout.addLayout(form_layout)
237 | layout.addLayout(layout_buttons)
238 |
239 | self.setLayout(layout)
240 |
241 |
242 | class UnpacMeSearchConfigDialog(QDialog):
243 | def __init__(self, config, parent=None):
244 | super(UnpacMeSearchConfigDialog, self).__init__(parent)
245 | self.config = config
246 | self.setWindowTitle("UnpacMe Search Config")
247 | self.resize(320, 240)
248 |
249 | layout = QVBoxLayout()
250 |
251 | form_layout = QFormLayout()
252 |
253 | self.lbl_api_key = QLabel("API Key:")
254 | self.w_api_key = QLineEdit()
255 | self.w_api_key.setEchoMode(QLineEdit.Password)
256 | self.w_api_key.setFixedWidth(300)
257 | self.w_api_key.setText(self.config['api_key'])
258 |
259 | self.lbl_loglevel = QLabel("Log Level:")
260 |
261 | self.cmb_loglevels = QComboBox()
262 | self.cmb_loglevels.addItems(['DEBUG', "INFO", "ERROR"])
263 | self.cmb_loglevels.setEditable(False)
264 |
265 | self.lbl_preview = QLabel("Search Preview:")
266 |
267 | self.chk_preview = QCheckBox()
268 | self.chk_preview.setChecked(self.config['preview'])
269 |
270 | self.lbl_wildcard = QLabel("Auto Wildcard:")
271 | self.chk_wildcard = QCheckBox()
272 | self.chk_wildcard.setChecked(self.config['auto'])
273 |
274 | self.lbl_goodware = QLabel("Search Goodware:")
275 | self.chk_goodware = QCheckBox()
276 | self.chk_goodware.setChecked(self.config['goodware'])
277 |
278 | form_layout.addRow(self.lbl_api_key, self.w_api_key)
279 | form_layout.addRow(self.lbl_loglevel, self.cmb_loglevels)
280 | form_layout.addRow(self.lbl_preview, self.chk_preview)
281 | form_layout.addRow(self.lbl_wildcard, self.chk_wildcard)
282 | form_layout.addRow(self.lbl_goodware, self.chk_goodware)
283 |
284 | self.btn_search = QPushButton("Save", self)
285 | self.btn_search.clicked.connect(self.accept)
286 | self.btn_cancel = QPushButton("Cancel", self)
287 | self.btn_cancel.clicked.connect(self.reject)
288 |
289 | layout_buttons = QHBoxLayout()
290 | layout_buttons.addWidget(self.btn_cancel)
291 | layout_buttons.addWidget(self.btn_search)
292 |
293 | layout.addLayout(form_layout)
294 | layout.addLayout(layout_buttons)
295 |
296 | self.setLayout(layout)
297 |
298 | def get_config(self):
299 | self.config['goodware'] = self.chk_goodware.isChecked()
300 | self.config['auto'] = self.chk_wildcard.isChecked()
301 | self.config['preview'] = self.chk_preview.isChecked()
302 | self.config['loglevel'] = self.cmb_loglevels.currentText()
303 | self.config['api_key'] = self.w_api_key.text()
304 | return self.config
305 |
306 |
307 | class UnpacMeResultWidget(idaapi.PluginForm):
308 |
309 | def __init__(self, search_term: str, results: Dict[str, Any]):
310 | super(UnpacMeResultWidget, self).__init__()
311 | self.search_term = search_term
312 | self.results = results
313 | self.goodware_hashes = []
314 | self.id_map = {}
315 | self.result_table = None
316 |
317 | def OnCreate(self, form):
318 | self.parent = self.FormToPyQtWidget(form)
319 | self.PopulateForm()
320 |
321 | def copy_selected_cells(self) -> None:
322 |
323 | if not self.result_table:
324 | return
325 |
326 | selection = self.result_table.selectedRanges()
327 |
328 | selected_result = ""
329 | if not selection:
330 | logger.debug("No selection")
331 | return
332 |
333 | for r in range(selection[0].topRow(), selection[0].bottomRow() + 1):
334 | for c in range(selection[0].leftColumn(), selection[0].rightColumn() + 1):
335 | try:
336 | selected_result += str(self.result_table.item(r, c).text()) + "\t"
337 | except AttributeError:
338 | selected_result += "\n"
339 | selected_result = selected_result[:-1] + "\n"
340 |
341 | QApplication.clipboard().setText(selected_result)
342 |
343 | def handle_click(self, item):
344 |
345 | if item.column() == 4:
346 |
347 | if self.id_map[item.text()]['malware']:
348 | webbrowser.open(f"https://www.unpac.me/results/{self.id_map[item.text()]['id']}?hash={item.text()}")
349 | return
350 |
351 | gwv = GoodwareView(self.id_map[item.text()]['metadata'])
352 | gwv.exec_()
353 |
354 | elif item.column() == 1:
355 |
356 | if 'Unknown' == item.text() or item.text() == '':
357 | logger.warning("No family")
358 | return
359 |
360 | term = f'malware:"{item.text()}"'.encode("ascii")
361 | webbrowser.open(f'https://www.unpac.me/search?terms={base64.b64encode(term).decode("ascii")}')
362 |
363 | def PopulateForm(self):
364 |
365 | goodware_matches = 0
366 | unknown_matches = 0
367 | malicious_matches = 0
368 |
369 | layout = QVBoxLayout()
370 |
371 | summary_pane = QHBoxLayout()
372 | summary_pane.setAlignment(Qt.AlignLeft)
373 |
374 | summary_layout = QFormLayout()
375 |
376 | lbl_logo = QLabel()
377 | lbl_logo.setGeometry(10, 10, 100, 100)
378 |
379 | pixmap = QPixmap()
380 | pixmap.loadFromData(QByteArray(base64.b64decode(UPMS_ICON_32_ENCODED)))
381 | lbl_logo.setPixmap(pixmap)
382 | summary_pane.addWidget(lbl_logo)
383 |
384 | search_term = self.search_term
385 | if len(self.search_term) > 16:
386 | search_term = self.search_term[:16] + "..."
387 |
388 | btn_copy = QPushButton("Copy Search Pattern")
389 | btn_copy.clicked.connect(self.copy_text_to_clipboard)
390 |
391 | btn_copy_selected = QPushButton("Copy Selected Results")
392 | btn_copy_selected.clicked.connect(self.copy_selected_cells)
393 |
394 | summary_layout.addRow(QLabel("Search Term:"), QLabel(search_term))
395 | summary_layout.addRow(QLabel("Matches:"), QLabel(f"{self.results['result_count']}"))
396 | summary_layout.addRow(QLabel("First Seen:"), QLabel(f"{datetime.fromtimestamp(self.results['first_seen']).strftime('%Y-%m-%d')}"))
397 | summary_layout.addRow(QLabel("Last Seen:"),
398 | QLabel(f"{datetime.fromtimestamp(self.results['last_seen']).strftime('%Y-%m-%d')}"))
399 |
400 | line = QFrame()
401 | line.setFrameShape(QFrame.HLine)
402 | line.setFrameShadow(QFrame.Sunken)
403 | summary_layout.addRow(line)
404 | summary_layout.addRow(btn_copy)
405 | summary_layout.addRow(btn_copy_selected)
406 | summary_layout.setVerticalSpacing(0)
407 | summary_pane.addLayout(summary_layout)
408 |
409 | self.result_table = QTableWidget()
410 |
411 | self.result_table.setRowCount(0)
412 |
413 | self.result_table.setColumnCount(6)
414 | self.result_table.itemDoubleClicked.connect(self.handle_click)
415 |
416 | self.result_table.setSortingEnabled(True)
417 | self.result_table.setMinimumHeight(600)
418 |
419 | self.result_table.setHorizontalHeaderLabels(["Classification",
420 | "Malware Family",
421 | "Labels",
422 | "Threat Type",
423 | "SHA256",
424 | "Last Seen"])
425 |
426 | results = self.results['results']
427 | last_row = 0
428 | for row, result in enumerate(results):
429 | last_row = row
430 | self.result_table.insertRow(row)
431 | try:
432 | self.id_map[result['sha256']] = {
433 | 'id': result["analysis"][0]["id"],
434 | 'malware': True
435 | }
436 | except Exception as pex:
437 | logger.error(f"Error parsing results.. Error: {pex}")
438 | logger.error(result)
439 |
440 | sha256_item = QTableWidgetItem(result['sha256'])
441 | sha256_item.setToolTip("View latest Analysis on UnpacMe")
442 |
443 | self.result_table.setItem(row, 4, QTableWidgetItem(sha256_item))
444 | self.result_table.setItem(row, 5, QTableWidgetItem(str(datetime.fromtimestamp(result['last_seen']).strftime('%Y-%m-%d'))))
445 | malware_family = []
446 | classification_type = ""
447 | threat_type = ""
448 | labels = []
449 | family_lower = []
450 | for entry in result['malwareid']:
451 |
452 | try:
453 | if entry['malware_family'].lower() not in family_lower:
454 | family_lower.append(entry['malware_family'].lower())
455 | malware_family.append(entry['malware_family'])
456 | except KeyError:
457 | logger.debug("No malware family")
458 | except AttributeError:
459 | logger.debug("No malware family")
460 |
461 | if entry['type'] == 'unpacme':
462 | if not classification_type:
463 | classification_type = entry['classification_type']
464 |
465 | if not threat_type:
466 | threat_type = entry['threat_type']
467 | labels.append(entry['name'])
468 | try:
469 | family_str = "\n".join([x.capitalize() for x in set(malware_family)])
470 | except TypeError:
471 | family_str = ""
472 |
473 | label_str = "\n".join(list(set(labels)))
474 | if not label_str:
475 | label_str = ""
476 | family_widget = QTableWidgetItem(family_str)
477 | family_widget.setToolTip("Search for malware family on UnpacMe.")
478 | self.result_table.setItem(row, 1, family_widget)
479 | self.result_table.setItem(row, 2, QTableWidgetItem(label_str))
480 |
481 | # if there is no set classificaiton type, but
482 | # there are applied labels (i.e. malpedia) set the classification type to malicious
483 | if not classification_type:
484 | if family_str:
485 | classification_type = "MALICIOUS"
486 |
487 | if classification_type == "MALICIOUS":
488 | ct_widget = QTableWidgetItem(classification_type)
489 | # ff0000
490 | ct_widget.setBackground(QColor(255, 0, 0))
491 | malicious_matches += 1
492 | else:
493 | classification_type = "UNKNOWN"
494 | ct_widget = QTableWidgetItem(classification_type)
495 | # 6c757d
496 | ct_widget.setBackground(QColor(108, 117, 125))
497 | unknown_matches += 1
498 |
499 | ct_widget.setForeground(QColor(255, 255, 255))
500 |
501 | self.result_table.setItem(row, 0, ct_widget)
502 | self.result_table.setItem(row, 3, QTableWidgetItem(threat_type))
503 |
504 | if self.results['goodware_results']:
505 | self.goodware_row_start = last_row + 1
506 | for row, result in enumerate(self.results['goodware_results'], start=last_row + 1):
507 | self.goodware_hashes.append(result['sha256'])
508 |
509 | self.id_map[result['sha256']] = {
510 | 'malware': False,
511 | 'metadata': result
512 | }
513 |
514 | self.result_table.insertRow(row)
515 | ct_widget = QTableWidgetItem("GOODWARE")
516 | # 228B22
517 | ct_widget.setBackground(QColor(34, 139, 34))
518 | ct_widget.setForeground(QColor(255, 255, 255))
519 | self.result_table.setItem(row, 0, ct_widget)
520 | sha256_item = QTableWidgetItem(result['sha256'])
521 | sha256_item.setToolTip("View details...")
522 | self.result_table.setItem(row, 4, sha256_item)
523 | goodware_matches += 1
524 |
525 | self.result_table.resizeRowsToContents()
526 | self.result_table.setEditTriggers(QTableWidget.NoEditTriggers)
527 |
528 | header = self.result_table.horizontalHeader()
529 | header.setSectionResizeMode(QHeaderView.ResizeToContents)
530 |
531 | count_summary_layout = QFormLayout()
532 |
533 | gwc = QLabel(f"{goodware_matches}")
534 | gwc.setStyleSheet("background-color: #228B22; padding: 5px; color: #ffffff;")
535 |
536 | uc = QLabel(f"{unknown_matches}")
537 | uc.setStyleSheet("background-color: #6c757d; padding: 5px; color: #ffffff;")
538 |
539 | mc = QLabel(f"{malicious_matches}")
540 | mc.setStyleSheet("background-color: #ff0000; padding: 5px; color: #ffffff;")
541 |
542 | count_summary_layout.addRow(QLabel("Goodware:"), gwc)
543 | count_summary_layout.addRow(QLabel("Unknown:"), uc)
544 | count_summary_layout.addRow(QLabel("Malicious:"), mc)
545 |
546 | summary_pane.addLayout(count_summary_layout)
547 |
548 | # Add the summary pane to the main layout
549 | layout.addLayout(summary_pane, Qt.AlignLeft)
550 |
551 | layout.addWidget(self.result_table, Qt.AlignLeft)
552 | layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
553 | layout.addStretch(1)
554 |
555 | self.parent.setLayout(layout)
556 |
557 | def copy_text_to_clipboard(self):
558 |
559 | clipboard = QGuiApplication.clipboard()
560 | clipboard.setText(self.search_term)
561 |
562 | logger.info(f"Text copied to clipboard: {self.search_term}")
563 |
564 | def OnClose(self, form):
565 | pass
566 |
567 |
568 | class UnpacMeSearch(object):
569 |
570 | def __init__(self, api_key):
571 | self.api_key = f"Key {api_key}"
572 | self.base_site = "https://api.unpac.me/api/"
573 | self.api_version = "v1"
574 | self.search_endpoint = "/private/search/term/"
575 | self.search_types = {"hex": "bytes",
576 | "ascii": "string.ascii",
577 | "wide": "string.wide"
578 | }
579 |
580 | def _search(self, data: str, type: str, search_goodware=False) -> Dict[str, Any]:
581 | try:
582 | url = f"{self.base_site}{self.api_version}{self.search_endpoint}{self.search_types[type]}"
583 | auth_header = {'Authorization': self.api_key}
584 | search_data = {'value': data}
585 |
586 | if search_goodware:
587 | search_data['repo_type'] = 'goodware'
588 |
589 | logger.debug(f"Search Data: {search_data}")
590 | logger.debug("Calling Unpac.me API endpoint")
591 | logger.debug(f"URL: {url}")
592 | search_response = requests.post(url, json=search_data, headers=auth_header)
593 | if search_goodware:
594 | logger.debug(search_response.json())
595 |
596 | if search_response.status_code == 404:
597 |
598 | jres = search_response.json()
599 | logger.debug(jres)
600 | if "warning" in jres.keys():
601 | idc.warning(jres['warning'])
602 |
603 | return {}
604 |
605 | if search_response.status_code != 200:
606 | logger.error(f"Error in search...try again {search_response.status_code}")
607 | idc.warning(f"Unexpected response from UnpacMe...please try again. Code: {search_response.status_code}")
608 | return {}
609 |
610 | return search_response.json()
611 | except Exception as ex:
612 | logger.error(f"Error making request {ex}")
613 | idc.warning(f"Unexpected error UnpacMe...please try again. Error: {ex}")
614 |
615 | def search(self, data: str, type: str) -> Dict[str, Any]:
616 | try:
617 | ida_kernwin.show_wait_box("HIDECANCEL\nSearching...")
618 | time.sleep(0.5)
619 | search_results = self._search(data, type, search_goodware=False)
620 | return search_results
621 | finally:
622 | ida_kernwin.hide_wait_box()
623 |
624 | def search_goodware(self, data: str, type: str) -> Dict[str, Any]:
625 | try:
626 | ida_kernwin.show_wait_box("HIDECANCEL\nSearching Goodware...")
627 | time.sleep(0.5)
628 | search_results = {
629 | 'goodware_results': [],
630 | 'matched_goodware_files': 0
631 | }
632 | search_results.update(self._search(data, type, search_goodware=True))
633 | return search_results
634 | finally:
635 | ida_kernwin.hide_wait_box()
636 |
637 |
638 | class BaseSearchHandler(ida_kernwin.action_handler_t):
639 | def __init__(self):
640 | super(BaseSearchHandler, self).__init__()
641 | self.unpacme_search = None
642 | self.result_widget = None
643 |
644 | def build_result(self, result: Dict[str, Any], search_string: str):
645 |
646 | if not result or 'results' not in result.keys():
647 | idc.warning("No results found for the pattern.")
648 | return False
649 |
650 | label_map = {}
651 | classification_map = {}
652 |
653 | for r in result['results']:
654 | classification_type = ""
655 | family = ""
656 | for entry in r['malwareid']:
657 |
658 | label = entry.get('malware_family', None)
659 | if not family:
660 | family = label
661 |
662 | classification = entry.get('classification_type', None)
663 | if classification:
664 | classification_type = classification
665 |
666 | if not classification_type:
667 | classification_type = 'UNKNOWN'
668 |
669 | if family in label_map:
670 | label_map[family] += 1
671 | else:
672 | label_map[family] = 1
673 |
674 | if classification_type in classification_map:
675 | classification_map[classification_type] += 1
676 | else:
677 | classification_map[classification_type] = 1
678 | try:
679 | classification_map['GOODWARE'] = result['matched_goodware_files']
680 | except KeyError:
681 | classification_map['GOODWARE'] = 0
682 |
683 | result['label_map'] = label_map
684 | result['classification_map'] = classification_map
685 | logger.info(classification_map)
686 | logger.info(label_map)
687 |
688 | if self.result_widget:
689 | self.result_widget.Close(ida_kernwin.PluginForm.WCLS_CLOSE_LATER)
690 |
691 | self.result_widget = UnpacMeResultWidget(search_string, result)
692 | self.result_widget.Show("UnpacMe Search")
693 | if "warning" in result.keys():
694 | idc.warning(result['warning'])
695 |
696 | def activate(self, ctx):
697 | # Delay loading of the UnpacMeSearch class until we need it
698 | # This prevents possible password prompt on IDA startup to access the keystore
699 | if self.unpacme_search is None:
700 | self.unpacme_search = UnpacMeSearch(keyring.get_password('unpacme', 'api_key'))
701 |
702 | def update(self, ctx):
703 | pass
704 |
705 |
706 | class SearchHandler(BaseSearchHandler):
707 |
708 | def __init__(self, preview, auto_wildcard, search_goodware):
709 | super(SearchHandler, self).__init__()
710 |
711 | self.unpacme_search = None
712 |
713 | self.preview = preview
714 | self.auto_wildcard = auto_wildcard
715 | self.search_goodware = search_goodware
716 |
717 | # https://www.hex-rays.com/products/ida/support/idadoc/276.shtml
718 | self.wildcard_types = [ida_ua.o_mem, ida_ua.o_phrase, ida_ua.o_displ, ida_ua.o_far, ida_ua.o_near]
719 | self.result_widget = None
720 |
721 | def update(self, ctx):
722 |
723 | if ida_kernwin.get_widget_type(ctx.widget) in [ida_kernwin.BWN_DISASM, ida_kernwin.BWN_PSEUDOCODE, ida_kernwin.BWN_DUMP]:
724 | return ida_kernwin.AST_ENABLE_FOR_WIDGET
725 | return ida_kernwin.AST_DISABLE_FOR_WIDGET
726 |
727 | def _process_selected_code_range(self, start, end):
728 | iterations = 0
729 | search_bytes = []
730 | code_block = ""
731 |
732 | offset = start
733 |
734 | while offset < end:
735 | iterations += 1
736 | if iterations > 100:
737 | break
738 | instr_string = []
739 | try:
740 | cur_offset = offset
741 | flags = ida_bytes.get_full_flags(cur_offset)
742 |
743 | if not ida_bytes.is_code(flags):
744 | logger.debug("Processing as data")
745 | ibytes = idc.get_bytes(cur_offset, idc.get_item_size(cur_offset), 0)
746 | for b in ibytes:
747 | instr_string.append("{0:02x}".format(b))
748 | continue
749 |
750 | ins = ida_ua.insn_t()
751 | idaapi.decode_insn(ins, cur_offset)
752 | instruction_size = ins.size
753 |
754 | logger.debug("------------------------------")
755 | logger.debug(f"Current Offset: {hex(cur_offset)}")
756 | logger.debug("Instruction Size: %d" % instruction_size)
757 | logger.debug(f"Bytes: {binascii.hexlify(idc.get_bytes(cur_offset, instruction_size, 0))}")
758 | logger.debug(f"Flags: {flags}")
759 | logger.debug(idc.generate_disasm_line(cur_offset, 0))
760 | logger.debug("------------------------------")
761 |
762 | op1 = ins.ops[0]
763 | op2 = ins.ops[1]
764 |
765 | ibytes = idc.get_bytes(cur_offset, ins.size, 0)
766 | code_block += " ".join("{0:02x}".format(b) for b in bytearray(ibytes))
767 | if len(ibytes) <= 4:
768 | code_block += "\t"
769 |
770 | logger.debug(idc.generate_disasm_line(offset, 0))
771 | code_block += f"\t{idc.generate_disasm_line(offset, 0)}\n"
772 |
773 | if (
774 | op1.type not in self.wildcard_types and op2.type not in self.wildcard_types) or not self.auto_wildcard:
775 | logger.debug("No wildcards")
776 | ibytes = idc.get_bytes(cur_offset, instruction_size, 0)
777 | for b in ibytes:
778 | instr_string.append("{0:02x}".format(b))
779 | continue
780 |
781 | if op1.type in self.wildcard_types:
782 | logger.debug("Wildcarding op1")
783 |
784 | # wild card instruction
785 | if op1.offb == 0 and op2.offb == 0:
786 | logger.debug("Wildcarding entire instruction")
787 | instr_string.append("?? " * int(instruction_size))
788 | continue
789 |
790 | # wildcard instruction with op1
791 | if op2.offb > 0:
792 | op1_size = op2.offb - op1.offb
793 | else:
794 | op1_size = instruction_size - op1.offb
795 | logger.debug(f"op1_size: {op1_size}")
796 |
797 | if op1.offb == 0:
798 | instr_string.append("?? " * int(op1_size))
799 | else:
800 | logger.debug("Getting op")
801 | logger.debug(f"ibytes: {ibytes}")
802 | for b in ibytes[:op1.offb]:
803 | instr_string.append("{0:02x}".format(b))
804 |
805 | instr_string.append("?? " * int(op1_size))
806 |
807 | # continue
808 | if op2.offb > 0:
809 | if op2.type in self.wildcard_types:
810 | logger.debug("Wildcarding op2")
811 | instr_string.append("?? " * int(instruction_size - op2.offb))
812 | else:
813 | for b in ibytes[op2.offb:]:
814 | instr_string.append("{0:02x}".format(b))
815 |
816 | continue
817 |
818 | # Check of op2 neds to be wildcarded
819 | if op2.type in self.wildcard_types:
820 | logger.debug("Wildcarding op2")
821 |
822 | if op2.offb == 0:
823 | logger.debug("Wildcarding op2")
824 | instr_string.append("?? " * ins.size)
825 | continue
826 |
827 | # emit all bytes up to the start of op2
828 | # cases where op1 needs to be wildcared are already handled
829 | for b in ibytes[:op2.offb]:
830 | instr_string.append("{0:02x}".format(b))
831 |
832 | op2_size = instruction_size - op2.offb
833 | instr_string.append("?? " * int(op2_size))
834 | except Exception as ex:
835 | logger.error(f"Exception: {ex}")
836 | finally:
837 |
838 | instruction_byte_string = ''.join(instr_string)
839 | instruction_byte_string = instruction_byte_string.replace(" ", "")
840 | instruction_byte_string = ' '.join(instruction_byte_string[i:i + 2] for i in range(0, len(instruction_byte_string), 2))
841 |
842 | search_bytes.append(''.join(instruction_byte_string))
843 |
844 | next_offset = idc.next_head(offset, end)
845 |
846 | logger.debug(f"Next Offset: {hex(next_offset)}")
847 |
848 | if next_offset >= end:
849 | break
850 |
851 | if next_offset in BAD_OFFSETS:
852 | break
853 |
854 | offset = next_offset
855 | logger.debug(f"Next Offset: {hex(offset)}")
856 |
857 | return search_bytes, code_block
858 |
859 | def _is_string_lit(self, address: int) -> bool:
860 | flags = ida_bytes.get_flags(address)
861 | is_strlit = ida_bytes.is_strlit(flags)
862 |
863 | if is_strlit:
864 | return True
865 |
866 | insn = idaapi.insn_t()
867 | idaapi.decode_insn(insn, address)
868 | logger.debug("Checking ops")
869 | for op in [0, 1]:
870 | logger.debug(f"Op Type {insn.ops[op].type}")
871 |
872 | if insn.ops[op].type in [idaapi.o_far, idaapi.o_imm]:
873 | string_addr = insn.ops[op].addr
874 |
875 | if insn.ops[op].type == idaapi.o_imm:
876 | string_addr = insn.ops[0].value
877 | logger.debug(f"String Address: {hex(string_addr)}")
878 |
879 | if not string_addr:
880 | continue
881 |
882 | # Check if it's a string
883 | if idaapi.is_strlit(idaapi.get_flags(string_addr)):
884 | logger.debug(f"Reference to string literal at {hex(string_addr)}")
885 | return True
886 |
887 | return False
888 |
889 | def _get_string_lit(self, address: int) -> Tuple[str, int]:
890 | try:
891 |
892 | flags = ida_bytes.get_flags(address)
893 | is_strlit = ida_bytes.is_strlit(flags)
894 | if is_strlit:
895 | string_type = idc.get_str_type(address)
896 | size = ida_bytes.get_max_strlit_length(address, string_type)
897 | logger.debug(f"String Type: {string_type}, String Size {size}")
898 | string_data = ida_bytes.get_strlit_contents(address, size, string_type)
899 | return string_data.decode('utf-8'), string_type
900 |
901 | insn = idaapi.insn_t()
902 | idaapi.decode_insn(insn, address)
903 | logger.debug("Checking ops")
904 |
905 | string_data = ""
906 | string_type = 0
907 |
908 | for op in [0, 1]:
909 | logger.debug(f"Op Type {insn.ops[op].type}")
910 | if insn.ops[op].type in [idaapi.o_far, idaapi.o_imm]:
911 | string_addr = insn.ops[op].addr
912 |
913 | if insn.ops[op].type == idaapi.o_imm:
914 | string_addr = insn.ops[0].value
915 | logger.debug(f"String Address: {hex(string_addr)}")
916 |
917 | if not string_addr:
918 | continue
919 |
920 | # Check if it's a string
921 | if not idaapi.is_strlit(idaapi.get_flags(string_addr)):
922 | continue
923 |
924 | string_type = idc.get_str_type(string_addr)
925 |
926 | size = ida_bytes.get_max_strlit_length(string_addr, string_type)
927 | string_data = ida_bytes.get_strlit_contents(string_addr, size, string_type)
928 |
929 | return string_data.decode('utf-8'), string_type
930 |
931 |
932 | except Exception as ex:
933 | logger.error(f"Error getting string literal {ex}")
934 | return "", 0
935 |
936 | def activate(self, ctx: Any) -> bool:
937 | super(SearchHandler, self).activate(ctx)
938 |
939 | start = idc.read_selection_start()
940 |
941 | if start in BAD_OFFSETS:
942 | # Range isn't selected get the current cursor position
943 | start_address = ida_kernwin.get_screen_ea()
944 | if start_address in BAD_OFFSETS:
945 | logger.debug("Nothing selected..")
946 | return False
947 |
948 | logger.debug("Checking if address contains string")
949 |
950 | if not self._is_string_lit(start_address):
951 | logger.debug("Not a string literal")
952 | return False
953 |
954 | search_string, string_type = self._get_string_lit(start_address)
955 | if not search_string:
956 | logger.warning("Unable to get string data")
957 | return False
958 |
959 | search_type = "ascii"
960 | if string_type not in [ida_nalt.STRTYPE_C, ida_nalt.STRTYPE_PASCAL]:
961 | search_type = "wide"
962 | result = self.unpacme_search.search(search_string, search_type)
963 | if self.search_goodware:
964 | gw_result = self.unpacme_search.search_goodware(search_string, search_type)
965 | result.update(gw_result)
966 | else:
967 |
968 | end = idc.read_selection_end()
969 |
970 | if start in BAD_OFFSETS or end in BAD_OFFSETS:
971 | logger.debug("Nothing selected")
972 | idc.warning("Nothing Selected!")
973 | return False
974 |
975 | if start > end:
976 | logger.debug("Start is greater than end")
977 | idc.warning("Start is greater than end")
978 | return False
979 |
980 | code_block = ""
981 | search_bytes = []
982 | logger.debug(f'Start: {hex(start)} End: {hex(end)}')
983 |
984 | flags = ida_bytes.get_full_flags(start)
985 | if not ida_bytes.is_code(flags):
986 | size = end - start
987 | logger.debug(f"Selected data size: {size}")
988 | ibytes = idc.get_bytes(start, size, False)
989 | for b in ibytes:
990 | search_bytes.append("{0:02x}".format(b))
991 | else:
992 | search_bytes, code_block = self._process_selected_code_range(start, end)
993 |
994 | search_str = ''.join(search_bytes)
995 | search_str = search_str.replace(" ", "")
996 | search_string = ' '.join(search_str[i:i + 2] for i in range(0, len(search_str), 2))
997 |
998 | if self.preview:
999 | dialog = SearchPreview(search_bytes, code_block)
1000 | preview_result = dialog.exec_()
1001 |
1002 | if preview_result == QDialog.Accepted:
1003 | logger.debug(f"Search Bytes: {search_string}")
1004 | search_string = dialog.get_search_pattern()
1005 |
1006 | if not search_string:
1007 | logger.error("No bytes to search")
1008 | idc.warning("No bytes to search")
1009 | return False
1010 |
1011 | result = self.unpacme_search.search(search_string, "hex")
1012 | if self.search_goodware:
1013 | gw_result = self.unpacme_search.search_goodware(search_string, "hex")
1014 | result.update(gw_result)
1015 | else:
1016 | return False
1017 | else:
1018 | if not search_string:
1019 | logger.error("No bytes to search")
1020 | idc.warning("No bytes to search")
1021 | return False
1022 |
1023 | result = self.unpacme_search.search(search_string, "hex")
1024 | if self.search_goodware:
1025 | gw_result = self.unpacme_search.search(search_string, "hex")
1026 | result.update(gw_result)
1027 |
1028 | if not result:
1029 | idc.warning("No results found for the pattern.")
1030 | return False
1031 |
1032 | self.build_result(result, search_string)
1033 | return True
1034 |
1035 | def term(self):
1036 | pass
1037 |
1038 |
1039 | class StringWindowHandler(BaseSearchHandler):
1040 | def __init__(self, search_goodware: bool):
1041 | super(StringWindowHandler, self).__init__()
1042 | self.search_goodware = search_goodware
1043 | self.unpacme_search = None
1044 | self.result_widget = None
1045 |
1046 | def activate(self, ctx):
1047 | super(StringWindowHandler, self).activate(ctx)
1048 |
1049 | # for selection in ctx.chooser_selection:
1050 | _, string_size, string_type, search_string = ida_kernwin.get_chooser_data(
1051 | ctx.widget_title,
1052 | ctx.chooser_selection[0]
1053 | )
1054 |
1055 | search_string = search_string.replace("\\\\", "\\")
1056 |
1057 | logger.info(f"Selected String: {search_string}")
1058 | logger.debug(f"String Size: {string_size}")
1059 | logger.debug(f"String Type: {string_type}")
1060 |
1061 | search_type = "ascii"
1062 |
1063 | # TODO sort this out
1064 | if string_type not in ['C', 'PASCAL']:
1065 | search_type = "wide"
1066 | result = self.unpacme_search.search(search_string, search_type)
1067 | if self.search_goodware:
1068 | gw_result = self.unpacme_search.search_goodware(search_string, search_type)
1069 | result.update(gw_result)
1070 |
1071 | if not result or 'results' not in result.keys():
1072 | idc.warning("No results found for the pattern.")
1073 | return False
1074 |
1075 | self.build_result(result, search_string)
1076 |
1077 | return True
1078 |
1079 | def update(self, ctx):
1080 | if ida_kernwin.get_widget_type(ctx.widget) == ida_kernwin.BWN_STRINGS:
1081 | return ida_kernwin.AST_ENABLE_FOR_WIDGET
1082 | return ida_kernwin.AST_DISABLE_FOR_WIDGET
1083 |
1084 |
1085 | class UnpacMeByteSearchPlugin(ida_idaapi.plugin_t):
1086 | flags = ida_idaapi.PLUGIN_KEEP
1087 |
1088 | comment = "UnpacMe Search"
1089 | help = "UnpacMe Byte Search"
1090 | wanted_name = "UnpacMe Byte Search"
1091 | wanted_hotkey = ""
1092 |
1093 | _version = "1.1.1"
1094 |
1095 | def _banner(self):
1096 | return f"""
1097 | ##################
1098 | UnpacMe Search\n
1099 | Version: {UnpacMeByteSearchPlugin._version}\n\n
1100 | ##################
1101 | """
1102 |
1103 | def __init__(self):
1104 | self.config = None
1105 | self.search_handler = None
1106 | self.string_window_handler = None
1107 |
1108 | def init(self):
1109 | try:
1110 | ida_kernwin.msg(self._banner())
1111 | logger.debug("Loading config..")
1112 | self.config = self.load_configuration()
1113 | logger.setLevel(logging._checkLevel(self.config['loglevel'].upper()))
1114 |
1115 | self.search_handler = SearchHandler(self.config['preview'], self.config['auto'], self.config['goodware'])
1116 | self.string_window_handler = StringWindowHandler(self.config['goodware'])
1117 |
1118 | if self.config.pop('default', False):
1119 | logger.info("Running default configuration")
1120 | self.edit_config()
1121 |
1122 | logger.debug("== UnpacMe Search Config ==")
1123 | for c in self.config:
1124 | logger.debug(f" -> {c}: {self.config[c]}")
1125 |
1126 | self.actions = [
1127 | ida_kernwin.action_desc_t(
1128 | "unpacme_search",
1129 | "UnpacMe Byte Search",
1130 | self.search_handler,
1131 | "Shift-Alt-s",
1132 | "UnpacMe Byte Search",
1133 | UPMS_MENU_ICON),
1134 | ida_kernwin.action_desc_t(
1135 | "unpacme_string_search",
1136 | "UnpacMe String Search",
1137 | self.string_window_handler,
1138 | "",
1139 | "UnpacMe String Search",
1140 | UPMS_MENU_ICON)
1141 | ]
1142 |
1143 | for action_desc in self.actions:
1144 | ida_kernwin.register_action(action_desc)
1145 |
1146 | ida_kernwin.attach_action_to_menu(
1147 | "Edit/Plugins/",
1148 | "unpacme_search",
1149 | ida_kernwin.SETMENU_APP
1150 | )
1151 |
1152 | self.menus = Menus()
1153 | self.menus.hook()
1154 |
1155 | logger.info("UnpacmeSearchPlugin initialized.")
1156 |
1157 | except Exception as ex:
1158 | logger.error('Error initializing UnpacmeSearchPlugin %s' % ex)
1159 | idc.warning('Error initializing UnpacmeSearchPlugin %s' % ex)
1160 |
1161 | return ida_idaapi.PLUGIN_KEEP
1162 |
1163 | def save_configuration(self, config_name='unpacme_search.cfg'):
1164 | path = ida_diskio.get_user_idadir()
1165 | config_path = os.path.join(path, config_name)
1166 |
1167 | with open(config_path, 'w') as outf:
1168 | outf.write(json.dumps(self.config))
1169 |
1170 | def load_configuration(self, config_name='unpacme_search.cfg'):
1171 | path = ida_diskio.get_user_idadir()
1172 | config_path = os.path.join(path, config_name)
1173 |
1174 | if not os.path.exists(config_path):
1175 | logger.info("No config file!")
1176 |
1177 | return {
1178 | 'default': True,
1179 | 'loglevel': 'INFO',
1180 | 'preview': True,
1181 | 'auto': True,
1182 | 'goodware': True
1183 | }
1184 |
1185 | with open(config_path, 'r') as inf:
1186 | config_data = json.loads(inf.read())
1187 |
1188 | return config_data
1189 |
1190 | def edit_config(self):
1191 | logger.debug("Loading config")
1192 | config = self.load_configuration()
1193 | config.pop('default', None)
1194 |
1195 | logger.debug("Getting API Key")
1196 | config['api_key'] = keyring.get_password("unpacme", "api_key")
1197 | if not config['api_key']:
1198 | logger.warning("No API Key found!")
1199 |
1200 | config_handler = UnpacMeSearchConfigDialog(config)
1201 | config_result = config_handler.exec_()
1202 | logger.debug("Config dialog closed")
1203 |
1204 | if config_result == QDialog.Accepted:
1205 | logger.debug("Getting updated config")
1206 | config = config_handler.get_config()
1207 | key = config.pop('api_key')
1208 | logger.debug(config)
1209 | logger.info("Saving config")
1210 | if key:
1211 | keyring.set_password("unpacme", "api_key", key)
1212 | self.config = config
1213 | self.save_configuration()
1214 |
1215 | # set these so we don't have to reload
1216 | self.search_handler.auto_wildcard = config['auto']
1217 | self.search_handler.preview = config['preview']
1218 | self.search_handler.search_goodware = config['goodware']
1219 | logger.setLevel(logging._checkLevel(config['loglevel'].upper()))
1220 |
1221 | return
1222 |
1223 | def run(self, arg):
1224 | self.edit_config()
1225 | return
1226 |
1227 | def term(self):
1228 | if self.actions:
1229 | for action_desc in self.actions:
1230 | ida_kernwin.unregister_action(action_desc.name)
1231 |
1232 |
1233 | class Menus(ida_kernwin.UI_Hooks):
1234 |
1235 | def finish_populating_widget_popup(self, form, popup):
1236 |
1237 | if idaapi.BWN_DISASM:
1238 | ida_kernwin.attach_action_to_popup(form, popup, "unpacme_search", "UnpacMe Byte Search")
1239 |
1240 | if idaapi.BWN_STRINGS:
1241 | ida_kernwin.attach_action_to_popup(form, popup, "unpacme_string_search", "UnpacMe String Search")
1242 |
1243 |
1244 |
1245 |
1246 | def PLUGIN_ENTRY():
1247 | return UnpacMeByteSearchPlugin()
--------------------------------------------------------------------------------