-
Notifications
You must be signed in to change notification settings - Fork 32
/
Copy pathscan.py
executable file
·478 lines (322 loc) · 10.9 KB
/
scan.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
'''
SCAN FOR VULNERABLE (HIJACKABLE) BINARIES (v1.0, 3/2015)
scans the list of running processes or the entire file-system for applications that either
1) contain a weak import (LC_LOAD_WEAK_DYLIB) that doesn't exist
2) contain multiple run-path search paths, and a run-path import that doesn't exist in the primary search path
NOTES:
1) requires macholib
2) this is proof-of-concept code ;)
'''
import os
import sys
import shlex
import argparse
import subprocess
#supported archs
SUPPORTED_ARCHITECTURES = ['i386', 'x86_64']
#executable binary
MH_EXECUTE = 2
#dylib
MH_DYLIB = 6
#bundles
MH_BUNDLE = 8
#make sure python version is ok
# ->and machO module is installed
def checkEnv():
#global import
global macholib
#get python version
pythonVersion = sys.version_info
#check that python is at least 2.7
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
#err msg
print('ERROR: requires python 2.7+ (found: %s)' % (pythonVersion))
#bail
return False
#try import macholib
try:
#import
import macholib.MachO
#handle exception
# ->bail w/ error msg
except ImportError:
#err msg
print('ERROR: could not load required module (macholib)')
#bail
return False
#got to here
# ->env looks ok!
return True
#check for non intel architectures
# ->ensure things like iPhone files that are lying around aren't processed
def isSupportedArchitecture(macho):
#flag
supported = False
#check macho headers for supported arch
for machoHeader in macho.headers:
#check
if macholib.MachO.CPU_TYPE_NAMES.get(machoHeader.header.cputype, machoHeader.header.cputype) in SUPPORTED_ARCHITECTURES:
#ok!
supported = True
#bail
break
return (supported, machoHeader)
#get list of loaded binaries
# ->'ps' sometimes doesn't give the full path, so we use lsof
def loadedBinaries():
#list of loaded bins
binaries = []
#exec lsof
lsof = subprocess.Popen('lsof /', shell=True, stdout=subprocess.PIPE)
#get outpu
output = lsof.stdout.read()
#close
lsof.stdout.close()
#wait
lsof.wait()
#parse/split output
# ->grab file name and check if its executable
for line in output.split('\n'):
try:
#split on spaces up to 8th element
# ->this is then the file name (which can have spaces so grab rest/join)
binary = ' '.join(shlex.split(line)[8:])
#skip non-files (fifos etc....) or non executable files
if not os.path.isfile(binary) or not os.access(binary, os.X_OK):
#skip
continue
#save binary
binaries.append(binary)
except:
#ignore
pass
#filter out dup's
binaries = list(set(binaries))
return binaries
#list all mach-O binaries on the file-system
def installedBinaries(rootDirectory = None):
#all executable binaries
binaries = []
#init
if not rootDirectory:
rootDirectory = '/'
#recursively walk (starting at r00t)
for root, dirnames, filenames in os.walk(rootDirectory):
#check all files
for filename in filenames:
#make full
# ->use realpath to resolve symlinks
fullName = os.path.realpath(os.path.join(root, filename))
#skip non-files (fifos etc....)
if not os.path.isfile(fullName):
#skip
continue
#only check executable files
if os.access(fullName, os.X_OK):
#save
binaries.append(fullName)
return binaries
#resolve paths that start with '@executable_path', or '@loader_path'
# ->note: since we are dealing only with main executables, both these resolve the same way
def resolvePath(binaryPath, unresolvedPath):
#return var
# ->init to what was passed in, since might not be able to resolve
resolvedPath = unresolvedPath
#resolve '@loader_path'
if unresolvedPath.startswith('@loader_path'):
#resolve
resolvedPath = os.path.abspath(os.path.split(binaryPath)[0] + unresolvedPath.replace('@loader_path', ''))
#resolve '@executable_path'
elif unresolvedPath.startswith('@executable_path'):
#resolve
resolvedPath = os.path.abspath(os.path.split(binaryPath)[0] + unresolvedPath.replace('@executable_path', ''))
return resolvedPath
#parse all binaries
# ->extract imports, etc
def parseBinaries(binaries):
#dictionary of parsed binaries
parsedBinaries = {}
#scan all binaries
for binary in binaries:
#wrap
try:
#try load it (as mach-o)
macho = macholib.MachO.MachO(binary)
if not macho:
#skip
continue
except:
#skip
continue
#check if it's a supported (intel) architecture
# ->also returns the supported mach-O header
(isSupported, machoHeader) = isSupportedArchitecture(macho)
if not isSupported:
#skip
continue
#skip binaries that aren't main executables, dylibs or bundles
if machoHeader.header.filetype not in [MH_EXECUTE, MH_DYLIB, MH_BUNDLE]:
#skip
continue
#dbg msg
#print ' scanning: %s' % binary
#init dictionary for process
parsedBinaries[binary] = {'LC_RPATHs': [], 'LC_LOAD_DYLIBs' : [], 'LC_LOAD_WEAK_DYLIBs': [] }
#save type
parsedBinaries[binary]['type'] = machoHeader.header.filetype
#iterate over all load
# ->save LC_RPATHs, LC_LOAD_DYLIBs, and LC_LOAD_WEAK_DYLIBs
for loadCommand in machoHeader.commands:
#handle LC_RPATH's
# ->resolve and save
if macholib.MachO.LC_RPATH == loadCommand[0].cmd:
#grab rpath
rPathDirectory = loadCommand[-1].rstrip('\0')
#always attempt to resolve '@executable_path' and '@loader_path'
rPathDirectory = resolvePath(binary, rPathDirectory)
#save
parsedBinaries[binary]['LC_RPATHs'].append(rPathDirectory)
#handle LC_LOAD_DYLIB
# ->save (as is)
elif macholib.MachO.LC_LOAD_DYLIB == loadCommand[0].cmd:
#tuple, last member is path to import
importedDylib = loadCommand[-1].rstrip('\0')
#save
parsedBinaries[binary]['LC_LOAD_DYLIBs'].append(importedDylib)
#handle for LC_LOAD_WEAK_DYLIB
# ->resolve (except for '@rpaths') and save
elif macholib.MachO.LC_LOAD_WEAK_DYLIB == loadCommand[0].cmd:
#tuple, last member is path to import
weakDylib = loadCommand[-1].rstrip('\0')
#always attempt to resolve '@executable_path' and '@loader_path'
weakDylib = resolvePath(binary, weakDylib)
#save
parsedBinaries[binary]['LC_LOAD_WEAK_DYLIBs'].append(weakDylib)
return parsedBinaries
#process binaries
# ->find vulnerable thingz
def processBinaries(parsedBinaries):
#results
# ->list of dictionaries
vulnerableBinaries = {'rpathExes': [], 'weakBins': []}
#scan all parsed binaries
for key in parsedBinaries:
#grab binary entry
binary = parsedBinaries[key]
#STEP 1: check for vulnerable @rpath'd imports
# note: only do this for main executables, since dylibs/bundles can share @rpath search dirs w/ main app, etc
# which we can't reliably resolve (i.e. this depends on the runtime/loadtime env)
#check for primary @rpath'd import that doesn't exist
if binary['type']== MH_EXECUTE and len(binary['LC_RPATHs']):
#check all @rpath'd imports for the executable
# ->if there is one that isn't found in a primary LC_RPATH, the executable is vulnerable :)
for importedDylib in binary['LC_LOAD_DYLIBs']:
#skip non-@rpath'd imports
if not importedDylib.startswith('@rpath'):
#skip
continue
#print 'has @rath\'d import: %s' % importedDylib
#chop off '@rpath'
importedDylib = importedDylib.replace('@rpath', '')
#check the first rpath directory (from LC_RPATHs)
# ->is the rpath'd import there!?
if not os.path.exists(binary['LC_RPATHs'][0] + importedDylib):
#not found
# ->means this binary is vulnerable!
vulnerableBinaries['rpathExes'].append({'binary': key, 'importedDylib': importedDylib, 'LC_RPATH': binary['LC_RPATHs'][0]})
#bail
break
#STEP 2: check for vulnerable weak imports
# can check all binary types...
#check binary
for weakDylib in binary['LC_LOAD_WEAK_DYLIBs']:
#got to resolve weak @rpath'd imports before checking if they exist
if weakDylib.startswith('@rpath'):
#skip @rpath imports in dylibs and bundles, since they can share @rpath search dirs w/ main app, etc
# which we can't reliably resolve (i.e. this depends on the runtime/loadtime env.)
if binary['type'] != MH_EXECUTE:
#skip
continue
#skip @rpath imports if binary doesn't have any LC_RPATHS
if not len(binary['LC_RPATHs']):
#skip
continue
#chop off '@rpath'
weakDylib = weakDylib.replace('@rpath', '')
#just need to check first LC_RPATH directory
if not os.path.exists(binary['LC_RPATHs'][0] + weakDylib):
#not found
# ->means this binary is vulnerable!
vulnerableBinaries['weakBins'].append({'binary': key, 'weakDylib': weakDylib, 'LC_RPATH': binary['LC_RPATHs'][0]})
#bail
break
#path doesn't need to be resolved
# ->check/save those that don't exist
elif not os.path.exists(weakDylib):
#not found
# ->means this binary is vulnerable!
vulnerableBinaries['weakBins'].append({'binary': key, 'weakBin': weakDylib})
#bail
break
return vulnerableBinaries
if __name__ == '__main__':
#dbg msg
print '\nDYLIB HIJACK SCANNER (p. wardle)'
print 'finds applications that may be vulnerable to dylib hijacking\n'
#handle -h flag
# ->print help/about msg
if 2 == len(sys.argv) and '-h' == sys.argv[1]:
#dbg msg(s)
print ' no args: will scan entire file system'
print ' -l will scan just loaded processes\n'
#bail
sys.exit(0)
#check that env is compatible
if not checkEnv():
#bail
sys.exit(-1)
#check for -l flag
# ->indicates scan of just loaded processes
if 2 == len(sys.argv) and '-l' == sys.argv[1]:
#dbg msg
print 'getting list of loaded (running) processes...'
#get list of loaded binaries
binaries = loadedBinaries()
#get list of *all* loaded files
# ->this is default behavior
else:
#dbg msg
print 'getting list of all process executables on system...'
#get list of executable files on the file-system
binaries = installedBinaries()
#parse binares
# ->extract LC_RPATHs, imports, etc
parsedBinaries = parseBinaries(binaries)
#process/scan em
vulnerableBinaries = processBinaries(parsedBinaries)
#display binaries that are vulnerable to rpath hijack
if len(vulnerableBinaries['rpathExes']):
#dbg msg
print '\nfound %d binaries vulnerable to multiple rpaths:' % len(vulnerableBinaries['rpathExes'])
#iterate over all and print
for binary in vulnerableBinaries['rpathExes']:
#dbg msg
print '%s has multiple rpaths (%s)\n' % (binary['binary'], binary)
#binary didn't have any
else:
#dbg msg
print '\ndid not find any vulnerable to multiple rpaths'
#display binaries that are vulnerable to weak import hijack
if len(vulnerableBinaries['weakBins']):
#dbg msg
print '\nfound %d binaries vulnerable to weak dylibs:' % len(vulnerableBinaries['weakBins'])
#iterate over all and print
for binary in vulnerableBinaries['weakBins']:
#dbg msg
print '%s has weak import (%s)\n' % (binary['binary'], binary)
#binary didn't have any
else:
#dbg msg
print '\ndid not find any missing LC_LOAD_WEAK_DYLIBs'
#dbg msg
print '\nscan complete\n'