TCC 数据库:开发者的噩梦,攻击者的狂欢

苹果,我完全无法理解你

前言

最近几周,我花了不少时间在 macOS 的自动化流程以及 MDM (Mobile Device Management) 的开发上。

事实上我一直以来都难以理解为什么有那么多企业选择使用 macOS 作为员工的工作电脑。

姑且可以认为多数的科技公司从业者都具备一些必要的计算机常识和命令行技能,但是 macOS 本身其实是基于 XNU 内核构建的 Darwin ,而 Darwin 本身又包含了大量来自 BSD 的特性,这些特性意味着用户在深入使用的时候,往往不得不面对一些在 BSD 和 Linux 中同名同姓却完全不同的命令,例如 mount

而更加好死不死的是,Apple 还额外贴心地从 macOS 10.11 开始加入了 SIP (System Integrity Protection),也就是著名的苹果是你的爸爸组件。

这意味着无论你是卑微的个人设备还是财大气粗的企业采购设备,都要面临你的 root 不是真正的 root 这样的糟糕体验。而更加搞笑的是,有时候 SIP 不但没有真正保护用户安全,还接二连三的爆出涉及 TCC.db 的各种权限漏洞,给黑客大开便捷之门。有时候真觉得 macOS 上开发工作流太噩梦了。

本篇我就稍微聊一聊 TCC.db 这个数据库。

TCC.db 是 macOS 10.9 之后引入的一个数据库,用于记录用户对于各种系统服务的授权情况。系统级 TCC.db 的完整路径是 /Library/Application Support/com.apple.TCC/TCC.db。而对于每一个单独的用户,其实还有一个 TCC.db,位于 $HOME/Library/Application Support/com.apple.TCC/TCC.db

这个数据库的结构非常简单,只有 6 个表:

  • access
  • active_policy
  • expired
  • access_overrides
  • admin
  • policies

事实上这 6 个表里,在默认情况下有且只有 access 这一个表是有有效数据的。其他的表利用 sqlite3 查看的话可以发现都是空表,而 admin 表也仅有一行:

sqlite3 "./Library/Application Support/com.apple.TCC/TCC.db" "select * from access_overrides;"
sqlite3 "./Library/Application Support/com.apple.TCC/TCC.db" "select * from active_policy;"
sqlite3 "./Library/Application Support/com.apple.TCC/TCC.db" "select * from expired;"
sqlite3 "./Library/Application Support/com.apple.TCC/TCC.db" "select * from policies;"
sqlite3 "./Library/Application Support/com.apple.TCC/TCC.db" "select * from admin;"
version|20

PS: 无端推测,iOS 的权限实现大概率也是基于类似的机制。

然而这一切对开发者友善的前提是,macOS 需要在 Darwin 的命令行支持或系统开发接口中,也复刻一套这样的授权机制。然而事实上是,macOS 没有做到这一点,也似乎并不打算做好这些支持。

稍微对 SIP 有所接触的人应该会很容易察觉到,苹果对于用户可以在自己的机器上可以做什么这件事情上,做了非常多的限制。在最近的几个版本的 macOS 中,对于所有系统相关的目录,无论用户本身是否是 Administrator,都仅能做只读操作;即便用户通过 sudo su 提权到 root,也无法对这些目录进行任何的写操作。

然而 macOS 就仿佛脑子神经搭错了一样,把 TCC.db 放在了一个普通用户可以进行读写的位置。这就留下隐患了。

当然,macOS 在正常情况下对 TCC.db 还是进行了许多的保护,但是在过去的几年中,这些保护可以被二十多种方法[1]绕过,TCC 提权漏洞在几乎每一个版本的 macOS 中都有出现。这些方法包括且不限于:

  • 插件注入
  • 进程注入[4]
  • /Library & $HOME 挂载 [5], [6]
  • App 行为漏洞利用
  • /usr/bin/grep 注入

这样就呈现出了一个非常怪异的状态。macOS 在每一个版本里都留下了方便攻击者的 TCC 提权方式,却对有着 Administrator 权限的用户进行了严格的命令行指令的限制。举几个例子,以下操作就必须通过往 TCC.db 中写入数据来实现:

  • 通过 /usr/bin/env 在系统后台静默更新特定用户的壁纸。
  • 通过 /bin/bash 静默禁用&启用用户的麦克风和摄像头。这条用过 OBS 的人应该不陌生。

