2025-02-27 14:01:14 +08:00
import hashlib
2025-11-04 22:56:03 +08:00
import logging
from util import dword , bytes_sub
2025-02-27 14:01:14 +08:00
2025-10-29 18:50:31 +08:00
GLOBAL_CERT = bytes . fromhex ( """
2025-02-27 14:01:14 +08:00
30 82 01 0 a 02 82 01 01 00 bf 65 30 f3 bd 67 e7
a6 9 d f8 db 18 b2 b9 c1 c0 5 f fe fb e5 4 b 91 df
6 f 38 da 51 cc ea c4 d3 04 bd 95 27 86 c1 13 ca
73 15 44 4 d 97 f5 10 b9 52 21 72 16 c8 b2 84 5 f
45 56 32 e7 c2 6 b ad 2 b d9 df 52 d6 e9 d1 2 a ba
35 e4 43 ab 54 e7 91 c5 ce d1 f1 ba a5 9 f f4 ca
db 89 04 3 d f8 9 f 6 a 8 b 8 a 29 39 f8 4 c 0 d b8 a0
6 d 51 c4 74 24 64 fe 1 a 23 97 f3 61 ea de c8 97
dc 57 60 34 be 2 c 18 50 3 b d1 76 3 b 49 2 a 39 9 a
37 18 53 8 f 1 d 4 c 82 b1 a0 33 43 57 19 ad 67 e7
af 09 fb 04 54 a9 ea c0 c1 e9 32 6 c 77 92 7 f 9 f
7 c 08 7 c e8 a1 5 d a4 fc 40 e6 6 e 18 db bf 45 53
4 b 5 c a7 9 d f2 8 f 7 e 6 c 04 b0 4 d ee 99 25 9 a 87
84 6 e 9 e fe 3 c 72 ec b0 64 dd 2 e db ad 32 fa 1 d
4 b 2 c 1 a 78 85 7 c bc 2 c d0 d7 83 77 5 f 92 d5 db
59 10 96 53 2 e 5 d c7 42 12 b8 61 cb 2 c 5 f 46 14
9 e 93 b0 53 21 a2 74 34 2 d 02 03 01 00 01
2025-10-29 18:50:31 +08:00
""" )
2025-02-27 14:01:14 +08:00
2025-11-04 22:56:03 +08:00
logger = logging . getLogger ( " runtime " )
2025-02-27 14:01:14 +08:00
class RuntimeInfo :
def __init__ ( self , file_path : str ) - > None :
self . file_path = file_path
2025-10-29 18:50:31 +08:00
if file_path . endswith ( " .pyd " ) :
2025-02-27 14:01:14 +08:00
self . extract_info_win64 ( )
else :
# TODO: implement for other platforms
self . extract_info_win64 ( )
2025-11-20 16:06:53 +08:00
self . serial_number = self . part_1 [ 12 : 18 ] . decode ( " utf-8 " , errors = " replace " )
2025-02-27 14:01:14 +08:00
self . runtime_aes_key = self . calc_aes_key ( )
def __str__ ( self ) - > str :
2025-10-29 18:50:31 +08:00
trial = self . serial_number == " 000000 "
product = " "
2025-02-27 14:01:14 +08:00
for c in self . part_3 [ 2 : ] :
if 32 < = c < = 126 :
product + = chr ( c )
else :
break
2025-10-29 18:50:31 +08:00
return f """ \
2025-02-27 14:01:14 +08:00
== == == == == == == == == == == ==
2025-10-29 18:50:31 +08:00
Pyarmor Runtime ( { " Trial " if trial else self . serial_number } ) Information :
2025-02-27 14:01:14 +08:00
Product : { product }
AES key : { self . runtime_aes_key . hex ( ) }
Mix string AES nonce : { self . mix_str_aes_nonce ( ) . hex ( ) }
2025-10-29 18:50:31 +08:00
== == == == == == == == == == == == """
2025-02-27 14:01:14 +08:00
def __repr__ ( self ) - > str :
2025-10-29 18:50:31 +08:00
return f " RuntimeInfo(part_1= { self . part_1 } , part_2= { self . part_2 } , part_3= { self . part_3 } ) "
2025-02-27 14:01:14 +08:00
def extract_info_win64 ( self ) - > None :
2025-10-29 18:50:31 +08:00
"""
2025-02-27 14:01:14 +08:00
Try to find useful information from ` pyarmor_runtime . pyd ` file ,
and store all three parts in the object .
2025-10-29 18:50:31 +08:00
"""
with open ( self . file_path , " rb " ) as f :
2025-02-27 14:01:14 +08:00
data = f . read ( 16 * 1024 * 1024 )
2026-02-23 21:03:20 +08:00
cur = data . find ( b " pyarmor-vax " )
if cur == - 1 :
# Specially, check UPX (GH-12, GH-35)
if data . find ( b " UPX! " ) != - 1 and data . find ( b " UPX0 " ) != - 1 :
logger . error (
f " { self . file_path } seems to be packed by UPX. Before it can be processed, you need to unpack it first: Download UPX from https://github.com/upx/upx, and run `upx -d { self . file_path } ` (you may need to escape the file path) in the command line. "
)
else :
logger . error (
f " { self . file_path } does not contain ' pyarmor-vax ' . Maybe it ' s packed, obfuscated, or generated by an unsupported version of Pyarmor. "
)
raise ValueError ( f " { self . file_path } does not contain ' pyarmor-vax ' " )
2025-02-27 14:01:14 +08:00
2025-10-29 18:50:31 +08:00
if data [ cur + 11 : cur + 18 ] == b " \x00 " * 7 :
2026-02-23 21:03:20 +08:00
# Do not log. Skip this file silently and find another.
2025-10-29 18:50:31 +08:00
raise ValueError ( f " { self . file_path } is a runtime template " )
2025-03-01 15:25:03 +08:00
2025-11-04 22:56:03 +08:00
# Align with pyd file and executable address:
# In .pyd files b"pyarmor-vax" locates at 0x???2C
# But not .so
data = bytearray ( bytes_sub ( data , cur - 0x2C , 0x800 ) )
if data [ 0x5C ] & 1 != 0 :
logger . error (
2026-02-23 21:03:20 +08:00
f ' External key file " .pyarmor.ikey " is not supported yet, but it will be supported once we get a sample (like this one). Please open an issue on https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot/issues to make this tool stronger. ( { self . file_path } ) '
2025-11-04 22:56:03 +08:00
)
raise NotImplementedError ( f ' { self . file_path } uses " .pyarmor.ikey " ' )
if dword ( data , 0x4C ) != 0 :
xor_flag = 0x60 + dword ( data , 0x48 )
xor_target = 0x60 + dword ( data , 0x50 )
xor_length = int . from_bytes ( data [ xor_flag + 1 : xor_flag + 4 ] , " little " )
if data [ xor_flag ] == 1 :
for i in range ( xor_length ) :
# MUT data
data [ xor_target + i ] ^ = data [ xor_flag + 4 + i ]
self . part_1 = bytes_sub ( data , 0x2C , 20 )
2025-02-27 14:01:14 +08:00
2025-11-04 22:56:03 +08:00
part_2_offset = dword ( data , 0x50 )
part_2_len = dword ( data , 0x54 )
self . part_2 = bytes_sub ( data , 0x60 + part_2_offset , part_2_len )
2025-02-27 14:01:14 +08:00
2025-11-04 22:56:03 +08:00
var_a1 = 0x60 + dword ( data , 0x58 )
part_3_len = dword ( data , var_a1 + 4 )
self . part_3 = bytes_sub ( data , var_a1 + 0x20 , part_3_len )
2025-02-27 14:01:14 +08:00
def calc_aes_key ( self ) - > bytes :
2025-10-29 18:50:31 +08:00
return hashlib . md5 (
self . part_1 + self . part_2 + self . part_3 + GLOBAL_CERT
) . digest ( )
2025-02-27 14:01:14 +08:00
def mix_str_aes_nonce ( self ) - > bytes :
return self . part_3 [ : 12 ]
2025-10-12 20:42:39 +08:00
@classmethod
2025-10-29 18:50:31 +08:00
def default ( cls ) - > " RuntimeInfo " :
2025-10-12 20:42:39 +08:00
instance = cls . __new__ ( cls )
2025-10-29 18:50:31 +08:00
instance . file_path = " <default> "
instance . part_1 = b " pyarmor-vax-000000 \x00 \x00 "
instance . part_2 = bytes . fromhex ( """
2025-10-12 20:42:39 +08:00
30 81 89 02 81 81 00 A8 ED 64 F4 83 49 13 FC 0 F
86 6 F 00 5 A 8 F E4 91 AA ED 1 C EA D4 BB 4 C 3 F 7 C
24 21 01 A8 D0 7 D 93 F4 BF E7 FB 8 C 06 57 88 6 A
2 E 9 B 54 53 D5 7 B 8 F F6 83 DF 72 00 42 A3 2 D 18
30 AD 3 A E4 F1 E4 3 A 3 C 8 C EA F5 46 F3 BB 75 62
11 84 FB 3 F 3 B 4 C 35 61 4 E 46 A1 E0 9 E 3 C B6 7 A
BA 52 C5 B6 40 F6 AD AB BC D5 CF 5 B 40 CB 8 D 13
C4 28 B8 90 93 C4 76 01 09 8 E 05 1 E 61 FA 90 4 C
BF 67 D4 A7 D5 82 C1 02 03 01 00 01
2025-10-29 18:50:31 +08:00
""" )
instance . part_3 = bytes . fromhex ( """
2025-10-12 20:42:39 +08:00
69 2 E 6 E 6 F 6 E 2 D 70 72 6 F 66 69 74 73 E7 5 A 41
9 B DC 77 53 CA 1 D E7 04 EB EF DA C9 A3 6 C 0 F 7 B
00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00
2025-10-29 18:50:31 +08:00
""" )
instance . serial_number = " 000000 "
2025-10-12 20:42:39 +08:00
instance . runtime_aes_key = instance . calc_aes_key ( )
return instance
2025-02-27 14:01:14 +08:00
2025-10-29 18:50:31 +08:00
if __name__ == " __main__ " :
2025-02-27 14:01:14 +08:00
import sys
2025-10-29 18:50:31 +08:00
2025-02-27 14:01:14 +08:00
if len ( sys . argv ) < 2 :
2025-10-29 18:50:31 +08:00
print ( " Usage: python runtime.py path/to/pyarmor_runtime[.pyd|.so|.dylib] " )
2025-02-27 14:01:14 +08:00
exit ( 1 )
for i in sys . argv [ 1 : ] :
runtime = RuntimeInfo ( i )
print ( runtime )