带你理解iOS/Mac沙箱机制

8,893 阅读9分钟

iOS/Mac总是给人一种“安全”的印象,那它都有哪些安全技术?其中核心的“沙箱”技术又是如何实现的呢?对于Mac平台未沙盒化的应用,如何自定义启用进程沙盒化控制来降低安全风险?本文带你深入探究走入“沙箱”的世界~

iOS/Mac安全技术

iOS/Mac提供了”纵深的防御“的安全模型,就像城堡安全防御结构一样,如下图所示:

从外到内依次提供了:Develop ID签名及网闸、沙箱、POSIX安全模型及Keychain钥匙串存储加密。除此之外,还包括了比如网络传输安全、APP传输安全、防火墙、磁盘加密、文件保险箱等。

其中被大家所熟悉的更多的是签名+沙箱,iOS及Mac上架App Store的都需要进行签名及开启沙盒化,但对于Mac平台可以使用DevelopID签发而不需要上架App Store,这些应用大部分都未经沙盒化处理,因此存在一定的安全风险。那如何有效的控制”未沙盒化“的进程安全风险呢?我们就从沙箱原理道来~

沙箱概念

“沙箱|”给人的感觉就是进程运行在一个有保护的环境中执行,不会做出规定范围内不允许做的事情;其实质更多的是进程的系统资源访问受到系统的监控及限制,如网络、某些特殊路径、文件的读写等。

应用沙箱限制包括但不限于:

  • 无法突破应用程序目录之外的目录。应用程序只能看到自己的根目录以及沙箱容器目录,对于iOS为根目录为/var/mobile/Application/<app-GUID>,沙箱目录为/var/mobile/containers/var/containers;对于macOS而言,沙箱目录位于~/Library/Containers/<appid>,或者使用了AppGroup则为~/Library/Group Containers/<application-group-id>。但是可以允许访问使用文件选择器用户自己指定的文件,.entitlements指定的文件,以及临时目录、命令行工具目录以及特定的只读文件。对于根目录限制,类似chroot系统调用来修改应用程序的根目录效果。
  • 无法访问系统上的其他进程,即使具有同样UID的进程,应用程序认为自己就是系统上执行的唯一进程。
  • 无法直接使用任何硬件设备,除非.entitlement权利文件中指定的硬件设备;
  • 无法动态生成代码,mmapmprotect系统调用(分别对应于Mach中的vm_map_entervm_map_protect调用)的底层实现被修改了,防止任何将可写内存页面设置为可执行的企图。具体代码中做了判定,做当前内存区域被修改为可读可执行,且不是动态代码签名允许的JIT映射外,是不允许修改内存区域的权利的。
  • 除了用户能执行的操作的一个子集外,无法执行任何其他操作。应用程序不具有root权限(除了Apple自己的应用程序外)。

可以通过“活动监视器”来查看进程的沙盒化情况,如下:

对于macOS Catalina及以下系统,也可以通过asctl sandbox check --pid [pid]来检测是否开启沙盒化。

沙箱原理

强制访问控制

沙箱的实现依赖于Mac一项重要的安全特性:强制访问控制(Mandatory Access Control, MAC),这项特性来源于Trusted BSD,其允许更为精细的安全模型,添加了对象级别的安全性,从而增强了简单粗暴地UNIX模型:限定特定进程针对具体文件或资源(例如套接字和IPC等)的访问权限,而不仅仅通过UNIX权限模型进行限制。

MAC中的关键概念是标签(label) ,来进行预定义的分类,系统中的文件集合或其他对象的集合都可以应用标签来进行分类。如果请求访问的某个对象没有提供匹配的标签,则MAC就会拒绝访问请求。macOS对其进行了扩充,包含了安全策略的支持。这些安全可以应用于各种各样的操作,而不只是对象

MAC提供了一个稳固的基础,可以向其添加组件,而组件并不要求是内核中的一部分,可以是插件的形式来对系统的安全性进行控制。可以在MAC中注册特殊的内核扩展,让其负责实施某个安全策略。从内核的角度看,各种系统调用的实现都插入了对MAC的调用,因此每个系统调用首先都必须通过MAC的验证。如果策略模块没有注册特定的回调函数,则这些回调函数就直接返回0(表示验证通过)。由策略模块来提供验证逻辑,而MAC层本身并不会做任何决策。

内核还提供了一些MAC专用的系统调用,大部分调用都和FreeBSD一致,比如execve的MAC专用系统调用__mac_execv,以及支持MAC的对indirect系统调用封装的__mac_syscall

沙箱架构

沙箱整个结构图如下:

其中主要包括了如下模块:

  • libSystem.dylib: 提供sandbox_initsandbox_free_error等函数。
  • libSandbox.dylib: 提供解析,编译,生成*.sb的沙盒profile的函数。
  • sandbox.kext:提供了system call的hook函数
  • AppleMatch.kext:提供了解析profile的函数

沙箱整个大致流程如下图所示:

