Files
ctf/2026/cscg/rev/connivance/dissasemble_connivance.py
2026-03-22 21:53:40 +01:00

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)