利用 Automator 快速符号化 Crash 文件

利用 Automator 快速符号化 Crash 文件

背景

起因是最近有接到一个临时协助任务,其中有几个重要的流程:

  • QA 方导出 .crash 文件(必要的)
  • 我方要根据测试提供的 crash 文件的build number,去下载对应的 xx.app.dSYM
  • 把下载的dSYM给合作方
  • 合作方解析crash文件

从上的步骤可以看出第一步不可省略。第二、三步完全可以干掉,流程越多越浪费时间。
第四步也可以我们自己做,就可以优化成 QA 直接解析好 crash 文件然后给合作方。

那么就提效了提效 50% 是不是,两个人的事情一个人搞定 (那么就可以卷点别的)

初版方案

小插曲

一开始第一周我写了个Shell,调试通过之后就没继续,就干其他大活了(这里有个有悲剧)
……
第二周的时候,不知怎的崩溃出奇的多(应该是合作方更新SDK之后导致的)
当时我正Coding热火朝天,QA和合作方夺命的Call
我就去找那个当时写好的shell脚本,一通翻箱倒柜之后,我悟了,悲剧来了,找不到了
呵呵,被自己强迫症日常清理垃行为给清理了(自己有个日常清理的垃圾的行为,无奈Mac配置就这样)

打工人不得不含着泪重新写了一份 (源码在下面),快速应付下那边的夺命Call
然后我就在想这个事,为啥要我来做,也没啥技术含量,为啥不可自动化?
Bingo~ 说来就来

Shell 源码

1
2
3
4
5
6
7
crash_txt=$1
crash_log=${crash_txt%%.*}.log
# find /Applications/Xcode.app -name symbolicatecrash -type f
# cp /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash symbolicatecrash
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
./symbolicatecrash $1 $2 > $crash_log
open $crash_log -a sublime

具体如何符号化解析这里就不再唠叨了,网上一大堆:附一个参考链接

使用 Automator 的自动化方案

要使用 Automator 还需要编写 AppleScript 代码。

工具 & 语言

  • 工具:Automator Service, 脚本编辑器
  • 语言:AppleScriptShell

automator

automator script editor

脚本编辑器,AppleScript调试用。

好玩的 AppleScript

下面是一些好玩的 AppleScript 代码,唤起你的好奇心:

1
2
3
4
5
6
7
8
9
10
display dialog "你说假如地球没有了空气,我们会怎样...
那么没有工程目录,后面该怎么办?" default answer "会死" buttons {"我知道了"} ¬
default button "我知道了" with title "Handsome ERROR"
set theInput to text returned of the result
--display dialog text returned of the result
if theInput is equal to "会死" then
display dialog "没救了" with title "ERROR" buttons {"我知道了"} ¬
default button "我知道了"
end if
--忽略下面部分
1
2
3
4
5
say "Hello world"

display dialog "Hello World" with title "Alert"

display notification "Hello World" with title "Notification"

或者直接在终端里面跑

1
osascript -e "display notification \"Hello World\" with title \"Notification\""

-- single comment# single comment 是单行注释

(* this mutli comment *) 是多行注释

Markdown问题AppleScript脚本里意外出现<p data-line这种代码忽略

AppleScript 需要注意的问题

主要还是路径问题 ApeleScript 获取的路径如下:

1
Macintosh HD:Users:xxxxxxx:Documents:xxxxx.app_副本_2.dSYM:

这种冒号的路径在shell命令行根本没法用,所以下面代码成了常客:

冒号字符串 打包成数组 set my_array to split(input as string, ":")

1
2
3
4
5
6
7
on split(the_string, the_delimiter)
set old_delimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to the_delimiter
set the_array to every text item of the_string
set AppleScript's text item delimiters to old_delimiters
return the_array
end split

字符串 set target_path to join(my_array, "/"),这里要注意拼接文件与文件夹用的index下标不同:

1
2
3
4
5
6
7
8
9
10
11
12
on join(the_array, the_delimiter)
set split_str to the_delimiter
set target to " "
set list_length to the length of the_array
set list_length to list_length - 1
set short_list to items 2 through list_length of the_array