而这些操作本身理应由 Administrator 通过命令行是可以轻松实现的。但是由于 macOS 的糟糕的权限设计,用户不得不深入到 TCC.db 里去,用各种很 Tricky 的方式来实现。

详解 access 表结构

在上面的章节里,我们查看了 TCC.db 所包含的数据表。而里面最有用的 access 表的结构大概是这么个样子:

CREATE TABLE access (
    service TEXT NOT NULL,
    client TEXT NOT NULL,
    client_type INTEGER NOT NULL,

--  allowed INTEGER NOT NULL,       -- Removed in Big Sur
--  prompt_count INTEGER NOT NULL,  -- Removed in Big Sur

    auth_value INTEGER NOT NULL,    -- Added in Big Sur
    auth_reason INTEGER NOT NULL,   -- Added in Big Sur
    auth_version INTEGER NOT NULL,  -- Added in Big Sur

    csreq BLOB,
    policy_id INTEGER,

    -- Added in Mojave
    indirect_object_identifier_type INTEGER,
    indirect_object_identifier TEXT NOT NULL DEFAULT "UNUSED",
    indirect_object_code_identity BLOB,

    flags INTEGER,
    last_modified INTEGER NOT NULL DEFAULT (CAST(strftime('%s','now') AS INTEGER))
)

这样的多维结构,使得用户可以在非常细致的颗粒度上控制自己的设备。例如,你可以授权某个应用访问你的摄像头,但是不授权它访问你的麦克风;你可以授权某个应用访问你的通讯录,但是不授权它访问你的日历;你可以授权某个应用访问你的照片,但是不授权它访问你的照片库。

