Skip to content

Commit 4d7831c

Browse files
committed
Added the Binary Ninja version of annotate_bcc
1 parent 5e3541f commit 4d7831c

4 files changed

Lines changed: 232 additions & 42 deletions

File tree

bn/bn_annotate_bcc.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
"""
2+
Minimum Python version: 3.9
3+
4+
This Binary Ninja script adds comments in the decompiler view for all references to constants in
5+
BCC functions.
6+
7+
IMPORTANT: For this to work, the pointer variable must be of type QWORD* in every function.
8+
Unlike the IDA one, the first dereference is not our target expressions, instead, it follows the pattern:
9+
r12 = *(arg1 + (sx.q(*(arg1 + 0x10)) << 3) + 0x10)
10+
11+
Also unlike IDA, there are no v4[1], v4[2], v4[3]... Instead, Binary Ninja display those access as pointer arithmetics like:
12+
int64_t rsi = (*(rax_4 + 0x1d0))()
13+
"""
14+
15+
import re
16+
import json
17+
from typing import Optional
18+
19+
# This should be the path to your *.elf.json
20+
JSON_PATH = ""
21+
22+
def wait_modification():
23+
return bv.update_analysis_and_wait()
24+
25+
def open_json_file(json_path: str):
26+
if not json_path:
27+
return
28+
with open(json_path, 'r') as fp:
29+
print(f"Opened file {json_path} with read permissions.")
30+
return json.load(fp)
31+
32+
33+
def create_and_rename_function(offset: int, name: str) -> None:
34+
"""Create function at offset and rename it."""
35+
existing_func = bv.get_function_at(offset)
36+
if existing_func:
37+
print(f"Function already exists at {hex(offset)}, renaming to '{name}'")
38+
existing_func.name = name
39+
return
40+
41+
func = bv.create_user_function(offset)
42+
if not func:
43+
print(f"Failed to create function at {hex(offset)}")
44+
return
45+
46+
wait_modification()
47+
48+
func.name = name
49+
print(f"Created and named function '{name}' at {hex(offset)}")
50+
51+
class ConstsAnnotator():
52+
53+
def __init__(self, func, consts: list) -> None:
54+
self.alias_map: dict[str, str] = {}
55+
self.comments_added = 0
56+
self.func = func
57+
self.consts = consts
58+
self.target_var = None
59+
60+
def find_target_constant(self):
61+
"""Find the target constant pointer and set consts_base."""
62+
for block in self.func.hlil:
63+
for instr in block:
64+
if (type(instr) == HighLevelILVarInit and
65+
hasattr(instr, 'detailed_operands') and
66+
type(instr.detailed_operands[1][1]) == HighLevelILDeref): # HighLevelILDeref (arg1 + (sx.q(*(arg1 + 0x10)) << 3) + 0x10)
67+
68+
target_var = instr.operands[0]
69+
70+
print(f"Found consts pointer identifier {instr}")
71+
return target_var
72+
return None
73+
74+
def map_constant_xrefs(self, target_var):
75+
refs = self.func.get_hlil_var_refs(target_var)
76+
print(f"Found XRefs: {refs}" if refs else "There are no XRefs")
77+
78+
for ref in refs:
79+
ref_dest = ref.func.hlil[ref.expr_id].operands[0]
80+
if hasattr(ref_dest, 'identifier'): # Variable type
81+
ref_dest_id = ref_dest.identifier
82+
elif hasattr(ref_dest, 'var') and hasattr(ref_dest.var, 'identifier'): # HighLevelILVar
83+
ref_dest_id = ref_dest.var.identifier
84+
else:
85+
continue
86+
87+
ref_src = ref.func.hlil[ref.expr_id].operands[1]
88+
# We will only track variable aliases, like v1012 = v4
89+
# Deref's won't be tracked
90+
if hasattr(ref_src, 'var') and hasattr(ref_src.var, 'identifier'): # HighLevelILVar
91+
ref_src_id = ref_src.var.identifier
92+
else:
93+
continue
94+
95+
if ref_src_id == target_var.identifier:
96+
self.alias_map[ref_dest_id] = ref_src_id
97+
98+
def find_constant(self):
99+
self.target_var = self.find_target_constant()
100+
if self.target_var:
101+
self.map_constant_xrefs(self.target_var)
102+
103+
print("\nAlias Mapping Results:")
104+
if self.alias_map:
105+
for dest_var, src_var in self.alias_map.items():
106+
print(f" {dest_var} -> {src_var}")
107+
else:
108+
print(" No aliases found")
109+
110+
def place_comments(self):
111+
def visit_expr(expr):
112+
# Check if this is a dereference of our target variable + offset
113+
if (type(expr) == HighLevelILDeref and
114+
type(expr.src) == HighLevelILAdd and
115+
type(expr.src.left) == HighLevelILVar and
116+
type(expr.src.right) == HighLevelILConst):
117+
118+
var_id = expr.src.left.var.identifier
119+
offset = expr.src.right.constant
120+
121+
# Check if this is our target variable or an alias to it
122+
if (var_id == self.target_var.identifier or
123+
self.alias_map.get(var_id) == self.target_var.identifier):
124+
125+
# Convert byte offset to array index (offset >= 24 means index >= 3)
126+
if offset >= 24 and (offset - 24) % 8 == 0:
127+
array_index = (offset - 24) // 8 + 3
128+
129+
if 0 <= array_index - 3 < len(self.consts):
130+
comment = self.consts[array_index - 3]
131+
addr = getattr(expr, 'address', None) or getattr(expr.instr, 'address', None)
132+
if addr:
133+
try:
134+
self.func.set_comment_at(addr, comment)
135+
self.comments_added += 1
136+
print(f"Added comment '{comment}' at {hex(addr)}")
137+
except Exception as e:
138+
print(f"Failed to set comment: {e}")
139+
140+
if hasattr(expr, 'operands'):
141+
for operand in expr.operands:
142+
if hasattr(operand, '__iter__') and not isinstance(operand, str):
143+
for sub_op in operand:
144+
visit_expr(sub_op)
145+
elif hasattr(operand, 'operands'):
146+
visit_expr(operand)
147+
148+
for block in self.func.hlil:
149+
for instr in block:
150+
visit_expr(instr)
151+
152+
153+
def annotate_consts_in_decompilation(ea: int, consts: list) -> None:
154+
func = bv.get_function_at(ea)
155+
if not func:
156+
print(f"No function found at {hex(ea)}")
157+
return
158+
159+
if not func.hlil:
160+
print(f"No HLIL for function at {hex(ea)}")
161+
return
162+
163+
annotator = ConstsAnnotator(func, consts)
164+
annotator.find_constant()
165+
annotator.place_comments()
166+
167+
print(f"Added {annotator.comments_added} comments to function at {hex(ea)}")
168+
169+
wait_modification()
170+
171+
def main() -> None:
172+
data = open_json_file(JSON_PATH)
173+
if data is None:
174+
print("Failed to load JSON data")
175+
return
176+
177+
print(f"Processing {len(data)} entries...")
178+
179+
for entry in data:
180+
offset = entry['offset']
181+
name = entry['name']
182+
consts = entry['consts']
183+
184+
print("-------------------------------------------------------")
185+
print(f"Offset: {hex(offset)}, Name: {name}, Constants: {len(consts)}")
186+
187+
create_and_rename_function(offset, name)
188+
annotate_consts_in_decompilation(offset, consts)
189+
190+
main()
File renamed without changes.
Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,42 @@
1-
"""IDAPython implementation of "get_key_via_md5" for statically obtaining key."""
2-
3-
import ida_bytes
4-
import hashlib
5-
6-
# This string is a unique license ID of the person who obfuscated the script, in the free version it would always be 000000 and in paid versions its unique per person
7-
PYARMOR_STRING = b"pyarmor-vax-007106\x00\x00"
8-
9-
# References to these are in the "get_key_via_md5" function
10-
"""
11-
md5_process(
12-
v6,
13-
(char *)&unk_64944060 + g_dword_64944050_0x20_rsaoffset,
14-
(unsigned int)g_dword_64944054_0x10E_rsakeylen);// rsa key
15-
"""
16-
INFO_BLOB_ADDR = 0x64944060
17-
# First xmmword that is xored:
18-
# xmmword_64948140 = (__int128)_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_64948140), si128);
19-
RSA_KEY2_ADDR = 0x64948140
20-
# From a global dword passed to md5_process
21-
RSA_KEY2_SIZE = 0x10E
22-
# Byte value that RSA_KEY2 is xored with
23-
RSA_XOR_KEY = 0xF1
24-
25-
26-
md = hashlib.md5()
27-
md.update(PYARMOR_STRING)
28-
29-
rsakey_size = int.from_bytes(ida_bytes.get_bytes(INFO_BLOB_ADDR - 0xC, 4), 'little')
30-
rsakey = ida_bytes.get_bytes(INFO_BLOB_ADDR + 0x20, rsakey_size)
31-
32-
sig_offset = int.from_bytes(ida_bytes.get_bytes(INFO_BLOB_ADDR - 0x8, 4), 'little')
33-
hashed_area_size = int.from_bytes(ida_bytes.get_bytes(INFO_BLOB_ADDR + sig_offset + 4, 4), 'little')
34-
hashed_area = ida_bytes.get_bytes(INFO_BLOB_ADDR + sig_offset + 0x20, hashed_area_size)
35-
36-
rsakey2 = bytes([b ^ RSA_XOR_KEY for b in ida_bytes.get_bytes(RSA_KEY2_ADDR, RSA_KEY2_SIZE)])
37-
38-
md.update(rsakey)
39-
md.update(hashed_area)
40-
md.update(rsakey2)
41-
42-
print(md.hexdigest())
1+
"""IDAPython implementation of "get_key_via_md5" for statically obtaining key."""
2+
3+
import ida_bytes
4+
import hashlib
5+
6+
# This string is a unique license ID of the person who obfuscated the script, in the free version it would always be 000000 and in paid versions its unique per person
7+
PYARMOR_STRING = b"pyarmor-vax-007106\x00\x00"
8+
9+
# References to these are in the "get_key_via_md5" function
10+
"""
11+
md5_process(
12+
v6,
13+
(char *)&unk_64944060 + g_dword_64944050_0x20_rsaoffset,
14+
(unsigned int)g_dword_64944054_0x10E_rsakeylen);// rsa key
15+
"""
16+
INFO_BLOB_ADDR = 0x64944060
17+
# First xmmword that is xored:
18+
# xmmword_64948140 = (__int128)_mm_xor_si128(_mm_load_si128((const __m128i *)&xmmword_64948140), si128);
19+
RSA_KEY2_ADDR = 0x64948140
20+
# From a global dword passed to md5_process
21+
RSA_KEY2_SIZE = 0x10E
22+
# Byte value that RSA_KEY2 is xored with
23+
RSA_XOR_KEY = 0xF1
24+
25+
26+
md = hashlib.md5()
27+
md.update(PYARMOR_STRING)
28+
29+
rsakey_size = int.from_bytes(ida_bytes.get_bytes(INFO_BLOB_ADDR - 0xC, 4), 'little')
30+
rsakey = ida_bytes.get_bytes(INFO_BLOB_ADDR + 0x20, rsakey_size)
31+
32+
sig_offset = int.from_bytes(ida_bytes.get_bytes(INFO_BLOB_ADDR - 0x8, 4), 'little')
33+
hashed_area_size = int.from_bytes(ida_bytes.get_bytes(INFO_BLOB_ADDR + sig_offset + 4, 4), 'little')
34+
hashed_area = ida_bytes.get_bytes(INFO_BLOB_ADDR + sig_offset + 0x20, hashed_area_size)
35+
36+
rsakey2 = bytes([b ^ RSA_XOR_KEY for b in ida_bytes.get_bytes(RSA_KEY2_ADDR, RSA_KEY2_SIZE)])
37+
38+
md.update(rsakey)
39+
md.update(hashed_area)
40+
md.update(rsakey2)
41+
42+
print(md.hexdigest())

0 commit comments

Comments
 (0)