feat: extract bcc native part

This commit is contained in:
2025-09-13 12:34:02 +08:00
parent eca034ca45
commit 747ed7cf50
3 changed files with 61 additions and 48 deletions

View File

@@ -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. 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] > [!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) > **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 ### 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. 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 - [ ] Multi-platform pyarmor_runtime executable
- [ ] Support more obfuscating options - [ ] Support more obfuscating options
- [ ] Regenerate pyc for other backends - [ ] 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)

View File

@@ -51,21 +51,24 @@ def find_data_from_bytes(data: bytes, max_count=-1) -> List[bytes]:
# compressed or coincident, skip # compressed or coincident, skip
data = data[5:] data = data[5:]
continue continue
result.append(data[:header_len + body_len])
# maybe followed by data for other Python versions from the same file, complete_object_length = header_len + body_len
# we do not extract them
followed_by_another_equivalent = int.from_bytes( # maybe followed by data for other Python versions or another part of BCC
data[56:60], 'little') != 0 next_segment_offset = int.from_bytes(data[56:60], 'little')
data = data[header_len + body_len:] data_next = data[next_segment_offset:]
while followed_by_another_equivalent \ while next_segment_offset != 0 and data_next.startswith(b'PY00') and len(data_next) >= 64:
and data.startswith(b'PY00') \ header_len = int.from_bytes(data_next[28:32], 'little')
and len(data) >= 64: body_len = int.from_bytes(data_next[32:36], 'little')
header_len = int.from_bytes(data[28:32], 'little') complete_object_length = next_segment_offset + header_len + body_len
body_len = int.from_bytes(data[32:36], 'little')
followed_by_another_equivalent = int.from_bytes( if int.from_bytes(data_next[56:60], 'little') == 0:
data[56:60], 'little') != 0 break
data = data[header_len + body_len:] 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 return result

View File

@@ -27,39 +27,6 @@ def general_aes_ctr_decrypt(data: bytes, key: bytes, nonce: bytes) -> bytes:
return cipher.decrypt(data) 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): async def decrypt_file_async(exe_path, seq_file_path, path, args):
logger = logging.getLogger('shot') logger = logging.getLogger('shot')
try: 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: with open(dest_path + '.1shot.raw', 'wb') as f:
f.write(data) 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_offset = int.from_bytes(data[28:32], 'little')
cipher_text_length = int.from_bytes(data[32:36], 'little') cipher_text_length = int.from_bytes(data[32:36], 'little')
nonce = data[36:40] + data[44:52] nonce = data[36:40] + data[44:52]