repeat with dir in short_list
set target to target & split_str & dir
end repeat
return target
end join

实现过程

思路分析

1、定位dSYM路径

2、定位xx.crash件路径

3、唤起终端,切入指定路径

4、symbolicatecrash解析并重定向输入结果

5、自动打开展示结果

其实这前两步有个大坑:重复下载 dSYM 文件以及导出的 xxx.crash 文件路径会存在空格。在AppleScript调用Shell的时候路径有空格,会报错找不到对应的文件。

解决办法

  • 利用 AppleScript 给文件重命名
  • 借助Automator 现有的快捷操作修改

期间有周报群群主指点使用 AppleScript 借助 quoted 这个 API 来转义引用空格。
结果是终端识别了,但是symbolicatecrash还是不识别,虽然结果不尽人意,但是学到了新技能。

如果你看过AppleScript API,除了想哭就没别的,上面说的很清楚干啥用,但是不知道语法该咋写。因为没写过这种自然语法,每次都是不停的尝试、失败,尝试、失败,尝试、失败。
AppleScript小众到谷歌都没有,大部分都是查阅Stack Overflow

我这边选择是第二种

xxx.crash 文件名有空格的解决办法是直接重命名,查找之后直接把空格替换成下划线。dSYM 父目录路径空格,这边多次导出之后会导致父目录存在空格,这个相对上面就比较复杂。
这里有几点思考:

在事物本身很难解决问题时,我们就需要放开视野,跳出事物本身,提升更高的角度去思考
当你这么想了,你思考问题的维度和角度就变了
在我们这个问题上,既然它的路径上存在空格,我给它换个不存在的路径不就好了
是不是一个很简单的解决办法,所以有时候不要太局限一点一面

一点瞎扯淡

其实日常编码或者修复 BUG 的过程中也会遇到类似情况,我们在一个问题上纠结好久好久到快死了吧!但是问题还没能解决,这个时候就可以尝试:

  • 冷静下来
  • 刻意放慢节奏
  • 全身心放松下来
  • 想点别的换换脑子或者睡一觉(我通常就是睡觉)
  • 冥想(这个相对高级 需要练习)

不去想这个问题一段时间之后,慢慢就会发现脑子开始活络起来,之前的问题解决办法好像一下子思路如泉涌,睡一觉精神也恢复了,思路也有了,简直两全其美是不是,比死磕一天啥都没有强千百倍吧,最后还得被喷延误工期,拉胯身体,最后无奈身不由己加入996.icu 这个 Big Party

工程创建

1、选中dSYM文件 -> 右键 -> 服务 -> 创建服务

2、弹出一个快捷操作的模板空工程,可以配置参数入口(因为第一步选中了,参数就不需要配置了)

3、然后就可以拖拽你要的操作(类似于storyboardxib操作)

4、保存 -> 命名,就会自动存储到本机的~/Libray/Services目录

所有的快捷操作,工作流都会在这个目录,就是说你想用别人写好的最后安装的也是这个目录

示例图:

完整的操作步骤

脚本交互

  • Shell 调用 AppleScript可以用osascript -e
  • AppleScript调用Shell可以用do shell script & do script
  • do script需要配合终端

示例:

1
2
3
4
5
6
7
tell application "Terminal"
activate
--set new_tab to do script "echo fire"
delay 1
do script "pwd" in front window
do script "ls" in front window
end tell

演示

gif
模糊了点,为了加载快,压缩的有点狠,但是也能看大概流程就OK了

有两种使用方式启动 dSYM 自动化服务:

  • 首先选中 dSYM 文件,然后右键 -> 快捷操作 -> dSYM
  • 首先选中 dSYM文件,快捷键即可(这里需要到 Finder -> Service 偏好设置里面配置好按键)

automator rightkey

执行流程如下:

1、启动之后就自动去/Users/$(whoami)/Downloads/目录文件下搜索.carsh文件

这里写死Downloads目录的原因是想提高搜索速度,所有导出的时候选择的就是Downloads目录。如果你想要全局搜索也不是不是可以, 但是你得等等Spotlight

2、搜索完毕之后会列出该目录下所有的 .crash 文件。

3、选择对应的文件(build number 一致),就会打开一个终端进入解析流程。

