前言
这次跟大家分享的漏洞在2018年初就已经得到彻底修复(CVE-2017-7170),但是它仍然是我在macOS上最喜欢的发现!我一直想记录关于此漏洞的具体细节,这一次终于得偿所愿~
在DefCon 25上,我发表了一个名为“Death By 1000 Installers”的演讲。
在这次演讲中,我强调了在大量的第三方安装程序中都存在缺陷,使得攻击者能够在本地提升至root权限。
经过我的研究发现安装程序(拥有高权限)经常会调用不安全的API接口或者执行不安全的操作。
其中一个不安全但又被广泛使用的API就是AuthorizationExecuteWithPrivileges函数。这个函数的功能简而言之就是在用户通过身份认证后,该函数就会以高权限执行特定路径下的二进制文件(路径通过pathToTool参数传入)。
Apple明确指出该API已被弃用,不应再被使用。理由是此API没有验证将要执行的二进制文件(且是要以root权限运行)。这意味着本地没有高权限的攻击者或者恶意软件可以暗中篡改、替换它,从而将他们的权限升级到root:
许多人(包括我自己)认为,如果API执行的是受保护的二进制文件(SIP),那么这个问题将会迎刃而解。(在这种情况下,非特权代码无法对二进制文件做篡改、替换等操作):
1int reboot() {
2
3 ...
4
5 AuthorizationExecuteWithPrivileges(authRef, "/sbin/reboot",
6 kAuthorizationFlagDefaults, (char**)args, NULL);
7}
在演讲结束之后,我深入研究了此API的内在机理,并成功发现了一个系统级别的缺陷,使得任何对AuthorizationExecuteWithPrivileges的调用都会受到本地权限提升攻击!
AuthorizationExecuteWithPrivileges
要理解这个广泛使用的API在Apple实现中的缺陷,我们需要了解它的工作过程。因为在我的DefCon演讲中有关于它的详细介绍,所以下面我们就简单地提一下。
首先,让我们看看一些调用AuthorizationExecuteWithPrivileges函数并以root权限执行二进制文件的代码。顺便提一下,许多(第三方)安装程序中都有这样的代码。
1 //run binary as root
2 BOOL runAsRoot(char* path)
3{
4 //return/status var
5 BOOL bRet = NO;
6
7 //authorization ref
8 AuthorizationRef authorizatioRef = {0};
9
10 //args
11 char *args[] = {NULL};
12
13 //flag creation of ref
14 BOOL authRefCreated = NO;
15
16 //status code
17 OSStatus osStatus = -1;
18
19 //create authorization ref
20 osStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment,
21 kAuthorizationFlagDefaults, &authorizatioRef);
22 if(errAuthorizationSuccess != osStatus)
23 {
24 //err msg
25 NSLog(@"AuthorizationCreate() failed with %d", osStatus);
26
27 //bail
28 goto bail;
29 }
30
31 //set flag indicating auth ref was created
32 authRefCreated = YES;
33
34 //run cmd as root
35 // will ask user for password...
36 osStatus = AuthorizationExecuteWithPrivileges(authorizatioRef, path, 0, args, NULL);
37 if(errAuthorizationSuccess != osStatus)
38 {
39 //err msg
40 NSLog(@"AuthorizationExecuteWithPrivileges() failed with %d", osStatus);
41
42 //bail
43 goto bail;
44 }
45
46 //no errors
47 bRet = YES;
48
49 bail:
50
51 //free auth ref
52 if(YES == authRefCreated)
53 {
54 //free
55 AuthorizationFree(authorizatioRef, kAuthorizationFlagDefaults);
56 }
57
58 return bRet;
59}
在创建授权引用AuthorizationRef(通过AuthorizationCreate API)之后,上述示例代码调用AuthorizationExecuteWithPrivileges函数,从而触发、弹出身份验证对话框:
假设用户输入了正确的账号、密码,那么二进制文件(通过path参数传递给函数)就会以高权限执行!
接下来我们需要深入分析这个过程背后的机理,因为这也是最终导致API实现发生缺陷的关键。
下面是程序(即安装程序)调用AuthorizationExecuteWithPrivileges API时的流程图:
如图中所示,当安装程序(或其他程序)希望通过AuthorizationExecuteWithPrivileges执行特权操作时:
- 首先调用“授权API”(即AuthorizationExecuteWithPrivileges),该API生成XPC消息到“授权进程”(authd)。
- 授权进程向权限数据库发出请求,因为需要用户进行验证,数据库向“安全代理”发送另一条XPC消息。
- “安全代理”向用户显示实际的身份验证对话框。
- 如果提供了有效的身份验证凭据,则允许特权操作执行。
现在让我们仔细梳理一下缺陷发生时相关的步骤。
查看AuthorizationExecuteWithPrivileges的源码实现(见libsecurity_authorization/lib/trampolineClient.cpp),我们可以看到授权引用第一次”具体化“(注:具体化的含义就是授权引用给到了实际中间变量,从而进一步传递,这里就是给到extForm),是在调用AuthorizationMakeExternalForm时:
在调试模式下(lldb),单步跳过AuthorizationMakeExternalForm调用,这样就可以dump出”具体化“后的授权引用(也就是变量: extForm
, 类型: AuthorizationExternalForm
)
$ lldb installer
(lldb) target create "installer"
Current executable set to 'installer' (x86_64).
frame #0: 0x00007fff7c909dee Security`AuthorizationExecuteWithPrivileges + 48
Security`AuthorizationExecuteWithPrivileges:
-> 0x7fff7c909dee <+48>: callq 0x7fff7c908e0a ; AuthorizationMakeExternalForm
...
(lldb) reg read $rsi
rsi = 0x7fff5fbffab0
(lldb) x/20xb $0x7fff5fbffab0
0x7fff5fbffab0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fff5fbffab8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fff5fbffac0: 0x00 0x00 0x00 0x00
(lldb) ni
(lldb) x/20xb 0x7fff5fbffab0
0x7fff5fbffab0: 0xdb 0x49 0x27 0xe3 0x87 0x27 0x4a 0x61
0x7fff5fbffab8: 0xa6 0x86 0x01 0x00 0x00 0x00 0x00 0x00
0x7fff5fbffac0: 0x00 0x00 0x00 0x00
像在其他地方描述的一样,这个extForm基本上就是一个随机的12字节句柄,与authd服务中的一个令牌相关联。
更具体一点,它实际上是一个AuthorizationBlob(见authd_private.h)
1typedef struct AuthorizationBlob {
2 uint32_t data[2];
3} AuthorizationBlob;
4
5typedef struct AuthorizationExternalBlob {
6 AuthorizationBlob blob;
7 int32_t session;
8} AuthorizationExternalBlob;
AuthorizationExecuteWithPrivileges函数接下来调用AuthorizationExecuteWithPrivilegesExternalForm,
并传入初始化后的AuthorizationExternalForm以及其他参数:
如果在authorizationexecutewithesexternalform函数处下断点,我们可以通过检查调用栈(通过bt debugger命令)来确认这个执行流:
$ lldb installer
(lldb) target create "installer"
Current executable set to 'installer' (x86_64).
(lldb) b AuthorizationExecuteWithPrivilegesExternalForm
Breakpoint 1: where = Security`AuthorizationExecuteWithPrivilegesExternalForm
(lldb) r
Process 485 launched: '/Users/user/Desktop/installer' (x86_64)
Process 485 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
Security`AuthorizationExecuteWithPrivilegesExternalForm:
-> 0x7fff7c909e26 <+0>: pushq %rbp
0x7fff7c909e27 <+1>: movq %rsp, %rbp
0x7fff7c909e2a <+4>: pushq %r15
0x7fff7c909e2c <+6>: pushq %r14
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00007fff7c909e26 Security`AuthorizationExecuteWithPrivilegesExternalForm
frame #1: 0x00007fff7c909e0c Security`AuthorizationExecuteWithPrivileges + 78
frame #2: 0x0000000100000e48 installer`runAsRoot + 168
frame #3: 0x0000000100000d8b installer`main + 43
如下图所示,AuthorizationExecuteWithPrivilegesExternalForm函数之后调用execv来执行一个名为security_authtrampoline且为setuid权限的系统二进制文件:
$ ls -lart /usr/libexec/security_authtrampoline
-rws--x--x 1 root wheel 36368 /usr/libexec/security_authtrampoline
security_authtrampoline进程调用AuthorizationCopyRights函数,该函数向authd发送XPC消息:
如前所述,authd在确定请求可以继续之后,向“安全代理”发送一个请求,进而向用户显示身份验证对话框,并捕获和验证所需凭据。
如果用户提供了正确的凭据,在验证逻辑成功返回时,”具体化“的AuthorizationRef(通过AuthorizationExecuteWithPrivileges函数传入)便完成了初始化并得到“授权”。接着,security_auth_trampoline继续执行并调用execv以高权限执行操作(即安装程序传递给AuthorizationExecuteWithPrivileges函数的二进制文件路径):
嗅探授权引用
AuthorizationExecuteWithPrivileges作为编程接口是足够精简的,高度抽象的,去掉了大量复杂操作(例如各种进程、守护进程和代理之间的交互)。然而这也恰恰导致了问题的发生。
回想一下,AuthorizationExecuteWithPrivileges函数执行的第一个操作是通过调用AuthorizationMakeExternalForm函数来“具体化”授权引用。这样做是为了可以将授权引用传递给security_authtrampoline进程。
“具体化”转换的实现细节无关紧要,但是我们需要注意这种具体化的形式不能公开,因为就像Apple官方指出的那样:“任何进程都可以使用这个外部授权引用来获取权限。”
在Authorization.h文件中同样有这一警告。
安全警告:
应用程序需要注意不要向潜在的攻击者公开AuthorizationExternalForm,因为这将赋予他们额外的权限。
这个警告引起了我的兴趣,按照这一说法,如果我们(作为非特权用户)可以找到一种方法来“嗅探”或捕获任何“具体化”的授权引用,我们不就可以(重新)利用它们来执行任意的高权限操作了嘛!
回到authorizationexecutewithesexternalform函数,我们来看一下它是如何将“具体化”的授权引用传递给security_authtrampoline进程的。这一部分源代码位于libsecurity_authorization/lib/trampolineClient.cpp:
1define TRAMPOLINE "/usr/libexec/security_authtrampoline"
2
3OSStatus AuthorizationExecuteWithPrivilegesExternalForm(
4 const AuthorizationExternalForm * extForm,
5 const char *pathToTool,
6 AuthorizationFlags flags,
7 char *const *arguments,
8 FILE **communicationsPipe)
9{
10 ...
11
12 // create the mailbox file
13 FILE *mbox = tmpfile();
14 if (!mbox)
15 return errAuthorizationInternal;
16 if (fwrite(extForm, sizeof(*extForm), 1, mbox) != 1) {
17 fclose(mbox);
18 return errAuthorizationInternal;
19 }
20 fflush(mbox);
21
22 ...
23
24 // make text representation of the temp-file descriptor
25 char mboxFdText[20];
26 snprintf(mboxFdText, sizeof(mboxFdText), "auth %d", fileno(mbox));
27
28 const char **argv = argVector(trampoline, pathToTool, mboxFdText, arguments);
29
30 ....
31 const char *trampoline = TRAMPOLINE;
32 execv(trampoline, (char *const*)argv);
在上面的代码中我们可以看到,”具体化“的授权引用传递给了AuthorizationExecuteWithPrivilegesExternalForm函数(通过extForm变量),接着函数创建了一个临时文件来存储授权引用,之后通过命令行参数(argv)的方式将文件描述符(mboxFdText)传递给了security_authtrampoline:
security_authtrampoline读入”具体化“的授权引用并通过调用AuthorizationCreateFromExternalForm函数将其抽象回授权引用(类型:AuthorizationRef)
1//
2// Main program entry point.
3//
4// Arguments:
5// argv[0] = my name
6// argv[1] = path to user tool
7// argv[2] = "auth n", n=file descriptor of mailbox temp file
8// argv[3..n] = arguments to pass on
9//
10// File descriptors (set by fork/exec code in client):
11// 0 -> communications pipe (perhaps /dev/null)
12// 1 -> notify pipe write end
13// 2 and above -> unchanged from original client
14//
15int main(int argc, const char *argv[])
16{
17 ...
18
19
20 // read the external form
21 AuthorizationExternalForm extForm;
22 int fd;
23 if (sscanf(mboxFdText, "auth %d", &fd) != 1)
24 return errAuthorizationInternal;
25 if (lseek(fd, 0, SEEK_SET) ||
26 read(fd, &extForm, sizeof(extForm)) != sizeof(extForm)) {
27 close(fd);
28 return errAuthorizationInternal;
29 }
30
31 // internalize the authorization
32 AuthorizationRef auth;
33 if (OSStatus error = AuthorizationCreateFromExternalForm(&extForm, &auth))
34 fail(error);
35 secdebug("authtramp", "authorization recovered");
乍一看,通过临时文件传递敏感的“具体化”授权引用不是一个好主意,但仔细一看整个过程似乎可以“安全地”完成。但实际上它确实是一个巨大的安全隐患,为本地非特权攻击者提供了访问所有授权引用的权限!
下面让我们仔细看看AuthorizationExecuteWithPrivilegesExternalForm函数中负责创建并输出“具体化”的授权引用部分的代码:
1// create the mailbox file
2FILE *mbox = tmpfile();
3if (!mbox)
4 return errAuthorizationInternal;
5if (fwrite(extForm, sizeof(*extForm), 1, mbox) != 1) {
6 fclose(mbox);
7 return errAuthorizationInternal;
8}
tmpfile
API创建一个随机命名的临时文件(通过mkstemp,在$TMPDIR下),然后立即删除它(通过unlink),之后会向调用者返回一个文件句柄:
1 FILE *
2 tmpfile(void)
3{
4 FILE *fp;
5
6 ...
7
8 fd = mkstemp(buf);
9 if(fd != -1)
10 (void)unlink(buf);
11
12 return (fp);
13}
由于该文件是随即命名的,而且在创建之后立刻被删除,看上去好像并不会被其他进程打开。换句话说其他(恶意)进程无法访问这个临时文件的内容。(从安全角度上看,临时文件无法读取是一件好事,因为它包含有AuthorizationExternalForm结构数据)。
当然,调用AuthorizationExecuteWithPrivileges的进程拥有该文件的句柄FILE*(通过tmpfile返回的),并且因为security_authtrampoline是派生的子进程,故可以共享此文件句柄。
尽管如此,敏感的AuthorizationExternalForm结构被写入到一个临时文件中,这“感觉”不是个好主意。后边证明确实如此!
非特权攻击者无法访问(打开)临时文件是因为文件已经unlink掉了,从而在默认的文件系统中无法读取临时文件的”原始字节“(”具体化“的授权引用)。但是如果临时文件被写入另一个已经创建好的文件系统,像ramdisk,那么便可以读取临时文件的原始字节!
那么作为本地没有特权的攻击者如何做到这一点呢?非常简单,只需要将用户的临时文件目录软链接到你创建的ramdisk(如此便可以直接读取临时文件内容了)
回想一下我们的目标(作为本地无特权攻击者),就是嗅探到传递给security_authtrampoline的临时文件中的AuthorizationExternalForm。
在一个存在此漏洞的系统上(在发现这个bug的时候是OSX 10.4以后的所有版本),我们可以通过以下步骤完成攻击:
- 创建(或者通过格式转换、挂载)一个ramdisk:
hdiutil attach -nobrowse -nomount ram://2048
diskutil erasevolume HFS+ "RamDisk" /dev/disk2
- 创建一个从用户的临时目录到ramdisk的软链接。这个操作是被允许的,因为/tmp的所有者为root,用户的临时目录所有者为用户。
$ ls -lart /var/folders/yx/bp25tm5x4l32k5297qwc7wcd4m022r/
drwx------ 140 patrick staff 4760 Aug 28 09:37 T
rm -rf /var/folders/yx/bp25tm5x4l32k5297qwc7wcd4m022r/T
ln -s /Volumes/RamDisk/ /var/folders/yx/bp25tm5x4l32k5297qwc7wcd4m022r/T
$ ls -lart /var/folders/yx/bp25tm5x4l32k5297qwc7wcd4m022r/
lrwxr-xr-x 1 user staff 17 Aug 27 22:37 T -> /Volumes/RamDisk/
- 等待一些程序(如安装程序)调用AuthorizationExecuteWithPrivileges API。在发现这个bug的时候,几乎所有希望执行任何特权操作(安装、更新等)的第三方程序都调用了这个API。虽然一些交互式攻击者可能希望立即获取root权限,从而方便留下维持持久性的后门程序等。
- 嗅探并恢复AuthorizationExternalForm。只要有程序调用AuthorizationExecuteWithPrivileges(即使是为了执行“安全”操作,如执行/sbin/reboot),AuthorizationExternalForm结构将都会被写入(我们的)ramdisk。作为一个非特权用户,我们可以从ramdisk中读取原始字节来恢复AuthorizationExternalForm:
$ hexdump -s 0x73000 -n 32 -v -e'1/1 "%02x"' /dev/rdisk2
abdf4fe44eb4476ead8601000000000000000000000000000000000000000000
- 等待用户进行身份验证。虽然我们现在可以访问AuthorizationExternalForm,但是如果我们马上尝试使用它,就会得到一个授权错误提示,因为它实际上还没有被用户授权。(用户必须在授权对话框中输入他们的凭据)。所以我们可以设置一个循环,去调用AuthorizationCopyRights(没有使用kAuthorizationFlagInteractionAllowed,因为我们不想主动弹出身份认证对话框),直到用户验证通过(通过AuthorizationExecuteWithPrivileges调用返回结果)。
- 得到root权限一旦用户通过了调用AuthorizationExecuteWithPrivileges触发的授权对话框进行的身份验证,AuthorizationExternalForm(我们已经恢复了!)便也得到了”授权“,因此可以(由任何人使用!)作为root用户执行特权操作。换句话说,我们现在可以生成任何命令或是二进制文件,并以root权限执行!#起飞~
这里需要指出的是如果合法进程调用AuthorizationExecuteWithPrivileges,且调用了AuthorizationFree(authRef, kAuthorizationFlagDestroyRights)释放授权引用;(这是正确做法),那么AuthorizationExternalForm就会失效。但是我们可以监测security_authtrampoline子进程的状态,一旦派生完毕,就向主进程发送kill -STOP使其暂停。因为security_authtrampoline(通过SecurityAgent)正在显示一个窗口(授权对话框),所以挂起进程不会造成任何影响。这也就确保了我们可以在AuthorizationExternalForm失效之前使用它!只要正确地调用kill -CONT,我们的流程就不会有任何问题。
POC演示视频地址。
修复方案
我是在2017年向Apple报告了此漏洞,Apple随即采取措施缓解危害,并在之后发布了最终的更全面的修复方案。
短期的修复措施是通过系统完整性保护(SIP,注意sunlnk flag),使得非特权用户无法将用户的临时目录软链接到另一个文件位置(例如,ramdisk)
$ ls -lO@d $TMPDIR
drwx------@ 161 patrick staff sunlnk /var/folders/pw/sv96s36d0qgc_6jh4...000gn/T/
在2018年初, macOS 10.13.1版本修复了底层问题(最终分配的漏洞编号:CVE-2017-7170)
最终的修复方案非常直接(见`AuthorizationTrampoline.cpp)。不再将”具体化“的授权引用写入临时文件再将文件句柄传入security_authtrampoline,而是AuthorizationExecuteWithPrivileges将句柄传入管道:
1// make text representation of the pipe handle
2char pipeFdText[20];
3snprintf(pipeFdText, sizeof(pipeFdText), "auth %d", dataPipe[READ]);
4const char **argv = argVector(trampoline, pathToTool, pipeFdText, arguments);
5
6...
之后将”具体化“的AuthorizationRef写入管道:
1...
2switch (fork())
3
4// parent
5default: {
6
7 write(dataPipe[WRITE], extForm, sizeof(*extForm)) != sizeof(*extForm));
8
9 ...
这是一种将”具体化“的AuthorizationRef传递给子进程security_authtrampoline的安全做法。
总结
在OSX/macOS系统中一直探索就会发现有趣的bugs!这次我们讨论了我最喜欢的漏洞发现之一;一个稳定的本地特权提升漏洞,影响OSX/macOS约13年!(可能是在OSX Tiger中引入的)。
离我向Apple公司报告该漏洞,公司随即做出修复已经有一段时间了(尽管修复工作是默默进行且没有奖励),然而,我一直想写一篇关于这个bug更多细节的文章,于是便有了这一次的分享!
发表评论
您还未登录,请先登录。
登录