1#!/usr/bin/python
2#
3# gkrecord - record Gatekeeper rejection activity
4#
5# gkrecord filename
6#
7import sys
8import os
9import signal
10import errno
11import subprocess
12import tempfile
13import plistlib
14
15
16#
17# Usage and fail
18#
19def usage():
20	print >>sys.stderr, "Usage: %s outputfile" % sys.argv[0]
21	sys.exit(2)
22
23def fail(whatever):
24	print >>sys.stderr, "%s: %s" % (sys.argv[0], whatever)
25	sys.exit(1)
26
27
28#
29# Argument processing
30#
31if len(sys.argv) != 2:
32	usage()
33outputfile = sys.argv[1]
34
35
36#
37# If the output file already exists, bail
38#
39if os.path.exists(outputfile):
40	fail("already exists: %s" % outputfile)
41
42
43#
44# Places and things
45#
46collect = "/tmp/gke/"
47
48
49# must be root 
50if os.getuid() != 0:
51	fail("Must have root privileges")
52
53
54#
55# Make sure Gatekeeper is disabled
56#
57subprocess.check_call(["/usr/sbin/spctl", "--master-disable"])
58
59
60#
61# make sure we have a fresh syspolicyd and get its pid
62#
63subprocess.check_call(["/usr/sbin/spctl", "--assess", "--ignore-cache", "/bin/ls"])
64try:
65	psax = subprocess.check_output("ps ax|grep syspolicyd|grep -v grep", shell=True).split("\n")
66	if len(psax) != 2:	# [ found_syspolicyd, '' ]
67		fail("Cannot find syspolicyd")
68	spd_pid = int(psax[0].split()[0])
69except subprocess.CalledProcessError:
70	fail("Cannot find syspolicyd")
71
72
73#
74# run collector dtrace script until dtrace dies.
75# recorder_mode arguments are (path, type, label, cdhash, flags)
76#
77DSCRIPT = '''
78syspolicy$1:::recorder_mode { printf("RECORD;%d;%d", arg1, arg4); }
79
80self unsigned char *cdhash;
81
82syspolicy$1:::recorder_mode
83{
84	self->cdhash = copyin(arg3, 20);
85	printf(";%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x%02.2x",
86		self->cdhash[0], self->cdhash[1], self->cdhash[2], self->cdhash[3], self->cdhash[4],
87		self->cdhash[5], self->cdhash[6], self->cdhash[7], self->cdhash[8], self->cdhash[9],
88		self->cdhash[10], self->cdhash[11], self->cdhash[12], self->cdhash[13], self->cdhash[14],
89		self->cdhash[15], self->cdhash[16], self->cdhash[17], self->cdhash[18], self->cdhash[19]);
90	printf(";%s\\n", copyinstr(arg0));
91}
92
93syspolicy$1:::recorder_mode_adhoc_path
94{
95	printf("SIGNATURE;%d;%s;%s\\n", arg1, copyinstr(arg2), copyinstr(arg0));
96}
97
98syspolicy$1:::assess-outcome-unsigned
99{
100	printf("UNSIGNED;%d;%s\\n", arg1, copyinstr(arg0));
101}
102
103syspolicy$1:::assess-outcome-broken
104{
105	printf("BROKEN;%d;%d;%s\\n", arg1, arg2, copyinstr(arg0));
106}
107'''
108
109def sigint(sig, ctx):
110	os.kill(spd_pid, signal.SIGINT)
111signal.signal(signal.SIGINT, sigint)
112
113(authfd, authfile) = tempfile.mkstemp()
114dtrace = subprocess.Popen(["dtrace", "-qs", "/dev/stdin", str(spd_pid)], stdin=subprocess.PIPE, stdout=authfd, stderr=subprocess.PIPE)
115print "Exercise the programs to be whitelisted now. Interrupt this script (^C) when you are done."
116(stdout, stderr) = dtrace.communicate(input=DSCRIPT)
117signal.signal(signal.SIGINT, signal.SIG_DFL)
118if stderr:
119	fail("dtrace failed: %s" % stderr)
120os.lseek(authfd, os.SEEK_SET, 0)	# rewind
121
122
123#
124# Collect all the data into dicts
125#
126auth = { }
127sigs = { }
128unsigned = { }
129badsigned = { }
130errors = { }
131
132file = os.fdopen(authfd, "r")
133for line in file:
134	(cmd, s, args) = line.strip().partition(";")
135	if s != ";":
136		continue	# spurious
137#	print cmd, "--->", args
138	if cmd == "RECORD":
139		(type, status, cdhash, path) = args.split(";", 3)
140		auth[path] = dict(
141			path=path,
142			type=type,
143			status=status,
144			cdhash=cdhash,
145			version=2
146		)
147	elif cmd == "SIGNATURE":
148		(type, sigpath, path) = args.split(";", 2)
149		with open(sigpath, "r") as sigfile:
150			sigdata = sigfile.read()
151		sigs[path] = dict(
152			path=path,
153			type=type,
154			signature=plistlib.Data(sigdata)
155		)
156	elif cmd == "UNSIGNED":
157		(type, path) = args.split(";", 1)
158		unsigned[path] = dict(
159			path=path,
160			type=type
161		)
162	elif cmd == "BROKEN":
163		(type, exception, path) = args.split(";", 2)
164		badsigned[path] = dict(
165			path=path,
166			type=type,
167			exception=exception
168		)
169
170# unsigned code that had a good detached signature recorded is okay
171for rec in sigs:
172	if rec in unsigned:
173		del unsigned[rec]
174
175
176#
177# Pack them up as a single output (plist) file
178#
179gkedict = dict(
180	authority = auth,
181	signatures = sigs
182)
183plistlib.writePlist(gkedict, outputfile)
184
185
186#
187# Report on any problems found
188#
189for rec in unsigned.values():
190	print >>sys.stderr, "PROBLEM: unsigned type %d object not whitelisted: %s" % (rec["type"], rec["path"])
191for rec in badsigned.values():
192	print >>sys.stderr, "PROBLEM: broken code signature; object not whitelisted: %s" % rec["path"]
193
194
195#
196# Done
197#
198print "Recorded %d authorization(s), %d signature(s) in %s" % (len(auth), len(sigs), outputfile)
199sys.exit(0)
200