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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
lea64 = b'\x48\x8d'
call64 = b'\xFF'
little = lambda x: int.from_bytes(x, byteorder='little')
# iterate over matches for lea64
def leas(f, start=None, end=None, op=lea64):
match = f.find(op, start, end)
while match != -1:
yield match
match = f.find(op, match + 1, end)
# give the position and loaded virtual address for each lea match
def addr(f, start=None, end=None, op=lea64, reg=1):
arg = len(op) + reg
for i in leas(f, start, end, op):
yield [i, i + arg + 4 + little(f[i + arg: i + arg + 4])]
# VA offsets for .text and .rdata
def offsets(f):
# .text567_size_want
off = (f.index(b".text"), f.index(b".rdata"))
return [a - b for a, b in zip(
[little(f[i + 0xc : i + 0x10]) for i in off],
[little(f[i + 0x14 : i + 0x18]) for i in off],
)]
targets = [
b'Could not verify RL version',
b'Dunno how the user got here',
b'User wishes to inject anyway',
]
# gives storage locations of target phrases accounting for offsets
def rel32(f):
text, rdata = offsets(f)
return [f.index(i) + rdata - text for i in targets]
# gives positions of references to the target phrases
def refs(f, rel=None, it=None):
default = rel is None and it is None
rel, it = rel or rel32(f), it or addr(f)
res = tuple(zip(*[(rel.index(x), i) for i, x in it if x in rel]))
if default:
assert res[0] == (0, 1, 2, 0, 1, 2)
return (res[1][:3], res[1][3:])
return res
jcc32 = [b'\x0F' + int.to_bytes(i, 1, 'little') for i in range(0x80, 0x90)]
jcc8 = list(bytes(range(0x70, 0x80)))
# gives "inject anyway" jmp RVAs
def branches(f, ref=None):
ref = ref or refs(f)
def inner(a, b):
seq = f[a : b]
op32 = [seq.index(i) for i in jcc32 if i in seq]
op8 = [seq.index(i) for i in jcc8 if i in seq]
loc32 = [a + i + 6 + little(seq[i + 2 : i + 6]) for i in op32]
loc8 = [a + i + 2 + little(seq[i + 1 : i + 2]) for i in op8]
return loc8 + loc32
return [max(m for m in inner(i, j) if m <= k) for i, j, k in ref]
optionals = (
(2, "Magic"),
(1, "MajorLinkerVersion"),
(1, "MinorLinkerVersion"),
(4, "SizeOfCode"),
(4, "SizeOfInitializedData"),
(4, "SizeOfUninitializedData"),
(4, "AddressOfEntryPoint"),
(4, "BaseOfCode"),
(8, "ImageBase"),
(4, "SectionAlignment"),
(4, "FileAlignment"),
(2, "MajorOperatingSystemVersion"),
(2, "MinorOperatingSystemVersion"),
(2, "MajorImageVersion"),
(2, "MinorImageVersion"),
(2, "MajorSubsystemVersion"),
(2, "MinorSubsystemVersion"),
(4, "Win32VersionValue"),
(4, "SizeOfImage"),
(4, "SizeOfHeaders"),
(4, "CheckSum"),
(2, "Subsystem"),
(2, "DllCharacteristics"),
(8, "SizeOfStackReserve"),
(8, "SizeOfStackCommit"),
(8, "SizeOfHeapReserve"),
(8, "SizeOfHeapCommit"),
(4, "LoaderFlags"),
(4, "NumberOfRvaAndSizes"),
)
def header(name, offset=0):
for size, field in optionals:
if field == name:
return (offset, offset + size)
offset += size
raise Exception()
# gives offset and size of DLL directory
def idata(f, off=None):
off = off or offsets(f)
magic = f.index(b'\x0b\x02')
iaddr = header("NumberOfRvaAndSizes", magic + 0xc)
isize = header("NumberOfRvaAndSizes", magic + 0x10)
return little(f[slice(*iaddr)]) - off[1], little(f[slice(*isize)])
ox = lambda x: tuple(map(hex, x))
# gives IAT start and end
def user32(f, dlls=None, size=None, off=None):
off = off or offsets(f)
dlls, size = idata(f, off) if dlls is None else (dlls, size)
rows = [dlls + i for i in range(0, size, 20)]
rvas = [little(f[i + 0xc : i + 0x10]) for i in rows]
tabs = [little(f[i + 0x10 : i + 0x14]) for i in rows]
matching = b'USER32.dll'
substr = lambda i: f[i - off[1] : i - off[1] + len(matching)]
ptrs = [n for n, i in enumerate(rvas) if i != 0 and substr(i) == matching]
assert len(ptrs) == 1
ptr = ptrs[0]
end = min(i for i in tabs if i > tabs[ptr])
return tabs[ptr] - off[1], end - off[1]
# gives start of 8 byte IAT line as a .text RVA
def fns(f, iat=None, end=None, off=None):
off = off or offsets(f)
iat, end = user32(f, off=off) if iat is None else (dlls, size)
rvas = [little(f[i : i + 4]) for i in range(iat, end, 8)]
vas = [i - off[1] for i in rvas if i != 0]
hints = [f[i : i + 2] for i in vas]
names = [f[i + 2 : f.index(b'\x00', i + 2)] for i in vas]
return iat + names.index(b'MessageBoxA') * 8 + off[1] - off[0]
def shunts(f, dbg=True):
loads, msg = refs(f), fns(f)
calls = [refs(f, (msg,), addr(f, *ref[:2], call64))[1][0] for ref in loads]
skips = branches(f, loads)
paired = tuple(zip(calls, skips))
if dbg:
print(tuple(map(ox, paired)))
return paired
jmp = lambda x: b'\xE9' + int.to_bytes(x, 4, "little")
def mask(f, space=6):
prev, out = 0, b''
for pos, end in shunts(f):
update = jmp(end - pos - 5)
assert len(update) == 5
out += f[prev:pos] + update + b'\x90' * (space - len(update))
prev = pos + space
out += f[prev:]
assert len(f) == len(out)
return out
def dll(path, dest=None):
global Path
from pathlib import Path
path = Path(path)
path = path if path.is_file() else path / "bakkesmod.dll"
with open(path, "rb") as fp:
f = fp.read()
f = mask(f)
dest = path.parents[0] if dest is None else dest
dest = dest if dest.is_file() else dest / "bakkesmod_promptless.dll"
with open(dest, "wb") as fp:
fp.write(f)
if __name__ == "__main__":
import sys
dll(*sys.argv[1:])
|