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.
> [!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)

View File

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

View File

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