forked from jthuraisamy/SysWhispers2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
syswhispers.py
270 lines (229 loc) · 11.4 KB
/
syswhispers.py
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
#!/usr/bin/python3
import argparse
import json
import os
import random
import struct
class SysWhispers(object):
def __init__(self, function_prefix):
self.__function_prefix = function_prefix
self.seed = random.randint(2 ** 28, 2 ** 32 - 1)
self.typedefs: list = json.load(open('./data/typedefs.json'))
self.prototypes: dict = json.load(open('./data/prototypes.json'))
def generate(self, function_names: list = (), basename: str = 'syscalls'):
if not function_names:
function_names = list(self.prototypes.keys())
elif any([f not in self.prototypes.keys() for f in function_names]):
raise ValueError('Prototypes are not available for one or more of the requested functions.')
# Change default function prefix.
if self.__function_prefix != 'Nt':
new_function_names = []
for function_name in function_names:
new_function_name = function_name.replace('Nt', self.__function_prefix, 1)
if new_function_name != function_name:
self.prototypes[new_function_name] = self.prototypes[function_name]
del self.prototypes[function_name]
new_function_names.append(new_function_name)
function_names = new_function_names
# Write C file.
with open ('./data/base.c', 'rb') as base_source:
with open(f'{basename}.c', 'wb') as output_source:
base_source_contents = base_source.read().decode()
base_source_contents = base_source_contents.replace('<BASENAME>', os.path.basename(basename), 1)
output_source.write(base_source_contents.encode())
# Write ASM file.
basename_suffix = 'stubs'
basename_suffix = basename_suffix.capitalize() if os.path.basename(basename).istitle() else basename_suffix
basename_suffix = f'_{basename_suffix}' if '_' in basename else basename_suffix
with open(f'{basename}{basename_suffix}.asm', 'wb') as output_asm:
output_asm.write(b'IFDEF RAX\n\n.CODE\n\nELSE\n\n.MODEL FLAT, C\n.CODE\n\nASSUME FS:NOTHING\n\nENDIF\n\n'
b'EXTERN SW2_GetSyscallNumber: PROC\n\n')
for function_name in function_names:
output_asm.write((self._get_function_asm_code(function_name) + '\n').encode())
output_asm.write(b'end')
# Write header file.
with open('./data/base.h', 'rb') as base_header:
with open(f'{basename}.h', 'wb') as output_header:
# Replace <SEED_VALUE> with a random seed.
base_header_contents = base_header.read().decode()
base_header_contents = base_header_contents.replace('<SEED_VALUE>', f'0x{self.seed:08X}', 1)
# Write the base header.
output_header.write(base_header_contents.encode())
# Write the typedefs.
for typedef in self._get_typedefs(function_names):
output_header.write(typedef.encode() + b'\n\n')
# Write the function prototypes.
for function_name in function_names:
output_header.write((self._get_function_prototype(function_name) + '\n\n').encode())
# Write the endif line.
output_header.write('#endif\n'.encode())
print('Complete! Files written to:')
print(f'\t{basename}.h')
print(f'\t{basename}.c')
print(f'\t{basename}{basename_suffix}.asm')
def _get_typedefs(self, function_names: list) -> list:
def _names_to_ids(names: list) -> list:
return [next(i for i, t in enumerate(self.typedefs) if n in t['identifiers']) for n in names]
# Determine typedefs to use.
used_typedefs = []
for function_name in function_names:
for param in self.prototypes[function_name]['params']:
if list(filter(lambda t: param['type'] in t['identifiers'], self.typedefs)):
if param['type'] not in used_typedefs:
used_typedefs.append(param['type'])
# Resolve typedef dependencies.
i = 0
typedef_layers = {i: _names_to_ids(used_typedefs)}
while True:
# Identify dependencies of current layer.
more_dependencies = []
for typedef_id in typedef_layers[i]:
more_dependencies += self.typedefs[typedef_id]['dependencies']
more_dependencies = list(set(more_dependencies)) # Remove duplicates.
if more_dependencies:
# Create new layer.
i += 1
typedef_layers[i] = _names_to_ids(more_dependencies)
else:
# Remove duplicates between layers.
for k in range(len(typedef_layers) - 1):
typedef_layers[k] = set(typedef_layers[k]) - set(typedef_layers[k + 1])
break
# Get code for each typedef.
typedef_code = []
for i in range(max(typedef_layers.keys()), -1, -1):
for j in typedef_layers[i]:
typedef_code.append(self.typedefs[j]['definition'])
return typedef_code
def _get_function_prototype(self, function_name: str) -> str:
# Check if given function is in syscall map.
if function_name not in self.prototypes:
raise ValueError('Invalid function name provided.')
num_params = len(self.prototypes[function_name]['params'])
signature = f'EXTERN_C NTSTATUS {function_name}('
if num_params:
for i in range(num_params):
param = self.prototypes[function_name]['params'][i]
signature += '\n\t'
signature += 'IN ' if param['in'] else ''
signature += 'OUT ' if param['out'] else ''
signature += f'{param["type"]} {param["name"]}'
signature += ' OPTIONAL' if param['optional'] else ''
signature += ',' if i < num_params - 1 else ');'
else:
signature += ');'
return signature
def _get_function_hash(self, function_name: str):
hash = self.seed
name = function_name.replace(self.__function_prefix, 'Zw', 1) + '\0'
ror8 = lambda v: ((v >> 8) & (2 ** 32 - 1)) | ((v << 24) & (2 ** 32 - 1))
for segment in [s for s in [name[i:i + 2] for i in range(len(name))] if len(s) == 2]:
partial_name_short = struct.unpack('<H', segment.encode())[0]
hash ^= partial_name_short + ror8(hash)
return hash
def _get_function_asm_code(self, function_name: str) -> str:
function_hash = self._get_function_hash(function_name)
# Generate 64-bit ASM code.
code = 'IFDEF RAX\n\n'
code += f'{function_name} PROC\n'
code += '\tmov [rsp +8], rcx ; Save registers.\n'
code += '\tmov [rsp+16], rdx\n'
code += '\tmov [rsp+24], r8\n'
code += '\tmov [rsp+32], r9\n'
code += '\tsub rsp, 28h\n'
code += f'\tmov ecx, 0{function_hash:08X}h ; Load function hash into ECX.\n'
code += '\tcall SW2_GetSyscallNumber ; Resolve function hash into syscall number.\n'
code += '\tadd rsp, 28h\n'
code += '\tmov rcx, [rsp +8] ; Restore registers.\n'
code += '\tmov rdx, [rsp+16]\n'
code += '\tmov r8, [rsp+24]\n'
code += '\tmov r9, [rsp+32]\n'
code += '\tmov r10, rcx\n'
code += '\tsyscall ; Invoke system call.\n'
code += '\tret\n'
code += f'{function_name} ENDP\n'
# Generate 32-bit ASM code
code += '\nELSE\n\n'
code += f'{function_name} PROC\n'
code += f'\tpush 0{function_hash:08X}h\n'
code += '\tcall SW2_GetSyscallNumber ; Resolve function hash into syscall number.\n'
code += '\tadd esp, 4\n'
code += '\tmov ecx, fs:[0c0h]\n'
code += '\ttest ecx, ecx\n'
code += '\tjne _wow64\n'
code += '\tlea edx, [esp+4h]\n'
code += '\tINT 02eh\n'
code += '\tret\n'
code += '\t_wow64:\n'
code += '\txor ecx, ecx\n'
code += '\tlea edx, [esp+4h]\n'
code += '\tcall dword ptr fs:[0c0h]\n'
code += '\tret\n'
code += f'{function_name} ENDP\n'
code += '\nENDIF\n'
return code
if __name__ == '__main__':
print(
" \n"
" . ,--. \n"
",-. . . ,-. . , , |-. o ,-. ,-. ,-. ,-. ,-. / \n"
"`-. | | `-. |/|/ | | | `-. | | |-' | `-. ,-' \n"
"`-' `-| `-' ' ' ' ' ' `-' |-' `-' ' `-' `--- \n"
" /| | @Jackson_T \n"
" `-' ' @modexpblog, 2021 \n\n"
"SysWhispers2: Why call the kernel when you can whisper?\n"
)
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--preset', help='Preset ("all", "common")', required=False)
parser.add_argument('-f', '--functions', help='Comma-separated functions', required=False)
parser.add_argument('-o', '--out-file', help='Output basename (w/o extension)', required=True)
parser.add_argument('--function-prefix', default='Nt', help='Function prefix', required=False)
args = parser.parse_args()
sw = SysWhispers(args.function_prefix)
if args.preset == 'all':
print('All functions selected.\n')
sw.generate(basename=args.out_file)
elif args.preset == 'common':
print('Common functions selected.\n')
sw.generate(
['NtCreateProcess',
'NtCreateThreadEx',
'NtOpenProcess',
'NtOpenProcessToken',
'NtTestAlert',
'NtOpenThread',
'NtSuspendProcess',
'NtSuspendThread',
'NtResumeProcess',
'NtResumeThread',
'NtGetContextThread',
'NtSetContextThread',
'NtClose',
'NtReadVirtualMemory',
'NtWriteVirtualMemory',
'NtAllocateVirtualMemory',
'NtProtectVirtualMemory',
'NtFreeVirtualMemory',
'NtQuerySystemInformation',
'NtQueryDirectoryFile',
'NtQueryInformationFile',
'NtQueryInformationProcess',
'NtQueryInformationThread',
'NtCreateSection',
'NtOpenSection',
'NtMapViewOfSection',
'NtUnmapViewOfSection',
'NtAdjustPrivilegesToken',
'NtDeviceIoControlFile',
'NtQueueApcThread',
'NtWaitForMultipleObjects'],
basename=args.out_file)
elif args.preset:
print('ERROR: Invalid preset provided. Must be "all" or "common".')
elif not args.functions:
print('ERROR: --preset XOR --functions switch must be specified.\n')
print('EXAMPLE: ./syswhispers.py --preset common --out-file syscalls_common')
print('EXAMPLE: ./syswhispers.py --functions NtTestAlert,NtGetCurrentProcessorNumber --out-file syscalls_test')
else:
functions = args.functions.split(',') if args.functions else []
sw.generate(functions, args.out_file)