CVE-2017-7170权限提升漏洞深入分析

前言

这次跟大家分享的漏洞在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执行特权操作时:

  1. 首先调用“授权API”(即AuthorizationExecuteWithPrivileges),该API生成XPC消息到“授权进程”(authd)。
  2. 授权进程向权限数据库发出请求,因为需要用户进行验证,数据库向“安全代理”发送另一条XPC消息。
  3. “安全代理”向用户显示实际的身份验证对话框。
  4. 如果提供了有效的身份验证凭据,则允许特权操作执行。

现在让我们仔细梳理一下缺陷发生时相关的步骤。

查看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以后的所有版本),我们可以通过以下步骤完成攻击:

  1. 创建(或者通过格式转换、挂载)一个ramdisk:
hdiutil attach -nobrowse -nomount ram://2048
diskutil erasevolume HFS+ "RamDisk" /dev/disk2
  1. 创建一个从用户的临时目录到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/
  1. 等待一些程序(如安装程序)调用AuthorizationExecuteWithPrivileges API。在发现这个bug的时候,几乎所有希望执行任何特权操作(安装、更新等)的第三方程序都调用了这个API。虽然一些交互式攻击者可能希望立即获取root权限,从而方便留下维持持久性的后门程序等。
  2. 嗅探并恢复AuthorizationExternalForm。只要有程序调用AuthorizationExecuteWithPrivileges(即使是为了执行“安全”操作,如执行/sbin/reboot),AuthorizationExternalForm结构将都会被写入(我们的)ramdisk。作为一个非特权用户,我们可以从ramdisk中读取原始字节来恢复AuthorizationExternalForm:
$ hexdump -s 0x73000 -n 32 -v -e'1/1 "%02x"' /dev/rdisk2
abdf4fe44eb4476ead8601000000000000000000000000000000000000000000
  1. 等待用户进行身份验证。虽然我们现在可以访问AuthorizationExternalForm,但是如果我们马上尝试使用它,就会得到一个授权错误提示,因为它实际上还没有被用户授权。(用户必须在授权对话框中输入他们的凭据)。所以我们可以设置一个循环,去调用AuthorizationCopyRights(没有使用kAuthorizationFlagInteractionAllowed,因为我们不想主动弹出身份认证对话框),直到用户验证通过(通过AuthorizationExecuteWithPrivileges调用返回结果)。
  2. 得到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更多细节的文章,于是便有了这一次的分享!

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章