├── .flake8 ├── .gitignore ├── README.md ├── config.py ├── example ├── decompiling_process.png └── main_window.png ├── main.py ├── pyinstxtractor.py ├── requirements.txt ├── src ├── close.png ├── header.png ├── hide.png ├── icon.ico └── preloader.gif ├── toggle.py ├── ui.py └── utils.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 90 3 | ignore = E501 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | 3 | # Linux template 4 | *~ 5 | # KDE directory preferences 6 | .directory 7 | # MacOS 8 | .DS_Store 9 | .AppleDouble 10 | .LSOverride 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EXE2PY-Decompiler 2 | 3 | With this program you can decompile executable files created using **pyinstaller** or **py2exe**. 4 | 5 | It is also possible to decompile individual cache files back into the original python source code. 6 | 7 |
8 | 9 | *Screenshot of the main application window* 10 | 11 | ![](https://github.com/topdefaultuser/EXE2PY-Decompiler/blob/master/example/main_window.png) 12 | 13 | 14 | **What's new:** 15 | 16 | === Jul 27, 2023 === 17 | 18 | - Full refactoring was performed 19 | 20 | - Updated README.md 21 | 22 | - Updated requirements.txt 23 | 24 | - Add logical disabling of the checkbox 'Decompile all additional libraries' if not .exe file is selected 25 | 26 | === Jun 11, 2021 === 27 | 28 | - Completely redesigned graphical interface 29 | 30 | - Added support for ***DRAG & DROP*** for ```LineEdit``` 31 | 32 | - Added the ability to specify a folder to decompile its contents 33 | 34 | - Code significantly rewritten 35 | 36 | 37 | **Fixes:** 38 | 39 | - Fixed the problem of decompiling version 3.6 code with older versions of python 40 | 41 | - Added the ability to interrupt the decompilation process 42 | 43 | - Added a banner that appears during the decompilation process 44 | 45 | 46 | **Note:** 47 | 48 | - If you cannot decompile one of the libraries from the **EXE2PY_Pycache** folder, check the "decompile all additional libraries" checkbox and try again. 49 | 50 | - The program can work on python versions 3.7 and older 51 | 52 | - With its help, it is possible to decompile programs - 3.4, 3.6, 3.7. 3.8 python versions. 53 | At the same time, the difference in versions does not matter. (With python version 3.8, you can decompile a program written in python 3.4, 3.6, etc). 54 | 55 | 56 |
57 | 58 | *Screenshot of the running application* 59 | 60 | 61 | ![](https://github.com/topdefaultuser/EXE2PY-Decompiler/blob/master/example/decompiling_process.png) 62 | 63 | ## Configuration: 64 | 65 | **Create virtual environment** 66 | 67 | `python -m venv "env"` 68 | 69 |
70 | 71 | **Activate virtual environment** 72 | 73 | Linux: `source ./env/bin/activate` 74 | 75 | Windows: `./env/Scripts/Activate.ps1` 76 | 77 |
78 | 79 | **Upgrade pip**: 80 | 81 | `python -m pip install --upgrade pip` 82 | 83 |
84 | 85 | **Install requirements:** 86 | 87 | `python -m pip install -r requirements.txt` 88 | 89 | ## Development: 90 | 91 | **Validate-flake8:** 92 | 93 | `flake8 filename.py` 94 | 95 |
96 | 97 | **Validate-pyright:** 98 | 99 | `pyright filename.py` 100 | 101 | ## Usage: 102 | 103 | `python main.py` 104 | 105 |
106 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from logging import INFO 2 | 3 | LOG_FILE = 'app.log' 4 | LOG_LEVEL = INFO 5 | 6 | EXTRACTED_DIR = 'EXE2PY_Extracted' 7 | EXTRACTING_LOG = 'output.log' 8 | MAIN_DIR = 'EXE2PY_Main' 9 | MODULES_DIR = 'EXE2PY_Modules' 10 | PYCACHE_DIR = 'EXE2PY_Pycache' 11 | -------------------------------------------------------------------------------- /example/decompiling_process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LookiMan/EXE2PY-Decompiler/c67d2777a0303e3f9564c85a3ad4e49af3f4a696/example/decompiling_process.png -------------------------------------------------------------------------------- /example/main_window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LookiMan/EXE2PY-Decompiler/c67d2777a0303e3f9564c85a3ad4e49af3f4a696/example/main_window.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from argparse import ArgumentParser 4 | from io import BytesIO 5 | from os import listdir 6 | from os import rename 7 | from os import path 8 | from platform import system 9 | from typing import Literal 10 | 11 | from PyQt5.QtWidgets import QApplication 12 | from PyQt5.QtWidgets import QMessageBox 13 | 14 | from config import EXTRACTED_DIR 15 | from config import EXTRACTING_LOG 16 | from config import LOG_FILE 17 | from config import LOG_LEVEL 18 | from config import MAIN_DIR 19 | from config import MODULES_DIR 20 | from config import PYCACHE_DIR 21 | 22 | from ui import MainWindow 23 | 24 | from utils import make_folders 25 | from utils import open_output_folder 26 | from utils import search_pyc_files 27 | from utils import START_BYTE 28 | from utils import PYTHON_HEADERS 29 | from utils import Platforms 30 | from utils import StatusCodes 31 | 32 | import logging 33 | 34 | logging.basicConfig(filename=LOG_FILE, level=LOG_LEVEL) 35 | log = logging.getLogger(__name__) 36 | 37 | import_errors = list() 38 | try: 39 | from uncompyle6.bin import uncompile 40 | except ImportError as e: 41 | import_errors.append(str(e)) 42 | 43 | try: 44 | import unpy2exe 45 | except ImportError as e: 46 | import_errors.append(str(e)) 47 | 48 | try: 49 | import pyinstxtractor 50 | except ImportError as e: 51 | import_errors.append(str(e)) 52 | 53 | 54 | class StreamBuffer: 55 | """Class mimicking sys.stdout to capture data""" 56 | 57 | def __init__(self) -> None: 58 | self.stream = None 59 | self.lines = list() 60 | 61 | def intercept_stream(self, stream) -> None: 62 | self.stream = stream 63 | sys.stdout = self 64 | 65 | def release_stream(self) -> None: 66 | sys.stdout = self.stream 67 | self.stream = None 68 | 69 | def write(self, line: str) -> None: 70 | self.lines.append(line) 71 | 72 | def flush(self) -> None: 73 | """ Stub """ 74 | pass 75 | 76 | def dump_logs(self, filename: str) -> None: 77 | with open(filename, mode='a') as file: 78 | file.write(''.join(self.lines)) 79 | self.lines = list() 80 | 81 | def is_has(self, text: str) -> bool: 82 | return any([line for line in self.lines if text in line]) 83 | 84 | 85 | class HeaderCorrectorMainFiles: 86 | """Class for picking a header for a .pyc file""" 87 | filename: str 88 | filedata: bytes 89 | 90 | def set_file(self, filename: str) -> None: 91 | self.filename = filename 92 | 93 | with open(self.filename, mode='rb') as file: 94 | self.filedata = file.read() 95 | 96 | def is_need_correct(self) -> bool: 97 | with open(self.filename, mode='rb') as file: 98 | return file.read(1) == START_BYTE 99 | 100 | def correct_file(self, header: bytes) -> None: 101 | with open(self.filename, mode='wb') as file: 102 | file.write(header) 103 | file.write(self.filedata) 104 | 105 | 106 | class HeaderCorrectorSubLibraries(HeaderCorrectorMainFiles): 107 | """Class for cache file header correction for python versions 3.7, 3.8""" 108 | header: bytes = b'\x00\x00\x00\x00' 109 | filedata: BytesIO 110 | 111 | def set_file(self, filename: str) -> None: 112 | self.filename = filename 113 | 114 | with open(self.filename, mode='rb') as file: 115 | self.filedata = BytesIO(file.read()) 116 | 117 | def is_need_correct(self) -> bool: 118 | with open(self.filename, mode='rb') as file: 119 | file.seek(12) 120 | return file.read(1) == START_BYTE 121 | 122 | def correct_file(self) -> None: 123 | with open(self.filename, mode='wb') as file: 124 | file.write(self.filedata.read(12)) 125 | file.write(self.header) 126 | file.write(self.filedata.read()) 127 | 128 | 129 | def decompile_pyc_file( 130 | filename: str, 131 | *, 132 | output_directory: str 133 | ) -> Literal[ 134 | StatusCodes.PYC_DECOMPILED_SUCCESSFULLY, 135 | StatusCodes.PYC_DECOMPILATION_ERROR, 136 | ]: 137 | is_decompiled_successfully = False 138 | corrector = HeaderCorrectorMainFiles() 139 | corrector.set_file(filename) 140 | 141 | sys.argv = ['uncompile', '-o', output_directory, filename] 142 | 143 | stream_buffer = StreamBuffer() 144 | stream_buffer.intercept_stream(sys.stdout) 145 | 146 | if corrector.is_need_correct(): 147 | for header in PYTHON_HEADERS: 148 | corrector.correct_file(header) 149 | try: 150 | uncompile.main_bin() 151 | except Exception: 152 | continue 153 | else: 154 | if stream_buffer.is_has('# Successfully decompiled file'): 155 | break 156 | else: 157 | uncompile.main_bin() 158 | 159 | if stream_buffer.is_has('# Successfully decompiled file'): 160 | is_decompiled_successfully = True 161 | 162 | stream_buffer.release_stream() 163 | stream_buffer.dump_logs(path.join(output_directory, EXTRACTING_LOG)) 164 | 165 | return StatusCodes.PYC_DECOMPILED_SUCCESSFULLY if is_decompiled_successfully else StatusCodes.PYC_DECOMPILATION_ERROR 166 | 167 | 168 | def decompile_pyc_files( 169 | filenames: list, 170 | *, 171 | output_directory: str 172 | ) -> Literal[ 173 | StatusCodes.PYC_DECOMPILED_SUCCESSFULLY, 174 | StatusCodes.PYC_PARTIALLY_DECOMPILATION, 175 | ]: 176 | is_decompiled_successfully = True 177 | 178 | for filename in filenames: 179 | status = decompile_pyc_file( 180 | filename, 181 | output_directory=output_directory, 182 | ) 183 | 184 | if is_decompiled_successfully and status != StatusCodes.PYC_DECOMPILED_SUCCESSFULLY: 185 | is_decompiled_successfully = False 186 | 187 | return StatusCodes.PYC_DECOMPILED_SUCCESSFULLY if is_decompiled_successfully else StatusCodes.PYC_PARTIALLY_DECOMPILATION 188 | 189 | 190 | def decompile_sub_library( 191 | filename: str, 192 | *, 193 | output_directory: str 194 | ) -> Literal[ 195 | StatusCodes.LIB_DECOMPILED_SUCCESSFULLY, 196 | StatusCodes.LIB_DECOMPILATION_ERROR, 197 | ]: 198 | is_decompiled_successfully = False 199 | 200 | corrector = HeaderCorrectorSubLibraries() 201 | corrector.set_file(filename) 202 | 203 | sys.argv = ['uncompile', '-o', output_directory, filename] 204 | 205 | stream_buffer = StreamBuffer() 206 | stream_buffer.intercept_stream(sys.stdout) 207 | 208 | for attempt in range(1, 3): 209 | if attempt == 2: 210 | # For python version 3.6, the file header is 4 bytes shorter, 211 | # so we first try to decompile the bytecode without changes, 212 | # if an error occurs, 4 bytes are added to the file header 213 | # to allow normal decompilation of cache files for versions 3.7 3.8 214 | corrector.correct_file() 215 | try: 216 | uncompile.main_bin() 217 | is_decompiled_successfully = True 218 | except Exception: 219 | continue 220 | 221 | stream_buffer.release_stream() 222 | stream_buffer.dump_logs(path.join(output_directory, EXTRACTING_LOG)) 223 | 224 | return StatusCodes.LIB_DECOMPILED_SUCCESSFULLY if is_decompiled_successfully else StatusCodes.LIB_DECOMPILATION_ERROR 225 | 226 | 227 | def decompile_sub_libraries( 228 | filenames: list, 229 | *, 230 | output_directory: str 231 | ) -> Literal[ 232 | StatusCodes.LIB_DECOMPILED_SUCCESSFULLY, 233 | StatusCodes.LIB_PARTIALLY_DECOMPILATION, 234 | ]: 235 | is_decompiled_successfully = True 236 | 237 | for filename in filenames: 238 | status = decompile_sub_library( 239 | filename, 240 | output_directory=output_directory 241 | ) 242 | 243 | if is_decompiled_successfully and status != StatusCodes.PYC_DECOMPILED_SUCCESSFULLY: 244 | is_decompiled_successfully = False 245 | 246 | return StatusCodes.LIB_DECOMPILED_SUCCESSFULLY if is_decompiled_successfully else StatusCodes.LIB_PARTIALLY_DECOMPILATION 247 | 248 | 249 | def decompile_with_pyinstxtractor( 250 | filename: str, 251 | *, 252 | is_need_decompile_sub_libraries: bool 253 | ) -> Literal[ 254 | StatusCodes.PYC_DECOMPILATION_ERROR, 255 | StatusCodes.LIB_DECOMPILATION_ERROR, 256 | StatusCodes.EXE_DECOMPILED_SUCCESSFULLY, 257 | StatusCodes.EXE_PARTIALLY_DECOMPILATION, 258 | ]: 259 | current_directory = path.dirname(filename) 260 | extract_directory = path.join(current_directory, EXTRACTED_DIR) 261 | modules_directory = path.join(current_directory, MODULES_DIR) 262 | scripts_directory = path.join(current_directory, MAIN_DIR) 263 | cache_directory = path.join(current_directory, PYCACHE_DIR) 264 | 265 | make_folders([ 266 | extract_directory, 267 | modules_directory, 268 | cache_directory, 269 | scripts_directory, 270 | ]) 271 | 272 | # Passing to pyinstxtractor the path to save the module cache 273 | pyinstxtractor.CACHE_DIRECTORY = cache_directory 274 | # Passing to pyinstxtractor the path to save the contents of the .exe file 275 | pyinstxtractor.EXTRACTION_DIR = extract_directory 276 | 277 | if (len(sys.argv)) > 1: 278 | sys.argv[1] = filename 279 | else: 280 | sys.argv.append(filename) 281 | 282 | stream_buffer = StreamBuffer() 283 | stream_buffer.intercept_stream(sys.stdout) 284 | 285 | pyinstxtractor.main() 286 | # Getting a list of possible main files 287 | files = pyinstxtractor.MAIN_FILES 288 | 289 | # These libraries are always on the list of core libraries, 290 | # but are not related to them 291 | if 'pyi_rth_multiprocessing' in files: 292 | files.remove('pyi_rth_multiprocessing') 293 | if 'pyiboot01_bootstrap' in files: 294 | files.remove('pyiboot01_bootstrap') 295 | if 'pyi_rth_qt4plugins' in files: 296 | files.remove('pyi_rth_qt4plugins') 297 | if 'pyi_rth__tkinter' in files: 298 | files.remove('pyi_rth__tkinter') 299 | if 'pyi_rth_pyqt5' in files: 300 | files.remove('pyi_rth_pyqt5') 301 | 302 | files = [path.join(extract_directory, filename) for filename in files] 303 | 304 | for filename in files: 305 | rename(filename, filename + '.pyc') 306 | 307 | files = [filename + '.pyc' for filename in files] 308 | 309 | stream_buffer.release_stream() 310 | stream_buffer.dump_logs(path.join(scripts_directory, EXTRACTING_LOG)) 311 | 312 | pyc_status = None 313 | lib_status = None 314 | 315 | try: 316 | pyc_status = decompile_pyc_files( 317 | files, 318 | output_directory=scripts_directory 319 | ) 320 | except Exception as e: 321 | log.exception(e) 322 | return StatusCodes.PYC_DECOMPILATION_ERROR 323 | 324 | if is_need_decompile_sub_libraries: 325 | cache_files = list( 326 | map( 327 | lambda file: path.join(cache_directory, file), 328 | listdir(cache_directory) 329 | ) 330 | ) 331 | 332 | try: 333 | lib_status = decompile_sub_libraries( 334 | cache_files, 335 | output_directory=modules_directory 336 | ) 337 | except Exception as e: 338 | log.exception(e) 339 | return StatusCodes.LIB_DECOMPILATION_ERROR 340 | 341 | if pyc_status == StatusCodes.PYC_DECOMPILED_SUCCESSFULLY and lib_status == StatusCodes.LIB_DECOMPILED_SUCCESSFULLY: 342 | status = StatusCodes.EXE_DECOMPILED_SUCCESSFULLY 343 | else: 344 | status = StatusCodes.EXE_PARTIALLY_DECOMPILATION 345 | 346 | return status 347 | 348 | 349 | def decompile_with_unpy2exe(filename: str) -> Literal[ 350 | StatusCodes.EXE_DECOMPILED_SUCCESSFULLY, 351 | StatusCodes.EXE_PARTIALLY_DECOMPILATION, 352 | ]: 353 | current_directory = path.dirname(filename) 354 | extract_directory = path.join(current_directory, EXTRACTED_DIR) 355 | scripts_directory = path.join(current_directory, MAIN_DIR) 356 | 357 | if system() == Platforms.WINDOWS.value: 358 | # The name contains forbidden symbols symbols for windows '<', '>' 359 | unpy2exe.IGNORE.append('.pyc') 360 | 361 | # Set a stub, because with the function headers 362 | # unpy2exe._generate_pyc_header bytecode is not compiled into source code 363 | unpy2exe._generate_pyc_header = lambda python_version, size: b'' 364 | unpy2exe.unpy2exe(filename, f'{sys.version_info[:2]}.{extract_directory}') 365 | 366 | status = decompile_pyc_files( 367 | search_pyc_files(extract_directory), 368 | output_directory=scripts_directory, 369 | ) 370 | 371 | if status == StatusCodes.PYC_DECOMPILED_SUCCESSFULLY: 372 | status_code = StatusCodes.EXE_DECOMPILED_SUCCESSFULLY 373 | else: 374 | status_code = StatusCodes.EXE_PARTIALLY_DECOMPILATION 375 | 376 | return status_code 377 | 378 | 379 | def decompile_executable( 380 | filename: str, 381 | *, 382 | is_need_decompile_sub_libraries: bool 383 | ) -> Literal[ 384 | StatusCodes.EXE_DECOMPILED_SUCCESSFULLY, 385 | StatusCodes.EXE_PARTIALLY_DECOMPILATION, 386 | StatusCodes.PYC_DECOMPILATION_ERROR, 387 | StatusCodes.LIB_DECOMPILATION_ERROR, 388 | StatusCodes.EXE_DECOMPILATION_ERROR, 389 | ]: 390 | try: 391 | return decompile_with_pyinstxtractor( 392 | filename, 393 | is_need_decompile_sub_libraries=is_need_decompile_sub_libraries, 394 | ) 395 | except (ValueError, TypeError, ImportError, FileNotFoundError, PermissionError) as e: 396 | log.exception(e) 397 | return StatusCodes.EXE_DECOMPILATION_ERROR 398 | except Exception: 399 | # Raise this exception if the program is not compiled with pyinstaller 400 | return decompile_with_unpy2exe(filename) 401 | 402 | 403 | def start_decompile( 404 | target: str, 405 | *, 406 | is_need_decompile_sub_libraries: bool, 407 | is_need_open_output_folder: bool, 408 | ) -> int: 409 | 410 | status = StatusCodes.UNEXPECTED_CONFIGURATION 411 | 412 | if not path.exists(target): 413 | status = StatusCodes.TARGET_NOT_EXISTS 414 | 415 | elif target.endswith('.pyc'): 416 | output_directory = path.dirname(target) 417 | 418 | status = decompile_pyc_file( 419 | target, 420 | output_directory=output_directory, 421 | ) 422 | 423 | if is_need_open_output_folder: 424 | open_output_folder(output_directory) 425 | 426 | elif target.endswith('.exe'): 427 | status = decompile_executable( 428 | target, 429 | is_need_decompile_sub_libraries=is_need_decompile_sub_libraries 430 | ) 431 | 432 | if is_need_open_output_folder: 433 | open_output_folder( 434 | path.join(path.dirname(target), MAIN_DIR) 435 | ) 436 | 437 | elif path.isdir(target): 438 | filenames = search_pyc_files(target) 439 | output_directory = target 440 | 441 | if filenames: 442 | status = decompile_pyc_files( 443 | filenames, 444 | output_directory=output_directory, 445 | ) 446 | 447 | if is_need_open_output_folder: 448 | open_output_folder(output_directory) 449 | else: 450 | status = StatusCodes.FILES_NOT_FOUND 451 | 452 | return status.value 453 | 454 | 455 | def main() -> None: 456 | app = QApplication(sys.argv) 457 | widget = MainWindow(start_decompile) 458 | 459 | if import_errors: 460 | QMessageBox.critical( 461 | widget.form, 462 | 'Critical error', 463 | '\n'.join(import_errors), 464 | QMessageBox.Ok, 465 | ) 466 | 467 | parser = ArgumentParser() 468 | parser.add_argument('-t', '--target', help='File or directory to decompile') 469 | args = parser.parse_args() 470 | 471 | if args.target: 472 | widget.lineEdit.setText(args.target) 473 | 474 | sys.exit(app.exec_()) 475 | 476 | 477 | if __name__ == '__main__': 478 | main() 479 | -------------------------------------------------------------------------------- /pyinstxtractor.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyInstaller Extractor v1.9 (Supports pyinstaller 3.3, 3.2, 3.1, 3.0, 2.1, 2.0) 3 | Author : Extreme Coders 4 | E-mail : extremecoders(at)hotmail(dot)com 5 | Web : https://0xec.blogspot.com 6 | Date : 29-November-2017 7 | Url : https://sourceforge.net/projects/pyinstallerextractor/ 8 | 9 | For any suggestions, leave a comment on 10 | https://forum.tuts4you.com/topic/34455-pyinstaller-extractor/ 11 | 12 | This script extracts a pyinstaller generated executable file. 13 | Pyinstaller installation is not needed. The script has it all. 14 | 15 | For best results, it is recommended to run this script in the 16 | same version of python as was used to create the executable. 17 | This is just to prevent unmarshalling errors(if any) while 18 | extracting the PYZ archive. 19 | 20 | Usage : Just copy this script to the directory where your exe resides 21 | and run the script with the exe file name as a parameter 22 | 23 | C:\path\to\exe\>python pyinstxtractor.py 24 | $ /path/to/exe/python pyinstxtractor.py 25 | 26 | Licensed under GNU General Public License (GPL) v3. 27 | You are free to modify this source. 28 | 29 | CHANGELOG 30 | ================================================ 31 | 32 | Version 1.1 (Jan 28, 2014) 33 | ------------------------------------------------- 34 | - First Release 35 | - Supports only pyinstaller 2.0 36 | 37 | Version 1.2 (Sept 12, 2015) 38 | ------------------------------------------------- 39 | - Added support for pyinstaller 2.1 and 3.0 dev 40 | - Cleaned up code 41 | - Script is now more verbose 42 | - Executable extracted within a dedicated sub-directory 43 | 44 | (Support for pyinstaller 3.0 dev is experimental) 45 | 46 | Version 1.3 (Dec 12, 2015) 47 | ------------------------------------------------- 48 | - Added support for pyinstaller 3.0 final 49 | - Script is compatible with both python 2.x & 3.x (Thanks to Moritz Kroll @ Avira Operations GmbH & Co. KG) 50 | 51 | Version 1.4 (Jan 19, 2016) 52 | ------------------------------------------------- 53 | - Fixed a bug when writing pyc files >= version 3.3 (Thanks to Daniello Alto: https://github.com/Djamana) 54 | 55 | Version 1.5 (March 1, 2016) 56 | ------------------------------------------------- 57 | - Added support for pyinstaller 3.1 (Thanks to Berwyn Hoyt for reporting) 58 | 59 | Version 1.6 (Sept 5, 2016) 60 | ------------------------------------------------- 61 | - Added support for pyinstaller 3.2 62 | - Extractor will use a random name while extracting unnamed files. 63 | - For encrypted pyz archives it will dump the contents as is. Previously, the tool would fail. 64 | 65 | Version 1.7 (March 13, 2017) 66 | ------------------------------------------------- 67 | - Made the script compatible with python 2.6 (Thanks to Ross for reporting) 68 | 69 | Version 1.8 (April 28, 2017) 70 | ------------------------------------------------- 71 | - Support for sub-directories in .pyz files (Thanks to Moritz Kroll @ Avira Operations GmbH & Co. KG) 72 | 73 | Version 1.9 (November 29, 2017) 74 | ------------------------------------------------- 75 | - Added support for pyinstaller 3.3 76 | - Display the scripts which are run at entry (Thanks to Michael Gillespie @ malwarehunterteam for the feature request) 77 | 78 | """ 79 | 80 | from __future__ import print_function 81 | import os 82 | import sys 83 | import struct 84 | import marshal 85 | import zlib 86 | import sys 87 | import imp 88 | import types 89 | import inspect 90 | import py_compile 91 | import time 92 | from uuid import uuid4 as uniquename 93 | 94 | 95 | ImportErrors = [] 96 | 97 | MAIN_FILES = [] 98 | EXTRACTION_DIR = None 99 | CACHE_DIRECTORY = None 100 | 101 | 102 | try: 103 | from colorama import Fore, init 104 | except ImportError: 105 | ImportErrors.append("Не найден модуль 'colorama'! \nУстановите модуль коммандой pip install colorama ") 106 | except Exception as exc: 107 | ImportErrors.append(exc) 108 | 109 | if len(ImportErrors) > 0: 110 | for error in ImportErrors: 111 | print(error) 112 | 113 | exit(1) 114 | 115 | init(autoreset=True) 116 | 117 | 118 | class CTOCEntry: 119 | def __init__(self, position, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name): 120 | self.position = position 121 | self.cmprsdDataSize = cmprsdDataSize 122 | self.uncmprsdDataSize = uncmprsdDataSize 123 | self.cmprsFlag = cmprsFlag 124 | self.typeCmprsData = typeCmprsData 125 | self.name = name 126 | 127 | 128 | class PyInstArchive: 129 | PYINST20_COOKIE_SIZE = 24 # For pyinstaller 2.0 130 | PYINST21_COOKIE_SIZE = 24 + 64 # For pyinstaller 2.1+ 131 | MAGIC = b'MEI\014\013\012\013\016' # Magic number which identifies pyinstaller 132 | 133 | def __init__(self, path): 134 | self.filePath = path 135 | 136 | 137 | def open(self): 138 | try: 139 | self.fPtr = open(self.filePath, 'rb') 140 | self.fileSize = os.stat(self.filePath).st_size 141 | except: 142 | print(Fore.RED + '[>] Error: Could not open {0}'.format(self.filePath)) 143 | return False 144 | return True 145 | 146 | 147 | def close(self): 148 | try: 149 | self.fPtr.close() 150 | except: 151 | pass 152 | 153 | 154 | def checkFile(self): 155 | print(Fore.CYAN + '[>] Processing {0}'.format(self.filePath)) 156 | # Check if it is a 2.0 archive 157 | self.fPtr.seek(self.fileSize - self.PYINST20_COOKIE_SIZE, os.SEEK_SET) 158 | magicFromFile = self.fPtr.read(len(self.MAGIC)) 159 | 160 | if magicFromFile == self.MAGIC: 161 | self.pyinstVer = 20 # pyinstaller 2.0 162 | print(Fore.CYAN + '[>] Pyinstaller version: 2.0') 163 | return True 164 | 165 | # Check for pyinstaller 2.1+ before bailing out 166 | self.fPtr.seek(self.fileSize - self.PYINST21_COOKIE_SIZE, os.SEEK_SET) 167 | magicFromFile = self.fPtr.read(len(self.MAGIC)) 168 | 169 | if magicFromFile == self.MAGIC: 170 | print(Fore.CYAN + '[>] Pyinstaller version: 2.1+') 171 | self.pyinstVer = 21 # pyinstaller 2.1+ 172 | return True 173 | 174 | print(Fore.RED + '[>] Error : Unsupported pyinstaller version or not a pyinstaller archive') 175 | return False 176 | 177 | 178 | def getCArchiveInfo(self): 179 | try: 180 | if self.pyinstVer == 20: 181 | self.fPtr.seek(self.fileSize - self.PYINST20_COOKIE_SIZE, os.SEEK_SET) 182 | 183 | # Read CArchive cookie 184 | (magic, lengthofPackage, toc, tocLen, self.pyver) = \ 185 | struct.unpack('!8siiii', self.fPtr.read(self.PYINST20_COOKIE_SIZE)) 186 | 187 | elif self.pyinstVer == 21: 188 | self.fPtr.seek(self.fileSize - self.PYINST21_COOKIE_SIZE, os.SEEK_SET) 189 | 190 | # Read CArchive cookie 191 | (magic, lengthofPackage, toc, tocLen, self.pyver, pylibname) = \ 192 | struct.unpack('!8siiii64s', self.fPtr.read(self.PYINST21_COOKIE_SIZE)) 193 | 194 | except: 195 | print(Fore.RED + '[>] Error : The file is not a pyinstaller archive') 196 | return False 197 | 198 | print(Fore.CYAN + '[>] Python version: {0}'.format(self.pyver)) 199 | 200 | # Overlay is the data appended at the end of the PE 201 | self.overlaySize = lengthofPackage 202 | self.overlayPos = self.fileSize - self.overlaySize 203 | self.tableOfContentsPos = self.overlayPos + toc 204 | self.tableOfContentsSize = tocLen 205 | 206 | print(Fore.CYAN + '[>] Length of package: {0} bytes'.format(self.overlaySize)) 207 | return True 208 | 209 | def get_pyver(self): 210 | return self.pyver 211 | 212 | def parseTOC(self): 213 | # Go to the table of contents 214 | self.fPtr.seek(self.tableOfContentsPos, os.SEEK_SET) 215 | 216 | self.tocList = [] 217 | parsedLen = 0 218 | 219 | # Parse table of contents 220 | while parsedLen < self.tableOfContentsSize: 221 | (entrySize, ) = struct.unpack('!i', self.fPtr.read(4)) 222 | nameLen = struct.calcsize('!iiiiBc') 223 | 224 | (entryPos, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name) = \ 225 | struct.unpack( \ 226 | '!iiiBc{0}s'.format(entrySize - nameLen), \ 227 | self.fPtr.read(entrySize - 4)) 228 | 229 | name = name.decode('utf-8').rstrip('\0') 230 | if len(name) == 0: 231 | name = str(uniquename()) 232 | print(Fore.YELLOW + '[!] Warning: Found an unamed file in CArchive. Using random name {0}'.format(name)) 233 | 234 | self.tocList.append( \ 235 | CTOCEntry( \ 236 | self.overlayPos + entryPos, \ 237 | cmprsdDataSize, \ 238 | uncmprsdDataSize, \ 239 | cmprsFlag, \ 240 | typeCmprsData, \ 241 | name \ 242 | )) 243 | 244 | parsedLen += entrySize 245 | print(Fore.CYAN + '[>] Found {0} files in CArchive'.format(len(self.tocList))) 246 | 247 | 248 | def extractFiles(self): 249 | print(Fore.YELLOW + '[>] Beginning extraction...please standby') 250 | 251 | if not os.path.exists(EXTRACTION_DIR): 252 | os.mkdir(EXTRACTION_DIR) 253 | 254 | os.chdir(EXTRACTION_DIR) 255 | 256 | for entry in self.tocList: 257 | basePath = os.path.dirname(entry.name) 258 | if basePath != '': 259 | # Check if path exists, create if not 260 | if not os.path.exists(basePath): 261 | os.makedirs(basePath) 262 | 263 | self.fPtr.seek(entry.position, os.SEEK_SET) 264 | data = self.fPtr.read(entry.cmprsdDataSize) 265 | 266 | if entry.cmprsFlag == 1: 267 | data = zlib.decompress(data) 268 | # Malware may tamper with the uncompressed size 269 | # Comment out the assertion in such a case 270 | assert len(data) == entry.uncmprsdDataSize # Sanity Check 271 | 272 | file = os.path.join(EXTRACTION_DIR, entry.name) 273 | 274 | with open(file, 'wb') as f: 275 | f.write(data) 276 | 277 | if entry.typeCmprsData == b's': 278 | print(Fore.GREEN + '[+] Possible entry point: {0}'.format(entry.name)) 279 | MAIN_FILES.append(entry.name) 280 | 281 | elif entry.typeCmprsData == b'z' or entry.typeCmprsData == b'Z': 282 | self._extractPyz(entry.name) 283 | 284 | 285 | def _extractPyz(self, name): 286 | with open(name, 'rb') as f: 287 | pyzMagic = f.read(4) 288 | assert pyzMagic == b'PYZ\0' # Sanity Check 289 | 290 | pycHeader = f.read(4) # Python magic value 291 | if imp.get_magic() != pycHeader: 292 | print(Fore.RED + '[!] Warning: The script is running in a different python version than the one used to build the executable') 293 | print(Fore.RED + ' Run this script in Python{0} to prevent extraction errors(if any) during unmarshalling'.format(self.pyver)) 294 | 295 | (tocPosition, ) = struct.unpack('!i', f.read(4)) 296 | f.seek(tocPosition, os.SEEK_SET) 297 | 298 | try: 299 | toc = marshal.load(f) 300 | except: 301 | print(Fore.RED + '[!] Unmarshalling FAILED. Cannot extract {0}. Extracting remaining files.'.format(name)) 302 | return 303 | 304 | print(Fore.CYAN + '[>] Found {0} files in PYZ archive'.format(len(toc))) 305 | 306 | # From pyinstaller 3.1+ toc is a list of tuples 307 | if type(toc) == list: 308 | toc = dict(toc) 309 | 310 | for key in toc.keys(): 311 | (ispkg, pos, length) = toc[key] 312 | f.seek(pos, os.SEEK_SET) 313 | 314 | fileName = key 315 | try: 316 | # for Python > 3.3 some keys are bytes object some are str object 317 | fileName = key.decode('utf-8') 318 | except: 319 | pass 320 | 321 | # Make sure destination directory exists, ensuring we keep inside dirName 322 | destName = os.path.join(CACHE_DIRECTORY, fileName) 323 | destDirName = os.path.dirname(destName) 324 | if not os.path.exists(destDirName): 325 | os.makedirs(destDirName) 326 | 327 | try: 328 | data = f.read(length) 329 | data = zlib.decompress(data) 330 | except: 331 | print(Fore.RED + '[!] Error: Failed to decompress {0}, probably encrypted. Extracting as is.'.format(fileName)) 332 | open(destName + '.pyc.encrypted', 'wb').write(data) 333 | continue 334 | 335 | with open(destName + '.pyc', 'wb') as pycFile: 336 | pycFile.write(pycHeader) # Write pyc magic 337 | pycFile.write(b'\0' * 4) # Write timestamp 338 | if self.pyver >= 33: 339 | pycFile.write(b'\0' * 4) # Size parameter added in Python 3.3 340 | pycFile.write(data) 341 | 342 | 343 | def get_pyver(): 344 | return pyver 345 | 346 | 347 | def main(): 348 | global pyver 349 | if len(sys.argv) < 2: 350 | print(Fore.YELLOW + '[>] Usage: pyinstxtractor.py ') 351 | 352 | else: 353 | arch = PyInstArchive(sys.argv[1]) 354 | if arch.open(): 355 | if arch.checkFile(): 356 | if arch.getCArchiveInfo(): 357 | pyver = arch.get_pyver() 358 | arch.parseTOC() 359 | arch.extractFiles() 360 | arch.close() 361 | print(Fore.GREEN + '[>] Successfully extracted pyinstaller archive: {0}'.format(sys.argv[1])) 362 | print(Fore.GREEN + '[>] You can now use a python decompiler on the pyc files within the extracted directory') 363 | else: 364 | raise Exception 365 | 366 | if __name__ == '__main__': 367 | main() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5==5.15.9 2 | pyautogui==0.9.54 3 | uncompyle6==3.9.0 4 | unpy2exe==0.4 5 | pyright==1.1.318 6 | flake8==6.0.0 7 | -------------------------------------------------------------------------------- /src/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LookiMan/EXE2PY-Decompiler/c67d2777a0303e3f9564c85a3ad4e49af3f4a696/src/close.png -------------------------------------------------------------------------------- /src/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LookiMan/EXE2PY-Decompiler/c67d2777a0303e3f9564c85a3ad4e49af3f4a696/src/header.png -------------------------------------------------------------------------------- /src/hide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LookiMan/EXE2PY-Decompiler/c67d2777a0303e3f9564c85a3ad4e49af3f4a696/src/hide.png -------------------------------------------------------------------------------- /src/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LookiMan/EXE2PY-Decompiler/c67d2777a0303e3f9564c85a3ad4e49af3f4a696/src/icon.ico -------------------------------------------------------------------------------- /src/preloader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LookiMan/EXE2PY-Decompiler/c67d2777a0303e3f9564c85a3ad4e49af3f4a696/src/preloader.gif -------------------------------------------------------------------------------- /toggle.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QCheckBox 2 | 3 | from PyQt5.QtCore import pyqtProperty 4 | from PyQt5.QtCore import pyqtSlot 5 | from PyQt5.QtCore import Qt 6 | from PyQt5.QtCore import QEasingCurve 7 | from PyQt5.QtCore import QSize 8 | from PyQt5.QtCore import QPoint 9 | from PyQt5.QtCore import QPointF 10 | from PyQt5.QtCore import QPropertyAnimation 11 | from PyQt5.QtCore import QRectF 12 | from PyQt5.QtCore import QSequentialAnimationGroup 13 | 14 | from PyQt5.QtGui import QColor 15 | from PyQt5.QtGui import QBrush 16 | from PyQt5.QtGui import QPainter 17 | from PyQt5.QtGui import QPaintEvent 18 | from PyQt5.QtGui import QPen 19 | 20 | 21 | class AnimatedToggle(QCheckBox): 22 | _transparent_pen = QPen(Qt.transparent) 23 | _light_grey_pen = QPen(Qt.lightGray) 24 | 25 | def __init__( 26 | self, 27 | parent=None, 28 | bar_color=Qt.gray, 29 | checked_color='#240DC4', 30 | handle_color=Qt.white, 31 | pulse_unchecked_color='#D5D5D5', 32 | pulse_checked_color='#4400B0EE', 33 | ) -> None: 34 | super().__init__(parent) 35 | 36 | self._bar_brush = QBrush(bar_color) 37 | self._bar_checked_brush = QBrush(QColor(checked_color).lighter()) 38 | self._handle_brush = QBrush(handle_color) 39 | self._handle_checked_brush = QBrush(QColor(checked_color)) 40 | self._pulse_unchecked_animation = QBrush(QColor(pulse_unchecked_color)) 41 | self._pulse_checked_animation = QBrush(QColor(pulse_checked_color)) 42 | 43 | self.setContentsMargins(8, 0, 8, 0) 44 | self._handle_position = 0 45 | self._pulse_radius = 0 46 | 47 | self.animation = QPropertyAnimation(self, b'handle_position', self) 48 | self.animation.setEasingCurve(QEasingCurve.InOutCubic) 49 | self.animation.setDuration(250) 50 | 51 | self.pulse_anim = QPropertyAnimation(self, b'pulse_radius', self) 52 | self.pulse_anim.setDuration(250) 53 | self.pulse_anim.setStartValue(10) 54 | self.pulse_anim.setEndValue(17) 55 | 56 | self.animations_group = QSequentialAnimationGroup() 57 | self.animations_group.addAnimation(self.animation) 58 | self.animations_group.addAnimation(self.pulse_anim) 59 | 60 | self.stateChanged.connect(self.setup_animation) 61 | 62 | def sizeHint(self) -> QSize: 63 | return QSize(58, 45) 64 | 65 | def hitButton(self, pos: QPoint) -> bool: 66 | return self.contentsRect().contains(pos) 67 | 68 | @pyqtSlot(int) 69 | def setup_animation(self, value: int) -> None: 70 | self.animations_group.stop() 71 | self.animation.setEndValue(bool(value)) 72 | self.animations_group.start() 73 | 74 | def paintEvent(self, event: QPaintEvent) -> None: 75 | contRect = self.contentsRect() 76 | handleRadius = round(0.24 * contRect.height()) 77 | 78 | p = QPainter(self) 79 | p.setRenderHint(QPainter.Antialiasing) 80 | 81 | p.setPen(self._transparent_pen) 82 | barRect = QRectF( 83 | 0, 0, contRect.width() - handleRadius, 0.40 * contRect.height() 84 | ) 85 | barRect.moveCenter(contRect.center()) 86 | rounding = barRect.height() / 2 87 | trailLength = contRect.width() - 2 * handleRadius 88 | xPos = contRect.x() + handleRadius + trailLength * self._handle_position 89 | 90 | if self.pulse_anim.state() == QPropertyAnimation.Running: 91 | p.setBrush( 92 | self._pulse_checked_animation 93 | if self.isChecked() 94 | else self._pulse_unchecked_animation 95 | ) 96 | p.drawEllipse( 97 | QPointF(xPos, barRect.center().y()), 98 | self._pulse_radius, 99 | self._pulse_radius, 100 | ) 101 | 102 | if self.isChecked(): 103 | p.setBrush(self._bar_checked_brush) 104 | p.drawRoundedRect(barRect, rounding, rounding) 105 | p.setBrush(self._handle_checked_brush) 106 | 107 | else: 108 | p.setBrush(self._bar_brush) 109 | p.drawRoundedRect(barRect, rounding, rounding) 110 | p.setPen(self._light_grey_pen) 111 | p.setBrush(self._handle_brush) 112 | 113 | p.drawEllipse(QPointF(xPos, barRect.center().y()), handleRadius, handleRadius) 114 | 115 | p.end() 116 | 117 | @pyqtProperty(float) 118 | def handle_position(self) -> float: 119 | return self._handle_position 120 | 121 | @handle_position.setter 122 | def handle_position(self, pos: float) -> None: 123 | self._handle_position = pos 124 | self.update() 125 | 126 | @pyqtProperty(float) 127 | def pulse_radius(self) -> float: 128 | return self._pulse_radius 129 | 130 | @pulse_radius.setter 131 | def pulse_radius(self, pos: float) -> None: 132 | self._pulse_radius = pos 133 | self.update() 134 | -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 1 | from os.path import join 2 | from platform import system 3 | from pyautogui import Size 4 | 5 | from PyQt5 import QtCore 6 | from PyQt5 import QtGui 7 | from PyQt5.QtCore import Qt 8 | from PyQt5.QtCore import QEvent 9 | from PyQt5.QtGui import QDragEnterEvent 10 | from PyQt5.QtGui import QDropEvent 11 | from PyQt5.QtGui import QMovie 12 | from PyQt5.QtGui import QMouseEvent 13 | from PyQt5.QtGui import QEnterEvent 14 | from PyQt5.QtWidgets import QFileDialog 15 | from PyQt5.QtWidgets import QWidget 16 | from PyQt5.QtWidgets import QLabel 17 | from PyQt5.QtWidgets import QLineEdit 18 | from PyQt5.QtWidgets import QMessageBox 19 | from PyQt5.QtWidgets import QSizePolicy 20 | 21 | from utils import get_screen_size 22 | from utils import Platforms 23 | from utils import STATUS_CODES 24 | 25 | from toggle import AnimatedToggle 26 | 27 | 28 | class GifPlayer(QtCore.QObject): 29 | def __init__(self, widget) -> None: 30 | super().__init__() 31 | self.central_widget = widget 32 | self.banner = QLabel(self.central_widget) 33 | self.banner.setGeometry( 34 | 0, 26, self.central_widget.width(), self.central_widget.height() 35 | ) 36 | self.banner.setStyleSheet('background-color: rgba(195, 195, 195, 100);') 37 | self.movie_screen = QLabel(self.central_widget) 38 | self.movie_screen.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 39 | self.movie_screen.setAlignment(Qt.AlignCenter) 40 | self.movie_screen.setScaledContents(True) 41 | self.movie_screen.setGeometry( 42 | QtCore.QRect( 43 | int(self.central_widget.width() // 2 - 62), 44 | int(self.central_widget.height() // 2 - 62), 45 | 124, 46 | 124, 47 | ) 48 | ) 49 | self.movie = QMovie(join('src', 'preloader.gif')) 50 | self.movie.setCacheMode(QMovie.CacheAll) 51 | self.movie_screen.setMovie(self.movie) 52 | self.movie_screen.hide() 53 | self.banner.hide() 54 | 55 | def start_animation(self) -> None: 56 | self.movie.start() 57 | self.banner.show() 58 | self.movie_screen.show() 59 | 60 | def stop_animation(self) -> None: 61 | self.movie.stop() 62 | self.banner.hide() 63 | self.movie_screen.hide() 64 | 65 | 66 | class Thread(QtCore.QThread): 67 | callback = QtCore.pyqtSignal(int) 68 | 69 | def __init__(self, function, **kwargs) -> None: 70 | super().__init__() 71 | self.is_running = True 72 | self.function = function 73 | self.kwargs = kwargs 74 | 75 | def run(self) -> None: 76 | self.callback.emit(self.function(**self.kwargs)) 77 | 78 | 79 | class DraggableLineEdit(QLineEdit): 80 | def __init__(self, *args, **kwargs) -> None: 81 | super().__init__(*args, **kwargs) 82 | 83 | self.setAcceptDrops(True) 84 | 85 | def dragEnterEvent(self, event: QDragEnterEvent) -> None: 86 | if event.mimeData().hasFormat('text/plain'): 87 | event.accept() 88 | 89 | elif event.mimeData().hasFormat('text/uri-list'): 90 | event.accept() 91 | 92 | else: 93 | event.ignore() 94 | 95 | def dropEvent(self, event: QDropEvent) -> None: 96 | self.setText(event.mimeData().text().replace('file:///', '')) 97 | 98 | 99 | class ClickedQLabel(QLabel): 100 | clicked = QtCore.pyqtSignal() 101 | 102 | def mouseReleaseEvent(self, event: QMouseEvent) -> None: 103 | self.clicked.emit() 104 | super().mouseReleaseEvent(event) 105 | 106 | 107 | class BaseMoveEvents(QWidget): 108 | def _get_window(self) -> QWidget: 109 | return self._parent.window() 110 | 111 | def _get_window_width(self) -> int: 112 | return self._parent.window().geometry().width() 113 | 114 | def _get_window_height(self) -> int: 115 | return self._parent.window().geometry().height() 116 | 117 | def _get_screen_size(self) -> Size: 118 | return get_screen_size() 119 | 120 | def mouseMoveEvent(self, event: QMouseEvent) -> None: 121 | win = self._get_window() 122 | screensize = self._get_screen_size() 123 | 124 | if self.b_move: 125 | x = event.globalX() + self.x_korr - self.lastPoint.x() 126 | y = event.globalY() + self.y_korr - self.lastPoint.y() 127 | if x >= screensize[0] - self._get_window_width(): 128 | x = screensize[0] - self._get_window_width() 129 | if x <= 0: 130 | x = 0 131 | if y >= screensize[1] - self._get_window_height(): 132 | y = screensize[1] - self._get_window_height() 133 | if y <= 0: 134 | y = 0 135 | win.move(x, y) 136 | 137 | super().mouseMoveEvent(event) 138 | 139 | def mousePressEvent(self, event: QMouseEvent) -> None: 140 | if event.button() == Qt.LeftButton: 141 | win = self._get_window() 142 | x_korr = win.frameGeometry().x() - win.geometry().x() 143 | y_korr = win.frameGeometry().y() - win.geometry().y() 144 | parent = self 145 | while not parent == win: 146 | x_korr -= parent.x() 147 | y_korr -= parent.y() 148 | parent = parent.parent() 149 | 150 | self.__dict__.update( 151 | { 152 | 'lastPoint': event.pos(), 153 | 'b_move': True, 154 | 'x_korr': x_korr, 155 | 'y_korr': y_korr, 156 | } 157 | ) 158 | else: 159 | self.__dict__.update({'b_move': False}) 160 | 161 | self.setCursor(Qt.SizeAllCursor) 162 | super().mousePressEvent(event) 163 | 164 | def mouseReleaseEvent(self, event: QMouseEvent) -> None: 165 | self.setCursor(Qt.ArrowCursor) 166 | super().mouseReleaseEvent(event) 167 | 168 | 169 | class Panel(QLabel, BaseMoveEvents): 170 | def __init__(self, parent: QWidget, width: int, height: int) -> None: 171 | super(QLabel, self).__init__(parent) 172 | self._parent = parent 173 | self._width = width 174 | self._height = height 175 | self.setGeometry(QtCore.QRect(0, 0, self._width, self._height)) 176 | self._pixmap = QtGui.QPixmap(join('src', 'header.png')) 177 | self.setScaledContents(True) 178 | self.setPixmap(self._pixmap) 179 | self.setObjectName('header') 180 | 181 | 182 | class Title(QLabel, BaseMoveEvents): 183 | def __init__(self, parent: QWidget, width: int, height: int) -> None: 184 | super(QLabel, self).__init__(parent) 185 | self._parent = parent 186 | self._width = width 187 | self._height = height 188 | self.font = QtGui.QFont() 189 | self.font.setFamily('Segoe UI Semibold') 190 | self.font.setPointSize(8) 191 | self.font.setWeight(75) 192 | self.font.setBold(True) 193 | self.setStyleSheet('color: rgb(255,255,255);') 194 | self.setFont(self.font) 195 | self.setGeometry(QtCore.QRect(10, 0, self._width, self._height)) 196 | self.setText('Заголовок') 197 | self.setObjectName('titleLabel') 198 | 199 | def set_title(self, title: str) -> None: 200 | self.setText(title) 201 | 202 | def get_title(self) -> str: 203 | return self.text() 204 | 205 | 206 | class BaseButtonEvents(QWidget): 207 | def enterEvent(self, event: QEnterEvent) -> None: 208 | self.setPixmap(self.pixmap_enter) 209 | super().enterEvent(event) 210 | 211 | def leaveEvent(self, event: QEvent) -> None: 212 | self.setPixmap(self.pixmap_leave) 213 | super().leaveEvent(event) 214 | 215 | 216 | class HideButton(ClickedQLabel, BaseButtonEvents): 217 | def __init__(self, parent: QWidget, x: int, y: int, size: int = 26) -> None: 218 | super(QLabel, self).__init__(parent) 219 | self.pixmap = QtGui.QPixmap(join('src', 'hide.png')) 220 | self.pixmap_leave = self.pixmap.copy(0, 0, size, size) 221 | self.pixmap_enter = self.pixmap.copy(size, 0, size * 2, size) 222 | 223 | self.setGeometry(QtCore.QRect(x, y, size, size)) 224 | self.setPixmap(self.pixmap_leave) 225 | self.setObjectName('minimize_button') 226 | self.setToolTip('Hide') 227 | 228 | 229 | class CloseButton(ClickedQLabel, BaseButtonEvents): 230 | def __init__(self, parent: QWidget, x: int, y: int, size: int = 26) -> None: 231 | super(QLabel, self).__init__(parent) 232 | self.pixmap = QtGui.QPixmap(join('src', 'close.png')) 233 | self.pixmap_leave = self.pixmap.copy(0, 0, size, size) 234 | self.pixmap_enter = self.pixmap.copy(size, 0, size * 2, size) 235 | 236 | self.setGeometry(QtCore.QRect(x, y, size, size)) 237 | self.setPixmap(self.pixmap_leave) 238 | self.setObjectName('close_button') 239 | self.setToolTip('Close') 240 | 241 | 242 | class Header: 243 | _height: int = 26 244 | 245 | def __init__(self, parent: QWidget) -> None: 246 | self.panel = Panel(parent, parent.width(), self._height) 247 | self.title = Title(parent, int(parent.width() * 0.8), self._height) 248 | self.hide_button = HideButton( 249 | parent, parent.width() - int(self._height * 2), 0, self._height 250 | ) 251 | self.close_button = CloseButton( 252 | parent, int(parent.width() - self._height), 0, self._height 253 | ) 254 | 255 | def width(self) -> int: 256 | return self._width 257 | 258 | def height(self) -> int: 259 | return self._height 260 | 261 | 262 | class BaseForm(QtCore.QObject): 263 | closeHandler = QtCore.pyqtSignal(dict) 264 | 265 | def _customize_window(self) -> None: 266 | """Creating a custom window""" 267 | 268 | self.form.setWindowFlags(Qt.FramelessWindowHint) 269 | icon = QtGui.QIcon() 270 | icon.addPixmap( 271 | QtGui.QPixmap(join('src', 'icon.ico')), QtGui.QIcon.Normal, QtGui.QIcon.Off 272 | ) 273 | self.form.setWindowIcon(icon) 274 | 275 | self.header = Header(self.form) 276 | self.header.hide_button.clicked.connect(self.minimize) 277 | self.header.close_button.clicked.connect(self.closeEvent) 278 | 279 | def closeEvent(self) -> None: 280 | self.close() 281 | 282 | def show(self) -> None: 283 | self.form.show() 284 | 285 | def hide(self) -> None: 286 | self.form.hide() 287 | 288 | def minimize(self) -> None: 289 | self.form.showMinimized() 290 | 291 | def close(self) -> None: 292 | self.form.close() 293 | 294 | def set_title(self, title: str) -> None: 295 | self.header.title.set_title(title) 296 | 297 | def get_title(self) -> str: 298 | return self.header.title.get_title() 299 | 300 | def set_background_image(self, path: str) -> None: 301 | self.background.set_image(path) 302 | 303 | def show_info(self, title: str, message_text: str) -> None: 304 | QMessageBox.information(self.form, title, message_text, QMessageBox.Ok) 305 | 306 | def show_warning(self, title: str, message_text: str) -> None: 307 | QMessageBox.warning(self.form, title, message_text, QMessageBox.Ok) 308 | 309 | def show_error(self, title: str, message_text: str) -> None: 310 | QMessageBox.critical(self.form, title, message_text, QMessageBox.Ok) 311 | 312 | 313 | class MainWindow(BaseForm): 314 | def __init__(self, worker: callable, *args, **kwargs) -> None: 315 | super().__init__(*args, **kwargs) 316 | self.worker = worker 317 | self.form = QWidget() 318 | self.form.setObjectName('MainWindow') 319 | self.form.setFixedSize(644, 175) 320 | self.form.setStyleSheet( 321 | '#MainWindow{background-color: #F2F2F2; border: 1 solid #000;}' 322 | ) 323 | 324 | self._customize_window() 325 | 326 | self.header.title.set_title('Decompiller 3.0') 327 | 328 | self.font = QtGui.QFont() 329 | self.font.setPointSize(10) 330 | 331 | self.label = QLabel(self.form) 332 | self.label.setObjectName('label') 333 | self.label.setGeometry(QtCore.QRect(12, 36, 601, 16)) 334 | self.label.setText('Enter the path to the program or script to be decompiled') 335 | self.label.setFont(self.font) 336 | 337 | self.lineEdit = DraggableLineEdit(self.form) 338 | self.lineEdit.setObjectName('lineEdit') 339 | self.lineEdit.setGeometry(QtCore.QRect(10, 66, 560, 30)) 340 | self.lineEdit.setStyleSheet( 341 | '#lineEdit{' 342 | 'border: 1px solid #DDDDDD;' 343 | 'padding-left: 5px;' 344 | 'border-radius: 4px;' 345 | 'border-top-right-radius: 0px;' 346 | 'border-bottom-right-radius: 0px;' 347 | 'background-color: #fff;' 348 | '}' 349 | ) 350 | 351 | self.file_search_button = ClickedQLabel(self.form) 352 | self.file_search_button.setObjectName('file_search_button') 353 | self.file_search_button.setGeometry(QtCore.QRect(570, 66, 60, 30)) 354 | self.file_search_button.setFont(self.font) 355 | self.file_search_button.setText('●●●') 356 | self.file_search_button.clicked.connect(self.select_file) 357 | self.file_search_button.setStyleSheet( 358 | '#file_search_button{' 359 | 'border-bottom: 3px solid #080386;' 360 | 'border-radius: 4px;' 361 | 'border-top-left-radius: 0px;' 362 | 'border-bottom-left-radius: 0px;' 363 | 'qproperty-alignment: AlignCenter;' 364 | 'background-color: #240DC4;' 365 | 'color: #ffffff;' 366 | '}\n' 367 | '#file_search_button:hover{' 368 | 'background-color: #080386;' 369 | '}' 370 | ) 371 | 372 | self.is_need_decompile_sub_libraries_label = QLabel(self.form) 373 | self.is_need_decompile_sub_libraries_label.setObjectName('decompile_sub_libraries_label') 374 | self.is_need_decompile_sub_libraries_label.setGeometry(QtCore.QRect(14, 106, 190, 24)) 375 | self.is_need_decompile_sub_libraries_label.setText('Decompile all additional libraries') 376 | self.is_need_decompile_sub_libraries_label.setFont(self.font) 377 | 378 | self.is_need_decompile_sub_libraries_checkbox = AnimatedToggle(self.form) 379 | self.is_need_decompile_sub_libraries_checkbox.move(210, 98) 380 | self.is_need_decompile_sub_libraries_checkbox.setFixedSize( 381 | self.is_need_decompile_sub_libraries_checkbox.sizeHint() 382 | ) 383 | self.is_need_decompile_sub_libraries_checkbox.show() 384 | 385 | self.is_need_open_output_folder_label = QLabel(self.form) 386 | self.is_need_open_output_folder_label.setObjectName('open_output_folder_label') 387 | self.is_need_open_output_folder_label.setGeometry(QtCore.QRect(14, 140, 190, 24)) 388 | self.is_need_open_output_folder_label.setText('Automatically open final folder') 389 | self.is_need_open_output_folder_label.setFont(self.font) 390 | 391 | self.is_need_open_output_folder_checkbox = AnimatedToggle(self.form) 392 | self.is_need_open_output_folder_checkbox.move(210, 132) 393 | self.is_need_open_output_folder_checkbox.setChecked(True) 394 | 395 | self.player = GifPlayer(self.form) 396 | 397 | self.start_button = ClickedQLabel(self.form) 398 | self.start_button.setObjectName('start_button') 399 | self.start_button.setGeometry(QtCore.QRect(490, 136, 140, 30)) 400 | self.start_button.setFont(self.font) 401 | self.start_button.setText('Decompile') 402 | self.start_button.clicked.connect(self.start_processing) 403 | self.start_button.setStyleSheet( 404 | '#start_button{' 405 | 'border-bottom: 3px solid #080386;' 406 | 'border-radius: 4px;' 407 | 'qproperty-alignment: AlignCenter;' 408 | 'background-color: #240DC4;' 409 | 'color: #ffffff;' 410 | '}\n' 411 | '#start_button:hover{' 412 | 'background-color: #080386;' 413 | '}' 414 | ) 415 | 416 | self.stop_button = ClickedQLabel(self.form) 417 | self.stop_button.setObjectName('stop_button') 418 | self.stop_button.setGeometry(QtCore.QRect(490, 136, 140, 30)) 419 | self.stop_button.setFont(self.font) 420 | self.stop_button.setText('Stop') 421 | self.stop_button.clicked.connect(lambda: self.stop_processing(401)) 422 | self.stop_button.setStyleSheet( 423 | '#stop_button{' 424 | 'border-bottom: 3px solid #9D0909;' 425 | 'border-radius: 4px;' 426 | 'qproperty-alignment: AlignCenter;' 427 | 'background-color: #ED0D0D;' 428 | 'color: #ffffff;' 429 | '}\n' 430 | '#stop_button:hover{' 431 | 'background-color: #9D0909;' 432 | '}' 433 | ) 434 | self.stop_button.hide() 435 | self.form.show() 436 | QtCore.QMetaObject.connectSlotsByName(self.form) 437 | 438 | def start_processing(self) -> None: 439 | self.player.start_animation() 440 | 441 | self.start_button.hide() 442 | self.stop_button.show() 443 | 444 | self.thread = Thread( 445 | self.worker, 446 | target=self.lineEdit.text().strip(), 447 | is_need_decompile_sub_libraries=self.is_need_decompile_sub_libraries_checkbox.isChecked(), 448 | is_need_open_output_folder=self.is_need_open_output_folder_checkbox.isChecked(), 449 | ) 450 | 451 | self.thread.callback.connect(self.stop_processing) 452 | self.thread.start() 453 | 454 | def stop_processing(self, status_code) -> None: 455 | self.stop_button.hide() 456 | self.player.stop_animation() 457 | self.start_button.show() 458 | 459 | if not STATUS_CODES.get(status_code): 460 | self.show_error( 461 | 'Critical', f'Unknown status code: {status_code}' 462 | ) 463 | 464 | elif status_code in range(200, 300): 465 | message = STATUS_CODES[status_code] 466 | self.show_info('Success', message) 467 | 468 | elif status_code in range(400, 500): 469 | message = STATUS_CODES[status_code] 470 | self.show_warning('Warning', message) 471 | 472 | elif status_code in range(500, 600): 473 | message = STATUS_CODES[status_code] 474 | self.show_error('Critical', message) 475 | 476 | def select_file(self) -> None: 477 | dlg = QFileDialog() 478 | dlg.setFileMode(QFileDialog.ExistingFile) 479 | 480 | if not dlg.exec_(): 481 | return 482 | 483 | file = dlg.selectedFiles() 484 | 485 | if not file: 486 | return 487 | 488 | file = file[0] 489 | 490 | if system() == Platforms.WINDOWS.value: 491 | file = file.replace('/', '\\') 492 | 493 | self.lineEdit.setText(file) 494 | 495 | state = True if file.endswith('.exe') else False 496 | 497 | self.is_need_decompile_sub_libraries_checkbox.setChecked(state) 498 | self.is_need_decompile_sub_libraries_checkbox.setDisabled(not state) 499 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import glob 4 | import webbrowser 5 | import shutil 6 | import pyautogui 7 | 8 | from enum import Enum 9 | 10 | 11 | PYTHON_HEADERS = [ 12 | b'\xee\x0c\r\n\x11;\x8d_\x93!\x00\x00', # 3.4 13 | b'3\r\r\n0\x07>]a4\x00\x00', # 3.6 14 | b'B\r\r\n\x00\x00\x00\x000\x07>]a4\x00\x00', # 3.7 15 | b'U\r\r\n\x00\x00\x00\x000\x07>]a4\x00\x00', # 3.8 16 | ] 17 | 18 | START_BYTE = b'\xe3' 19 | 20 | 21 | class Platforms(Enum): 22 | WINDOWS = 'Windows' 23 | 24 | 25 | class StatusCodes(Enum): 26 | # Success codes 27 | EXE_DECOMPILED_SUCCESSFULLY = 200 28 | EXE_PARTIALLY_DECOMPILATION = 201 29 | PYC_DECOMPILED_SUCCESSFULLY = 202 30 | PYC_PARTIALLY_DECOMPILATION = 203 31 | LIB_DECOMPILED_SUCCESSFULLY = 204 32 | LIB_PARTIALLY_DECOMPILATION = 205 33 | # User error codes 34 | USER_STOPED = 400 35 | UNEXPECTED_CONFIGURATION = 401 36 | UNEXPECTED_EXTENSION = 402 37 | TARGET_NOT_EXISTS = 403 38 | FILES_NOT_FOUND = 404 39 | # Unknown error codes 40 | EXE_DECOMPILATION_ERROR = 500 41 | PYC_DECOMPILATION_ERROR = 501 42 | LIB_DECOMPILATION_ERROR = 502 43 | 44 | 45 | # Return codes of some functions 46 | STATUS_CODES = { 47 | # Success messages 48 | StatusCodes.EXE_DECOMPILED_SUCCESSFULLY.value: 'Decompilation of the executable has been successfully completed', 49 | StatusCodes.EXE_PARTIALLY_DECOMPILATION.value: 'Decompilation of the executable file partially successful', 50 | StatusCodes.PYC_DECOMPILED_SUCCESSFULLY.value: 'Decompilation of the cache files has been successfully completed', 51 | StatusCodes.PYC_PARTIALLY_DECOMPILATION.value: 'Decompilation of cache files partially successful', 52 | StatusCodes.LIB_DECOMPILED_SUCCESSFULLY.value: 'Decompilation of the additional libraries has been successfully completed', 53 | StatusCodes.LIB_PARTIALLY_DECOMPILATION.value: 'Decompilation of the additional libraries partially successful', 54 | # User error messages 55 | StatusCodes.USER_STOPED.value: 'Process stopped by user', 56 | StatusCodes.UNEXPECTED_CONFIGURATION.value: 'Received unexpected combination of parameters for decompilation', 57 | StatusCodes.UNEXPECTED_EXTENSION.value: 'A file with an unexpected extension was transferred', 58 | StatusCodes.TARGET_NOT_EXISTS.value: 'A selected file or directory not exists', 59 | StatusCodes.FILES_NOT_FOUND.value: 'No files found in the folder', 60 | # Unknown error messages 61 | StatusCodes.EXE_DECOMPILATION_ERROR.value: 'An unexpected error occurred while decompiling the executable file', 62 | StatusCodes.PYC_DECOMPILATION_ERROR.value: 'An unexpected error occurred while decompiling the cache file', 63 | StatusCodes.LIB_DECOMPILATION_ERROR.value: 'An unexpected error occurred while decompiling an additional library', 64 | } 65 | 66 | 67 | def search_pyc_files(directory: str) -> list: 68 | return glob.glob(f'{directory}\\*.pyc') 69 | 70 | 71 | def make_folders(folders: list) -> None: 72 | for folder in folders: 73 | if os.path.exists(folder): 74 | remove_folder(folder) 75 | create_folder(folder) 76 | 77 | 78 | def create_folder(folder: str) -> None: 79 | try: 80 | os.mkdir(folder) 81 | except Exception as e: 82 | print(f'[!] Error creating folder: {folder}') 83 | print(f'[e] {e}') 84 | 85 | 86 | def open_output_folder(folder: str) -> None: 87 | webbrowser.open(folder) 88 | 89 | 90 | def remove_folder(folder: str) -> None: 91 | try: 92 | shutil.rmtree(folder) 93 | except Exception as e: 94 | print(f'[!] Error removing folder: {folder}') 95 | print(f'[e] {e}') 96 | 97 | 98 | def get_screen_size() -> pyautogui.Size: 99 | return pyautogui.size() 100 | --------------------------------------------------------------------------------