4、解析完毕之后会通过 sublime 打开。没有sublime会怎样? 就去掉 -a sublime

ApplesScript 代码负责的部分:

  • 冒号:转斜杠/
  • 调用了剪切板做缓存
  • display dialog 显示.crash文件的搜索结果
  • 唤起终端,执行解析

总结

这里本来想在Automator里面加一个调用Shell脚本的的服务,这样就可以静默解析不用唤起终端,调试过程中解析一直失败,因为运行解析的 symbolicatecrash 需要的环境变量报错,也在对应的目录进行了export,但是最终还是不行,最后还是选择唤起终端来执行操作,或许看起来更酷一点吧 哈哈哈。

其他文件读取/拷贝/搜索/重命名都是Automator提供现成服务。Automator真的很强大,但是你要发现它的美,学会使用它。

最后就是要告诫自己:该做的事还得及时做出来, 不然就是午饭没吃 午休没睡。

MachO 代码签名剖析

验证代码的正确性是计算机科学中最难的问题之一,因为不存在普遍意义的正确的算法,所以这一验证通常使用数字签名处理。数字签名主要做两部分工作:

  • 验证代码的来源是否合法。
  • 代码是否被修改过。

代码签名并非苹果独有技术,Java 和 Android 的 Dalvik 都在使用,但苹果公司是最早开始使用的。大家可以通过阅读下文思考代码来源是否合法和代码是否被修改过的验证是如何实现的。

本篇文章主要参考自 Jonathan Levin 的《最强 iOS 和 macOS 安全宝典》代码签名一章。

  • 测试环境:macOS 11.2.3。
  • 测试项目:/bin/ls 在 x86_64 架构下的 MachO 文件。iOS 下的文件与之相差不大。

代码签名格式

在了解代码签名机制前,非常有必要了解代码签名的包含的内容。代码签名附着在 MachO 的尾部。加载命令为LC_CODE_SIGNATURE,它指向一个超级二进制块Code Signature,该二进制块又包含了多个其他的子二进制块。之前写过一篇文章,有讲解如何手动解析这个签名二进制块:深入理解MachO数据解析规则

下面是该二进制块的层级结构:

超级二进制块是一个目录性质的结构,用于指定子二进制块的位置,各个子二进制块才是代码签名的主要角色。

子二进制块类型

子二进制块类型通过不同值进行表示:

二进制块类型
0x0000 代码目录
0x0002 需求
0x0005 授权
0x10000 CMS 二进制块
0x10001 身份证明(未使用)

本篇主要就是对这几个子二进制块的功能和部分实现进行分析。

二进制块的提取

jtool 是 Jonathan Levin 开发的一款主要用于 MachO 分析的高效工具,可以使用 homebrew 进行安装。

1
$ brew install jtool

我们可以使用 jtool 单独提取代码签名部分:

1
2
3
4
5
6
7
8
9
10
$ jtool -arch x86_64 -e signature /bin/ls
Extracting Code Signature (5728 bytes) into ls.signature
$ od -t x1 -A x ls.signature #原始字节内容
0000000 fa de 0c c0 00 00 14 86 00 00 00 03 00 00 00 00
0000010 00 00 00 24 00 00 00 02 00 00 02 61 00 01 00 00
0000020 00 00 02 9d fa de 0c 02 00 00 02 3d 00 02 01 00
0000030 00 00 00 00 00 00 00 7d 00 00 00 30 00 00 00 02
0000040 00 00 00 0e 00 00 d2 30 20 02 0b 0c 00 00 00 00
0000050 00 00 00 00 63 6f 6d 2e 61 70 70 6c 65 2e 6c 73
# ...

也可以使用 MachO 找到 Code Signature 块进行查看。

代码签名的子二进制块