沙盒化的进程通过系统调用访问系统资源,比如open来打开文件,就会从用户态陷入内核态。由于系统调用被”安插“了MAC层检查,因此会被拦截从而遍历检查策略模块。这里系统启动时会加载sandbox.kext沙箱内核扩展驱动,以及AppleMobileFileIntegrity.kext(AMFI)签名校验内核扩展,并注册至MAC强制访问层。因此,沙盒化的进程所有系统调用会被这两个策略模块过滤进行策略实施,来禁止或放行该系统调用,从而达到限制沙盒化进程访问系统资源的能力。

那沙盒化进程是如何被系统识别并注册至沙箱sandbox.kext策略模块的呢?别着急我们慢慢道来。 所有进程都依赖于libSystem.B.dylib动态库,如下图所示:

当进程启动加载该动态库时,会从程序的可执行文件中提取沙盒化权限,并通过xpc消息发送至secinitd守护进程。该守护进程会调用AppSandbox.framework来创建沙箱配置文件,进而调用__sandbox_ms__mac_syscall系统调用,进而来通过sandbox.kext来实施沙盒化权限策略。 整个流程如下图所示:

对于沙盒化进程权限,可通过codesign -d --entitlements :- [exec_file]来查看如下图所示:

进程的沙箱控制使用及实现

通过上面的沙箱原理分析可以看出,对于沙盒进程的沙盒化实施在用户态层主要依赖libSandbox.dylib动态库。该动态库包含了实施沙箱的主要API,包括但不限于如下API:

/* 开启沙箱
 @param profile见下,flags必须为SANDBOX_NAMED, errorbuf返回错误信息
 @return 0 and errorbuf NULL -> success -1 and errorbuf -> failed
 */
int sandbox_init(const char *profile, uint64_t flags, char **errorbuf);
//释放沙箱错误缓冲区
void sandbox_free_error(char *errorbuf);

进程可以通过调用sandbox_init主动进入沙盒,其中profile配置项包括如下:

上面可配置项可实现:限制网络、文件访问以及纯计算而不适用系统资源。示例代码如下:

#include <sandbox.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main(int argc, const char * argv[]) {
    
    char *error_buf = NULL;
    if (sandbox_init(kSBXProfileNoWrite, SANDBOX_NAMED, &error_buf) != 0)
        perror("sandbox_init error:");
    
    FILE *fp = fopen("/tmp/sandbox.txt" , "w");
    if (!fp) {
        perror("fopen error:");
        return 1;
    }
    
    char read_buf[1024] = {0};
    while (gets(read_buf) != NULL) {
        fwrite(read_buf, strlen(read_buf), 1, fp);
    }
    
    fclose(fp);
    
    return 0;
}

上述代码直接fopen报错:fopen error:: Operation not permitted。因为开启了kSBXProfileNoWrite即禁止任何文件写,比如fopen("xxx", "w"),或者fwrite(),但是对于fopen("xxx", "r")是不会限制的,或者对于沙箱开启前已经打开的文件描述符仍然是可用的。

虽然sandbox_init接口已经明确被弃用,但仍然是可用的,比如chrominum毅然使用了该接口。并且沙箱权利是可以被继承的,即对于fork衍生的子进程(不管是否变为孤儿进程)也同样具有父进程的沙箱权利,比如kSBXProfileNoWrite不可写则子进程也不具备写权利。不过对于已经开启沙盒化的进程,不能再次调用上传API,否则会存在冲突报错!

除了上面预先配置的配置文件之外,OS X 的沙箱包装命令 sandbox-exec 提供了一种灵活的配置语法,允许创建一个自定义的沙箱,该沙箱将在其中执行的应用程序的特定功能列入黑名单或白名单。 该工具实际是sandbox_init的包装器,在fork/exec调用前被执行,其核心实现如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h> // fchmod
#include <sandbox.h>
#include "sandbox.h" // My sandbox.h

typedef int bool;
#include <dlfcn.h>

void usage(void)
{
	
	fprintf(stderr, "Usage: sandbox-exec [options] command [args]\nOptions:\n  -f profile-file     Read profile from file.\n  -n profile-name     Use pre-defined profile.\n  -p profile-string   Specify profile on the command line.\n  -D key=value        Define a profile parameter.\n -t trace_file    Trace operations to trace_file\n Exactly one of -f, -n, -p must be specified.\n");

	exit(0x40);

} // usage


int debug = 0;
#define dprintf if (debug) printf

