从 Linux 或 Windows 迁移到 macOS,可能会感觉像是走进了一片陌生的新大陆。 由于 Linux 是开源的,Windows 有完善的文档且非常流行(而 macOS 两者都不占),macOS 有时会让人感到棘手。 在这篇博文中,我打算讨论你在 macOS 上可能首先注意到的一些事情——应用程序,到处都是应用程序!
如果你有 Windows 或 Linux 背景,应用程序(Apps)的概念可能会显得很奇怪。 我们都知道线程是“执行单元”,进程是拥有自己地址空间的线程容器——还有什么更复杂的东西吗? 其实,进程很少以单个文件的形式部署。在 Windows 和 Linux 上,代码运行可能需要很多东西,其中一些包括:
.dll、.so)。例如,C 运行时库(Windows 上的 msvcr<version>.dll、libc-<version>.so)以及其他依赖项。PE 的格式,该格式包含多个目录——其中之一是资源目录(甚至在一定程度上记录在此处),其中可能包含资源(图像、字符串等)。当然,资源也可以从磁盘动态加载。PE 文件本身(阅读此处)或目录文件中(即外部)。xml、ini、json)和 Windows 注册表。macOS 非常重视应用程序包。其理念是将程序运行所需的(几乎)所有内容打包到一个目录结构中——包括资源、本地化信息等。当然,并非所有内容都能很好地打包(例如 C 运行时库)——但这仍然意味着所有内容被优雅地捆绑在一起——无需浏览庞大的注册表,也无需阅读手册页来查找晦涩的配置文件位置。应用程序包只是以 .app 结尾的目录——尽管 UI 会隐藏 .app 扩展名(以及它是一个目录的事实)。
从攻击者的角度来看,这很有趣——由于应用程序包可以拥有任意图标并隐藏 .app 扩展名——可以通过诱骗毫无戒心的用户点击此类应用来投递恶意软件。例如,想象一个带有 PDF 图标的 Resume.app 文件。
应用程序包的目录结构可以轻松查看,这里显然以内置的 Calculator(计算器)应用为例:
jbo@McJbo ~ % cd /System/Applications/Calculator.app
jbo@McJbo Calculator.app % ll
total 0
drwxr-xr-x 3 root wheel 96 Mar 17 21:34 .
drwxr-xr-x 43 root wheel 1376 Mar 17 21:34 ..
drwxr-xr-x 9 root wheel 288 Mar 17 21:34 Contents
jbo@McJbo Calculator.app % cd Contents
jbo@McJbo Contents % ll
total 16
drwxr-xr-x 9 root wheel 288 Mar 17 21:34 .
drwxr-xr-x 3 root wheel 96 Mar 17 21:34 ..
-rw-r--r-- 1 root wheel 2147 Mar 17 21:34 Info.plist
drwxr-xr-x 3 root wheel 96 Mar 17 21:34 MacOS
-rw-r--r-- 204 root wheel 8 Mar 17 21:34 PkgInfo
drwxr-xr-x 4 root wheel 128 Mar 17 21:34 PlugIns
drwxr-xr-x 54 root wheel 1728 Mar 17 21:34 Resources
drwxr-xr-x 3 root wheel 96 Mar 17 21:34 _CodeSignature
-rw-r--r-- 1 root wheel 461 Mar 17 21:34 version.plist
jbo@McJbo Contents % cd MacOS
jbo@McJbo MacOS % ll
total 344
drwxr-xr-x 3 root wheel 96 Mar 17 21:34 .
drwxr-xr-x 9 root wheel 288 Mar 17 21:34 ..
-rwxr-xr-x 1 root wheel 540912 Mar 17 21:34 Calculator
jbo@McJbo MacOS %
正如你所看到的,Calculator.app 是一个目录。在它下面只有一个条目——另一个名为 Contents 的目录。
在 Contents 下面有多个条目:
Info.plist:包含应用程序的元数据。稍后会详细介绍。MacOS:包含应用程序的主可执行文件(如第三个目录列表所示)。PkgInfo:非必需。一个包含包信息的二进制文件。PlugIns:非必需。一个可能包含应用程序插件的目录。计算器有两个——一个用于“基本和科学”(Basic and Scientific),一个用于“十六进制”(Hexadecimal)(我真的不知道为什么它们要这样分开,也不关心)。Resources:非必需。顾名思义,包含资源。你可能会在那里找到几个条目,包括带有图标的 .icns 文件,以及与本地化相关的、带有 .lprroj 后缀的目录。_CodeSignature:非必需。顾名思义——包含代码签名信息。version.plist:非必需,包含版本信息。请注意,官方要求的条目非常少。事实上,我们甚至可以在不编译任何东西的情况下创建自己的第一个应用程序!
但首先我们必须讨论 Info.plist 文件。
你越深入地看 macOS,就越会发现这些奇怪的文件。它们不过是美化了的配置文件。
它们总是带有 .plist 扩展名,这只是其正式名称的简称:属性列表(Property list)文件。
不幸的是,Apple 维护了 3 种不同的 plist 格式:
xml 格式,人类可读。json 格式,未被广泛使用。bplist 来识别。幸运的是,有一个名为 plutil 的工具支持所有格式。要输出 plist 文件,只需使用 plutil -p。例如:
jbo@McJbo Contents % plutil -p Info.plist | head -n 20
{
"BuildMachineOSBuild" => "22A380007"
"CFBundleDevelopmentRegion" => "English"
"CFBundleExecutable" => "Calculator"
"CFBundleGetInfoString" => "10.14, Copyright © 2000-2018, Apple Inc."
"CFBundleHelpBookFolder" => "Calculator.help"
"CFBundleHelpBookName" => "com.apple.Calculator.help"
"CFBundleIconFile" => "AppIcon"
"CFBundleIconName" => "AppIcon"
"CFBundleIdentifier" => "com.apple.calculator"
"CFBundleInfoDictionaryVersion" => "6.0"
"CFBundleName" => "Calculator"
"CFBundlePackageType" => "APPL"
"CFBundleShortVersionString" => "10.16"
"CFBundleSignature" => "????"
"CFBundleSupportedPlatforms" => [
0 => "MacOSX"
]
"CFBundleVersion" => "223"
"CTIgnoreUserFonts" => 1
jbo@McJbo Contents %
plutil 中还内置了转换功能——我们现在不会演示这些功能。
Apple 文档 中说明了应用程序 Info.plist 的几项要求,但实际上必须的字段非常少。以下是一些有趣的字段:
CFBundleExecutable:主可执行文件的名称,预期位于 MacOS 目录下。CFBundleIconFile:图标文件的名称。非必需。CFBundleIdentifier:应用程序包的标识符。Apple 建议使用反向 DNS 表示法(例如 com.apple.calculator)。CFBundleName:包名称。有了这些知识,我们就可以创建自己第一个很棒的应用程序了,甚至不需要编写代码! 来看看:
#!/bin/zsh
# Create Bundle structure
mkdir -p ./MyApp.app/Contents/MacOS
# Create main executable file - a shell script in our case
cat <<EOF > ./MyApp.app/Contents/MacOS/MyApp
#!/bin/zsh
osascript -e 'tell app "Finder" to display dialog "Hello from MyApp!"'
EOF
chmod +x ./MyApp.app/Contents/MacOS/MyApp
# Create the Info.plist file
cat <<EOF > ./MyApp.app/Contents/Info.plist
<?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>CFBundleExecutable</key>
<string>MyApp</string>
<key>CFBundleIdentifier</key>
<string>com.myapp</string>
<key>CFBundleName</key>
<string>MyApp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
</dict>
</plist>
EOF
这将创建一个名为 MyApp 的新应用程序——点击它将直接运行 zsh 脚本(名为 MyApp)。
请注意它使用了 osascript,这是一个 AppleScript 解释器,也是一个马蜂窝,但它只会显示一个写着 Hello from MyApp! 的对话框。显然,你可以在那个 zsh shell 文件中编写任意代码。较新的 macOS 版本可能会显示一个提示,要求允许 zsh 调用 osascript——我们将在以后的文章中讨论为什么会发生这种情况,但请注意,首次批准后它就不会再询问了。
启动应用程序意味着仍然会创建一个进程——显然是由 CFBundleExecutable 指向的那个进程。它在什么进程下运行呢?让我们看看:
jbo@McJbo ~ % open -a Calculator
jbo@McJbo ~ % ps -A -j | grep Calculator | grep -v grep
jbo 12067 1 12067 0 1 S ?? 0:00.41 /System/Applications/Calculator.app/Contents/MacOS/Calculator
jbo@McJbo ~ %
open 命令相当于双击 Calculator(计算器)应用——这非常有趣,我们很快会讨论它。
现在 Calculator 正在运行,我们使用 ps 来显示正在运行的进程。正如预期的那样,/System/Applications/Calculator.app/Contents/MacOS/Calculator 就是运行的进程,它的 PID 是 12067。然而,它的父进程 ID 是 1!
macOS 上的进程 ID 1 是 /sbin/launchd。它是“系统级和每用户的守护进程/代理管理器”。你可以把它想象成 services.exe(如果你有 Windows 背景)或 systemd(如果你熟悉 Linux)。除了管理服务(在 macOS 上称为 Launch Agents 和 Launch Daemons)之外,它还是所有应用程序的父进程,这也是为什么在 macOS 上获得有意义的进程树颇具挑战性的原因之一。
有趣的是,你仍然可以通过直接调用 /System/Applications/Calculator.app/Contents/MacOS/Calculator 来将 Calculator 应用作为一个普通进程运行,但它不会成为 launchd 的子进程:
jbo@McJbo ~ % /System/Applications/Calculator.app/Contents/MacOS/Calculator &
[1] 12502
jbo@McJbo ~ % 2023-04-04 15:54:22.987 Calculator[12502:1297087] XType: XTFontStaticRegistry is enabled by Info.plist.
jbo@McJbo ~ % ps -A -j | grep Calculator | grep -v grep
jbo 12502 951 12502 0 1 SN s000 0:00.31 /System/Applications/Calculator.app/Contents/MacOS/Calculator
jbo@McJbo ~ % echo $$
951
jbo@McJbo ~ %
确实,Calculator 运行得很顺畅,但现在它成了我们终端的子进程。
在我们看到的行为中,有一点值得注意:攻击者可能会将其用于不同目的。例如,攻击者可以轻松地跳出进程树以逃避安全工具,以及滥用逻辑漏洞(如果你有时间,请阅读我的 macOS 沙盒逃逸漏洞分析)。
launchd 还有其他有趣的职责(请阅读关于 LaunchAgents 和 LaunchDaemons 的内容),但我们暂时不讨论这些。
这里我向你展示了一种类型的包,但还有很多其他类型(不是完整列表):
.app:我们已经见过这种,这些是作为应用程序容器的应用程序包。.framework:包含框架(Frameworks),这些是可加载的包。是的,在 macOS 中你可以对可加载文件(.dylib)调用 dlopen,也可以加载整个框架包(包含资源、代码等)。.kext:包含内核扩展(kernel extensions),这些是可加载的包,但面向的是 macOS 内核。在近期的操作系统版本中,Apple 确实在努力减少内核扩展的数量。.plugin:顾名思义,是插件的容器。这是一系列简短博文中的第一篇,旨在帮助大家过渡到 macOS 研究。
从进攻性安全的角度来看,我注意到的第一件事就是,将载荷打包成一个漂亮的应用程序结构是多么容易。
然而事情并没有那么简单——在接下来的几篇博文中,我们会发现,由于 macOS 的众多安全特性,获得代码执行并非那么容易。
敬请期待!
Jonathan Bar Or (https://jonathanbaror.com)