From 747ed7cf50f614a7a4d46afd4a603379eb898df7 Mon Sep 17 00:00:00 2001 From: Lil-Ran Date: Sat, 13 Sep 2025 12:34:02 +0800 Subject: [PATCH] feat: extract bcc native part --- README.md | 15 ++++++++++- helpers/detect.py | 31 ++++++++++++----------- helpers/shot.py | 63 ++++++++++++++++++++++------------------------- 3 files changed, 61 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 95c45c8..aac7468 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ This project aims to convert armored data back to bytecode assembly and (experimentally) source code. We forked the awesome [Decompyle++](https://github.com/zrax/pycdc) (aka pycdc), and added some processes on it like modifying abstract syntax tree. +> [!IMPORTANT] +> +> This tool should only be used on scripts you own or have permission to analyze. Please respect software licenses and terms of service. The author is not responsible for any misuse or damage caused by this tool. + +> [!NOTE] +> +> Like other decompilers, this tool is intended for professional users. You should have a basic understanding of Python bytecode. If not, you may need to ask for help from someone who does. + > [!WARNING] > > **Disassembly results are accurate, but decompiled code can be incomplete and incorrect.** [See issue #3](https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot/issues/3) @@ -16,7 +24,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.3 (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.x (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. @@ -68,3 +76,8 @@ Feel free to open an issue if you have any questions, suggestions, or problems. - [ ] Multi-platform pyarmor_runtime executable - [ ] Support more obfuscating options - [ ] Regenerate pyc for other backends +- [ ] Documentation (Do not accept PR about this) + +## Star Chart + +[![Stargazers over time](https://starchart.cc/Lil-House/Pyarmor-Static-Unpack-1shot.svg?variant=adaptive)](https://starchart.cc/Lil-House/Pyarmor-Static-Unpack-1shot) diff --git a/helpers/detect.py b/helpers/detect.py index d8a9c2e..9c61922 100644 --- a/helpers/detect.py +++ b/helpers/detect.py @@ -51,21 +51,24 @@ def find_data_from_bytes(data: bytes, max_count=-1) -> List[bytes]: # compressed or coincident, skip data = data[5:] continue - result.append(data[:header_len + body_len]) - # maybe followed by data for other Python versions from the same file, - # we do not extract them - followed_by_another_equivalent = int.from_bytes( - data[56:60], 'little') != 0 - data = data[header_len + body_len:] - while followed_by_another_equivalent \ - and data.startswith(b'PY00') \ - and len(data) >= 64: - header_len = int.from_bytes(data[28:32], 'little') - body_len = int.from_bytes(data[32:36], 'little') - followed_by_another_equivalent = int.from_bytes( - data[56:60], 'little') != 0 - data = data[header_len + body_len:] + complete_object_length = header_len + body_len + + # maybe followed by data for other Python versions or another part of BCC + next_segment_offset = int.from_bytes(data[56:60], 'little') + data_next = data[next_segment_offset:] + while next_segment_offset != 0 and data_next.startswith(b'PY00') and len(data_next) >= 64: + header_len = int.from_bytes(data_next[28:32], 'little') + body_len = int.from_bytes(data_next[32:36], 'little') + complete_object_length = next_segment_offset + header_len + body_len + + if int.from_bytes(data_next[56:60], 'little') == 0: + break + next_segment_offset += int.from_bytes(data_next[56:60], 'little') + data_next = data[next_segment_offset:] + + result.append(data[:complete_object_length]) + data = data[complete_object_length:] return result diff --git a/helpers/shot.py b/helpers/shot.py index e0a50c7..d62f50f 100644 --- a/helpers/shot.py +++ b/helpers/shot.py @@ -27,39 +27,6 @@ def general_aes_ctr_decrypt(data: bytes, key: bytes, nonce: bytes) -> bytes: return cipher.decrypt(data) -def bcc_fallback_method(seq_file_path: str, output_path: str = None): - # Fallback method for BCC mode deobfuscation - logger = logging.getLogger('shot') - logger.info(f'{Fore.YELLOW}Attempting BCC fallback method for: {seq_file_path}{Style.RESET_ALL}') - - if output_path is None: - output_path = seq_file_path + '.back.1shot.seq' - - try: - with open(seq_file_path, 'rb') as f: - origin = f.read() - - one_shot_header = origin[:32] # Header format - aes_key = one_shot_header[1:17] - - bcc_part_length = int.from_bytes(origin[0x58:0x5C], 'little') # If it is 0, it is not BCC part but bytecode part - bytecode_part = origin[32+bcc_part_length:] - aes_nonce = bytecode_part[36:40] + bytecode_part[44:52] # The same position as non-BCC file - - with open(output_path, 'wb') as f: - f.write(one_shot_header) - f.write(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 - except Exception as e: - error_details = traceback.format_exc() - logger.error(f'{Fore.RED}BCC fallback method failed: {e}{Style.RESET_ALL}') - logger.error(f'{Fore.RED}Error details: {error_details}{Style.RESET_ALL}') - return None - - async def decrypt_file_async(exe_path, seq_file_path, path, args): logger = logging.getLogger('shot') try: @@ -135,6 +102,36 @@ async def decrypt_process_async(runtimes: Dict[str, RuntimeInfo], sequences: Lis with open(dest_path + '.1shot.raw', 'wb') as f: f.write(data) + # Check BCC + if int.from_bytes(data[20:24], 'little') == 9: + cipher_text_offset = int.from_bytes(data[28:32], 'little') + cipher_text_length = int.from_bytes(data[32:36], 'little') + nonce = data[36:40] + data[44:52] + bcc_aes_decrypted = general_aes_ctr_decrypt( + data[cipher_text_offset:cipher_text_offset+cipher_text_length], runtime.runtime_aes_key, nonce) + data = data[int.from_bytes(data[56:60], 'little'):] + bcc_architecture_mapping = { + 0x2001: 'dll', # Windows x86-64 + 0x2003: 'so', # Linux x86-64 + } + while True: + if len(bcc_aes_decrypted) < 16: + break + bcc_segment_offset = int.from_bytes(bcc_aes_decrypted[0:4], 'little') + bcc_segment_length = int.from_bytes(bcc_aes_decrypted[4:8], 'little') + bcc_architecture_id = int.from_bytes(bcc_aes_decrypted[8:12], 'little') + bcc_next_segment_offset = int.from_bytes(bcc_aes_decrypted[12:16], 'little') + if bcc_architecture_id in bcc_architecture_mapping: + bcc_file_path = f'{dest_path}.1shot.bcc.{bcc_architecture_mapping[bcc_architecture_id]}' + else: + bcc_file_path = f'{dest_path}.1shot.bcc.0x{bcc_architecture_id:x}' + with open(bcc_file_path, 'wb') as f: + f.write(bcc_aes_decrypted[bcc_segment_offset:bcc_segment_offset+bcc_segment_length]) + logger.info(f'{Fore.GREEN}Extracted BCC mode native part: {bcc_file_path}{Style.RESET_ALL}') + if bcc_next_segment_offset == 0: + break + bcc_aes_decrypted = bcc_aes_decrypted[bcc_next_segment_offset:] + cipher_text_offset = int.from_bytes(data[28:32], 'little') cipher_text_length = int.from_bytes(data[32:36], 'little') nonce = data[36:40] + data[44:52]