苹果,我完全无法理解你
¶前言
最近几周,我花了不少时间在 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 - Transparency, Consent, and Control
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 中都有出现。这些方法包括且不限于:
这样就呈现出了一个非常怪异的状态。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 管理限制的服务名。比如kTCCServiceMicrophone
,kTCCServiceCamera
,kTCCServicePhotos
等等。完整的列表我放在本文末尾了。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 validate
和 bool 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.app
的 access
表的插入语句。
这个插入语句的作用呢,一般来说是用来帮助 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/env
的Blob
描述原文的二进制表示。构造方法上面一节已经说的清清楚楚了。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 | |
kTCCServiceTwitter | |
kTCCServiceFacebook | |
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 |