我们可以使用 jtool 查看代码签名内容的分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ jtool -arch x86_64 --sig -v /bin/ls
Blob at offset: 53808 (5728 bytes) is an embedded signature of 5254 bytes, and 3 blobs
Blob 0: Type: 0 @36: Code Directory (573 bytes)
Version: 20100
Flags: none (0x0)
Platform Binary
CodeLimit: 0xd230
Identifier: com.apple.ls (0x30)
CDHash: 46cc1da7c874a5853984a286ffecb48daf2f65f023d10258a31118acfc8a3697 (computed)
# of Hashes: 14 code + 2 special
Hashes @125 size: 32 Type: SHA-256
Requirements blob: a8ccc60c2a5bff15805beb8687c6a899db386d964a5eb3cf3c895753f6879cea (OK)
Bound Info.plist: Not Bound
Slot 0 (File page @0x0000): e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff (OK)
Slot 1 (File page @0x1000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 2 (File page @0x2000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 3 (File page @0x3000): 4a7cb3e6c1b3a6ac82e3575239ee53d4f0d3bed260fed63438fd21ce0d00392e (OK)
Slot 4 (File page @0x4000): 9ec9e4e02292dfda34ef3caa8317e8bfbcc41a46b18d994dba45febe31b8c660 (OK)
Slot 5 (File page @0x5000): 037285f744f366210cde48821261d4a5f5b739dcf0b82f94144613e92c4b7c07 (OK)
Slot 6 (File page @0x6000): be89c764e52382702918f2db62ff24d9df40410fe894b11d505a4abc1f854340 (OK)
Slot 7 (File page @0x7000): a6b322014743965656e796155c1e0bf22e19a3e8770a43f1111cfbc961037d26 (OK)
Slot 8 (File page @0x8000): a643fc9485d941019cbdeead1d5c47add9382417ebe4d15768221f3763553b84 (OK)
Slot 9 (File page @0x9000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 10 (File page @0xa000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 11 (File page @0xb000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 12 (File page @0xc000): 23304ae11c1ade4411cb63a0955eb644574b8af416e4e3818e382421272ae1b4 (OK)
Slot 13 (File page @0xd000): e0ca7b7000d04057e71c49365b1937711b3557f6b91e0fa144791c66de2a7a4d (OK)
Blob 1: Type: 2 @609: Requirement Set (60 bytes) with 1 requirement:
0: Designated Requirement (@20, 28 bytes): SIZE: 28
Ident: (com.apple.ls) AND Apple Anchor
Blob 2: Type: 10000 @669: Blob Wrapper (4585 bytes) (0x10000 is CMS (RFC3852) signature)
CA: Apple Certification Authority CN: Apple Root CA
CA: Apple Certification Authority CN: Apple Code Signing Certification Authority
CA: Apple Certification Authority CN: Apple Root CA
CA: Apple Certification Authority CN: Apple Root CA
CA: Apple Certification Authority CN: Apple Code Signing Certification Authority
CA: Apple Software CN: Software Signing
Time: 201222002625Zi

它有三个 Blob,即三个子二进制块,Blob 0 是代码签名 Blob 1是需求,Blob 2 是 CMS,下面是对这几个 Blob 的分析。

代码目录(Code Directory)

代码目录是签名块的主体,它提供了签名资源的散列值(哈希值)。代码签名并非对整个文件进行签名,因为有时二进制文件可能很大,计算全部内容占用资源较多;而且二进制的加载是按需加载,不会一开始就都全部映射到内存中。签名时会将整个 MachO 文件划分成多个页,每个页单独签名。

代码目录部分就是对签名信息的描述,其中包含了各个分页的签名值,签名算法和分页大小等内容。代码签名的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* C form of a CodeDirectory.
*/
typedef struct __CodeDirectory {
uint32_t magic; /* magic number (CSMAGIC_CODEDIRECTORY) */
uint32_t length; /* total length of CodeDirectory blob */
uint32_t version; /* compatibility version */
uint32_t flags; /* setup and mode flags */
uint32_t hashOffset; /* offset of hash slot element at index zero */
uint32_t identOffset; /* offset of identifier string */
uint32_t nSpecialSlots; /* number of special hash slots */
uint32_t nCodeSlots; /* number of ordinary (code) hash slots */
uint32_t codeLimit; /* limit to main image signature range */
uint8_t hashSize; /* size of each hash in bytes */
uint8_t hashType; /* type of hash (cdHashType* constants) */
uint8_t platform; /* platform identifier; zero if not platform binary */
uint8_t pageSize; /* log2(page size in bytes); 0 => infinite */
uint32_t spare2; /* unused (must be zero) */
/* Version 0x20100 */
uint32_t scatterOffset; /* offset of optional scatter vector */
/* Version 0x20200 */
uint32_t teamOffset; /* offset of optional team identifier */
/* followed by dynamic content as located by offset fields above */
} CS_CodeDirectory;

结合 CodeDirectory 的偏移量,可以从 MachOView 里查看到这部分数据的内容:

找到对应数据结构中的含义,我们关注其中三个 uint8_t 类型的值:

参数 含义
hashSize 0x20 hash 值大小,为 0x20 字节。
hashType 0x02 表示签名算法,0x01 表示 SHA-1,0x02表示SHA-256。从 macOS10.12 和 iOS11开始,苹果转向使用 SHA-256。
pageSize 0x0C 这里是一个计算公式:log2(PageSize) = 0x0C

根据公式算出分页大小:PageSize = 2 ^ 0x0C = 4096 = 0x1000 = 4K。这跟系统的内存分页大小是一致的。

由此可知整个 MachO 文件会按照 0x1000 字节的大小进行分页,分页使用 SHA-256 算出散列值。这些计算出的散列值会记录在代码插槽(Code Slots)里。

代码插槽验证

上面Slot 从 0 到 13 的标记对应的都是代码插槽。

有了计算规则我们还可以手动验证代码签名的正确性,我们以前三个代码插槽为例,也即前 0x1000 字节的内容,尝试手动计算其散列值。

1
2
3
4
5
6
7
$ lipo /bin/ls -thin x86_64 -output /tmp/ls_x86_64
$ dd bs=0x1000 skip=0 count=1 if=/tmp/ls_x86_64 2>/dev/null | openssl sha256
SHA256(stdin)= e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff
$ dd bs=0x1000 skip=1 count=1 if=/Users/zhangferry/ls_x86_64 2>/dev/null | openssl sha256
SHA256(stdin)= ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7
$ dd bs=0x1000 skip=2 count=1 if=/Users/zhangferry/ls_x86_64 2>/dev/null | openssl sha256
SHA256(stdin)= ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7

注意到后面两个插槽计算结果一样,这是因为这两部分数据为补齐位,它们全部为0。

跟前三个代码插槽的值进行对比:

1
2
3
Slot   0 (File page @0x0000):	e4a537939e00f4974e02b03d36e4dab75f7dc095d2214ba66bc53c73c145ceff (OK)
Slot 1 (File page @0x1000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)
Slot 2 (File page @0x2000): ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7 (OK)

发现两边散列值一样,输出内容后面的 OK 是 jtool 验证的结果。

这里可以看到代码插槽有14个,File page 里的内容表示相对起始地址。另外注意到输出部分有一句注释:

1
# of Hashes: 14 code + 2 special

其表示有 14 个代码插槽和 2 个特殊插槽。

特殊插槽

特殊插槽的出现是因为App由多个内容组成,并非只有二进制文件,为了保证这些非二进制文件的完整性,对它们也会进行签名,它们的签名值就是特殊插槽。因为代码插槽的索引是从0开始的,而且其大小不固定,为了把特殊插槽也能排列进去,就选用负数来表示特殊插槽的含义。以下是特殊插槽的定义:

# 插槽目的
-1 绑定的info.plist
-2 需求(requirement):二进制块嵌入代码签名
-3 资源目录:CodeSignature/CodeResources文件的散列值
-4 具体应用:实际上未被使用
-5 授权(entitlement):嵌入在代码签名中的授权

我们可以在上方 jtool 的输出内容里找到特殊插槽的内容:

1
2
Requirements blob:	a8ccc60c2a5bff15805beb8687c6a899db386d964a5eb3cf3c895753f6879cea (OK)
Bound Info.plist: Not Bound

因为特殊插槽作用是固定的,也就没用序号表示。

代码签名需求(Requirements)

目前代码签名只是分块取散列值,保存起来,但好像还不够强大。苹果公司已经为代码签名增加了另外一个机制:需求(requirements)。它可以自定义规则以施加特定限制,比如允许哪些动态库加载。

需求有一套特殊的语法规则,其表达由操作数和操作码组成,丰富的操作码集使得构建任何数量的逻辑条件成为可能。可以在requirements.h 文件里查看都有哪些操作码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum ExprOp {
opFalse, // unconditionally false
opTrue, // unconditionally true
opIdent, // match canonical code [string]
opAppleAnchor, // signed by Apple as Apple's product
opAnchorHash, // match anchor [cert hash]
opInfoKeyValue, // *legacy* match Info.plist field [key; value]
opAnd, // binary prefix expr AND expr
opOr, // binary prefix expr OR expr
opCDHash, // match hash of CodeDirectory directly
opNot, // logical inverse
opInfoKeyField, // Info.plist key field [string; match suffix]
opCertField, // Certificate field [cert index; field name; match suffix]
opTrustedCert, // require trust settings to approve one particular cert [cert index]
opTrustedCerts, // require trust settings to approve the cert chain
opCertGeneric, // Certificate component by OID [cert index; oid; match suffix]
opAppleGenericAnchor, // signed by Apple in any capacity
opEntitlementField, // entitlement dictionary field [string; match suffix]
exprOpCount // (total opcode count in use)
};

对需求的编译是由 csreq 进行的,对需求的验证可以使用 codesign -v

我们这里来尝试解读下已有的需求内容。

大部分二进制文件的需求只是验证签名身份,即使用证书是否为苹果所颁发。在 App Store 里的应用则使用更严格的规则集。我们可以查看 Xcode 的代码签名需求:

1
2
3
$ codesign -d -r- /Applications/Xcode.app/Contents/MacOS/Xcode
Executable=/Applications/Xcode.app/Contents/MacOS/Xcode
designated => (anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or 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] = APPLECOMPUTER) and identifier "com.apple.dt.Xcode"

注意这里有不少 1.2.840.113635 开头的标识,它代表的是国际通用标准证书中苹果公司的分支(iso.member-body.us.appleOID)。其中100对应了安全相关的一些定义 appleDataSecurity,详细内容可以看这里oidref.com

对照Xcode的签名需求,我们可以大致推断出这些规则的含义:

  • 由苹果签名且证书节点包含 6.1.9 即 Mac App Store App。
  • 或由苹果签名且证书节点包含 6.2.6 即 “dev_program”。(推测是开发版本的应用)
  • 其证书节点包含 6.1.13 即 Developer ID Applications。
  • 证书的团队标识符(OU)为 APPLECOMPUTER 且 BundleId 为 com.apple.dt.Xcode

注意其中最后一项的内容,限定了团队标识符和BundleId,这样就能够解决应用被重签名的问题了。

CMS

CMS 是Cryptographic Message Syntax的缩写,是一种标准的签名格式,由RFC3852定义。书中并没有提这部分内容,但我认为这部分恰恰是代码签名最关键的步骤。

CMS 格式的签名中,除了包含证书之外,还承载了一些其他的信息,比如签名属性 signedAttrs

上面说了 CodeDirectory 里保存了 MachO 分页的 Hash 值,只要保证这个 CodeDirectory 不被修改就可以了。所以对代码目录进行 Hash 计算,获得 CDHash,然后对这个 CDHash 进行签名就可以了。

注意这一步才是真正的签名,其开始涉及加密,前面的代码插槽只是提供摘要信息。

注意到 jtool 的签名输出里有这样一句:

1
CDHash:	     46cc1da7c874a5853984a286ffecb48daf2f65f023d10258a31118acfc8a3697 (computed)

这就是外部计算的 CDHash 值,用于跟 signedAttrs 里的内容进行对比。而更关键的是对 signedAttrs 的加密验证,实际验证流程比较复杂,感兴趣的小伙伴可以阅读这篇细说iOS代码签名(三)

我结合文中签名校验内容和上面的代码插槽,画出了表示签名校验的整个流程:

这里有两处 Hash 对比,一个是对 signedAttrs 的解密,确保其是可信任的。另一处是 CDHash 的对比,确保代码未被修改。

signerInfo 里包含了 signedAttrs 、签名使用的 Hash 算法、加密算法、签名数据等信息。再结合证书里的公钥,我们就可以验证,signedAttrs 的有效性。

授权

除了确保代码的真实性和完整性,代码签名还为苹果公司及其强大的安全机制提供了授权(entitlement)功能。授权文件也被包含在签名里,其散列值放在索引为-5的插槽中。授权文件是一个 XML 格式的文件,我们可以使用 jtool --ent 查看其内容,因为 ls 没有授权文件,我们以 Mac 端微信为例进行查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ jtool -arch x86_64 --ent /Applications/WeChat.app/Contents/MacOS/WeChat
<?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>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>5A4RE8SF68.com.tencent.xinWeChat</string>
</array>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
</dict>
</plist>

这里我们可以看到其中包含了沙盒、application-groups、声音输入、摄像头等一系列权限。在应用访问特定 API 的时候苹果可以根据这些授权判定该行为是否合法。因为苹果公司是应用的终极签名者,所以签名过程中也可以很容易的修改授权,比如 com.apple.security.sandbox.container-required 这一表示沙盒权限的值就会被强制安置到授权文件中。

强制验证代码签名

为了使代码签名真正有效,非常重要的一步就是要保证验证过程顺利执行,没有遗漏。当前签名验证是发生在内核模式下,而非用户模式下。签名的验证发生在两个阶段:加载可执行文件时、实际访问二进制代码时(Page Fault)。分成两个阶段也是出于性能方面的考虑,因为二进制文件是动态加载的,对于还没加载的部分仅当其加载如内存时,也即发生 Page Fault 时再进行签名验证。

可执行文件的加载

可执行文件的加载出现在execve()/mac_execve()posix_spawn()系统调用被触发的时候。对于MachO,exec_mach_imgact()会被调用,在解析文件时它会找到LC_CODE_SIGNATURE的位置。代码签名二进制块会被加载到内核的统一高速缓存缓冲区中。

Page Fault时的处理

可以查看 osfmk/vm/vm_fault.c 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* CODE SIGNING:
* When soft faulting a page, we have to validate the page if:
* 1. the page is being mapped in user space
* 2. the page hasn't already been found to be "tainted"
* 3. the page belongs to a code-signed object
* 4. the page has not been validated yet or has been mapped for write.
*/
#define VM_FAULT_NEED_CS_VALIDATION(pmap, page, page_obj) \
((pmap) != kernel_pmap /*1*/ && \
!(page)->cs_tainted /*2*/ && \
(page_obj)->code_signed /*3*/ && \
(!(page)->cs_validated || (page)->wpmapped /*4*/))

当 Page Fault 满足以上条件时将触发签名验证过程:

1、该页面正在用户空间中映射

2、这个页面还没有被发现为 tainted

3、该页属于一个代码签名对象

4、页面还没有被验证,或者还没有被映射为可写状态

代码签名的漏洞

代码签名机制虽然强大,保护着应用的安全,但依然被攻破过,以下讲解几例曾经出现的漏洞。

JIT(即时生成代码)

该情况发生在 Page Fault 过程,如果该页内容是用于 JIT,将会被特殊标记,可以创建和执行任意代码,而无需代码签名。

从 iOS 10 开始,苹果公司开始在64位的设备上加固 JIT。采用专门的 memcpy() 将JIT映射到可执行但不可读的内存上,然后可执行的 JIT 映射为不可写,可写的 JIT 映射为不可执行状态。

Jekyll 应用

Jekyll 应用的含义是应用在提交至 App Store 时表现为无害,但其实它包含恶意功能,只不过这些功能处于休眠状态。过审之后和本地服务器进行合作,自愿公开其地址空间和符号,通过代码注入或者返回导向编程(Return Oriented Programming,ROP),触发预置的恶意程序。

目前还没有可靠的打击 ROP 的方法,但因为沙盒机制的缘故,恶意代码的影响范围是可控的。

苹果公司使用 LLVM BitCode 向 App Store 提交应用的方案,也会使恶意应用难以事先知晓其地址空间。

内存锁定

从上面我们知道发生Page Fault会触发签名验证的流程,那如果没有Page Fault就不会存在签名验证了。按照mmap -> mlock -> memcpy -> mprotect 的调用顺序,应用可以修改可执行内存,以任何看起来合适的方式修补内存。虽然XNU通常会阻止将曾经可写的内存设置为r-x,但当内存锁定时,会绕过该检测。

苹果在iOS 9.3中修复了这个漏洞。

总结

我们再来尝试回答开头上面遗留的问题:

1、如何验证代码的来源是否合法?

主要通过证书来验证来源是否合法,所有的开发者证书都由苹果颁发,且被 Root CA 认证。另外依托于需求(requirements),还可以再扩展一些其他验证方式。

2、如何确认代码是否被修改过。

主要通过代码插槽和 CDHash,再对 CDHash 进行签名,就可确认其是否被修改过。注意实际验证流程有两处关键的 Hash 比对,可以再结合上面的流程图加深理解。

Category无法覆写系统方法?

Category无法覆写系统方法?

这是一次非常有趣的解决问题经历,以至于我认为解决方式可能比问题本身更有意思,另一点就是人多力量大,多人讨论就会获得多种思路。

首次提出这个问题的是反向抽烟,他遇到了不能用 Category 覆写系统方法的现象。问题抛到我这,我验证了这个有点奇怪的现象,并决定好好探究一下,重看了 Category 那部分源码仍没有找到合理解释,于是将这个问题抛到开发群里,最后由皮拉夫大王在此给出了最为合理的解释。之后我又顺着他的思路找到了一些更有力的证据。以下是这一过程的经历。

阅读更多
深入理解MachO数据解析规则

深入理解MachO数据解析规则

我们知道Apple设备可执行文件的存储格式是MachO,一个二进制文件。通常在做逆向或者静态分析的时候都会用到这个文件,分析MachO的常用工具是MachOView。今天借助于MachOView,主要分析Code Signature的存储规则。

本篇文章同时也是围绕这几个问题展开的:

1、MachOView是如何确认MachO内容的。

2、二进制数据是如何存储的,如何确认位置。

3、字节码含义如何解析。

阅读更多
CocoaPods对三方库的管理探究

CocoaPods对三方库的管理探究

CocoaPods是iOS开发中经常被用到的第三方库管理工具,我们有必要深入了解一下它对项目产生了什么影响,以及它是如何管理这些库的。

阅读更多

一位iOS开发者的进阶之旅

背景

这篇文章来源于v2ex上的一个帖子:”iOS开发有什么国人写的比较好的书籍推荐?”(原文链接)。这里汇总的基本都是lujie2012的回答,另外我还附带了一些他与别人的讨论内容。虽然帖子题目是推荐iOS书籍,但设计内容已经超出了这个题目,在我看来其中还迸发出很多有意思的观点,所以就想把内容整理出来。在经过其本人同意之后,有了如下内容,希望对大家有所帮助。

阅读更多

iOS面试总结(2020年6月)

都说今年互联网行情很差,作为被大家喊了好几年“iOS开发没人要了”的iOS行情更差。那真实情况是什么样的呢,以我的经历给大家分析下。应某个朋友建议,去掉这一句啊,目前iOS岗位还是挺多的,你可以这么想只要苹果爸爸不倒,iOS开发就不会没人要。但另一方面,招聘方对iOS开发的要求是在不断提高的,我们不能固步自封,满足现状,只有不断学习,不断进步,才能保持自身竞争力。

我的面试的阶段基本都在6月份,准备的阶段则要再往前推个半个月吧。期间约到了不少一二线互联网公司面试机会,前期由于准备不足也错失了一些机会,在之后的面试中不断总结经验,越来越有信心了,最终选择了爱奇艺。整体来看求职情况还算可以,不是很好但也不是很差,其中会带有一定运气成分,所以要换工作的话一定不要裸辞。

这里总结下这段时间的面试经历和一些心得,后面会附上期间遇到的面试题,大家可以尝试作答一下。

阅读更多

iOS面试备战-多线程

iOS面试中多线程绝对是最重要的知识点之一,它在日常开发中会被广泛使用,而且多线程是有很多区分度很高的题目可供考察的。这篇文章会梳理下多线程和GCD相关的概念和几个典型问题。因为GCD相关的API用OC看着更直管一些,所以这期实例就都用OC语言书写。

阅读更多