From 71f9fea419586f869f01460be0f5ad1cbb765199 Mon Sep 17 00:00:00 2001 From: Lil-Ran Date: Thu, 10 Apr 2025 18:10:13 +0800 Subject: [PATCH] chore --- .github/workflows/build.yml | 2 +- README.md | 5 +- helpers/shot.py | 122 ++++++++++++------------------------ 3 files changed, 43 insertions(+), 86 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8ab744..ac12dae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ name: Build on: push: - branches: [ci-build] + branches: [main] pull_request: jobs: diff --git a/README.md b/README.md index e4c2486..95c45c8 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ You don't need to execute the encrypted script. We decrypt them using the same a ### Universal -Currently we are trying to support Pyarmor 8.0 to 9.1.2 (latest), Python 3.7 - 3.13, on all operating systems, with obfuscating options as many as possible. (However, we only have limited tests.) +Currently we are trying to support Pyarmor 8.0 to 9.1.3 (latest), Python 3.7 - 3.13, on all operating systems, with obfuscating options as many as possible. (However, we only have limited tests.) You can run this tool in any environment, no need to be the same with obfuscated scripts or runtime. @@ -67,5 +67,4 @@ Feel free to open an issue if you have any questions, suggestions, or problems. - [ ] Multi-platform pyarmor_runtime executable - [ ] Support more obfuscating options -- [ ] Use asyncio for concurrency -- [ ] Pyarmor 7 and before (Later or never.) +- [ ] Regenerate pyc for other backends diff --git a/helpers/shot.py b/helpers/shot.py index 53ce3cb..fca1d70 100644 --- a/helpers/shot.py +++ b/helpers/shot.py @@ -2,15 +2,17 @@ import argparse from Crypto.Cipher import AES import logging import os -import subprocess -import sys -import time import asyncio import traceback import platform -import glob -from typing import Dict, List, Tuple, Optional -from colorama import init, Fore, Style +from typing import Dict, List, Tuple + +try: + from colorama import init, Fore, Style +except ImportError: + def init(**kwargs): pass + class Fore: CYAN = RED = YELLOW = GREEN = '' + class Style: RESET_ALL = '' from detect import detect_process from runtime import RuntimeInfo @@ -47,7 +49,7 @@ def bcc_fallback_method(seq_file_path: str, output_path: str = None): with open(output_path, 'wb') as f: f.write(one_shot_header) f.write(bytecode_part[:64]) - f.write(AES.new(aes_key, AES.MODE_CTR, nonce=aes_nonce, initial_value=2).decrypt(bytecode_part[64:])) + f.write(general_aes_ctr_decrypt(bytecode_part[64:], aes_key, aes_nonce)) logger.info(f'{Fore.GREEN}Successfully created BCC fallback file: {output_path}{Style.RESET_ALL}') return output_path @@ -92,7 +94,12 @@ async def decrypt_file_async(exe_path, seq_file_path, path, args): elif line.startswith('Unsupported opcode:'): if args.show_err_opcode or args.show_all: logger.error(f'PYCDC: {line} ({path})') - elif line.startswith('Something TERRIBLE happened'): + elif line.startswith(( + 'Something TERRIBLE happened', + 'Unsupported argument', + 'Unsupported Node type', + 'Unsupported node type', + )): # annoying wont-fix errors if args.show_all: logger.error(f'PYCDC: {line} ({path})') else: @@ -113,6 +120,9 @@ async def decrypt_process_async(runtimes: Dict[str, RuntimeInfo], sequences: Lis # Create a semaphore to limit concurrent processes semaphore = asyncio.Semaphore(args.concurrent) # Use the concurrent argument + # Get the appropriate executable for the current platform + exe_path = get_platform_executable(args) + async def process_file(path, data): async with semaphore: try: @@ -142,16 +152,14 @@ async def decrypt_process_async(runtimes: Dict[str, RuntimeInfo], sequences: Lis data[cipher_text_offset:cipher_text_offset+cipher_text_length], runtime.runtime_aes_key, nonce)) f.write(data[cipher_text_offset+cipher_text_length:]) - # Get the appropriate executable for the current platform - exe_path = get_platform_executable(args) - # Run without timeout returncode, stdout_lines, stderr_lines = await decrypt_file_async(exe_path, seq_file_path, path, args) # Check for specific errors that indicate BCC mode should_try_bcc = False error_message = "" - + + # FIXME: Probably false positive if returncode != 0: error_message = f"PYCDC returned {returncode}" # Check for specific error patterns that suggest BCC mode @@ -211,7 +219,7 @@ def get_platform_executable(args) -> str: Get the appropriate executable for the current platform """ logger = logging.getLogger('shot') - + # If a specific executable is provided, use it if args.executable: if os.path.exists(args.executable): @@ -219,70 +227,43 @@ def get_platform_executable(args) -> str: return args.executable else: logger.warning(f'{Fore.YELLOW}Specified executable not found: {args.executable}{Style.RESET_ALL}') - - # Determine the current platform + + helpers_dir = os.path.dirname(os.path.abspath(__file__)) + system = platform.system().lower() machine = platform.machine().lower() - - # Map platform to executable name - platform_map = { - 'windows': 'pyarmor-1shot.exe', - 'linux': 'pyarmor-1shot', - 'darwin': 'pyarmor-1shot' # macOS - } - - # Get the base executable name for the current platform - base_exe_name = platform_map.get(system, 'pyarmor-1shot') - + # Check for architecture-specific executables arch_specific_exe = f'pyarmor-1shot-{system}-{machine}' if system == 'windows': arch_specific_exe += '.exe' - - # Look for executables in the helpers directory - helpers_dir = os.path.dirname(os.path.abspath(__file__)) - - # First check for architecture-specific executable + arch_exe_path = os.path.join(helpers_dir, arch_specific_exe) if os.path.exists(arch_exe_path): logger.info(f'{Fore.GREEN}Using architecture-specific executable: {arch_specific_exe}{Style.RESET_ALL}') return arch_exe_path - + + platform_map = { + 'windows': 'pyarmor-1shot.exe', + 'linux': 'pyarmor-1shot', + 'darwin': 'pyarmor-1shot', + } + base_exe_name = platform_map.get(system, 'pyarmor-1shot') + # Then check for platform-specific executable platform_exe_path = os.path.join(helpers_dir, base_exe_name) if os.path.exists(platform_exe_path): logger.info(f'{Fore.GREEN}Using platform-specific executable: {base_exe_name}{Style.RESET_ALL}') return platform_exe_path - + # Finally, check for generic executable generic_exe_path = os.path.join(helpers_dir, 'pyarmor-1shot') if os.path.exists(generic_exe_path): logger.info(f'{Fore.GREEN}Using generic executable: pyarmor-1shot{Style.RESET_ALL}') return generic_exe_path - - # If no executable is found, use the default name - logger.warning(f'{Fore.YELLOW}No specific executable found for {system}-{machine}, using default: {base_exe_name}{Style.RESET_ALL}') - return os.path.join(helpers_dir, base_exe_name) - -def find_runtime_files(directory: str) -> List[str]: - """ - Find all pyarmor_runtime files in the given directory and its subdirectories - """ - runtime_files = [] - - # Common runtime file patterns - patterns = [ - 'pyarmor_runtime*.pyd', # Windows - 'pyarmor_runtime*.so', # Linux - 'pyarmor_runtime*.dylib' # macOS - ] - - for pattern in patterns: - for file_path in glob.glob(os.path.join(directory, '**', pattern), recursive=True): - runtime_files.append(file_path) - - return runtime_files + logger.critical(f'{Fore.RED}Executable {base_exe_name} not found, please build it first or download on https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot/releases {Style.RESET_ALL}') + exit(1) def parse_args(): @@ -343,11 +324,6 @@ def parse_args(): help='path to the pyarmor-1shot executable to use', type=str, ) - parser.add_argument( - '--auto-detect-runtime', - help='automatically detect and use all pyarmor_runtime files in the directory', - action='store_true', - ) return parser.parse_args() @@ -394,8 +370,8 @@ def main(): ) logger = logging.getLogger('shot') - print(f''' -{Fore.CYAN} ____ ____ + print(Fore.CYAN + r''' + ____ ____ ( __ ) ( __ ) | |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| | | | ____ _ ___ _ _ | | @@ -409,8 +385,7 @@ def main(): For technology exchange only. Use at your own risk. GitHub: https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot -{Style.RESET_ALL} -''') +''' + Style.RESET_ALL) # If menu option is selected or no directory is provided, show the menu if args.menu or not args.directory: @@ -447,23 +422,6 @@ def main(): decrypt_process(runtimes, sequences, args) return # single file mode ends here - # Auto-detect runtime files if requested - if args.auto_detect_runtime: - logger.info(f'{Fore.CYAN}Auto-detecting runtime files in {args.directory}...{Style.RESET_ALL}') - runtime_files = find_runtime_files(args.directory) - if runtime_files: - logger.info(f'{Fore.GREEN}Found {len(runtime_files)} runtime files{Style.RESET_ALL}') - for runtime_file in runtime_files: - try: - new_runtime = RuntimeInfo(runtime_file) - runtimes[new_runtime.serial_number] = new_runtime - logger.info(f'{Fore.GREEN}Found runtime: {new_runtime.serial_number} ({runtime_file}){Style.RESET_ALL}') - print(new_runtime) - except Exception as e: - logger.error(f'{Fore.RED}Failed to load runtime {runtime_file}: {e}{Style.RESET_ALL}') - else: - logger.warning(f'{Fore.YELLOW}No runtime files found in {args.directory}{Style.RESET_ALL}') - dir_path: str dirs: List[str] files: List[str]