├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------