This commit is contained in:
2025-04-10 18:10:13 +08:00
parent 7ff305a0f8
commit 71f9fea419
3 changed files with 43 additions and 86 deletions

View File

@@ -1,7 +1,7 @@
name: Build name: Build
on: on:
push: push:
branches: [ci-build] branches: [main]
pull_request: pull_request:
jobs: jobs:

View File

@@ -16,7 +16,7 @@ You don't need to execute the encrypted script. We decrypt them using the same a
### Universal ### 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. 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 - [ ] Multi-platform pyarmor_runtime executable
- [ ] Support more obfuscating options - [ ] Support more obfuscating options
- [ ] Use asyncio for concurrency - [ ] Regenerate pyc for other backends
- [ ] Pyarmor 7 and before (Later or never.)

View File

@@ -2,15 +2,17 @@ import argparse
from Crypto.Cipher import AES from Crypto.Cipher import AES
import logging import logging
import os import os
import subprocess
import sys
import time
import asyncio import asyncio
import traceback import traceback
import platform import platform
import glob from typing import Dict, List, Tuple
from typing import Dict, List, Tuple, Optional
from colorama import init, Fore, Style 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 detect import detect_process
from runtime import RuntimeInfo 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: with open(output_path, 'wb') as f:
f.write(one_shot_header) f.write(one_shot_header)
f.write(bytecode_part[:64]) 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}') logger.info(f'{Fore.GREEN}Successfully created BCC fallback file: {output_path}{Style.RESET_ALL}')
return output_path return output_path
@@ -92,7 +94,12 @@ async def decrypt_file_async(exe_path, seq_file_path, path, args):
elif line.startswith('Unsupported opcode:'): elif line.startswith('Unsupported opcode:'):
if args.show_err_opcode or args.show_all: if args.show_err_opcode or args.show_all:
logger.error(f'PYCDC: {line} ({path})') 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: if args.show_all:
logger.error(f'PYCDC: {line} ({path})') logger.error(f'PYCDC: {line} ({path})')
else: else:
@@ -113,6 +120,9 @@ async def decrypt_process_async(runtimes: Dict[str, RuntimeInfo], sequences: Lis
# Create a semaphore to limit concurrent processes # Create a semaphore to limit concurrent processes
semaphore = asyncio.Semaphore(args.concurrent) # Use the concurrent argument 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 def process_file(path, data):
async with semaphore: async with semaphore:
try: 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)) data[cipher_text_offset:cipher_text_offset+cipher_text_length], runtime.runtime_aes_key, nonce))
f.write(data[cipher_text_offset+cipher_text_length:]) 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 # Run without timeout
returncode, stdout_lines, stderr_lines = await decrypt_file_async(exe_path, seq_file_path, path, args) returncode, stdout_lines, stderr_lines = await decrypt_file_async(exe_path, seq_file_path, path, args)
# Check for specific errors that indicate BCC mode # Check for specific errors that indicate BCC mode
should_try_bcc = False should_try_bcc = False
error_message = "" error_message = ""
# FIXME: Probably false positive
if returncode != 0: if returncode != 0:
error_message = f"PYCDC returned {returncode}" error_message = f"PYCDC returned {returncode}"
# Check for specific error patterns that suggest BCC mode # 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 Get the appropriate executable for the current platform
""" """
logger = logging.getLogger('shot') logger = logging.getLogger('shot')
# If a specific executable is provided, use it # If a specific executable is provided, use it
if args.executable: if args.executable:
if os.path.exists(args.executable): if os.path.exists(args.executable):
@@ -219,70 +227,43 @@ def get_platform_executable(args) -> str:
return args.executable return args.executable
else: else:
logger.warning(f'{Fore.YELLOW}Specified executable not found: {args.executable}{Style.RESET_ALL}') 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() system = platform.system().lower()
machine = platform.machine().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 # Check for architecture-specific executables
arch_specific_exe = f'pyarmor-1shot-{system}-{machine}' arch_specific_exe = f'pyarmor-1shot-{system}-{machine}'
if system == 'windows': if system == 'windows':
arch_specific_exe += '.exe' 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) arch_exe_path = os.path.join(helpers_dir, arch_specific_exe)
if os.path.exists(arch_exe_path): if os.path.exists(arch_exe_path):
logger.info(f'{Fore.GREEN}Using architecture-specific executable: {arch_specific_exe}{Style.RESET_ALL}') logger.info(f'{Fore.GREEN}Using architecture-specific executable: {arch_specific_exe}{Style.RESET_ALL}')
return arch_exe_path 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 # Then check for platform-specific executable
platform_exe_path = os.path.join(helpers_dir, base_exe_name) platform_exe_path = os.path.join(helpers_dir, base_exe_name)
if os.path.exists(platform_exe_path): if os.path.exists(platform_exe_path):
logger.info(f'{Fore.GREEN}Using platform-specific executable: {base_exe_name}{Style.RESET_ALL}') logger.info(f'{Fore.GREEN}Using platform-specific executable: {base_exe_name}{Style.RESET_ALL}')
return platform_exe_path return platform_exe_path
# Finally, check for generic executable # Finally, check for generic executable
generic_exe_path = os.path.join(helpers_dir, 'pyarmor-1shot') generic_exe_path = os.path.join(helpers_dir, 'pyarmor-1shot')
if os.path.exists(generic_exe_path): if os.path.exists(generic_exe_path):
logger.info(f'{Fore.GREEN}Using generic executable: pyarmor-1shot{Style.RESET_ALL}') logger.info(f'{Fore.GREEN}Using generic executable: pyarmor-1shot{Style.RESET_ALL}')
return generic_exe_path 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)
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}')
def find_runtime_files(directory: str) -> List[str]: exit(1)
"""
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
def parse_args(): def parse_args():
@@ -343,11 +324,6 @@ def parse_args():
help='path to the pyarmor-1shot executable to use', help='path to the pyarmor-1shot executable to use',
type=str, 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() return parser.parse_args()
@@ -394,8 +370,8 @@ def main():
) )
logger = logging.getLogger('shot') logger = logging.getLogger('shot')
print(f''' print(Fore.CYAN + r'''
{Fore.CYAN} ____ ____ ____ ____
( __ ) ( __ ) ( __ ) ( __ )
| |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| | | |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| |
| | ____ _ ___ _ _ | | | | ____ _ ___ _ _ | |
@@ -409,8 +385,7 @@ def main():
For technology exchange only. Use at your own risk. For technology exchange only. Use at your own risk.
GitHub: https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot 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 menu option is selected or no directory is provided, show the menu
if args.menu or not args.directory: if args.menu or not args.directory:
@@ -447,23 +422,6 @@ def main():
decrypt_process(runtimes, sequences, args) decrypt_process(runtimes, sequences, args)
return # single file mode ends here 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 dir_path: str
dirs: List[str] dirs: List[str]
files: List[str] files: List[str]