int main (int argc, char **argv)
{

	debug = (getenv ("JDEBUG") != NULL);
	// This is as close as possible to the decompilation of OS X's (10.11.4) sandbox-exec
	// including calling sandbox_create_params() before processing arguments. 

	sbProfile_t *compiled_profile;

	char *err = NULL;

	sbParams_t *params=sandbox_create_params();

	if (!params) { fprintf(stderr,"Can't create params!\n"); exit(1);}

	if (argc < 2) { usage(); }



	int opt;

	char *profile = NULL;
	int cmd = 0;
	char *profileName = NULL;
	char *profileString = NULL;
	char *tracePath = NULL;

	while ((opt = getopt(argc,argv,"D:c:de:f:n:p:t:")) > -1)
	{
		switch (opt)
		{
			case 'f':  profile = optarg; break;
			case 'n':  profileName = optarg; break;
			case 'p':  profileString = optarg; break;
			case 't':  tracePath = optarg; break;
		
			default: usage();
		}
	}

	cmd = optind;
	
	if (tracePath && profileName)
	{
		fprintf(stderr, "tracing isn't implemented for named profiles; use -f or -p to specify a profile\n");		
		exit(0x40);
	}

	//compiled_profile = sandbox_compile_entitlements ("no-internet", params, &err);


	if (profile) compiled_profile = sandbox_compile_file (profile, params, &err);

	// The built-in profiles:
	// ----------------------
	// kSBXProfileNoInternet (no-internet)
	// kSBXProfileNoWriteExceptTemporary (no-write-except-temporary)
	// kSBXProfileNoWrite         (no-write)
	// kSBXProfileNoNetwork       (no-network)
	// kSBXProfilePureComputation (pure-computation)

	if (profileName) compiled_profile = sandbox_compile_named (profileName, params, &err);
	if (profileString) compiled_profile = sandbox_compile_string (profileString, params, &err);

	//sandbox_set_param (params, "x", "y");
	if(!compiled_profile) { fprintf(stderr, "No compiled profile. Error: %s\n", err); exit(2); } 


	int dump= 1;
	if (dump && compiled_profile->blob)
	{
	fprintf(stderr,"Profile: %s, Blob: %p Length: %d\n",
			(compiled_profile->name? compiled_profile->name : "(custom)" ), 
                         compiled_profile->blob, compiled_profile->len);
	int fd = open("/tmp/out.bin", O_WRONLY | O_TRUNC| O_CREAT);
	fchmod (fd, 0666);
	write(fd, compiled_profile->blob, compiled_profile->len);
	fprintf(stderr,"dumped compiled profile to /tmp/out.bin\n");
	}

	int flags = 0;
	int rc = 0;
	if (tracePath) { 
#ifdef SB459
		
		rc = sandbox_set_trace_path(compiled_profile,tracePath); 
#else
		void *sblibhandle = dlopen ("libsandbox.dylib", RTLD_GLOBAL);

		typedef int sstp(void *, char *);
		sstp * sandbox_set_trace_path = dlsym (sblibhandle, "sandbox_set_trace_path");

		if (!sandbox_set_trace_path)
		fprintf(stderr,"Warning: Tracing not supported in this sandbox version (can't get set_trace_path - %p)\n", sblibhandle);
		else {
			rc = sandbox_set_trace_path(compiled_profile,tracePath); 

		}
#endif
		if (rc == 0) fprintf(stderr,"Tracing to %s\n", tracePath);
		else fprintf(stderr,"Tracing error - Unable to trace to %s\n", tracePath);

	} 


	fprintf(stderr,"Applying container\n");
	rc =  sandbox_apply_container (compiled_profile, flags);

	if (rc != 0) { perror("sandbox_apply_container"); }
	
	fflush(NULL);
	if (compiled_profile) sandbox_free_profile(compiled_profile);
	fprintf (stderr, "EXECING %s\n", argv[optind]);
	execvp (argv[optind], argv+optind);

	perror("execvp");


}

我们所熟知的Chrome沙箱也是基于libsandbox.dylib中的API来实现,具体代码可见content/common/sandbox_mac.mm,最新的源码可见chromium.googlesource.com/chromium/sr…

沙箱开源限制如下类型资源:

  • 文件:读、写以及任意类型的操作
  • IPC:Posix以及SysV
  • Mach
  • 网络:输入/输出
  • 进程:执行及克隆
  • 信号
  • Sysctl
  • System

sandbox-exe使用规则如下:

$ sandbox-exec [-f profile-file] [-n profile-name] [-p profile-string] [-D key=value ...] command [arguments ...]

可以通过-f作用于配置文件,-n来作用于配置名称,以及-p直接通过配置字符串来生效,如下:

$ sandbox-exec –f /usr/share/sandbox/bsd.sb /bin/ls
$ sandbox-exec -n no-internet ping www.google.com

可以参考系统的沙箱配置文件,路径包括:

  • /Library/Sandbox/Profiles
  • /System/Library/Sandbox/Profiles
  • /usr/share/sandbox

比如限制特定端口的访问如下:

$ sandbox-exec -p '
(version 1)
(allow default)
(deny network-outbound (remote ip "*:80"))' curl www.baidu.com

具体的语法规则可见Apple's Sandbox Guide v1.0 @2011

友情感谢

[1] macOS 的安全性

[2] Security and Your Apps-wwdc15

[3] iOS和macOS沙盒技术分析

[4] App Sandbox Design Guide

[5] 深入解析Mac OSX&iOS操作系统

[6] apple沙盒研究之基础知识