feat: extract bcc native part
This commit is contained in:
15
README.md
15
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
|
||||
|
||||
[](https://starchart.cc/Lil-House/Pyarmor-Static-Unpack-1shot)
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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]
|
||||
|
Reference in New Issue
Block a user