forked from duo-labs/EFIgy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
EFIgyLite_cli.py
executable file
·461 lines (361 loc) · 19.3 KB
/
EFIgyLite_cli.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
#!/usr/bin/python
#-----------------------------------------------------------
# Filename : EFIgyLite_cli.py
#
# Description : Initial EFIgy client that uses the EFIgy
# API to check the EFI firmware version
# your Mac is running is the expected one.
# OS X/macOS 10.10,10.11,10.12 only for now.
#
# Created By : Rich Smith (@iodboi)
# Date Created : 3-Oct-2017 18:03
# Date Updated : 13-Oct-2017 18:06
#
# Version : 0.2 (post-Ekoparty #13 very tired release)
#
# License : BSD 3-Clause
#-----------------------------------------------------------
NAME = "EFIgyLite_cli"
VERSION = "0.2"
CODE_URL = "https://efigy.io"
import os
import sys
import json
import time
import types
import hashlib
import logging
import urllib2
import argparse
import commands
import platform
from uuid import getnode
from plistlib import readPlistFromString
from subprocess import Popen, PIPE
##For obvious reasons this only works on Macs
if platform.system() != "Darwin":
print "[!] This application only supports Apple Macs at this time. Sorry :'("
sys.exit(1)
##No support for 10.13 at this time
if int(platform.mac_ver()[0][3:5]) >= 13:
print "[!] Unsupported version of macOS detected '%s'. %s currently only supports 10.10.x-10.12.x"%(platform.mac_ver()[0], NAME)
print "Exiting ....."
sys.exit(1)
##Mac specific imports needed for direct Obj-C calls to get EFI & Board-ID's
## rather using iokit / system_profiler - Thanks to Piker-Alpha for the pointers on this. See their code here:
## https://github.com/Piker-Alpha/HandyScripts/blob/master/efiver.py & issue https://github.com/duo-labs/EFIgy/issues/8
import objc
from Foundation import NSBundle
IOKitBundle = NSBundle.bundleWithIdentifier_('com.apple.framework.IOKit')
functions = [
("IOServiceGetMatchingService", b"II@"),
("IOServiceMatching", b"@*"),
("IORegistryEntryFromPath", b"II*"),
("IORegistryEntryCreateCFProperty", b"@I@@I")
]
objc.loadBundleFunctions(IOKitBundle, globals(), functions)
##Get absolute path of where this module is executing from
MODULE_LOCATION = os.path.abspath(os.path.dirname(__file__))
##Set up logger
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class EFIgyCliError(Exception):
def __init__(self, message, last_response):
print "\n\nError: %s"%(message)
print "\nMost recent response received from the API endpoint:"
try:
response_err_data = json.loads(last_response.read())
print "\n\tURL: %s\n\tCode: %s\n\tMessage: %s\n"%(last_response.url, response_err_data["Code"], response_err_data["Message"])
except:
response_err_data = last_response.read()
print "\n\tURL: %s\n\tResponse: %s"%(last_response.url, response_err_data)
class EFIgyCli(object):
def __init__(self, api_server_url, quiet=False, debug=False, log_path=""):
##Display options
self.quiet = quiet
self.debug = debug
##set up logging
self.log_fo = None
self.log_path = ""
if log_path:
try:
self.log_path = os.path.join(log_path, "%s_%s.log"%(NAME,str(time.time()).replace(".","_")))
self.log_fo = open(self.log_path, "wb")
self.message("Writing output log to %s" % (self.log_path))
except Exception, err:
sys.stderr.write("[!] Error opening specified log file at '%s' - %s"%(self.log_path, err))
##Set initial variables
self.api_server = api_server_url
self.results = {}
self.last_response = None
##Set the salt to be the MAC address of the system, using the MAC as a salt in this manner
## helps ensure that the hashed sysuuid is pseudonymous. We don't want to know the sysuuid's
## value, but we do want it to be unique however. The Salt value is never submitted to the API
self.salt = hex(getnode())
##See if we can get the latest cacerts from the certifi module, if it's not available pull in a bundled one (may not be as up to date)
## This needs to be set explicitly in GET/POSTS to the AWS API otherwise you get SSL warnings and calls fail
try:
import certifi
self.cacert_path = certifi.where()
logger.debug("[+] Certifi module found")
except ImportError:
logger.debug("[-] Certifi module not found, falling back to bundled cecert.pem file")
self.cacert_path = os.path.join(MODULE_LOCATION, "cacert.pem")
##Check existence of the cacert.pem file
try:
os.stat(self.cacert_path)
logger.debug("[+] cacert file location: '%s'" % (self.cacert_path))
except OSError:
print "[-] Local cacert.pem file not found at location '%s'. Please check this location or pip install certifi."%(self.cacert_path)
raise
def message(self, data, newline="\n"):
"""
Show info to the user depending on verbosity level
:param data: - String of message to show
:return:
"""
#Are we logging to screen, file or both?
if not self.quiet:
print data
if self.log_fo:
self.log_fo.write(data+newline)
self.log_fo.flush()
def __make_api_get(self, api_path):
"""
Wrapper to make an API GET request, return the response and handle errors
:return:
"""
try:
self.last_response = urllib2.urlopen(self.api_server+api_path, cafile=self.cacert_path)
json_data = self.last_response.read()
##Check for errors
except urllib2.HTTPError, err:
error = "API HTTP error [%s] - '%s'" % (err.code, err.read())
raise EFIgyCliError(error, self.last_response)
except urllib2.URLError, err:
error = 'Problem calling API at location %s - %s'%(self.api_server+api_path, err)
raise EFIgyCliError(error, self.last_response)
##Decode json response into an object
try:
ret = json.loads(json_data)
except ValueError, err:
error = "Problem deserialising data, expecting JSON.\nError: %s\nData: %s"%(err, json_data)
raise EFIgyCliError(error, self.last_response)
##Return JSON deserialised object
return ret
def __make_api_post(self, api_path, data=None):
"""
Wrapper to make an API POST request, return the response and handle errors
:return:
"""
headers = {"Content-type": "application/json", "Accept": "application/json"}
x = json.dumps(data)
try:
req = urllib2.Request(self.api_server+api_path, x, headers)
self.last_response = urllib2.urlopen(req, cafile=self.cacert_path)
json_data = self.last_response.read()
##Check for errors
except urllib2.HTTPError, err:
error = "API HTTP error [%s] - '%s'" % (err.code, err)
raise EFIgyCliError(error, err)
except urllib2.URLError, err:
error = 'Problem calling API at location %s - %s'%(self.api_server+api_path, err)
raise EFIgyCliError(error, self.last_response)
##Decode json response into an object
try:
ret = json.loads(json_data)
except ValueError, err:
error = "Problem deserialising data, expecting JSON.\nError: %s\nData: %s"%(err, json_data)
raise EFIgyCliError(error, self.last_response)
##Return JSON deserialised object
#print "DEBUG - %s"%(ret), type(ret)
return ret
def _validate_response(self, response):
"""
Validate the response that came back from the API, return True if it's good, False if bad
:param response: API response, Dictionary expected
:return: True if response if valid, False if not
"""
##Check for unexpected response - all should be JSON dicts that have already been deserialised
if type(response) != types.DictionaryType:
self.message("\t[!] ERROR - Unexpected value returned from the API: '%s'"%(response))
return False
##Check for valid errors
if response.has_key("error") and response.has_key("msg"):
self.message("\t[!] ERROR - %s (%s)" % (response["msg"], response["timestamp"]))
return False
##Is this a valid response message
if response.has_key("msg"):
return True
##Catch all...dictionary returned but does not contain expected keys? Who know's what's going on here?!
else:
self.message("\t[!] ERROR - Unexpected dictionary response returned from the API: '%s'"%(response))
return False
def __call__(self):
try:
self.message("\nEFIgyLite API Information:")
api_version = self.__make_api_get("/version")
if api_version["version"]!= VERSION:
self.message("\n\t[!][!] EFIgyLite client version '%s' does not EFIgyLite API version '%s', bad things may happen please grab the latest version of the client from %s. [!][!]\n"%(VERSION,api_version,CODE_URL))
self.message("\tAPI Version: %s\n\tUpdated On: %s\n\n" % (api_version["version"], api_version["updated"]))
##Get the local system data to send to API to find out relevant EFI firmware info
submit_data = self.gather_system_versions()
if not submit_data:
self.cleanup()
return
##Send the datas to the API
self.results = self.submit_system_data()
##Is this a model of mac that is getting EFI updates?
if self.check_fw_being_updated():
##If yes Are you running a Mac model that hasn't seen any FW update?
##Is your firmware patched to the expected level given your OS
self.check_fw_versions()
##Are running the latest build number?
self.check_highest_build()
##Are you running an out of date OS minor version?
self.check_os_up_to_date()
##Clean up
self.cleanup()
except EFIgyCliError, err:
sys.stderr.write("%s"%(err))
def gather_system_versions(self):
"""
Get versions of EFI, Boot ROM, OS & Mac Device as well as the SysUUID
:return:
"""
self.message("Enumerated system informaton (This data will be sent to the API in order to determine your correct EFI version): ")
##Get Mac model ID, EFI & SMC ROM versions
self.hw_version = str(IORegistryEntryCreateCFProperty(IOServiceGetMatchingService(0, IOServiceMatching("IOPlatformExpertDevice")), "model", None, 0)).replace("\x00", "")
self.smc_version = str(IORegistryEntryCreateCFProperty(IOServiceGetMatchingService(0, IOServiceMatching("AppleSMC")),"smc-version", None, 0))
raw_efi = str(IORegistryEntryCreateCFProperty(IORegistryEntryFromPath(0, "IODeviceTree:/rom"), "version", None, 0)).replace("\x00", "").split(".")
self.efi_version = "%s.%s.%s" % (raw_efi[0], raw_efi[2], raw_efi[3])
##We like the uniqueness of the platforms UUID but we want to preserve privacy - hash it with salt to psuedononymise
sys_uuid = str(IORegistryEntryCreateCFProperty(IOServiceGetMatchingService(0, IOServiceMatching("IOPlatformExpertDevice")), "IOPlatformUUID", None, 0)).replace("\x00", "")
self.h_sys_uuid = hashlib.sha256(self.salt + sys_uuid).hexdigest()
##Get the Board-ID, this is how EFI files are matched to running hardware - Nastee
self.board_id = str(IORegistryEntryCreateCFProperty(IOServiceGetMatchingService(0, IOServiceMatching("IOPlatformExpertDevice")), "board-id", None, 0)).replace("\x00", "")
## Get OS version
self.os_version = commands.getoutput("sw_vers -productVersion")
## Get build number
self.build_num = commands.getoutput("sw_vers -buildVersion")
## Carve out the major version as we use this a bunch
self.os_maj_ver = ".".join(self.os_version.split(".")[:2])
self.message("\tHashed SysUUID : %s" % (self.h_sys_uuid))
self.message("\tHardware Version : %s" % (self.hw_version))
self.message("\tEFI Version : %s" % (self.efi_version))
self.message("\tSMC Version : %s" % (self.smc_version))
self.message("\tBoard-ID : %s" % (self.board_id))
self.message("\tOS Version : %s" % (self.os_version))
self.message("\tBuild Number : %s" % (self.build_num))
if not self.quiet:
agree = raw_input("\n[?] Do you want to continue and submit this request? [Y/N] ").upper()
if agree not in ["Y", "YES"]:
self.message("[-] OK! Not sending request to the API. Exiting.....")
return False
return True
def submit_system_data(self):
"""
Send the System info to the API so as the expected EFI version and other data can be
returned relevant to this system
:return:
"""
endpoint = "/apple/oneshot"
data_to_submit = {"hashed_uuid":self.h_sys_uuid, "hw_ver":self.hw_version, "rom_ver":self.efi_version,
"smc_ver":self.smc_version, "board_id":self.board_id, "os_ver":self.os_version, "build_num":self.build_num}
##POST this data to the API to get relevant info back
result_dict = self.__make_api_post(endpoint, data=data_to_submit)
return result_dict
def check_highest_build(self):
"""
Given the OS version are you running, what is the highest available build number? Are you running it?
:return:
"""
if not self.results.get("latest_build_number"):
self.results["latest_build_number"] = self.__make_api_get('/apple/"latest_build_number"/%s' % (self.os_maj_ver))
self.message("\nHighest build number check:")
##Validate response from API
if self._validate_response(self.results["latest_build_number"]):
##Valid response from API - now interpret it
if self.results["latest_build_number"]["msg"] == self.build_num:
self.message("\t[+] SUCCESS - You are running the latest build number (%s) of the OS version you have installed (%s)" % (self.build_num, self.os_version))
else:
self.message("\t[-] ATTENTION - You are NOT running the latest build number of your OS version (%s). Your build number is %s, the latest build number is %s" % (self.os_version, self.build_num, self.results["latest_build_number"]["msg"]))
def check_os_up_to_date(self):
"""
Given your major OS version are you running the latest minor patch?
"""
if not self.results.get("latest_os_version"):
self.results["latest_os_version"] = self.__make_api_get('/apple/latest_os_version/%s' % (self.os_maj_ver))
self.message("\nUp-to-date OS check:")
##Validate response from API
if self._validate_response(self.results["latest_os_version"]):
##Valid response from API - now interpret it
if self.os_version != self.results["latest_os_version"]["msg"]:
self.message("\t[-] ATTENTION - You are NOT running the most up to date version of the OS. Your OS version is %s, the latest versions is %s" % (self.os_version, self.results["latest_os_version"]["msg"]))
else:
self.message("\t[+] SUCCESS - You are running the latest major/minor/micro version of the OS you have installed (%s)" % (self.os_version))
def check_fw_being_updated(self):
"""
Does it look like this mac model is still receiving EFI firmware updates?
:return:
"""
if not self.results.get("efi_updates_released"):
##Call the API to see what the latest version of EFI you are expected to be running given OS ver and mac model
self.results["efi_updates_released"] = self.__make_api_get('/apple/no_firmware_updates_released/%s' % (self.hw_version))
##Validate response from API
if self._validate_response(self.results["efi_updates_released"]):
#Check to see if this is a model that has seen any EFI firmware updates
if self.results["efi_updates_released"]["msg"] == False:
self.message("\nEFI firmware version check:")
self.message("\t[-] ATTENTION - Your Mac model (%s) does not seem to have had any EFI updates released for it :'("%(self.hw_version))
return False
else:
return True
def check_fw_versions(self):
"""
Compare this systems versions to the firmware table to see if FW is at latest versions
:return:
"""
if not self.results.get("latest_efi_version"):
##Call the API to see what the latest version of EFI you are expected to be running given OS ver and mac model
self.results["latest_efi_version"] = self.__make_api_get('/apple/latest_efi_firmware/%s/%s' % (self.hw_version, self.build_num))
self.message("\nEFI firmware version check:")
##Validate response from API
if self._validate_response(self.results["latest_efi_version"]):
##Valid response from API - now interpret it
if self.results["latest_efi_version"]["msg"] == self.efi_version:
self.message("\t[+] SUCCESS - The EFI Firmware you are running (%s) is the expected version for the OS build you have installed (%s) on your %s" % (self.efi_version, self.build_num, self.hw_version))
else:
self.message("\t[-] ATTENTION - You are running an unexpected firmware version given the model of your system (%s) and OS build you have installed (%s). Your firmware is %s, the firmware we expected to see is %s.\n" % (self.hw_version, self.build_num, self.efi_version, self.results["latest_efi_version"]["msg"]))
def cleanup(self):
"""
Cleanup up so nothing dangles
:return:
"""
if self.log_fo:
self.log_fo.close()
if __name__ == "__main__":
##Process command line args
parser = argparse.ArgumentParser(description="%s v%s. App to assess Apple EFI firmware versions."%(NAME, VERSION), epilog="Visit %s for more information."%(CODE_URL))
parser.add_argument("-l","--log", help="File to log output to")
parser.add_argument("--debug", action="store_true", default=False, help="Show verbose debugging output to stdout")
parser.add_argument("-q", "--quiet", action="store_true", default=False, help="Silence stdout output and don't ask to submit data to API. Use with the --log option")
parser.add_argument("-v", "--version", action="store_true", default=False, help="Show client version")
args = parser.parse_args()
if args.debug:
logger.setLevel(logging.DEBUG)
if args.version:
print "%s %s"%(NAME, VERSION)
sys.exit(0)
if args.quiet:
logger.setLevel(logging.WARNING)
try:
efigy_cli = EFIgyCli("https://api.efigy.io", quiet=args.quiet, debug=args.debug, log_path=args.log)
efigy_cli()
print "\n"
except Exception, err:
print "[-] Fatal error in %s. Exiting....."%(NAME)
if args.debug:
import traceback
print "\nError:\n\t%s"%(err)
print "\n%s"%(traceback.format_exc())