-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathvtable_diff.py
More file actions
159 lines (118 loc) · 4.36 KB
/
vtable_diff.py
File metadata and controls
159 lines (118 loc) · 4.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import click
import json
import sys
from analysis_utils import get_correct_switch
from utils import eprint, create_r2_byte_pattern, sync_r2_output
import r2pipe
PACKET_INTERFACE_DISPATCHER_SIG = "49 8B 40 10 4C 8B 50 38"
"""
Signature for some dispatcher function that delegates packet opcodes to
a ZoneDown packet interface. In IDA it has a length of approximately 0x28D5.
It should just contain a large switch statement with handlers that call a
function pointer and look like this:
```
mov rax, [rcx]
lea r9, [r10+10h]
jmp qword ptr [rax+10h]
```
In reality this dispatcher actually does nothing, as it delegates to vtable
containing only nullsubs. However, the order of these nullsubs in the vtable
is fairly stable across versions for the ZoneDown opcodes. For minor patches,
they might also work for ZoneUp opcodes.
"""
def get_opcode_offset(r2):
orig_loc = r2.cmd("s") # Save original spot
r2.cmd("aei; aeim; aeip") # Initialize ESIL VM, stack, instruction pointer
r2.cmd('"aesue rax,0x0,>"') # continue until rax changes
r2.cmd('"aesue rax,0x0,>"') # continue until rax changes again
r2.cmd("aeso") # step
r2.cmd("aer rax=0x200") # set rax to some arbitrary number
r2.cmd("aeso") # step
regs = r2.cmdj("arj")
new_rax = regs["rax"]
opcode_offset = 0x200 - new_rax
# Clear the ESIL environment
r2.cmd("ar0; aeim-; aei-")
r2.cmd(f"s {orig_loc}") # Seek back to original spot
return opcode_offset
def extract_opcode_data(exe_file):
r2 = r2pipe.open(exe_file, ["-2"])
eprint(f"Radare loaded {exe_file}")
sync_r2_output(r2)
p = create_r2_byte_pattern(PACKET_INTERFACE_DISPATCHER_SIG)
target = r2.cmd(f"/x {p}").split()[0] # Find byte pattern
r2.cmd(f"s {target}") # Seek to target
## STEP 1: Grab switch cases
r2.cmd("f--") # Delete existing flags
r2.cmd("afr") # Analyze function recursively
switch_cases = r2.cmdj(f"fj")
eprint(f" Loaded switch cases")
## STEP 2: Grab opcode offset
opcode_offset = get_opcode_offset(r2)
eprint(f" Found opcode offset: {opcode_offset}")
r2.quit()
eprint(f" Grabbed blocks from packet handler")
## STEP 4: Process data
opcodes_db = dict()
switch_ea, packet_handler_switch = get_correct_switch(target, switch_cases)
eprint(f" Found switch at {switch_ea}")
vtable_offset = 0x10
for data in packet_handler_switch.values():
if len(data["opcodes"]) > 10:
continue
opcodes_db[vtable_offset] = int(data["opcodes"][0]) + opcode_offset
vtable_offset += 0x8
eprint(f" Loaded {len(opcodes_db)} cases from packet handler")
return opcodes_db
def find_opcode_matches(old_opcodes_db, new_opcodes_db):
matches = []
for offset, old_opcode in old_opcodes_db.items():
if offset in new_opcodes_db:
matches.append((old_opcode, new_opcodes_db[offset]))
return matches
def diff_exes(old_exe, new_exe):
old_opcodes_db = extract_opcode_data(old_exe)
new_opcodes_db = extract_opcode_data(new_exe)
if len(old_opcodes_db) != len(new_opcodes_db):
eprint(
f"ERROR: vtables have different sizes: {len(old_opcodes_db)} != {len(new_opcodes_db)}. Refusing to continue."
)
sys.exit(1)
opcodes_found = find_opcode_matches(old_opcodes_db, new_opcodes_db)
opcodes_object = []
for old, new in opcodes_found:
opcodes_object.append(
{
"old": [hex(old)],
"new": [hex(new)],
}
)
return opcodes_object
@click.command()
@click.argument(
"old_exe", type=click.Path(exists=True, dir_okay=False, resolve_path=True)
)
@click.argument(
"new_exe", type=click.Path(exists=True, dir_okay=False, resolve_path=True)
)
def vtable_diff(old_exe, new_exe):
"""
Generates an opcode diff file by comparing vtables. See comments in file
for a detailed explanation.
This script outputs to stdout, so pipe it to a json file.
The format of the output is a list (all fields are optional):
\b
[
{
"old": [opcode],
"new": [opcode],
},
...
]
Example:
python vtable_diff.py ffxiv_dx11.old.exe ffxiv_dx11.new.exe > diff.json
"""
opcodes_object = diff_exes(old_exe, new_exe)
print(json.dumps(opcodes_object, indent=2))
if __name__ == "__main__":
vtable_diff()