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
on:
push:
branches: [ci-build]
branches: [main]
pull_request:
jobs:

View File

@@ -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

View File

@@ -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]