以下为每个字段的详细解释:

  • service: 受 TCC 管理限制的服务名。比如 kTCCServiceMicrophonekTCCServiceCamerakTCCServicePhotos 等等。完整的列表我放在本文末尾了。
  • client: 申请访问服务的应用的 Bundle Identifier 或者绝对路径(例如 com.apple.finder 或者 /usr/libexec/sshd-keygen-wrapper
  • client_type: 申请访问服务的应用的类型。0 代表 Bundle Identifier,1 代表绝对路径。
  • allowed: (本字段仅存在于 Big Sur 之前的版本) 是否允许访问(1)或者拒绝(0
  • prompt_count: (本字段仅存在于 Big Sur 之前的版本) 用户被提示的次数。如果程序在第一次被拒绝后,仍然不断地申请访问,那么这个字段就会不断地增加。
  • auth_value: (本字段仅存在于 Big Sur 以及之后的版本) 访问权限的值。0 代表拒绝,1 代表未知,2 代表允许,3 代表有限制。例如,允许应用选择照片,但是不允许它访问整个照片库。
  • auth_reason: (本字段仅存在于 Big Sur 以及之后的版本) 用于描述 auth_value 是因何理由被设置的。一个常见的值是 3,代表 用户设置。完整的列表我也放在本文末尾了。
  • auth_version: (本字段仅存在于 Big Sur 以及之后的版本) 默认为 1,也可能会随着未来的 macOS 版本而改变。
  • csreq: 二进制代码签名要求 blob 必须满足特定的格式,以便获得访问权限。这是用于防止攻击者的欺骗/冒充。我会在下一个章节描述以下如何进行这部分内容的生成和解码。这里真得感谢 Keith Johnson,即便在英文互联网上,可能也就他那条回答真正解释清楚了这个字段。可以简单理解为对 client 目标进行 csreq 处理后的 Blob 值,我会在下一节详细解释。
  • policy_id: 这个字段与 MDM(Mobile Device Management) 策略相关,carlashley/tccprofile 可以用于生成这些配置文件。
  • indirect_object_identifier: 用于指定某个服务(例如 kTCCServiceAppleEvents)的目标客户端。这个字段可以是 Bundle Identifier 或者绝对路径,就像 client 字段一样。在某些情况下,这个字段会被设置为 UNUSED
  • indirect_object_identifier_type: 用于指定 indirect_object_identifier 字段的类型。0 代表 Bundle Identifier,1 代表绝对路径。
  • indirect_object_code_identity: 和 csreq 字段一样,这个字段也是用于防止攻击者的欺骗/冒充。但是这个字段的作用于 indirect_object_identifier 字段指定的客户端。可以简单理解为对 indirect_object_identifier 目标进行 csreq 处理后的 Blob 值,我会在下一节详细解释。
  • flags: 未知作用。值总是为 0,可能与 MDM 策略一起使用。
  • last_modified: 最后一次修改的时间戳。

如果你还不知道什么是 Blob,可以参考 The BLOB and TEXT Types.

有了这些字段的详细解释,我们就可以读懂甚至构造一条 TCC.db access 语句了。当然在开始之前,我们还需要补充一个知识点,就是 csreq。利用 csreq,我们可以解码一个二进制代码签名 Blob,亦或者从零开始构造一个 Blob

关于 csreq[3]

很多人一看到要构造一个 Blob 第一反应就是慌,事实上我也是一样。

不过我们在插入数据到 TCC.db 的时候,只需要构造一个满足特定格式的、非常短的 Blob 即可。这个 Blob 的格式是由 Apple 的 libsecurity_codesigning 库定义的,源代码可以在这里找到:libsecurity_codesigning/lib/requirement.h

比较粗略的看了一下,这个头文件定义了一个叫 Requirement 的类,用于表示苹果的代码签名要求(Code Signing Requirements)。Requirement 类的成员函数包括用于验证是否合法和满足格式要求的 void validatebool validates;还有用于声明格式的 kind 函数,不过目前唯一支持的表达式的类型是 opExpr

不过实际上我们并不需要手动写 csreq 的生成 & 翻译工具,macOS 本身就自带了一个同名的命令行工具 csreq。这个工具可以用来生成 Blob,也可以用来解码符合格式要求的 Blob。这个工具一个旧版本的源代码在这里:csreq.cpp

下面主要来说说怎么用吧。就以 TCC.db 插入时最常用的一条 Blob 为例,来看看怎么用 csreq 来生成和解码这个 Blob

# Convert the hex string into a binary blob
$ BLOB="FADE0C000000003000000001000000060000000200000012636F6D2E6170706C652E5465726D696E616C000000000003"
$ echo "$BLOB" | xxd -r -p > terminal-csreq.bin

# Ask csreq to tell us what it means
$ csreq -r- -t < terminal-csreq.bin
identifier "com.apple.Terminal" and anchor apple

从解码的结果来看,这条 Blob 代表了一个通过苹果官方签名的 com.apple.Terminal 对象。

那这条信息 identifier "com.apple.Terminal" and anchor apple 本身是怎么来的呢?或者说,我们应该怎么写这条原始文本,并确认其符合 Blob 的解析原文的格式要求呢?其实也很简单,使用另一个命令行工具 codesign 就可以获得任意已签名对象的 designated 字段,也就是 Blob 的合法描述原文:

$ codesign -d -r- /Applications/Utilities/Terminal.app
Executable=/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal
designated => identifier "com.apple.Terminal" and anchor apple

这里再举一个类似的例子,也就是游戏开发团队非常常用的 P4V 客户端,这玩意儿的 Blob 是:

fade0c000000009c00000001000000060000000600000006000000060000000200000010636f6d2e706572666f7263652e7034760000000f0000000e000000010000000a2a864886f763640602060000000000000000000e000000000000000a2a864886f7636406010d0000000000000000000b000000000000000a7375626a6563742e4f550000000000010000000a505959465359353453370000

我们来依样画葫芦地解码一下:

# Convert the hex string into a binary blob
BLOB="fade0c000000009c00000001000000060000000600000006000000060000000200000010636f6d2e706572666f7263652e7034760000000f0000000e000000010000000a2a864886f763640602060000000000000000000e000000000000000a2a864886f7636406010d0000000000000000000b000000000000000a7375626a6563742e4f550000000000010000000a505959465359353453370000"
echo "$BLOB" | xxd -r -p > p4v-csreq.bin

# Ask csreq to tell us what it means
$ csreq -r- -t < p4v-csreq.bin
identifier "com.perforce.p4v" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = PYYFSY54S7

# ask codesign what the requirement text from the application itself is
$ codesign -d -r- /Applications/p4v.app
Executable=/Applications/p4v.app/Contents/MacOS/p4v
designated => identifier "com.perforce.p4v" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = PYYFSY54S7

可以看到,就是这样简单的处理,就能获得任意对象合法的 Blob 描述原文。

那么第二个问题来了,既然可以通过 codesign 来获得 Blob 的描述原文,那么我们应该如何获得 Blob 本身呢?这个问题其实也很简单,只要把 Blob 的描述原文通过 csreq 转换成二进制格式即可。

这里我们继续用 p4v.app 为例:

# Get the requirement string from codesign
$ REQ_STR=$(codesign -d -r- /Applications/p4v.app/ 2>&1 | awk -F ' => ' '/designated/{print $2}')

# Convert the requirements string into it's binary representation(sadly it seems csreq requires the output to be a file; so we just throw it in /tmp)
$ echo "$REQ_STR" | csreq -r- -b /tmp/csreq.bin

# Convert the binary form to hex, and print it nicely for use in sqlite
$ REQ_HEX=$(xxd -p /tmp/csreq.bin  | tr -d '\n')
$ echo "X'$REQ_HEX'"
X'fade0c000000009c00000001000000060000000600000006000000060000000200000010636f6d2e706572666f7263652e7034760000000f0000000e000000010000000a2a864886f763640602060000000000000000000e000000000000000a2a864886f7636406010d0000000000000000000b000000000000000a7375626a6563742e4f550000000000010000000a505959465359353453370000'

如你所见,刚才的 Blob 描述原文,就这样简单的获取到了。看到这一步的你,应该已经有能力自由地获得任意目标的 Blob 描述原文,并将其转换成 Blob 本身。

动手构造一条 TCC.db access 语句

既然万事具备,说再多不如动手构造一条 TCC.db 的插入语句来得记忆深刻。

这里我们以 kTCCServiceAppleEvents 服务为例,构造一个允许 /usr/bin/env 通过 AppleEvents 服务访问 /System/Library/CoreServices/System Events.appaccess 表的插入语句。

这个插入语句的作用呢,一般来说是用来帮助 osascript 命令在执行 Apple Script 的时候,强制跳过一些用户 GUI 层的确认对话框,从而达到静默执行 Login Items 的目的。在类似 JAMF Pro 这样的企业级管理软件中,有不少类似的骚操作。

好,我们开始。

  • 首先是 service 字段,这个字段的值是 kTCCServiceAppleEvents,是我们的目标服务,也就是用于跳过一些强制行的用户确认 Prompts 的服务对象。下一节为参考表
  • 然后是 client 字段,这个字段的值是 /usr/bin/env,这个是我们的目标客户端,也就是我们要让它通过 kTCCServiceAppleEvents 服务访问 /System/Applications/System Preferences.app 的客户端。这里无论是 /usr/bin/env 还是其对应的 identifier,都是可以的,所以也可以写成 com.apple.env
  • client_type 字段,如果你 client 填的是 /usr/bin/env,也就是绝对路径,那就这个字段的值是 1;如果填的是 com.apple.env,也就是 identifier,那就这个字段的值是 0
  • auth_value 字段,那肯定是允许嘛。所以这个字段的值是 2
  • auth_reason 字段,这个字段的值是 3,也就是 User Set。表示是用户自己设置的(笑)。下一节为参考表
  • auth_version 字段,默认就是 1。别问,别管。
  • csreq 字段,这个字段就是对 /usr/bin/envBlob 描述原文的二进制表示。构造方法上面一节已经说的清清楚楚了。
  • policy_id 字段,我们暂时用不到,设置为 NULL
  • indirect_object_identifier_type 也就是被访问对象的类型,这里是 0,也就是 identifier
  • indirect_object_identifier 字段,这个字段的值是 /System/Library/CoreServices/System Events.app/identifier,也就是 com.apple.systemevents
  • indirect_object_code_identity 字段,这个字段的值是 /System/Library/CoreServices/System Events.app/Blob 二进制表示。构造方法也是参考上一节。
  • flags 字段,我们暂时用不到,设置为 NULL
  • last_modified 字段,这个字段的值只要是合法的时间戳就行,我自己一般喜欢用 2022-01-01 00:00:00,也就是 1642634565

那么现在,我们的插入语句已经全部完成了,写出来就是这么个样子:

INSERT INTO access VALUES(
    'kTCCServiceAppleEvents',   -- service
    '/usr/bin/env',                -- client
    1,                          -- client_type
    2,                          -- auth_value
    3,                          -- auth_reason
    1-- auth_version
    -- csreq
    X'fade0c000000002c0000000100000006000000020000000d636f6d2e6170706c652e656e7600000000000003',
    NULL,                       -- policy_id
    0,                          -- indirect_object_identifier_type
    'com.apple.systemevents',   -- indirect_object_identifier
    -- indirect_object_code_identity
    X'fade0c000000003400000001000000060000000200000016636f6d2e6170706c652e73797374656d6576656e7473000000000003',
    NULL,                       -- flags
    1642634565                  -- last_modified
);

之后我们就可以用 sqlite3 命令行工具,或者 DB Browser for SQLite 这样的 GUI 工具,将这条语句插入到 TCC.db 中了。

然后,你就可以通过构造一个 plist 文件和 launchctl 命令,给用户加载一些 Login Items,例如更换壁纸、更换 Dock 图标、更换桌面图标等等,而不需要经过用户在 GUI 的窗口确认了。

以下为一个简单的 plist 文件示例:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>{{ plist_name }}</string>
    <key>Program</key>
    <string>{{ apple_script_path }}</string>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

吐槽:明明非常正常的设备和系统管理操作,非要被苹果弄得像是黑客入侵操作一样。真有你的,库克。

Service & Auth_Reason 参考表[2]


SERVICE List
Value Description
kTCCServiceAddressBook client would like to access your contacts.
kTCCServiceAppleEvents client wants access to control indirect_object. Allowing control will provide access to documents and data in indirect_object, and to perform actions within that app.
kTCCServiceBluetoothAlways client would like to use Bluetooth.
kTCCServiceCalendar client would like to access your calendar.
kTCCServiceCamera client would like to access the camera.
kTCCServiceContactsFull client would like to access all of your contacts information.
kTCCServiceContactsLimited client would like to access your contacts basic information.
kTCCServiceFileProviderDomain client wants to access files managed by indirect_object.
kTCCServiceFileProviderPresence Do you want to allow client to see when you are using files managed by it? It will see which applications are used to access files and whether you are actively using them. It will not see when files that are not managed by it are accessed.
kTCCServiceLocation client would like to use your current location.
kTCCServiceMediaLibrary client would like to access Apple Music, your music and video activity, and your media library.
kTCCServiceMicrophone client would like to access the microphone.
kTCCServiceMotion client Would Like to Access Your Motion & Fitness Activity.
kTCCServicePhotos client Would Like to Access Your Photos
kTCCServicePhotosAdd client Would Like to Add to your Photos
kTCCServicePrototype3Rights client Would Like Authorization to Test Service Proto3Right.
kTCCServicePrototype4Rights client Would Like Authorization to Test Service Proto4Right.
kTCCServiceReminders client would like to access your reminders.
kTCCServiceScreenCapture client would like to capture the contents of the system display.
kTCCServiceSiri Would You Like to Use client with Siri?
kTCCServiceSpeechRecognition client Would Like to Access Speech Recognition.
kTCCServiceSystemPolicyDesktopFolder client would like to access files in your Desktop folder.
kTCCServiceSystemPolicyDeveloperFiles client would like to access a file used in Software Development.
kTCCServiceSystemPolicyDocumentsFolder client would like to access files in your Documents folder.
kTCCServiceSystemPolicyDownloadsFolder client would like to access files in your Downloads folder.
kTCCServiceSystemPolicyNetworkVolumes client would like to access files on a network volume.
kTCCServiceSystemPolicyRemovableVolumes client would like to access files on a removable volume.
kTCCServiceSystemPolicySysAdminFiles client would like to administer your computer. Administration can include modifying passwords, networking, and system settings.
kTCCServiceWillow client would like to access your Home data.
kTCCServiceSystemPolicyAllFiles Full Disk Access
kTCCServiceAccessibility Allows app to control your computer
kTCCServicePostEvent Allows to send keystrokes
kTCCServiceListenEvent Input Monitoring; to monitor input from your keyboard
kTCCServiceDeveloperTool Allows app to run software locally that do not meet the system’s security policy
kTCCServiceLiverpool Related to location services
kTCCServiceUbiquity Related to iCloud
kTCCServiceShareKit Related to the share feature(presumably from iOS)(ShareKit)
kTCCServiceLinkedIn LinkedIn
kTCCServiceTwitter Twitter
kTCCServiceFacebook Facebook
kTCCServiceSinaWeibo Sina Weibo
kTCCServiceTencentWeibo Tencent Weibo

AUTH_REASON List
Value Description
1 Error
2 User Consent
3 User Set
4 System Set
5 Service Policy
6 MDM Policy
7 Override Policy
8 Missing usage string
9 Prompt Timeout
10 Preflight Unknown
11 Entitled
12 App Type Policy

相关文献

  1. 20+ ways to bypass your mac os privacy mechanisms – Csaba Fitzl
  2. A deep dive into macOS TCC.db – Keith Johnson
  3. How to get csreq of macOS application on command line? – Keith Johnson
  4. CVE-2020–9934
  5. CVE-2021-1784
  6. CVE-2021-30920