文章目录
  1. 1. 前言
  2. 2. 系统扩展是什么
  3. 3. 能实现什么功能
  4. 4. Network Extension
  5. 5. Endpoint Security
  6. 6. 准备工作
  7. 7. 常见问题
    1. 7.0.1. 如何安装
    2. 7.0.2. 如何管理
    3. 7.0.3. 如何公证
    4. 7.0.4. 如何调试
    5. 7.0.5. 主App安装系统扩展时报错排查
    6. 7.0.6. 如何与系统扩展通讯
    7. 7.0.7. 其他错误
  • 8. 推荐阅读
  • 前言

    从iOS转入Mac安全类产品的开发已经有一年半时间了,我也从一开始负责网络和UI模块慢慢追加负责内核模块的开发和维护,总体看,Mac开发的各种功能的实现思路和方法,要比iOS广阔很多,能接触到的计算机知识体系也更加完整,收获很多。

    言归正传,今年苹果发布了M1芯片,在M1芯片的机器上又对内核开发做了很多限制,移除大量内核符号,导致我们软件无法采用原来的内核方案运作,被迫切换到系统扩展上来。由于领导的信任,对整个程序架构的调整和全新技术方案的落地就由我来主导了,经过几个月封闭式调研开发,目前基本已经能支持大部分原来内核扩展的功能了。本文就是我对系统扩展开发的记录和总结,现在,来聊一聊系统扩展的能力。

    系统扩展是什么

    在说系统扩展之前,先了解一下内核扩展。桌面程序的开发,一般分为应用层开发和内核开发(驱动开发),相比之下,iOS和安卓等移动端就只有应用层开发。关于内核的概念可以自行学习这不多说。

    我们知道,操作系统一般是指运行在硬件之上,为各类应用层软件系统硬件管理方案的软件系统,而内核扩展作为操作系统的一个扩展功能,和操作系统运行在同个层级。这就意味着,如果内核扩展出现BUG导致Crash,也就意味着操作系统会崩溃死机。而一般的应用层软件,出问题顶多就是软件自己Crash。

    所以开发内核扩展就必须小心翼翼,而且因为内核扩展是运行在内核层,可供使用的API也很少,还得用C语言来写,开发效率极低,调试效率极低。为了解决内核扩展这一系列问题,苹果在Mac 10.15开始推出系统扩展,用来取代内核扩展,并开始在后续新系统裁剪内核扩展符号,引导开发者切换到系统扩展。

    当然要求开发者使用系统扩展取代内核扩展的原因,还有一点是系统封闭性的增强,因为内核扩展我们能做的功能也非常多,比如Hook内核中的系统调用等,而系统扩展只是运行在应用层的一个普通程序,对系统的威胁性就降低很多了。

    能实现什么功能

    说了一大堆内核扩展和系统扩展的背景知识,这里大家应该有清晰的理解,那系统扩展到底能支持什么?

    目前看,系统扩展能支持的功能,基本能和内核扩展KPI对齐,细节上还是比内核扩展提供的KPI要弱一些。另外由于无法像内核扩展一样Hack操作系统的关键流程,Hook系统调用表,系统扩展总体能带来的功能要少一个数量级。

    目前苹果为系统扩展提供两个功能模块:Network Extensions(下文简称NE扩展),Endpoints Security。此外还为驱动软件比如USB接口提供一个类似的应用层扩展模块:DriverKit。

    Network Extension

    网络扩展这个功能在iOS8和Mac10.10的时候就已经有了。在Mac10.15之前,网络扩展必须通过商店下载的App才能支持,而且只有一个Provider类,在Mac10.15之后,网络扩展除了以原来的模式存在,还允许以系统扩展的模式运行。

    系统扩展模式下的网络扩展,除了具有Provider类,还具备main函数,它被以完整程序的形式运行起来。main函数代码如下:

    text

    autoreleasepool {

    NEProvider.startSystemExtensionMode()

    }

    dispatchMain()

    官网对系统扩展之网络扩展的文档还停留在好几年前的模式,官网网络扩展文档,这份文档里面的大部分描述都和内核KPI无关,这是一个坑,导致我调研网络扩展功能的时候走了很多弯路。旧的网络扩展API,网上有非常多的基于iOS的描述,这部分和Mac没有什么区别,这里就不多说了。

    简单讲,网络扩展主要提供两套不同层级的API。一个是Packet Tunnel, 对应的类是NEPacketTunnelProvider,提供IP层协议的流量,基于IP Packet实现的网络扩展,可以读取到整个电脑的IP报文,具体到编程,就是从苹果提供的NEPacketTunnelFlow里面读取到NSData数组和协议编号,其中协议编号主要是用来表示协议是IPv4还是IPv6,方法原型如下:

    text

    open func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void)

    通过这套API,就可以读取到整个电脑的所有网络流量了,市面上有部分VPN协议就是基于Packet实现的,比如著名的WireGuard。读取到流量之后,一般就是通过自己定义的VPN协议,把数据转发到后台服务器,后台服务器解析协议之后,提取Packet内容,写入UTUN等虚拟网卡,就能把流量从后台服务器发出去了,再读取网卡,把回包弄回到客户端,这样就实现了一套完整的VPN功能。

    苹果官方提供了网络扩展Demo,不过太久没维护只能支持到Swift3.0,我推荐一个仓库,已经支持到Swift5.0了:SimpleTunnel_Swift5.0

    第二套API,基于App层面的App Proxy Provider,对应的类有NEAppProxyProvider,以及子类NETransparentProxyProvider。 这套API主要是处理TCP/UDP流量,也就是基于IP层协议的两个传输层协议,苹果也提供了类似的Flow类已经读取数据的方法,读取出来的数据就是TCP或者UDP协议传输的数据NSData,不包括三次握手,不包括协议头部等数据。相比IP层协议,这方便开发者直接对内容进行处理。下面重点说一下这两个Provider类的区别。

    NEAppProxyProvider早在iOS 9就已经出现了,它的限制比较多,需要使用MDN进行配置,也就是描述文件,而且无法代理系统APP,想要代理什么APP的流量也需要逐个配置,另外如果流量来了,然后扩展拒绝代理的话,被代理的程序会直接得到网络中断的错误。

    如果了解内核Socket KPI的人应该能看明白,这些功能是无法取代内核Socket相关的KPI。苹果自己当然也明白,所以在推出系统扩展的时候,为了让系统扩展能取代内核Socket鉴权那套KPI,苹果在MacOS11系统发布的时候,也推出了子类NETransparentProxyProvider

    NETransparentProxyProvider类主要是提供了类似内核Socket KPI的功能,除了拥有AppProxy全部功能外,还拥有了更加强大的功能,不需要MDN配置,可以直接通过几行代码创建是VPN配置;允许设置子网和端口来代理所有OutBound流量,也就是往外发的流量,而且也代表了它能处理系统App的流量;允许选择性地跳过代理,跳过代理时,系统会让流量走原来的流程,被代理的程序无感知,这就意味着它拥有类似内核KPI的鉴权功能,即跳过流量则流量走原路,接管流量然后转发流量则实现了VPN功能,接管流量但是不转发流量则实现了拒绝网络请求的功能。

    相关API如下:

    text

    - (BOOL)handleNewFlow:(NEAppProxyFlow *)flow;


    - (BOOL)handleNewUDPFlow:(NEAppProxyUDPFlow *)flow initialRemoteEndpoint:(NWEndpoint *)remoteEndpoint;

    上面两个可被重写的方法,一个用来处理UDP流,另一个可以处理TCP和UDP,具体用法自行参考文档。

    相比内核KPI,NETransparentProxyProvider类少了处理InBound的功能,以及Inject Socket data,以及其他Socket KPI大大小小的Socket操作的功能,这里就不多说了。

    Endpoint Security

    Endpoint Security系统扩展(以下简称ES),是苹果全新推出的用来取代内核KAUTH的系统扩展,功能可对齐KAUTH,可能还更为强大。

    ES相比网络扩展要简单很多,支持监听事件,鉴权事件,这也就意味着可以通过系统扩展来限制用户启动某些程序,访问某些文件,或者执行某些权限。具体支持的事件可以看下面:

    text

    typedef enum {

    // The following events are available beginning in macOS 10.15

    ES_EVENT_TYPE_AUTH_EXEC

    , ES_EVENT_TYPE_AUTH_OPEN

    , ES_EVENT_TYPE_AUTH_KEXTLOAD

    , ES_EVENT_TYPE_AUTH_MMAP

    , ES_EVENT_TYPE_AUTH_MPROTECT

    , ES_EVENT_TYPE_AUTH_MOUNT

    , ES_EVENT_TYPE_AUTH_RENAME

    , ES_EVENT_TYPE_AUTH_SIGNAL

    , ES_EVENT_TYPE_AUTH_UNLINK

    , ES_EVENT_TYPE_NOTIFY_EXEC

    , ES_EVENT_TYPE_NOTIFY_OPEN

    , ES_EVENT_TYPE_NOTIFY_FORK

    , ES_EVENT_TYPE_NOTIFY_CLOSE

    , ES_EVENT_TYPE_NOTIFY_CREATE

    , ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA

    , ES_EVENT_TYPE_NOTIFY_EXIT

    , ES_EVENT_TYPE_NOTIFY_GET_TASK

    , ES_EVENT_TYPE_NOTIFY_KEXTLOAD

    , ES_EVENT_TYPE_NOTIFY_KEXTUNLOAD

    , ES_EVENT_TYPE_NOTIFY_LINK

    , ES_EVENT_TYPE_NOTIFY_MMAP

    , ES_EVENT_TYPE_NOTIFY_MPROTECT

    , ES_EVENT_TYPE_NOTIFY_MOUNT

    , ES_EVENT_TYPE_NOTIFY_UNMOUNT

    , ES_EVENT_TYPE_NOTIFY_IOKIT_OPEN

    , ES_EVENT_TYPE_NOTIFY_RENAME

    , ES_EVENT_TYPE_NOTIFY_SETATTRLIST

    , ES_EVENT_TYPE_NOTIFY_SETEXTATTR

    , ES_EVENT_TYPE_NOTIFY_SETFLAGS

    , ES_EVENT_TYPE_NOTIFY_SETMODE

    , ES_EVENT_TYPE_NOTIFY_SETOWNER

    , ES_EVENT_TYPE_NOTIFY_SIGNAL

    , ES_EVENT_TYPE_NOTIFY_UNLINK

    , ES_EVENT_TYPE_NOTIFY_WRITE

    , ES_EVENT_TYPE_AUTH_FILE_PROVIDER_MATERIALIZE

    , ES_EVENT_TYPE_NOTIFY_FILE_PROVIDER_MATERIALIZE

    , ES_EVENT_TYPE_AUTH_FILE_PROVIDER_UPDATE

    , ES_EVENT_TYPE_NOTIFY_FILE_PROVIDER_UPDATE

    , ES_EVENT_TYPE_AUTH_READLINK

    , ES_EVENT_TYPE_NOTIFY_READLINK

    , ES_EVENT_TYPE_AUTH_TRUNCATE

    , ES_EVENT_TYPE_NOTIFY_TRUNCATE

    , ES_EVENT_TYPE_AUTH_LINK

    , ES_EVENT_TYPE_NOTIFY_LOOKUP

    , ES_EVENT_TYPE_AUTH_CREATE

    , ES_EVENT_TYPE_AUTH_SETATTRLIST

    , ES_EVENT_TYPE_AUTH_SETEXTATTR

    , ES_EVENT_TYPE_AUTH_SETFLAGS

    , ES_EVENT_TYPE_AUTH_SETMODE

    , ES_EVENT_TYPE_AUTH_SETOWNER

    // The following events are available beginning in macOS 10.15.1

    , ES_EVENT_TYPE_AUTH_CHDIR

    , ES_EVENT_TYPE_NOTIFY_CHDIR

    , ES_EVENT_TYPE_AUTH_GETATTRLIST

    , ES_EVENT_TYPE_NOTIFY_GETATTRLIST

    , ES_EVENT_TYPE_NOTIFY_STAT

    , ES_EVENT_TYPE_NOTIFY_ACCESS

    , ES_EVENT_TYPE_AUTH_CHROOT

    , ES_EVENT_TYPE_NOTIFY_CHROOT

    , ES_EVENT_TYPE_AUTH_UTIMES

    , ES_EVENT_TYPE_NOTIFY_UTIMES

    , ES_EVENT_TYPE_AUTH_CLONE

    , ES_EVENT_TYPE_NOTIFY_CLONE

    , ES_EVENT_TYPE_NOTIFY_FCNTL

    , ES_EVENT_TYPE_AUTH_GETEXTATTR

    , ES_EVENT_TYPE_NOTIFY_GETEXTATTR

    , ES_EVENT_TYPE_AUTH_LISTEXTATTR

    , ES_EVENT_TYPE_NOTIFY_LISTEXTATTR

    , ES_EVENT_TYPE_AUTH_READDIR

    , ES_EVENT_TYPE_NOTIFY_READDIR

    , ES_EVENT_TYPE_AUTH_DELETEEXTATTR

    , ES_EVENT_TYPE_NOTIFY_DELETEEXTATTR

    , ES_EVENT_TYPE_AUTH_FSGETPATH

    , ES_EVENT_TYPE_NOTIFY_FSGETPATH

    , ES_EVENT_TYPE_NOTIFY_DUP

    , ES_EVENT_TYPE_AUTH_SETTIME

    , ES_EVENT_TYPE_NOTIFY_SETTIME

    , ES_EVENT_TYPE_NOTIFY_UIPC_BIND

    , ES_EVENT_TYPE_AUTH_UIPC_BIND

    , ES_EVENT_TYPE_NOTIFY_UIPC_CONNECT

    , ES_EVENT_TYPE_AUTH_UIPC_CONNECT

    , ES_EVENT_TYPE_AUTH_EXCHANGEDATA

    , ES_EVENT_TYPE_AUTH_SETACL

    , ES_EVENT_TYPE_NOTIFY_SETACL

    // The following events are available beginning in macOS 10.15.4

    , ES_EVENT_TYPE_NOTIFY_PTY_GRANT

    , ES_EVENT_TYPE_NOTIFY_PTY_CLOSE

    , ES_EVENT_TYPE_AUTH_PROC_CHECK

    , ES_EVENT_TYPE_NOTIFY_PROC_CHECK

    , ES_EVENT_TYPE_AUTH_GET_TASK

    // The following events are available beginning in macOS 11.0

    , ES_EVENT_TYPE_AUTH_SEARCHFS

    , ES_EVENT_TYPE_NOTIFY_SEARCHFS

    , ES_EVENT_TYPE_AUTH_FCNTL

    , ES_EVENT_TYPE_AUTH_IOKIT_OPEN

    , ES_EVENT_TYPE_AUTH_PROC_SUSPEND_RESUME

    , ES_EVENT_TYPE_NOTIFY_PROC_SUSPEND_RESUME

    , ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED

    , ES_EVENT_TYPE_NOTIFY_GET_TASK_NAME

    , ES_EVENT_TYPE_NOTIFY_TRACE

    , ES_EVENT_TYPE_NOTIFY_REMOTE_THREAD_CREATE

    , ES_EVENT_TYPE_AUTH_REMOUNT

    , ES_EVENT_TYPE_NOTIFY_REMOUNT

    // The following events are available beginning in macOS 11.3

    , ES_EVENT_TYPE_AUTH_GET_TASK_READ

    , ES_EVENT_TYPE_NOTIFY_GET_TASK_READ

    , ES_EVENT_TYPE_NOTIFY_GET_TASK_INSPECT

    // The following events are available beginning in macOS 12.0

    , ES_EVENT_TYPE_NOTIFY_SETUID

    , ES_EVENT_TYPE_NOTIFY_SETGID

    , ES_EVENT_TYPE_NOTIFY_SETEUID

    , ES_EVENT_TYPE_NOTIFY_SETEGID

    , ES_EVENT_TYPE_NOTIFY_SETREUID

    , ES_EVENT_TYPE_NOTIFY_SETREGID

    , ES_EVENT_TYPE_AUTH_COPYFILE

    , ES_EVENT_TYPE_NOTIFY_COPYFILE

    // ES_EVENT_TYPE_LAST is not a valid event type but a convenience

    // value for operating on the range of defined event types.

    // This value may change between releases and was available

    // beginning in macOS 10.15

    , ES_EVENT_TYPE_LAST

    } es_event_type_t;

    可以看到能支持的事件还是挺丰富的,甚至连MPROTECT这种高端系统调用都有。

    针对AUTH事件,鉴权形式可分成两种,一种是Allow/Deny模式,另一种是Flag模式。应该都很好理解,后者就是允许把事件的访问权限修改成只读操作,或者读写都拒绝。具体代码如下:

    text

    respt = es_respond_auth_result(client, message, ES_AUTH_RESULT_DENY, is_cache);


    respt = es_respond_flags_result(client, message, 0xffffffff, is_cache);

    需要注意的是,如果全部放行,填入0xffffffff。其他相关实例可以参考苹果官方Demo:

    ES系统扩展官方DEMO

    准备工作

    NE系统扩展,无需任何准备工作,照平常开发App的流程即可。不过需要注意一个问题,直接从Xcode里面勾选的NE Entitlement是错误的,需要手动添加-systemextension,举个例子,如果想开发NETransparentProxyProvider,使用Xcode勾选App proxy的时候,生成的Entitlement是app-proxy-provider,需要手动修改成app-proxy-provider-systemextension,而且由于Xcode13.1还不支持这个手动改过的Entitlements,无法支持自动管理证书和Provisioning Profile,需要手动下载具备app-proxy-provider-systemextension权限的Profile文件。具体操作如下:

    登录苹果开发账号管理界面(Certificates, Identifiers & Profiles),选择Developer ID Application类型的证书,在Identifiers里勾选NetworkExtension,无需勾选System Extension,因为这个是主App才需要的安装权限。这样下载下来的Profile文件中的NE Entitlements就是以systemextension结尾了。

    ES系统扩展则比较麻烦,首先需要申请Entitlements:com.apple.developer.endpoint-security.client

    和其他的Entitlements不同,ES的权限需要额外向苹果申请,申请入口可以从上面DEMO页面里找到。

    在申请权限还未被审批之前,可以通过关闭SIP的方式运行没有签名的ES扩展,另外还可以使用systemextensionsctl工具,设置DEBUG模式,方便调试ES扩展。至于NE扩展,调试方式和普通程序无太大区别。

    常见问题

    如何安装

    安装系统扩展,需要先将系统扩展放入主App中,可以先创建主App,然后在同一个工程里面创建系统扩并选择主App的Target,或者手动选择系统扩展包,在Build Phases配置里加上Embed System Extensions步骤,把系统扩展放到System Extensions目录,最终编译之后,系统扩展的App会被放在.app/Contents/Library/SystemExtensions目录下。并且已包名.systemextension命名。

    需要注意的是,主App必须具备com.apple.developer.system-extension.install的Entitlement,才能安装ES扩展。如果要安装NE扩展,还需要具备对应的NE Entitlement,例如app-proxy-provider-systemextension,否则在添加VPN配置的时候会报错。

    除此之外,还需要确保主App和扩展有相同的Team编号。

    如何管理

    借助苹果提供的systemextensionsctl工具,可以查询扩展的状态,卸载扩展。

    如何公证

    系统扩展需要经过公证,才能安装到普通电脑上。具体公证方法参考苹果官方文档:

    系统扩展App公证

    如何调试

    正常调试即可,参考官方文档:

    系统扩展调试

    主App安装系统扩展时报错排查

    主App安装ES系统扩展报错较少,基本上配置好Entitlements就可以了。

    NE则有较多要求。首先需要保证Entitlements中的App-Groups和主App保持一致。其次确保NE的Info.plist文件中MachServiceName必须是主App的Groups的子集或者保持一致。

    text

    <?xml version="1.0" encoding="UTF-8"?>

    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

    <plist version="1.0">

    <dict>

    <key>NetworkExtension</key>

    <dict>

    <key>NEMachServiceName</key>

    <string>0000CSH000.com.xxx.app-group.ne</string>

    <key>NEProviderClasses</key>

    <dict>

    <key>com.apple.networkextension.app-proxy</key>

    <string>$(PRODUCT_MODULE_NAME).AppProxyProvider</string>

    </dict>

    </dict>

    </dict>

    </plist>

    如何与系统扩展通讯

    苹果官方推荐使用XPC技术,系统扩展也自带了全局XPC Mach服务,只需要在plist文件中设置好服务名字即可:

    text

    NetworkExtension,在info.plist设置NEMachServiceName字段

    EndpointSecurity, 在info.plist设置 NSEndpointSecurityMachServiceName

    随后其他程序只要把系统扩展当作全局XPC使用即可:

    text

    [[NSXPCConnection alloc] initWithMachServiceName:@"NSEndpointSecurityMachServiceName字段的值" options:0];

    其他错误

    做好上面说的,基本就能正常运行跑起来了,如果还有问题,目前相关资料很少,只能从苹果论坛得到信息了。地址:SystemExtensions论坛 其中活跃用户:eskimo,是苹果系统扩展相关开发工程师,他的回答基本就是标准答案了。

    推荐阅读

    系统扩展官方介绍

    系统扩展之网络扩展特性WWDC官方资料

    文章目录
    1. 1. 前言
    2. 2. 系统扩展是什么
    3. 3. 能实现什么功能
    4. 4. Network Extension
    5. 5. Endpoint Security
    6. 6. 准备工作
    7. 7. 常见问题
      1. 7.0.1. 如何安装
      2. 7.0.2. 如何管理
      3. 7.0.3. 如何公证
      4. 7.0.4. 如何调试
      5. 7.0.5. 主App安装系统扩展时报错排查
      6. 7.0.6. 如何与系统扩展通讯
      7. 7.0.7. 其他错误
  • 8. 推荐阅读