├── .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 | [![UnpacMe](https://img.shields.io/badge/Threat_Hunting-UnpacMe-AA00B4)](https://www.unpac.me/) [![Chat](https://img.shields.io/badge/Support-Discord-5462EB)](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 | Example Results 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 | Example Results 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 | String Search 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 | Example Results 59 |

60 | 61 | ## Configuration 62 | 63 | The plugin has the following configuration options that can be set via the plugin menu. 64 | 65 |

66 | Example Results 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() --------------------------------------------------------------------------------