99 lines
2.9 KiB
Python
99 lines
2.9 KiB
Python
import struct
|
|
import sys
|
|
import argparse
|
|
|
|
# The complete instruction set mapped from the VMContext dispatch table
|
|
OPCODES = {
|
|
1: "BITWISE_NOT",
|
|
2: "VERIFY_FILE_HASH",
|
|
6: "PUSH_TEXT_BASE",
|
|
8: "JMP_IF_GTE",
|
|
12: "PUSH_RODATA_SIZE",
|
|
13: "PUSH_SIZE",
|
|
16: "VERIFY_HASH",
|
|
21: "STORE_VAR_NO_CLONE",
|
|
22: "AES_DECRYPT",
|
|
23: "BITVEC_COMPARE",
|
|
24: "POP",
|
|
25: "APPEND_IMM",
|
|
28: "JMP_IF_EQUAL",
|
|
29: "PUSH_IMM",
|
|
30: "CLONE_PUSH",
|
|
33: "STORE_TO_INPUT",
|
|
38: "NOP",
|
|
39: "ALIGN",
|
|
40: "DECRYPT_BLOCK",
|
|
44: "SLICE_SECOND_HALF",
|
|
45: "JMP_IF_NOT_FLAG",
|
|
47: "JMP_IF_NOT_EQUAL",
|
|
48: "JMP_IF_FLAG",
|
|
53: "COPY_VAR_TO_STACK",
|
|
57: "CONCAT",
|
|
59: "STORE_TO_INPUT",
|
|
61: "PUSH_RODATA_BASE",
|
|
63: "BIGINT_ADD",
|
|
64: "JMP",
|
|
66: "SHA256",
|
|
69: "NOP",
|
|
74: "PUSH_TEXT_SIZE",
|
|
75: "PUSH_VAR",
|
|
78: "NOP",
|
|
79: "READ_FILE",
|
|
80: "RSA_VERIFY",
|
|
81: "SHR1",
|
|
83: "HALT",
|
|
84: "SLICE_FIRST_HALF",
|
|
87: "PUSH_PTR",
|
|
88: "TRUNCATE",
|
|
89: "SLICE",
|
|
90: "STORE_VAR",
|
|
91: "PUSH_NULL",
|
|
94: "LOAD_FILE",
|
|
}
|
|
|
|
def disassemble(file_path, is_raw_file=False):
|
|
try:
|
|
with open(file_path, 'rb') as f:
|
|
data = f.read()
|
|
except FileNotFoundError:
|
|
print(f"Error: Could not find file '{file_path}'")
|
|
return
|
|
|
|
# If parsing the raw connivance.bin directly from disk, we must skip
|
|
# the 256-byte RSA signature header to reach the first instruction.
|
|
# If parsing a memory dump of the decrypted 'v8' buffer, start at 0.
|
|
start_offset = 256 if is_raw_file else 0
|
|
bytecode = data[start_offset:]
|
|
|
|
print(f"{'Offset':<8} | {'Opcode':<6} | {'Mnemonic':<20} | {'Operand 1 (64-bit)':<20} | {'Operand 2 (32-bit)'}")
|
|
print("-" * 82)
|
|
|
|
# Read exactly 16 bytes at a time (sizeof VMInstruction)
|
|
for ip in range(0, len(bytecode), 16):
|
|
chunk = bytecode[ip : ip+16]
|
|
|
|
# Stop if we hit a partial instruction at the end of the buffer
|
|
if len(chunk) < 16:
|
|
break
|
|
|
|
# Unpack the struct using Little Endian ('<')
|
|
# H: uint16 (opcode)
|
|
# H: uint16 (padding)
|
|
# I: uint32 (operand2)
|
|
# Q: uint64 (operand1)
|
|
opcode, pad, op2, op1 = struct.unpack('<HHIQ', chunk)
|
|
|
|
# Look up the command name, default to UNK_XX if missing
|
|
mnemonic = OPCODES.get(opcode, f"UNK_{opcode}")
|
|
|
|
# Print the formatted assembly line
|
|
print(f"{ip:08X} | {opcode:<6} | {mnemonic:<20} | 0x{op1:<18X} | 0x{op2:X}")
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="VM Bytecode Disassembler")
|
|
parser.add_argument("target_file", help="Path to the dumped .bin file")
|
|
parser.add_argument("--raw", action="store_true", help="Skip the 256-byte header if analyzing the raw encrypted file on disk")
|
|
|
|
args = parser.parse_args()
|
|
disassemble(args.target_file, is_raw_file=args.raw)
|