签名的校验并非一次性完成,在安装、启动、和运行时有着不同的校验规则。
App安装时的校验由位于iOS设备上的/usr/lib/libmis.dylib (dyld_shared_cache)提供。
App的安装是由/usr/libexec/installd
完成的,installd
会通过libmis.dylib
校验ProvisioningProfile、Entitlements及签名的合法性,并递归地校验签名时每一个步骤生成的哈希值:CDHash, Code Directory, _CodeSignature/CodeResources。
1 2 3 4 5 |
|
进程启动时,loader会先将可执行文件加载到虚拟内存,在加载的过程中mach_loader会自动解析MachO文件中的LC_CODE_SIGNATURE并进行校验,可以参考mach_loader的代码 bsd/kern/mach_loader.c
load_code_signature
在解析完签名的数据后会调用mac_vnode_check_singature
函数进行验证,而这个函数会被名为AFMI
(AppleMobileFileIntegrity)的内核扩展(kext)通过Hook的方式接管,而AFMI只是一层壳,最终也是调用了libmis.dylib来实现签名的校验,这一校验过程基本与安装时一致,防止安装后的篡改。
需要注意的是,加载过程中为了提升加载效率,签名校验并不会去检查Code Directory与实际的代码是否匹配,仅仅只检查了CMS Signature及CDHash的合法性。
当一页代码被加载到虚拟内存后,会立即触发page fault
,此时内核中的vm_fault
函数会被调用,紧接着调用vm_fault_enter
,在vm_fault_enter
的实现中会判断代码页是否需要签名校验,并执行校验的操作,参考代码osfmk/vm/vm_fault.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
对于宏VM_FAULT_NEED_CS_VALIDATION
的解释是
1 2 3 4 5 6 7 8 9 10 |
|
vm_page_validate_cs
会计算当前代码页的哈希值,并与签名中CodeDirectory记录的值进行比对,完成代码签名的验证。如果不符,且不满足系统预设的例外条件,则会向内核发出CS_KILL指令,将进程结束。
至此签名的校验流程就全部完成了。
越狱之后,签名校验机制会被破坏掉,否则用于实现越狱的代码自身就无法运行。比如在iOS6/7时代,典型的方式是替换 libmis.dylib
中的_MISValidateSignature
函数,使其永远返回验证成功,简单粗暴但很有效,因此越狱的设备可以不受签名限制运行任意程序。但是单纯解决掉这个函数只是解决了MachO文件的Load问题,运行时仍然会有沙盒和Code Directory的校验,想要对系统完全的控制权必须同时解决掉这两个问题。
由于沙盒机制的实现分散在系统的各个角落,没有简单的方式可以将沙盒一刀切地屏蔽掉,因此一般越狱并不会破坏掉沙盒。但因为越狱设备签名校验机制被绕过,不再会根据embedded.mobileprovision文件检查Entitlements的合法性,因此我们可以在沙盒范围内,声明任意的权限。Code Directory的校验在内核层,破解难度相对较大,并且完全没有必要进行破解,因为Code Directory只是单纯地校验未加密的哈希值而已,只需要按照代码签名的格式做好Code Directory即可。
越狱之父Saurik为此创造了ldid这个工具,用于给越狱设备上的程序制造”假”的签名。使用ldid进行签名只需要指定一个可选的Entitlements
文件,签名之后,产生的LC_CODE_SIGNATURE中只会两个有效的Blob,分别是 Code Directory和 Entitlements,并没有最重要的CMS Signature部分,因为_MISCalidateSignature
永远都会告诉系统签名是正确的。
1 2 3 4 5 6 7 8 |
|
有的时候出于各种原因,我们需要对一个App进行重签名,然后在自己的设备上进行测试。回顾一下签名的必备条件:
开发者证书和密钥我们已经有了,对于Entitlements和embedded.mobileprovision文件,为了确保重签后的App能够正常运行,必须使用和原App相同或者至少包含原App所需权限的Entitlements文件。这个并不难操作,只需要新建一个工程,开启相应的功能,让Xcode自动为我们生成即可。但是Entitlements文件中还有一些跟Team ID和App ID相关的配置,这两个是没有办法伪造的,因为我们不能使用已经被其他开发者注册过的ID。使用自己的ID一般也不会有什么问题,但在某些情况下可能导致最终的程序逻辑出现异常,这根具体的代码实现细节有关。
现在,只要确保有正确的Entitlements文件,Provisioning Profile与Entitlements文件匹配,且包含重签时使用的证书及目标设备的UUID,就可以进行重签名了,如果重签名后无法安装,请检查Provisioning Profile文件是否满足上述条件。
Entitlements文件中还标识了application-identifier
,也就是Bundle ID,正常签名的App中,这个值和Info.plist中的CFBundleIdentifier
的值是相同的,但实际在签名校验过程中,系统并不会检查二者是否一致。因此即使Entitlements中与Info.plist文件使用了不同的Bundle ID,理论上也不会影响重签名之后的运行。
需要注意,App中除了可执行程序文件外,还会可能会有Frameworks及Plugins,里面都会包含二进制的代码文件,他们的哈希值也会被存储在 _CodeSignature/CodeResources中。所有的二进制代码都必须进行签名,而签名后二进制文件的哈希值就会产生变化,因此需要先对这两个文件夹下的二进制文件进行签名,再对App进行签名。
重签名的基本流程,使用-f参数可以强制覆盖掉已有的签名
1 2 3 4 5 6 7 8 |
|
reference | link | |
---|---|---|
Code Signing Guide | https://developer.apple.com/… | |
ASN.1 JavaScript decoder | http://lapo.it/asn1js/ | |
Cryptographic Message Syntax (CMS) | https://www.ietf.org/rfc/rfc3852.txt | |
iSign in python | https://github.com/saucelabs/isign | |
CodeSigning (RSACon 2015) | http://newosxbook.com/articles/CodeSigning.pdf | |
jtool | http://www.newosxbook.com/tools/jtool.html | |
mistool | http://newosxbook.com/tools/mistool.html | |
evasi0n7 jailbreak writeup | https://geohot.com/e7writeup.html | |
iOS hacker’s handbook | https://books.google.com.hk/books?id=1kDcjKcz9GwC |
万事具备,只欠东风,已经具备了签名所需的所有条件,接下来就可以开始研究签名的具体过程了。
在编译iOS App时,Xcode在编译的打包的流程中会自动进行代码签名, 可以在编译日志界面找到一个Sign
的步骤,内部是调用了codesign
这个命令对app进行签名
codesign有几个关键参数
--sign sign_identity
指定签名所用的证书,可以指定证书的名字,比如"iPhone Developer: xxx (xxx)"
也可以直接写证书文件的sha1值,xcode中就是直接指定sha1值的。通过观察图中的sha1值可以看出xcode自动选择了刚申请的最新证书。--entitlements entitlements_file
指定签名所需要的entitlements文件,这里的entitlements文件跟前面看到的并不是同一个文件,而是基于原有entitlements文件,补充上缺省权限后生成的临时文件1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
如果想对比签名前后的区别,可以在Build Settings
中找到Code Signing Identity
,选择Other
并将内容清除(即设置为空),即可跳过代码签名。分别编译一个不签名的版本和签名的版本,对比可以发现
_CodeSignature
文件夹,里面只有一个文件CodeResources
embedded.mobileprovision
文件其中embedded.mobileprovision
就是前文提到的Provisioning Profile文件,它直接被拷贝到了app的根目录并重命名,在此不再赘述,重点研究下另外两个不同点。
首先是_CodeSingature/CodeResources
,这是一个plist文件,里面保存了app中每个文件(除了App的可执行文件)的明文哈希值
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 |
|
files
和files2
分别是旧版本和新版本的文件列表,而rules
与rules2
分别是与之对应的规则说明,里面描述了计算hash时需要被排除的文件以及每个文件的权重。
files
中保存的是每个文件的sha1值,而files2
中同时保存了sha1和sha256,因为sha1在计算机硬件高度发达的今天,已经相对没有那么安全了,因此最新的签名算法中,引入了sha256。注意,这里的hash值都是base64编码的明文,有些文章说这些值是使用私钥加密的哈希,这是很不负责任的错误说法,通过几条简单的命令就可以进行验证:
1 2 3 4 5 6 7 8 9 10 11 |
|
_CodeSignature/CodeResources
文件的主要作用是保存签名时每个文件的哈希值,而这些哈希值并不需要都进行加密,因为非对称加密的性能是比较差的,全部都加密只会拖慢签名和校验的速度。其实只需要确保这个文件没有被篡改,自然也就可以确保每个文件都是签名时的原始状态,这一点在后续的内容中可以得到验证。
使用otool -l
对比签名前后的二进制文件,可以发现签名后二进制文件多了一个名为LC_CODE_SIGNATURE
的Load Command
1 2 3 4 5 6 |
|
MachOView中查看如下
代码签名是一段纯二进制的数据,可以在https://opensource.apple.com/source/Security/Security-55471/sec/Security/Tool/codesign.c.auto.html 看到一些结构定义,结合数据定义来分析
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 |
|
这部分是典型的数据头结构,声明了5个Blob,以及每个Blob的类型和相对签名头部的偏移量。接下来把每个部分分别提取出来进行分析。
CodeDirectory是签名数据中最终要的部分,直译过来就是代码目录,其实里面是整个MachO文件的哈希值,这里的哈希并不是一次性对整个文件进行哈希,而是将MachO文件按照pageSize(一般是4k也就是4096字节)进行分页,每一页单独计算哈希,并按照顺序保存下来,就像目录一样。
细心的同学会发现上面的数据中出现了两个CodeDirectory,type分别是0x0
和0x1000
,这也是历史遗留问题,0x0
对应的是旧版本的代码签名,使用sha1算法进行哈希值的计算,而0x1000
是后来引入的,采用sha256作为哈希算法,除了算法和哈希的长度不同之外,其他内容基本是一样的。取第一个进行分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
hashOffset就是”目录”第一页的偏移,从这个位置(0xD409)可以提取到一串20字节的sha1值(图中黄色⑤):
1
|
|
这个值代表的就是该文件第一页的哈希值,通过以下命令计算文件前4096字节的sha1可进行验证
1 2 |
|
而紧接着的20个字节就是第二页的哈希值,以此类推,直到原始文件的最后一页。
由于文件不一定是pageSize的整数倍,最后一页往往不足”一整页”的大小,因此需要额外的字段codeLimit
记录文件的实际大小,也就是需要签名的数据的实际大小,通过这个值计算出最后一页的实际大小,并提取相应数据计算最后一页的签名。例子中codeLimit=0xD300
,很容易得出最后一页大小为0x300
1 2 |
|
计算出最后一页的sha1值与CodeDirectory中(图中黄色⑥)一致。
nCodeSlots记录了文件的总页数14,可通过0xD300 / 0x1000 = 13.1875
得出确实是14页。
细心的朋友已经发现了,④ identifier和 ⑤ hashSlots 之间有一段多出的数据⑦,并且CodeDirectory中还有一个奇怪的值nSpecialSlots=5
,整个文件的哈希值都已经包含在⑤和⑥之间了,这多出来的数据是怎么回事呢?
原来,在第一页的前面,还有5个特殊的负数页,用来保存这些额外信息的哈希值。
序号 | 对应内容 | |
---|---|---|
-1 | App根目录的Info.plist文件 | |
-2 | Requirements(代码签名的第二部分) | |
-3 | Resource Directory (_CodeSignature/CodeResources文件) | |
-4 | 暂未使用 | |
-5 | Entitlements (代码签名的第三部分) |
同样地,出于性能考虑,这些哈希值并未经过任何加密,只需要确保这些哈希值未经篡改,就可以说明代码本身没有被篡改。
用于指定签名校验时的一些额外的约束,签名时codesign命令会自动生成这部分数据,但目前并没有看到什么地方使用了它,就不深入分析了,官方文档有对这部分内容的详细描述
通过头部的偏移定位到数据的位置,显然,这是一个Blob结构
1 2 3 4 |
|
之前由Xcode生成的Entitlements文件被整个嵌入到签名数据中。
CMS是Cryptographic Message Syntax
的缩写,是一种标准的签名格式,由RFC3852定义。还记得Provisioning Profile的签名吗?它们是相同的格式。CMS格式的签名中,除了包含前面我们推导出的加密哈希和证书之外,还承载了一些其他的信息。由于是二进制格式,不方便分析,可以将其内容从MachO文件中剥离出来,再找合适的工具进行解析。根据偏移量定位到CMS Signature的位置0xDA46
1 2 3 4 |
|
除去头部的8个字节,把对应的内容提取出来
1
|
|
可以将导出的cms_signature文件上传到在线ASN.1解析工具(支持CMS格式解析)进行分析
文件被解析为树状结构,看起来还是不够直观,因为这个工具只是按照数据格式把内容进行了格式化,但是并没有标注所有字段的确切含义。其实我们还可以使用openssl进行查看,但是因为Mac上自带的openssl以及通过HomeBrew安装的openssl都是没有开启cms支持的,所以可以将文件拷贝到linux机器上或者自行编译openssl进行查看,具体方法在此不表。
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 39 40 |
|
由于输出内容太多,将部分内容做了删减,可以观察到签名中主要包含了这些内容
由于在Code Directory中已经保存了所有资源及代码的哈希值,那么我们只需要确保CodeDirectory不被篡改,即可确保整个app的完整性, 因此CMS Signature中只需要对CodeDirectory进行签名即可。而signedAttrs中支持这样一种特性:可以先计算被签名数据的哈希,然后再对哈希值进行签名。听起来有点绕,不过仔细体会一下应该不难理解。
我们把CodeDirectory的内容抠出来,计算其哈希值,以第一个CodeDirectory为例,计算其sha1:
1 2 |
|
这个值叫做CDHash(Code Directory’s Hash),对比前面从cms_signature中解析出的 signedAttrs,会发现这两个值是一样的,也就是说CodeDirectory的哈希值被放在了signerInfos->signedAttrs中,作为最终真正被签名
(计算哈希并加密)的内容。
根据RFC5652 – Cryptographic Message Syntax (CMS)中的规定,整个signedAttrs的内容会作为最终被签名的对象,我们可以按照RFC的规则来手动验证签名的计算过程。结合在线ASN.1解析工具的解析结果,定位到signedAttrs的偏移量为4016,先将这部分内容通过dd或者openssl命令提取出来,由于dd命令需要知道偏移和长度,而openssl可以直接将指定起始位置的整个节点dump出来,使用openssl会更为方便一些
1 2 3 |
|
这是一段ASN.1编码的数据,使用BER(BasicEncoding Rules)规则编码,在编码时,表示SET OF
的tag(编码为0x31)会被替换为IMPLICIT [0]
(编码为0xA0),因此,在计算时需要将数据还原,即将首字节a0
替换回31
。
1 2 3 4 |
|
计算其哈希值,由于singerInfos->digestAlgorithm指明了使用sha256,所以我们计算这个文件的sha256值
1 2 |
|
这个hash值最终会使用开发者证书对应的私钥进行加密,得到签名数据,并保存在signerInfos->signature中。如果要验证签名,则需要使用公钥对签名数据进行解密, 再将解密后的数据与上述hash值进行对比。
首先先从文件中分别提取签名的开发者证书和最终的签名数据,然后再从开发者证书中提取公钥对其进行解密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
解密后的数据, 可以看出跟我们自己计算的signedAttrs的hash值是相同的,如此一来也就完成了整个代码签名的校验。
至此,我们已经从头到尾剖析了iOS代码签名的生成方式及数据结构,在这个过程中,至少存在4次计算哈希的行为,并且是环环相扣的
只有最后一步的哈希值是被加密的, 前面几步的哈希值是否加密都不影响签名的效果,只要任意内容有变化,均会因某个环节的哈希不匹配而导致签名校验的失败。
相信上面的二进制分析已经让你眼花缭乱了,不过已经有大神做出了jtool这个工具,它是一款强大的MachO二进制分析工具,用来替代otool、nm、segedit等命令,也包括codesign的部分功能。通过以下命令可以将代码签名解析为可读的文本格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
在Xcode Organizer中导出或者提交App时,Xcode会将Entitlements文件及embedded.mobileprovision文件替换为对应的版本,并使用对应的证书重新签名,主要区别如下
类型 | Entitlements | Provisioning Profile | 证书 | |
---|---|---|---|---|
AppStore | 不可调试,推送为生产环境 | 无ProvisionedDevices | 发布证书 | |
Ad Hoc | 不可调试,推送为生产环境 | 允许安装到已注册的测试设备 | 发布证书 | |
Development | 可调试,推送为测试环境 | 允许安装到已注册的测试设备 | 开发证书 | |
Enterprise | 不可调试,推送为生产环境 | ProvisionAllDevices | 企业级发布证书 |
本篇完。
下一篇:细说iOS代码签名(四):签名校验、越狱、重签名
]]>在了解了签名和证书的基本结构之后,我们来研究一下iOS的开发者证书,它是开发过程中必不可少的东西,相信大家都有接触。众所周知,iOS设备并不能像Android那样任意地安装app,app必须被Apple签名之后才能安装到设备上。而开发者在开发App的时候需要频繁地修改代码并安装到设备上进行测试,不可能每次都先上传给Apple进行签名,因此需要一种不需要苹果签名就可以运行的机制。
这个机制的实现方式是:
Provisioning Profile
那么先研究一下开发者证书是如何产生的:在Xcode 8及之后的版本,Xcode会自动帮我们管理证书,我们可能根本不会有机会去研究它,但是在早期的版本中,需要我们自己动手操作,获取开发者证书主要有两个步骤
在Keychain菜单栏选择”从证书颁发机构请求证书…”
这个操作会产生一个名为CertificateSigningRequest.certSigningRequest
的签名请求文件,在生成这个文件之前其实Keychain已经自动生成了一对公、私钥
可以在Keychain中选中这个条目,右键选择导出,将密钥文件导出为p12文件,使用openssl查看其内容
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 |
|
这里出现了几个熟悉的面孔:
p, q
n = p * q
e
, 这里固定为 0x10001 (65535)d
CSR文件的内容其实就是个人信息、公钥(Modulus + PublicExponent),以及自签名(使用自己的私钥进行签名), 可通过openssl命令查看其内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
在苹果开发者网站,将CSR提交给Apple进行签名,Apple会返回一个签好名的证书文件
,后缀名为cer
。
先查看一下他的sha1
值,后面会用到
1 2 |
|
双击即可将其导入到Keychain中,Keychain会自动把它之前创建CSR时自动生成的密钥归为一组。无论是在证书列表中查看还是在密钥列表中查看,都能看到与之匹配的另一半
。
查看证书的内容
可以从证书中得到几个关键信息:
CA
现在应该可以理解证书和密钥的关系了,密钥中保存了私钥和公钥,私钥用于签名,而证书里面有且只有公钥,并且是被第三方CA
“认证” 过,用于解密和校验。
一般我们说使用证书
签名,实际上是使用与证书所匹配的私钥进行签名
,证书
只是作为签名数据的一部分被嵌入到签名结构中。如果Keychain中只有证书,没有对应的密钥文件,是无法进行签名的,会得到Missing private key
之类的报错提示。
图中可以看到这个证书的签发者是Apple Worldwide Developer Relations Certification Authority
,在Keychain中搜索这个名字, 可以看到它的证书详情。我们会发现,它的类型是中级证书颁发机构(中级CA)
,它也包含签名,并且是由另外一个叫做Apple Root CA
的根证书颁发机构(根CA)
进行签发的,这样就形成了一条证书链。而继续查看Apple Root CA
的证书,会发现它是自签名的,因为它会被内置在设备中,设备无条件信任它,也就不需要其他的机构为其背书了。
这样的证书链机制可以简化根证书颁发机构的工作,同时提升证书管理的安全性。将颁发底层证书的工作分散给多个中级证书颁发机构进行处理,根证书颁发机构只需要对下一级机构的证书进行管理和签发,降低根证书颁发机构私钥的使用频率,也就降低了私钥泄露的风险。中级证书颁发机构各司其职,即使出现私钥泄露这样的重大安全事故,也不至于波及整个证书网络。
开发者证书按用途可分为Development证书和Distribution证书:
除了普通开发者证书(个人开发者账号和公司开发者账号使用的证书)外,还有一种特殊的企业级开发者证书
,这种证书签名的App可以被直接安装在任意的iOS设备上,只要用户主动信任该证书即可。它的作用是方便企业给内部员工分发生产力工具,比如往往存在这样一些场景:企业内部无法访问互联网,自然也就无法通过AppStore安装应用,或是使用私有API,完成一些AppStore不允许的功能。前面所说的不需要苹果签名即可安装运行的机制同样适用于企业级开发者证书,并且是企业级开发者证书的基础。
从证书的申请方式和内容来看,企业级开发者证书和普通开发者证书并无不同,只是开发者账号的申请方式和费用有区别。此外,Apple对这两种证书所能提供的Provisioning Profile有细微的差异,下一节马上就会分析。
除了开发者证书,在进行iOS代码签名的时候还需要有这两个文件,他们是被签名内容的一部分
沙盒(Sandbox)技术是iOS安全体系中非常重要的一项技术,他的目的是通过各种技术手段限制App的行为,比如可读写的路径,允许访问的硬件,允许使用的服务等等,即使应用出现任意代码执行的漏洞,也无法影响到沙盒外的系统。(图来自Apple开发者网站)
通常所说的Entitlements(授权文件),也就是指iOS沙盒的配置文件,这个文件中声明了app所需的权限,如果app中使用到了某项沙盒限制的功能,但没有声明对应的权限,可能运行到相关的代码时会直接Crash。
全新的iOS工程中是没有这个文件的,如果在Capabilities
中开启了一些需要权限的功能之后,Xcode会自动(Xcode 8及之后的版本)生成Entilements文件,并将对应的权限声明添加到Entitlements文件中。
这个文件其实是xml格式的plist
文件,内容如下
1 2 3 4 5 6 7 8 |
|
实际上,这个文件的内容并非是全部的授权内容,因为缺省状态下,App默认会包含以下与Team ID及App ID相关的权限声明:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
其中get-task-allow
代表是否允许被调试,它在开发阶段是必需的一项权限,而在进行Archive打包用于上架时会被去除。
进行代码签名时,会将这个Entitlements文件(如有)与上述缺省内容进行合并,得到最终的授权文件,并嵌入二进制代码中,作为被签名内容的一部分,由代码签名保证其不可篡改性。
Xcode对Provisioning Profile的解释是
A provisioning profile is a collection of digital entities that uniquely ties developers and devices to an authorized iPhone Development Team and enables a device to be used for testing.
Provisioning Profile在这里就起到了一个对设备和开发者授权的作用,他将开发者账号、证书、entitlements文件以及设备进行了绑定。
同样地,在开发过程中,Xcode 8及后续版本默认情况下会自动帮我们管理Provisioining Profile,自动下载的Provisioning Profile都被存放在~/Library/MobileDevice/Provisioning\ Profiles/
路径下,以UUID
格式命名。直接拖拽下图中的齿轮图标到Finder中也可以将其复制出来。
由于这个文件是被苹果签过名的,所以我们没有办法伪造或者修改这个文件,它使用的是标准的CMS(Cryptographic Message Syntax)格式,可以通过security命令查看它的签名信息,并将文件的内容提取出来:
1 2 3 4 5 |
|
Provisioning Profile统一都是由Apple iPhone OS Provisioning Profile Signing
进行签名的,机构名称言简意赅。导出的provision.plist内容如下
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 39 40 41 42 43 |
|
很明显可以看出这是一个xml格式的plist文件,里面的内容不难理解,最关键的是这几项
1 2 3 4 |
|
com.apple.developer.siri
。ProvisionsAllDevices
取代,代表授权任意设备。这些信息中有任何变动的时候,比如开发者证书有新增或者失效,在Capabilities中启用了当前App从未使用过的新功能,或是将新的iPhone连接到Xcode用于测试,Xcode都会自动重新申请Provisioning Profile。
Provisioning Profile会被内置在App中,置于App根目录下的embedded.mobileprovision
。安装App时如果签名校验通过,这个文件会自动被拷贝到iOS设备的/Library/MobileDevice/Provisioning\ Profiles/
路径下。由于该文件已被Apple官方签名,系统可以无条件信任它,并用它来校验App的签名、权限,以及本机的UUID等是否满足来自官方的授权。通过这种方式,间接信任了使用开发者证书签名的App,让iOS设备可以运行非苹果官方签名的App。
假如你有一台越狱的设备,查看任意一个从AppStore上下载下来的App,里面都不会有embedded.mobileprovision这个文件,因为经过Apple重新签名以后,设备就不再需要它了。
本篇完。
下一篇:细说iOS代码签名(三):签名的过程及代码签名的数据结构
]]>数字签名其实跟我们手写的签名类似,代表一个特定的主体(签名者)对特定内容(被签名数据)的署名和认可,签名是对信息发送行为真实性的有效保障。数字签名在很多领域都有应用,iOS的代码签名正是其中最典型的一种,我们可以先尝试分析一下iOS上代码签名的目的和好处。
代码签名的首要任务是保证设备及系统的安全性,只有被苹果设备认可的证书签名的代码才能够被执行,否则在安装或者运行时会因为无法通过内核的签名校验而失败。iOS的系统中内置了来自苹果的CA证书,系统自身的代码都是被苹果”签名“过的, 而用户从AppStore下载的App也都已被苹果官方进行签名。签名机制可以有效地防止来自外部的攻击。
这里存在两种场景:
除了能够避免非授权的恶意代码运行,代码签名还可以有效地限制app的行为,这部分功能主要是由Sandbox机制来保证,但Sandbox的配置是绑定在签名中的,就是通常所说的Entitlements文件。试想,如果Entitlements文件可以被任意修改,那么Sandbox也就失去了意义,所以Entitlements文件也是强制签名保护的对象。对于越狱来说,如果无法绕过签名和Sandbox,再强大的提权漏洞也无计可施。
代码签名还给苹果带来了一个巨大的好处:App分发的绝对控制权。在iOS平台上(面向未越狱的用户)公开发行App的合法途径有且只有一种,就是上传到苹果官方的AppStore供用户下载。苹果会对App进行严格的审查并签名,App的功能及支付渠道也因此可以受苹果的严格管制,这为苹果带来的经济效益不言而喻。
签名的本质是用于验证数据的合法性,确保被签名的数据来自特定的来源,并且未经篡改。它基于非对称加密,和哈希算法,研究签名之前需要对这两种算法有一定的了解。
也叫非对称加密,它在加密和解密时使用的是不同的密钥,具有这样的特征:
a
和 b
,满足 a ≠ b
a
加密的数据只能用b
进行解密,a
自身无法解密,反之亦然最常见的公钥加密算法是RSA公钥加密算法,也是签名中普遍使用的算法。其数学原理如下:
p
, q
,并计算他们的乘积n = p * q
φ(n) = φ(p) * φ(q) = (p-1) * (q-1)
e
,满足1 < e < φ(n)
,且与φ(n)
互质e
对于φ(n)
的乘法逆元d
,e * d = 1 mod φ(n)
{n, e}
和 {n, d}
分别组成这个算法的一对密钥p
, 若使用{n, e}
作为加密密钥,其密文计算方法为 c = p ^ e mod n
单向函数
,已知{c, n, e}
无法计算出p
{n, d}
进行解密, p' = c * d mod n
逆函数
n
是相同的,那么如果已知了e
和d
其中的一个,想要计算另一个,必须知道φ(n)
,也就是必须先将n
分解质因数
,得到p
和q
,但由于n
的值非常大,这样的计算量基本上是不可能
的,也就保障了算法的安全性理论上 {n, e}
和 {n, d}
可以互换,任何一个都可以是公钥或者私钥,加密和解密的函数也可以互换。但实践中,一般固定设置e=65537(0x10001)
,相当于公开的一个约定,这样一来{n, e}
就只能作为公钥使用。
也叫散列或者摘要算法,对一段任意长度的数据,通过一定的映射和计算,得到一个固定长度的值,这个值就被称为这段数据的哈希值(hash)。给定一个哈希算法,它一定具有以下特征:
哈希碰撞
,实际应用中认为这是小概率事件(数学意义上的”不可能事件”),优秀的哈希算法都是碰撞率极低
的。计算
出原始数据,这一点非常重要!常见的哈希算法有: md5, sha1, sha256等,其中sha1长度为160bits,而sha256长度为256bits,二者相比,sha256的取值范围更大,因此碰撞和破解的概率更低,也就相对更安全。
有了上面这两种算法作为基础,就可以组建一个签名和验证签名的体系了,如下图所示
假如A
要给B
发送一段数据d
,先对其签名:
d
的哈希值h
,并使用自己的私钥a
对 h
进行加密,得到的密文c
就是签名得到签名后,将数据d
和签名c
通过某种方式发送给B
,此时B
收到了数据d'
以及签名c'
,需要验证这段数据是否被篡改,以及是否是A
发送的
d'
的哈希值h'
,使用A
的公钥b
将签名c'
解密,得到h''
。通过对比h'
和h''
是否一致,就可以知道数据或签名是否被篡改。并且,如果哈希值是匹配的,能够说明这段数据一定是由A
签名并发出的常见的签名算法:
上面这个例子中,任何需要接受A
的消息的人都需要事先保存A
的公钥。这样的方案存在一个很大的问题:公钥如何分发?如果B
要接受来自很多不同来源的数据,不可能事先将所有来源的公钥都提前保存下来,并且这样无法适应来源变动(增加、删除、变更)等带来的变化。因此,一般会把公钥当做签名的一部分,随着数据一起分发,接收方不需要事先保存任何数据来源的公钥。
但是这样会引入一个新的问题:如何知道数据中所携带的公钥就是否是发送者自己的公钥?
这涉及到密钥的管理和分发,细节展开的话是一个非常大的课题。简单来说,可以把公钥和所有者的信息保存在一个文件里,并让一个可信的第三者使用其私钥对这个文件进行签名,得到一个签了名的公钥文件,这个文件就叫做证书
。证书会作为签名的一部分,随着数据一起分发。
这里出现了一个有意思的事情,数据签名中的证书本身也是一段数据(公钥+所有者信息)以及其签名组成的,但证书中的签名是简单签名,一般只有哈希值和签发者名称,不会再将签发者的证书包含在签名中,否则就陷入无限递归的死循环了。
此时我们还需要使用第三者的公钥验证这个证书的合法性。虽然需要多验证一步,但是这样一来,本地不再需要保存每个数据来源的公钥,只需要保存这个第三者的证书(公钥)即可,每个数据来源的证书都由这个可信的第三者进行签发,这个可信的第三者就被称为证书颁发机构(Certification Authority),简称CA
。
实际上,CA的证书可能也是由其他更高一级的CA进行签发的,这种情况会产生3级甚至3级以上的证书链,系统中只需要保存最高级CA的证书,中间CA的证书和信息提供者的证书依次进行递归校验即可。
可以通过这个命令导出Xcode应用中可执行程序的签名证书,mac OS上的代码签名格式与iOS平台是相同的
1
|
|
当前文件夹下会产生三个证书文件cert0
cert1
cert2
。其中cert0是由cert1签发的,可以使用cert1验证其合法性,同理cert2可以验证cert1的合法性。而对于cert2,只需要对比系统的keychain中是否有相同的证书文件即可。通过下面的命令可以分别查看他们的所有者名称:
1 2 3 4 |
|
本篇完。
下一篇: 细说iOS代码签名(二):开发者证书、Entitlements、Provisioning Profile
]]>2008年苹果发布iOS2.0时引入了强制代码签名(Mandatory Code Signing)技术,为了能够严格控制设备上能够运行的代码,这为iOS设备的安全性和苹果的AppStore生态奠定了坚实的基础。作为iOSer总是要跟代码签名打交道的,相信大部分人对代码签名都是一知半解,本文将会由浅入深,深挖代码签名的内部细节。
数字签名其实跟我们手写的签名类似,代表一个特定的主体(签名者)对特定内容(被签名数据)的署名和认可,签名是对信息发送行为真实性的有效保障。数字签名在很多领域都有应用,iOS的代码签名正是其中最典型的一种,我们可以先尝试分析一下iOS上代码签名的目的和好处。
代码签名的首要任务是保证设备及系统的安全性,只有被苹果设备认可的证书签名的代码才能够被执行,否则在安装或者运行时会因为无法通过内核的签名校验而失败。iOS的系统中内置了来自苹果的CA证书,系统自身的代码都是被苹果”签名“过的, 而用户从AppStore下载的App也都已被苹果官方进行签名。签名机制可以有效地防止来自外部的攻击。
这里存在两种场景:
除了能够避免非授权的恶意代码运行,代码签名还可以有效地限制app的行为,这部分功能主要是由Sandbox机制来保证,但Sandbox的配置是绑定在签名中的,就是通常所说的Entitlements文件。试想,如果Entitlements文件可以被任意修改,那么Sandbox也就失去了意义,所以Entitlements文件也是强制签名保护的对象。对于越狱来说,如果无法绕过签名和Sandbox,再强大的提权漏洞也无计可施。
代码签名还给苹果带来了一个巨大的好处:App分发的绝对控制权。在iOS平台上(面向未越狱的用户)公开发行App的合法途径有且只有一种,就是上传到苹果官方的AppStore供用户下载。苹果会对App进行严格的审查并签名,App的功能及支付渠道也因此可以受苹果的严格管制,这为苹果带来的经济效益不言而喻。
签名的本质是用于验证数据的合法性,确保被签名的数据来自特定的来源,并且未经篡改。它基于非对称加密,和哈希算法,研究签名之前需要对这两种算法有一定的了解。
也叫非对称加密,它在加密和解密时使用的是不同的密钥,具有这样的特征:
a
和 b
,满足 a ≠ b
a
加密的数据只能用b
进行解密,a
自身无法解密,反之亦然最常见的公钥加密算法是RSA公钥加密算法,也是签名中普遍使用的算法。其数学原理如下:
p
, q
,并计算他们的乘积n = p * q
φ(n) = φ(p) * φ(q) = (p-1) * (q-1)
e
,满足1 < e < φ(n)
,且与φ(n)
互质e
对于φ(n)
的乘法逆元d
,e * d = 1 mod φ(n)
{n, e}
和 {n, d}
分别组成这个算法的一对密钥p
, 若使用{n, e}
作为加密密钥,其密文计算方法为 c = p ^ e mod n
单向函数
,已知{c, n, e}
无法计算出p
{n, d}
进行解密, p' = c * d mod n
逆函数
n
是相同的,那么如果已知了e
和d
其中的一个,想要计算另一个,必须知道φ(n)
,也就是必须先将n
分解质因数
,得到p
和q
,但由于n
的值非常大,这样的计算量基本上是不可能
的,也就保障了算法的安全性理论上 {n, e}
和 {n, d}
可以互换,任何一个都可以是公钥或者私钥,加密和解密的函数也可以互换。但实践中,一般固定设置e=65537(0x10001)
,相当于公开的一个约定,这样一来{n, e}
就只能作为公钥使用。
也叫散列或者摘要算法,对一段任意长度的数据,通过一定的映射和计算,得到一个固定长度的值,这个值就被称为这段数据的哈希值(hash)。给定一个哈希算法,它一定具有以下特征:
哈希碰撞
,实际应用中认为这是小概率事件(数学意义上的”不可能事件”),优秀的哈希算法都是碰撞率极低
的。计算
出原始数据,这一点非常重要!常见的哈希算法有: md5, sha1, sha256等,其中sha1长度为160bits,而sha256长度为256bits,二者相比,sha256的取值范围更大,因此碰撞和破解的概率更低,也就相对更安全。
有了上面这两种算法作为基础,就可以组建一个签名和验证签名的体系了,如下图所示
假如A
要给B
发送一段数据d
,先对其签名:
d
的哈希值h
,并使用自己的私钥a
对 h
进行加密,得到的密文c
就是签名得到签名后,将数据d
和签名c
通过某种方式发送给B
,此时B
收到了数据d'
以及签名c'
,需要验证这段数据是否被篡改,以及是否是A
发送的
d'
的哈希值h'
,使用A
的公钥b
将签名c'
解密,得到h''
。通过对比h'
和h''
是否一致,就可以知道数据或签名是否被篡改。并且,如果哈希值是匹配的,能够说明这段数据一定是由A
签名并发出的常见的签名算法:
上面这个例子中,任何需要接受A
的消息的人都需要事先保存A
的公钥。这样的方案存在一个很大的问题:公钥如何分发?如果B
要接受来自很多不同来源的数据,不可能事先将所有来源的公钥都提前保存下来,并且这样无法适应来源变动(增加、删除、变更)等带来的变化。因此,一般会把公钥当做签名的一部分,随着数据一起分发,接收方不需要事先保存任何数据来源的公钥。
但是这样会引入一个新的问题:如何知道数据中所携带的公钥就是否是发送者自己的公钥?
这涉及到密钥的管理和分发,细节展开的话是一个非常大的课题。简单来说,可以把公钥和所有者的信息保存在一个文件里,并让一个可信的第三者使用其私钥对这个文件进行签名,得到一个签了名的公钥文件,这个文件就叫做证书
。证书会作为签名的一部分,随着数据一起分发。
这里出现了一个有意思的事情,数据签名中的证书本身也是一段数据(公钥+所有者信息)以及其签名组成的,但证书中的签名是简单签名,一般只有哈希值和签发者名称,不会再将签发者的证书包含在签名中,否则就陷入无限递归的死循环了。
此时我们还需要使用第三者的公钥验证这个证书的合法性。虽然需要多验证一步,但是这样一来,本地不再需要保存每个数据来源的公钥,只需要保存这个第三者的证书(公钥)即可,每个数据来源的证书都由这个可信的第三者进行签发,这个可信的第三者就被称为证书颁发机构(Certification Authority),简称CA
。
实际上,CA的证书可能也是由其他更高一级的CA进行签发的,这种情况会产生3级甚至3级以上的证书链,系统中只需要保存最高级CA的证书,中间CA的证书和信息提供者的证书依次进行递归校验即可。
可以通过这个命令导出Xcode应用中可执行程序的签名证书,mac OS上的代码签名格式与iOS平台是相同的
1
|
|
当前文件夹下会产生三个证书文件cert0
cert1
cert2
。其中cert0是由cert1签发的,可以使用cert1验证其合法性,同理cert2可以验证cert1的合法性。而对于cert2,只需要对比系统的keychain中是否有相同的证书文件即可。通过下面的命令可以分别查看他们的所有者名称:
1 2 3 4 |
|
在了解了签名和证书的基本结构之后,我们来研究一下iOS的开发者证书,它是开发过程中必不可少的东西,相信大家都有接触。众所周知,iOS设备并不能像Android那样任意地安装app,app必须被Apple签名之后才能安装到设备上。而开发者在开发App的时候需要频繁地修改代码并安装到设备上进行测试,不可能每次都先上传给Apple进行签名,因此需要一种不需要苹果签名就可以运行的机制。这个机制的实现方式是:
Provisioning Profile
那么先研究一下开发者证书是如何产生的:在Xcode 8及之后的版本,Xcode会自动帮我们管理证书,我们可能根本不会有机会去研究它,但是在早期的版本中,需要我们自己动手操作,获取开发者证书主要有两个步骤
在Keychain菜单栏选择”从证书颁发机构请求证书…”
这个操作会产生一个名为CertificateSigningRequest.certSigningRequest
的签名请求文件,在生成这个文件之前其实Keychain已经自动生成了一对公、私钥
可以在Keychain中选中这个条目,右键选择导出,将密钥文件导出为p12文件,使用openssl查看其内容
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 |
|
这里出现了几个熟悉的面孔:
p, q
n = p * q
e
, 这里固定为 0x10001 (65535)d
CSR文件的内容其实就是个人信息、公钥(Modulus + PublicExponent),以及自签名(使用自己的私钥进行签名), 可通过openssl命令查看其内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
在苹果开发者网站,将CSR提交给Apple进行签名,Apple会返回一个签好名的证书文件
,后缀名为cer
。
先查看一下他的sha1
值,后面会用到
1 2 |
|
双击即可将其导入到Keychain中,Keychain会自动把它之前创建CSR时自动生成的密钥归为一组。无论是在证书列表中查看还是在密钥列表中查看,都能看到与之匹配的另一半
。
查看证书的内容
可以从证书中得到几个关键信息:
CA
现在应该可以理解证书和密钥的关系了,密钥中保存了私钥和公钥,私钥用于签名,而证书里面有且只有公钥,并且是被第三方CA
“认证” 过,用于解密和校验。
一般我们说使用证书
签名,实际上是使用与证书所匹配的私钥进行签名
,证书
只是作为签名数据的一部分被嵌入到签名结构中。如果Keychain中只有证书,没有对应的密钥文件,是无法进行签名的,会得到Missing private key
之类的报错提示。
图中可以看到这个证书的签发者是Apple Worldwide Developer Relations Certification Authority
,在Keychain中搜索这个名字, 可以看到它的证书详情。我们会发现,它的类型是中级证书颁发机构(中级CA)
,它也包含签名,并且是由另外一个叫做Apple Root CA
的根证书颁发机构(根CA)
进行签发的,这样就形成了一条证书链。而继续查看Apple Root CA
的证书,会发现它是自签名的,因为它会被内置在设备中,设备无条件信任它,也就不需要其他的机构为其背书了。
这样的证书链机制可以简化根证书颁发机构的工作,同时提升证书管理的安全性。将颁发底层证书的工作分散给多个中级证书颁发机构进行处理,根证书颁发机构只需要对下一级机构的证书进行管理和签发,降低根证书颁发机构私钥的使用频率,也就降低了私钥泄露的风险。中级证书颁发机构各司其职,即使出现私钥泄露这样的重大安全事故,也不至于波及整个证书网络。
开发者证书按用途可分为Development证书和Distribution证书:
除了普通开发者证书(个人开发者账号和公司开发者账号使用的证书)外,还有一种特殊的企业级开发者证书
,这种证书签名的App可以被直接安装在任意的iOS设备上,只要用户主动信任该证书即可。它的作用是方便企业给内部员工分发生产力工具,比如往往存在这样一些场景:企业内部无法访问互联网,自然也就无法通过AppStore安装应用,或是使用私有API,完成一些AppStore不允许的功能。前面所说的不需要苹果签名即可安装运行的机制同样适用于企业级开发者证书,并且是企业级开发者证书的基础。
从证书的申请方式和内容来看,企业级开发者证书和普通开发者证书并无不同,只是开发者账号的申请方式和费用有区别。此外,Apple对这两种证书所能提供的Provisioning Profile有细微的差异,下一节马上就会分析。
除了开发者证书,在进行iOS代码签名的时候还需要有这两个文件,他们是被签名内容的一部分
沙盒(Sandbox)技术是iOS安全体系中非常重要的一项技术,他的目的是通过各种技术手段限制App的行为,比如可读写的路径,允许访问的硬件,允许使用的服务等等,即使应用出现任意代码执行的漏洞,也无法影响到沙盒外的系统。(图来自Apple开发者网站)
通常所说的Entitlements(授权文件),也就是指iOS沙盒的配置文件,这个文件中声明了app所需的权限,如果app中使用到了某项沙盒限制的功能,但没有声明对应的权限,可能运行到相关的代码时会直接Crash。
全新的iOS工程中是没有这个文件的,如果在Capabilities
中开启了一些需要权限的功能之后,Xcode会自动(Xcode 8及之后的版本)生成Entilements文件,并将对应的权限声明添加到Entitlements文件中。
这个文件其实是xml格式的plist
文件,内容如下
1 2 3 4 5 6 7 8 |
|
实际上,这个文件的内容并非是全部的授权内容,因为缺省状态下,App默认会包含以下与Team ID及App ID相关的权限声明:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
其中get-task-allow
代表是否允许被调试,它在开发阶段是必需的一项权限,而在进行Archive打包用于上架时会被去除。
进行代码签名时,会将这个Entitlements文件(如有)与上述缺省内容进行合并,得到最终的授权文件,并嵌入二进制代码中,作为被签名内容的一部分,由代码签名保证其不可篡改性。
Xcode对Provisioning Profile的解释是
A provisioning profile is a collection of digital entities that uniquely ties developers and devices to an authorized iPhone Development Team and enables a device to be used for testing.
Provisioning Profile在这里就起到了一个对设备和开发者授权的作用,他将开发者账号、证书、entitlements文件以及设备进行了绑定。
同样地,在开发过程中,Xcode 8及后续版本默认情况下会自动帮我们管理Provisioining Profile,自动下载的Provisioning Profile都被存放在~/Library/MobileDevice/Provisioning\ Profiles/
路径下,以UUID
格式命名。直接拖拽下图中的齿轮图标到Finder中也可以将其复制出来。
由于这个文件是被苹果签过名的,所以我们没有办法伪造或者修改这个文件,它使用的是标准的CMS(Cryptographic Message Syntax)格式,可以通过security命令查看它的签名信息,并将文件的内容提取出来:
1 2 3 4 5 |
|
Provisioning Profile统一都是由Apple iPhone OS Provisioning Profile Signing
进行签名的,机构名称言简意赅。导出的provision.plist内容如下
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 39 40 41 42 43 |
|
很明显可以看出这是一个xml格式的plist文件,里面的内容不难理解,最关键的是这几项
1 2 3 4 |
|
com.apple.developer.siri
。ProvisionsAllDevices
取代,代表授权任意设备。这些信息中有任何变动的时候,比如开发者证书有新增或者失效,在Capabilities中启用了当前App从未使用过的新功能,或是将新的iPhone连接到Xcode用于测试,Xcode都会自动重新申请Provisioning Profile。
Provisioning Profile会被内置在App中,置于App根目录下的embedded.mobileprovision
。安装App时如果签名校验通过,这个文件会自动被拷贝到iOS设备的/Library/MobileDevice/Provisioning\ Profiles/
路径下。由于该文件已被Apple官方签名,系统可以无条件信任它,并用它来校验App的签名、权限,以及本机的UUID等是否满足来自官方的授权。通过这种方式,间接信任了使用开发者证书签名的App,让iOS设备可以运行非苹果官方签名的App。
假如你有一台越狱的设备,查看任意一个从AppStore上下载下来的App,里面都不会有embedded.mobileprovision这个文件,因为经过Apple重新签名以后,设备就不再需要它了。
万事具备,只欠东风,已经具备了签名所需的所有条件,接下来就可以开始研究签名的具体过程了。
在编译iOS App时,Xcode在编译的打包的流程中会自动进行代码签名, 可以在编译日志界面找到一个Sign
的步骤,内部是调用了codesign
这个命令对app进行签名
codesign有几个关键参数
--sign sign_identity
指定签名所用的证书,可以指定证书的名字,比如"iPhone Developer: xxx (xxx)"
也可以直接写证书文件的sha1值,xcode中就是直接指定sha1值的。通过观察图中的sha1值可以看出xcode自动选择了刚申请的最新证书。--entitlements entitlements_file
指定签名所需要的entitlements文件,这里的entitlements文件跟前面看到的并不是同一个文件,而是基于原有entitlements文件,补充上缺省权限后生成的临时文件1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
如果想对比签名前后的区别,可以在Build Settings
中找到Code Signing Identity
,选择Other
并将内容清除(即设置为空),即可跳过代码签名。分别编译一个不签名的版本和签名的版本,对比可以发现
_CodeSignature
文件夹,里面只有一个文件CodeResources
embedded.mobileprovision
文件其中embedded.mobileprovision
就是前文提到的Provisioning Profile文件,它直接被拷贝到了app的根目录并重命名,在此不再赘述,重点研究下另外两个不同点。
首先是_CodeSingature/CodeResources
,这是一个plist文件,里面保存了app中每个文件(除了App的可执行文件)的明文哈希值
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 |
|
files
和files2
分别是旧版本和新版本的文件列表,而rules
与rules2
分别是与之对应的规则说明,里面描述了计算hash时需要被排除的文件以及每个文件的权重。
files
中保存的是每个文件的sha1值,而files2
中同时保存了sha1和sha256,因为sha1在计算机硬件高度发达的今天,已经相对没有那么安全了,因此最新的签名算法中,引入了sha256。注意,这里的hash值都是base64编码的明文,有些文章说这些值是使用私钥加密的哈希,这是很不负责任的错误说法,通过几条简单的命令就可以进行验证:
1 2 3 4 5 6 7 8 9 10 11 |
|
_CodeSignature/CodeResources
文件的主要作用是保存签名时每个文件的哈希值,而这些哈希值并不需要都进行加密,因为非对称加密的性能是比较差的,全部都加密只会拖慢签名和校验的速度。其实只需要确保这个文件没有被篡改,自然也就可以确保每个文件都是签名时的原始状态,这一点在后续的内容中可以得到验证。
使用otool -l
对比签名前后的二进制文件,可以发现签名后二进制文件多了一个名为LC_CODE_SIGNATURE
的Load Command
1 2 3 4 5 6 |
|
MachOView中查看如下
代码签名是一段纯二进制的数据,可以在https://opensource.apple.com/source/Security/Security-55471/sec/Security/Tool/codesign.c.auto.html 看到一些结构定义,结合数据定义来分析
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 |
|
这部分是典型的数据头结构,声明了5个Blob,以及每个Blob的类型和相对签名头部的偏移量。接下来把每个部分分别提取出来进行分析。
CodeDirectory是签名数据中最终要的部分,直译过来就是代码目录,其实里面是整个MachO文件的哈希值,这里的哈希并不是一次性对整个文件进行哈希,而是将MachO文件按照pageSize(一般是4k也就是4096字节)进行分页,每一页单独计算哈希,并按照顺序保存下来,就像目录一样。
细心的同学会发现上面的数据中出现了两个CodeDirectory,type分别是0x0
和0x1000
,这也是历史遗留问题,0x0
对应的是旧版本的代码签名,使用sha1算法进行哈希值的计算,而0x1000
是后来引入的,采用sha256作为哈希算法,除了算法和哈希的长度不同之外,其他内容基本是一样的。取第一个进行分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
hashOffset就是”目录”第一页的偏移,从这个位置(0xD409)可以提取到一串20字节的sha1值(图中黄色⑤):
1
|
|
这个值代表的就是该文件第一页的哈希值,通过以下命令计算文件前4096字节的sha1可进行验证
1 2 |
|
而紧接着的20个字节就是第二页的哈希值,以此类推,直到原始文件的最后一页。
由于文件不一定是pageSize的整数倍,最后一页往往不足”一整页”的大小,因此需要额外的字段codeLimit
记录文件的实际大小,也就是需要签名的数据的实际大小,通过这个值计算出最后一页的实际大小,并提取相应数据计算最后一页的签名。例子中codeLimit=0xD300
,很容易得出最后一页大小为0x300
1 2 |
|
计算出最后一页的sha1值与CodeDirectory中(图中黄色⑥)一致。
nCodeSlots记录了文件的总页数14,可通过0xD300 / 0x1000 = 13.1875
得出确实是14页。
细心的朋友已经发现了,④ identifier和 ⑤ hashSlots 之间有一段多出的数据⑦,并且CodeDirectory中还有一个奇怪的值nSpecialSlots=5
,整个文件的哈希值都已经包含在⑤和⑥之间了,这多出来的数据是怎么回事呢?
原来,在第一页的前面,还有5个特殊的负数页,用来保存这些额外信息的哈希值。
序号 | 对应内容 | |
---|---|---|
-1 | App根目录的Info.plist文件 | |
-2 | Requirements(代码签名的第二部分) | |
-3 | Resource Directory (_CodeSignature/CodeResources文件) | |
-4 | 暂未使用 | |
-5 | Entitlements (代码签名的第三部分) |
同样地,出于性能考虑,这些哈希值并未经过任何加密,只需要确保这些哈希值未经篡改,就可以说明代码本身没有被篡改。
用于指定签名校验时的一些额外的约束,签名时codesign命令会自动生成这部分数据,但目前并没有看到什么地方使用了它,就不深入分析了,官方文档有对这部分内容的详细描述
通过头部的偏移定位到数据的位置,显然,这是一个Blob结构
1 2 3 4 |
|
之前由Xcode生成的Entitlements文件被整个嵌入到签名数据中。
CMS是Cryptographic Message Syntax
的缩写,是一种标准的签名格式,由RFC3852定义。还记得Provisioning Profile的签名吗?它们是相同的格式。CMS格式的签名中,除了包含前面我们推导出的加密哈希和证书之外,还承载了一些其他的信息。由于是二进制格式,不方便分析,可以将其内容从MachO文件中剥离出来,再找合适的工具进行解析。根据偏移量定位到CMS Signature的位置0xDA46
1 2 3 4 |
|
除去头部的8个字节,把对应的内容提取出来
1
|
|
可以将导出的cms_signature文件上传到在线ASN.1解析工具(支持CMS格式解析)进行分析
文件被解析为树状结构,看起来还是不够直观,因为这个工具只是按照数据格式把内容进行了格式化,但是并没有标注所有字段的确切含义。其实我们还可以使用openssl进行查看,但是因为Mac上自带的openssl以及通过HomeBrew安装的openssl都是没有开启cms支持的,所以可以将文件拷贝到linux机器上或者自行编译openssl进行查看,具体方法在此不表。
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 39 40 |
|
由于输出内容太多,将部分内容做了删减,可以观察到签名中主要包含了这些内容
由于在Code Directory中已经保存了所有资源及代码的哈希值,那么我们只需要确保CodeDirectory不被篡改,即可确保整个app的完整性, 因此CMS Signature中只需要对CodeDirectory进行签名即可。而signedAttrs中支持这样一种特性:可以先计算被签名数据的哈希,然后再对哈希值进行签名。听起来有点绕,不过仔细体会一下应该不难理解。
我们把CodeDirectory的内容抠出来,计算其哈希值,以第一个CodeDirectory为例,计算其sha1:
1 2 |
|
这个值叫做CDHash(Code Directory’s Hash),对比前面从cms_signature中解析出的 signedAttrs,会发现这两个值是一样的,也就是说CodeDirectory的哈希值被放在了signerInfos->signedAttrs中,作为最终真正被签名
(计算哈希并加密)的内容。
根据RFC5652 – Cryptographic Message Syntax (CMS)中的规定,整个signedAttrs的内容会作为最终被签名的对象,我们可以按照RFC的规则来手动验证签名的计算过程。结合在线ASN.1解析工具的解析结果,定位到signedAttrs的偏移量为4016,先将这部分内容通过dd或者openssl命令提取出来,由于dd命令需要知道偏移和长度,而openssl可以直接将指定起始位置的整个节点dump出来,使用openssl会更为方便一些
1 2 3 |
|
这是一段ASN.1编码的数据,使用BER(BasicEncoding Rules)规则编码,在编码时,表示SET OF
的tag(编码为0x31)会被替换为IMPLICIT [0]
(编码为0xA0),因此,在计算时需要将数据还原,即将首字节a0
替换回31
。
1 2 3 4 |
|
计算其哈希值,由于singerInfos->digestAlgorithm指明了使用sha256,所以我们计算这个文件的sha256值
1 2 |
|
这个hash值最终会使用开发者证书对应的私钥进行加密,得到签名数据,并保存在signerInfos->signature中。如果要验证签名,则需要使用公钥对签名数据进行解密, 再将解密后的数据与上述hash值进行对比。
首先先从文件中分别提取签名的开发者证书和最终的签名数据,然后再从开发者证书中提取公钥对其进行解密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
解密后的数据, 可以看出跟我们自己计算的signedAttrs的hash值是相同的,如此一来也就完成了整个代码签名的校验。
至此,我们已经从头到尾剖析了iOS代码签名的生成方式及数据结构,在这个过程中,至少存在4次计算哈希的行为,并且是环环相扣的
只有最后一步的哈希值是被加密的, 前面几步的哈希值是否加密都不影响签名的效果,只要任意内容有变化,均会因某个环节的哈希不匹配而导致签名校验的失败。
相信上面的二进制分析已经让你眼花缭乱了,不过已经有大神做出了jtool这个工具,它是一款强大的MachO二进制分析工具,用来替代otool、nm、segedit等命令,也包括codesign的部分功能。通过以下命令可以将代码签名解析为可读的文本格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
在Xcode Organizer中导出或者提交App时,Xcode会将Entitlements文件及embedded.mobileprovision文件替换为对应的版本,并使用对应的证书重新签名,主要区别如下
类型 | Entitlements | Provisioning Profile | 证书 | |
---|---|---|---|---|
AppStore | 不可调试,推送为生产环境 | 无ProvisionedDevices | 发布证书 | |
Ad Hoc | 不可调试,推送为生产环境 | 允许安装到已注册的测试设备 | 发布证书 | |
Development | 可调试,推送为测试环境 | 允许安装到已注册的测试设备 | 开发证书 | |
Enterprise | 不可调试,推送为生产环境 | ProvisionAllDevices | 企业级发布证书 |
签名的校验并非一次性完成,在安装、启动、和运行时有着不同的校验规则。
App安装时的校验由位于iOS设备上的/usr/lib/libmis.dylib (dyld_shared_cache)提供。
App的安装是由/usr/libexec/installd
完成的,installd
会通过libmis.dylib
校验ProvisioningProfile、Entitlements及签名的合法性,并递归地校验签名时每一个步骤生成的哈希值:CDHash, Code Directory, _CodeSignature/CodeResources。
1 2 3 4 5 |
|
进程启动时,loader会先将可执行文件加载到虚拟内存,在加载的过程中mach_loader会自动解析MachO文件中的LC_CODE_SIGNATURE并进行校验,可以参考mach_loader的代码 bsd/kern/mach_loader.c
load_code_signature
在解析完签名的数据后会调用mac_vnode_check_singature
函数进行验证,而这个函数会被名为AFMI
(AppleMobileFileIntegrity)的内核扩展(kext)通过Hook的方式接管,而AFMI只是一层壳,最终也是调用了libmis.dylib来实现签名的校验,这一校验过程基本与安装时一致,防止安装后的篡改。
需要注意的是,加载过程中为了提升加载效率,签名校验并不会去检查Code Directory与实际的代码是否匹配,仅仅只检查了CMS Signature及CDHash的合法性。
当一页代码被加载到虚拟内存后,会立即触发page fault
,此时内核中的vm_fault
函数会被调用,紧接着调用vm_fault_enter
,在vm_fault_enter
的实现中会判断代码页是否需要签名校验,并执行校验的操作,参考代码osfmk/vm/vm_fault.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
对于宏VM_FAULT_NEED_CS_VALIDATION
的解释是
1 2 3 4 5 6 7 8 9 10 |
|
vm_page_validate_cs
会计算当前代码页的哈希值,并与签名中CodeDirectory记录的值进行比对,完成代码签名的验证。如果不符,且不满足系统预设的例外条件,则会向内核发出CS_KILL指令,将进程结束。
至此签名的校验流程就全部完成了。
越狱之后,签名校验机制会被破坏掉,否则用于实现越狱的代码自身就无法运行。比如在iOS6/7时代,典型的方式是替换 libmis.dylib
中的_MISValidateSignature
函数,使其永远返回验证成功,简单粗暴但很有效,因此越狱的设备可以不受签名限制运行任意程序。但是单纯解决掉这个函数只是解决了MachO文件的Load问题,运行时仍然会有沙盒和Code Directory的校验,想要对系统完全的控制权必须同时解决掉这两个问题。
由于沙盒机制的实现分散在系统的各个角落,没有简单的方式可以将沙盒一刀切地屏蔽掉,因此一般越狱并不会破坏掉沙盒。但因为越狱设备签名校验机制被绕过,不再会根据embedded.mobileprovision文件检查Entitlements的合法性,因此我们可以在沙盒范围内,声明任意的权限。Code Directory的校验在内核层,破解难度相对较大,并且完全没有必要进行破解,因为Code Directory只是单纯地校验未加密的哈希值而已,只需要按照代码签名的格式做好Code Directory即可。
越狱之父Saurik为此创造了ldid这个工具,用于给越狱设备上的程序制造”假”的签名。使用ldid进行签名只需要指定一个可选的Entitlements
文件,签名之后,产生的LC_CODE_SIGNATURE中只会两个有效的Blob,分别是 Code Directory和 Entitlements,并没有最重要的CMS Signature部分,因为_MISCalidateSignature
永远都会告诉系统签名是正确的。
1 2 3 4 5 6 7 8 |
|
有的时候出于各种原因,我们需要对一个App进行重签名,然后在自己的设备上进行测试。回顾一下签名的必备条件:
开发者证书和密钥我们已经有了,对于Entitlements和embedded.mobileprovision文件,为了确保重签后的App能够正常运行,必须使用和原App相同或者至少包含原App所需权限的Entitlements文件。这个并不难操作,只需要新建一个工程,开启相应的功能,让Xcode自动为我们生成即可。但是Entitlements文件中还有一些跟Team ID和App ID相关的配置,这两个是没有办法伪造的,因为我们不能使用已经被其他开发者注册过的ID。使用自己的ID一般也不会有什么问题,但在某些情况下可能导致最终的程序逻辑出现异常,这根具体的代码实现细节有关。
现在,只要确保有正确的Entitlements文件,Provisioning Profile与Entitlements文件匹配,且包含重签时使用的证书及目标设备的UUID,就可以进行重签名了,如果重签名后无法安装,请检查Provisioning Profile文件是否满足上述条件。
Entitlements文件中还标识了application-identifier
,也就是Bundle ID,正常签名的App中,这个值和Info.plist中的CFBundleIdentifier
的值是相同的,但实际在签名校验过程中,系统并不会检查二者是否一致。因此即使Entitlements中与Info.plist文件使用了不同的Bundle ID,理论上也不会影响重签名之后的运行。
需要注意,App中除了可执行程序文件外,还会可能会有Frameworks及Plugins,里面都会包含二进制的代码文件,他们的哈希值也会被存储在 _CodeSignature/CodeResources中。所有的二进制代码都必须进行签名,而签名后二进制文件的哈希值就会产生变化,因此需要先对这两个文件夹下的二进制文件进行签名,再对App进行签名。
重签名的基本流程如下,使用-f参数可以强制覆盖掉已有的签名
1 2 3 4 5 6 7 8 |
|
reference | link | |
---|---|---|
Code Signing Guide | https://developer.apple.com/… | |
ASN.1 JavaScript decoder | http://lapo.it/asn1js/ | |
Cryptographic Message Syntax (CMS) | https://www.ietf.org/rfc/rfc3852.txt | |
iSign in python | https://github.com/saucelabs/isign | |
CodeSigning (RSACon 2015) | http://newosxbook.com/articles/CodeSigning.pdf | |
jtool | http://www.newosxbook.com/tools/jtool.html | |
mistool | http://newosxbook.com/tools/mistool.html | |
evasi0n7 jailbreak writeup | https://geohot.com/e7writeup.html | |
iOS hacker’s handbook | https://books.google.com.hk/books?id=1kDcjKcz9GwC |
苹果在WWDC 2015大会上引入了bitcode,随后在Xcode7中添加了在二进制中嵌入bitcode(Enable Bitcode)的功能,并且默认设置为开启状态。很多开发者在集成第三方SDK的时候都被bitcode坑过一把,然后google百度一番发现只要关闭bitcode就可以了,但是大部分开发者都不清楚bitcode到底是什么东西。这篇文档将给大家详细地介绍与bitcode有关的内容。
研究bitcode之前需要先了解一下LLVM,因为bitcode是由LLVM引入的一种中间代码(Intermediate Representation,简称IR),它是源代码被编译为二进制机器码过程中的中间表示形态,它既不是源代码,也不是机器码。从代码组织结构上看它比较接近机器码,但是在函数和指令层面使用了很多高级语言的特性。
LLVM是一套优秀的编译器框架,目前NDK/Xcode均采用LLVM作为默认的编译器。LLVM的编译过程可以简单分为3个部分:
在这个体系中,不同语言的源代码将会被转化为统一的bitcode格式,三个模块可以充分复用,防止重复造轮子。如果要开发一门新的x语言
,只需要造一个x语言的前端,将x语言的源代码编译为bitcode,优化和后端的事情完全不用管。同理,如果新的芯片架构问世,则只需要基于LLVM重新写一套目标平台的后端,非常方便。
既然bitcode是代码的一种表示形式,因此它也会有自己的一套独立的语法,可以通过一个简单的例子来一探究竟,这里以clang为例,swift的操作和结果可能稍有不同。
本文所涉及的内容可以自行操作,也可以直接下载我写这篇文章时保存的副本
先编写一段helloworld代码(test.c):
1 2 3 4 5 |
|
通过以下命令可以将源代码编译为object文件:
1 2 3 |
|
其实,这个命令同时完成了前端、优化、后端三个部分,可以通过 -emit-llvm -c
将前端这一步单独拆出来,这样就可以看到bitcode了:
1 2 3 4 5 6 7 8 9 |
|
bitcode文件使用后缀名.bc
表示,可以看到,将bitcode文件作为clang的输入,编出的object文件跟直接编源代码是相同的。然后在来看一下bitcode文件:
1 2 3 4 5 6 7 8 9 10 11 |
|
通过hexdump可以看出这个文件并非文本文件,全是乱码,这样的文件是很难分析的。其实LLVM提供了llvm-dis
/ llvm-as
两个工具,用于将bitcode在二进制格式和可读的文本格式之间进行相互的转化,但遗憾的是Xcode的编译器工具链中并没有附带这个命令,因此只能另寻他法。
我们知道通过编译器的-S
参数可以将源代码编译为文本的assembly代码,不进行最后一步assembly到机器码的翻译工作,而assembly和机器码是等价的两种表示形式,bitcode同样也是有文本和二进制(bitcode)两种等价表示形式,clang也为bitcode保留了这一特性,可以通过-emit-llvm -S
将源代码编译为文本格式的bitcode, 也叫做LLVM Assembly Language,一般后缀名使用.ll
:
1
|
|
test.ll的全部内容如下
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 |
|
这样看上去就很清晰明了了,我们重点关注下函数定义这部分,我加了一些注释方便理解
1 2 3 4 5 6 7 8 9 10 11 12 |
|
这段代码不难阅读, 其含义和逻辑与我们所写的源代码基本一致,只是用了另外一种语法表示出来。因为没有经过优化,函数中的前两条语句其实是多余的,这在之后的优化阶段会被消除(dead_strip)。bitcode的具体语法在此不做展开,虽然这个例子看起来非常简单易懂,但真实场景中,bitcode的语法远比这个复杂,有兴趣的同学可以直接阅读LLVM Language Reference Manual。
在对bitcode有了一个直观的认识之后,再来看一下Apple围绕bitcode做了什么。Xcode中对Enable Bitcode这个配置的解释是:
以下摘自Xcode Help
https://help.apple.com/xcode/mac/10.1/index.html?localePath=en.lproj#/itcaec37c2a6
Enable Bitcode (ENABLE_BITCODE)
Activating this setting indicates that the target or project should generate bitcode during compilation for platforms and architectures that support it. For Archive builds, bitcode will be generated in the linked binary for submission to the App Store. For other builds, the compiler and linker will check whether the code complies with the requirements for bitcode generation, but will not generate actual bitcode.
具体展开一下:
-fembed-bitcode
-fembed-bitcode-marker
, 只是在object文件中做了标记,表明我可以有bitcode,但是现在暂时没有带上它
。因为本地编译调试时并不需要bitcode,只有AppStore需要这玩意儿,去掉这个不必要的步骤,会加快编译速度。-fembed-bitcode
参数,这样任何类型的Build都会带上bitcode接下来看一下 Enable Bitcode 之后,编译出的文件发生了什么变化, 直接在clang的参数中添加 -fembed-bitcode
即可
1
|
|
编译之后可以通过tool工具查看object文件的结构,此时你需要对Mach-O文件有一些基本的了解
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 |
|
或者使用MachOView
可以发现生成的 object 文件中多了两个 Section,分别是 __LLVM,__bitcode
和 __LLVM,__cmdline
,并且otool的输出中给出了这两个section在object文件中的偏移和大小,通过 dd
命令可以很方便地将这两个Section提取出来
1 2 3 4 5 6 7 8 |
|
还有一种更便捷的方式,Xcode 提供的 segedit
命令可以直接将指定的Section导出,只需要给定Section的名字,和上面的命令效果是一样的,并且更为方便
1 2 3 |
|
观察一下导出的文件
1 2 3 4 5 6 7 |
|
不难得出结论:
__LLVM,__bitcode
正是完整的,未经任何加密或者压缩的bitcode文件,通过 -fembed-bitcode
参数,clang把对应的bitcode文件整个嵌入到了object文件中__LLVM,__cmdline
是编译这个文件所用到的参数,如果要通过导出的bitcode重新编译这个object文件,必须带上这些参数
cc1
也就是clang中真正”前端”部分的参数(clang命令其实是整合了各个环节,所以clang一个命令可以从源代码编出可执行文件),所以编译时要带上-cc1
首先, 来测试一下导出的bitcode文件结合cmdline能否编译出正常的object:
1 2 3 4 5 6 |
|
没有任何问题,并且通过内嵌的bitcode编译出的object文件与直接从源代码编译出来的object完全一样!鹅妹子嘤~!
回到遗留的问题:为什么导出的bitcode文件和直接编译的bitcode会不一样?明明编出的object都是一模一样的!这是因为二进制的bitcode文件中还保存了一些与实际代码无关的meta信息。如果能将bitcode转换为文本格式,将能更直观地进行对比。前面已经提到,xcode中并没有附带转换工具,但是我们依然可以通过clang来完成这一操作,还记得前面用过的 -emit-llvm -S
吗?
1
|
|
神奇吧?输入虽然已经是bitcode了,并非源代码,但是clang也能”编译”出LLVM Assembly。其实clang内部是先将输入的文件转换成Module对象,然后再执行对应的处理:
所以完全可以通过clang进行bitcode和LLVM Assembly的相互转换。
现在,可以对比一下前后两次生成的.ll
文件:
1 2 3 4 5 |
|
除了ModuleID,也就是来源的文件名以外,其余部分完全相同,这也就解决了前面的疑虑。
再来回顾一下,前文提到非Archive类型的build,比如直接⌘ + B
,即使开启了bitcode,也不会编出bitcode,那么会产生什么样的文件呢?通过观察编译日志可以看出xcode在此时使用了-fembed-bitcode-marker
这样一个参数,我们来试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
这样的方式编译出的文件结构与-fembed-bitcode
的结果是一样的,唯一的区别就是 __LLVM,__bitcode
和 __LLVM,__cmdline
的内容并没有将实际的bitcode文件和编译参数嵌入进来,取而代之的一个字节的占位符 0x00
已经搞清楚了bitcode是如何嵌入在object文件里的,但是object只是编译过程的中间产物,真正运行的代码是多个object文件经过链接之后的可执行文件,接下来要分析下object中嵌入的bitcode是如何被链接的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
object中的 __LLVM,__bitcode
和 __LLVM,__cmdline
不见了,取而代之的是一个 __LLVM,__bundle
的Section, 通过名字可以基本推断出object中的bitcode被打包在了一起,把它从可执行文件中dump出来一探究竟:
1 2 3 |
|
这个bundle文件是一个xar
格式的压缩包,xar格式包含了一个xml
格式的文件头(TOC),里面用于存放各种文件的基本属性以及一些附加附加信息,可以通过xar命令查看并解压
1 2 3 4 5 6 7 8 9 10 |
|
查看导出的toc.xml
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 39 40 41 42 43 44 45 46 47 |
|
header的结构非常清晰,内容基本包含这些:
__LLVM,__cmdline
的内容从bundle中解压出来的文件,就是object中嵌入的bitcode,通过MD5对比可以看出链接时对bitcode文件自身没有做任何处理。可以注意到,用于编译各个bitcode文件的参数(cmdline)被放进了TOC中文件描述的区域,而TOC中多出了一个部分用于存放链接时所需要的信息和必要的参数,有了这些信息, 我们不难通过bitcode重新编译,并链接出一个新的可执行文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
看!我们成功利用bitcode重新编了一份一模一样的可执行文件出来。
现在可以理解,为什么苹果要强推bitcode了吧?开发者把bitcode提交到App Store Connect之后,如果苹果发布了使用新芯片的iPhone,支持更高效的指令,开发者不需要做任何操作,App Store Connect自己就可以编译出针对新产品优化过的app并通过App Store分发给用户,不需要开发者自己重新打包上架,这样一来苹果的Store生态就不需要依赖开发者的积极性了。
前面已经提到,如果要以bitcode方式上传app,必须在开启bitcode的状态下,进行Archive打包,才会得到带有bitcode的app。大部分app都会依赖一堆第三方sdk,如果此时项目里依赖的某一个或者几个sdk没有开启bitcode,那么很遗憾,Xcode会拒绝编译并给出类似这样的提示:
ld: ‘name_of_the_library_or_framework’ does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target.
ld: bitcode bundle could not be generated because ‘name_of_the_library_or_framework’ was built without full bitcode.
第一种提示表示这个第三方库完全没有开启bitcode,而第二种提示表示它只有bitcode-marker,也就是说它的开发者虽然在工程配置中设置了 Enable Bitcode 为 YES,但并没有以Archive方式编译,可能只是⌘ + B,然后顺手把Products拷贝出来交付了。
遇到这种问题,也需要分两种情况来看:
当使用Archive方式打包出带有bitcode的包时,你会发现这个包里的二进制文件比没有开启bitcode时大出了许多,多出来的其实就是bitcode的体积,并且bitcode的体积,一般要比二进制文件本身还要大出许多
1 2 3 4 5 6 7 |
|
当然,这部分内容并不会导致用户下载到的APP变大,因为用户下载到的代码中只会有机器码,不会包含bitcode。有的项目开启bitcode之后会发现二进制的体积增大到超出了苹果对二进制体积的限制,但是完全不用担心,苹果的限制只是针对__TEXT
段,而嵌入的bitcode是存储在单独的__LLVM
段,不在苹果的限制范围内。
打包出带有bitcode的xcarchive之后,可以导出Development IPA进行上线前的最终测试,或者上传到App Store Connect进行提审上架。进行此类操作时会发现Xcode Organizer中多出了bitcode相关的选项:
导出Development版本时,可以勾选Rebuild from Bitcode
,这时导出会变的很慢,因为Xcode在后台通过bitcode重新编译代码,这样导出的ipa最接近最终用户从AppStore下载的版本,为什么说是接近呢,因为苹果使用的编译器版本很可能和本地Xcode不一样,并且苹果可能在编译时增加额外的优化步骤,这些都会导致苹果编译后的二进制文件跟本地编译的版本产生差异。而如果不勾选此选项,则会直接使用Archive时编译出的二进制代码,并把bitcode从二进制中去除以减小体积。
导出Store版本或者直接进行上传时,默认会勾选Include bitcode for iOS content
,如果不勾选,则跟前面类似,将会去除内嵌的bitcode,直接使用本地编译的二进制代码
勾选后生成的ipa中将会只包含bitcode
,这个ipa是无法重签后安装到设备上进行测试的,因为里面没有任何可执行代码:
__TEXT
和 __DATA
等跟已编译好的二进制相关的内容会被全部去除,但是会保留__LINKEDIT
中的部分信息,其中最重要的就是 LC_UUID
,用于在重编之后能跟原始的符号文件对应起来,如果用户下载经过AppStore重编之后的app发生了Crash,得到的backtrace地址是跟本地编译的版本对应不起来的,需要结合UUID和从App Store Connect下载的dSYM文件才能得到符号化的crash信息。
bitcode不能翻译为字节码(bytecode),显然从字面上看这两个词代表的含义并不等同:字节码是按照字节存取的,一般其控制代码的最小宽度是一个字节(也即8个bits),而bitcode是按位(bit)存取,最大化利用空间。比如用bitcode中使用6-bit characters
来编码只包含字母/数字的字符串
1 2 3 4 5 |
|
在这种编码模式下,4字节的字符串abcd
只用3个字节就可以表示
1 2 3 |
|
完整的编码格式可以参考官方文档LLVM Bitcode File Format
bitcode的格式目前是一直在变化的,并且无法向前兼容,举例来说Xcode8的编译器无法读取并解析xcode9产生的bitcode。
另外苹果的bitcode格式与社区版LLVM的bitcode有一定差异,但苹果并不会及时开源Xcode最新版编译器的代码,所以如果你使用第三方基于社区版LLVM制作的编译器进行开发,不要尝试开启并提交bitcode到App Store Connect,否则会因为App Store Connect解析不了你的bitcode而被拒。
如果一个app同时要支持armv7和arm64两种架构,那么同一个源代码文件将会被编译出两份bitcode,也就是说,在一开始介绍LLVM的那张图中,并不是代表同一份bitcode代码可以直接被编译为不同目标机器的机器码。
LLVM只是统一了中间语言的结构和语法格式,但不能像Java那样,Compile Once & Run Everywhere.
可以通过otool检查二进制文件,网上有很多类似这样的方法:
1
|
|
通过判断是否包含 __LLVM
或者关键字来判断是否支持bitcode,其实这种方式是完全错误的,通过前面的测试可以知道,这种方式区分不了bitcode和bitcode-marker,确定是否包含bitcode,还需要检查otool输出中__LLVM
Segment 的长度,如果长度只有1个字节,则并不能代表真正开启了bitcode:
1 2 3 4 5 6 |
|
从科学严谨的角度来说,无法给出确定的答案,但是这个问题跟“二进制文件是否能反编译出源代码”是一样的道理。编译是一个将源代码一层一层不断低级化的过程,每一层都可能会丢失一些特性,产生不可逆的转换,把源代码编译为bitcode或是二进制机器码是五十步之于百步的关系。在通常情况下,反编译bitcode跟反编译二进制文件比要相对容易一些,但通过bitcode反编译出和源代码语义完全相同的代码,也是几乎不可能的。
另外,从安全的角度考虑,Xcode 引入了 Symbol Hiding
和 Debug info Striping
机制,在链接时,bitcode中所有非导出符号均被隐藏,取而代之的是 __hidden#0_
或者 __ir_hidden#1_
这样的形式,debug信息也只保留了line-table,所有跟文件路径、标识符、导出符号等相关的信息全部都从bitcode中移除,相当于做了一层混淆,防止源代码级别的信息泄露,可谓是煞费苦心。
470 points
1 2 3 4 |
|
I didn’t have enough time to solve this challenge since I’m busy at work. It’s a pity that my team didn’t, neither. But I have to say it’s a very challenging one. Combination of crypto and SQL injection.
It seemed to be a web challenge because the entrance was a website. So let’s start with HTTP requests and responses. In the source code of the page, a path to secret login page was commented.
1 2 3 4 5 6 7 8 9 |
|
The login page set a cookie like this(using httpie)
1 2 3 4 5 6 7 8 9 10 11 |
|
It’s easy to say that the cookie is two parts of base64 encoded string concatenated by a |
.
1
|
|
Different cookies was returned when repeating the same request. Modify the tail of the cookie will got a message Error has occur from decrypt..
, but the head won’t.
Look at the two parts of cookie:
1 2 3 4 5 6 7 |
|
Now I believe it’s a Padding Oracle
Problem. I’ve read about it in Web Security by White Hats (刺总的《白帽子讲Web安全》). Part 1
is the 8 bytes iv
of encryption, and Part 2
, obviously is 8 blocks of encrypted data, with 8 bytes in each block.
Every Block cipher can only deal with a message with fixed length (usually the same length as the key), so plain message is divided into several blocks and each block will be encrypted separately. To avoid data pattern sniffing, a vector is added befor encryption in CBC mode.
1 2 3 4 |
|
Vector of each plain data block is the encrypted data of previous block. The Initial Vector for the first data block is provided additionally.
Length of every block must be exactly the same with the key. In this case, the length is 8 bytes. If there is less than 8 bytes(or just equal to 8 bytes) in the last block, a padding is introduced.
1 2 3 4 5 6 7 8 9 |
|
While decrypting, cipher will check the value of the last byte in the decrypted message. Assume that value is 0x04, then check the value of the last 4 bytes. It will be fine if they all equal to 0x04 and the 4 bytes will be directly removed to recover the original length of plain message. Otherwise a decryption exception occured as I tried above.
We know a bad padding format of the last block will cause exception, so if we craft a fake data which can make the padding match the right format, the data will be accepted by the server without throwing a decryption exception(This does not means it will be completely accepted by server without any other excpetions because the data is totally a mess). At this moment we know the last few bytes in the decrypted message, is one of the padding format.
We’ve got last bytes of plain block and the vector(we craft it), so we can get the last bytes of intermediate value of the corresponding encrypted block by
1
|
|
and then, the real plain block
1
|
|
To make it clear, we can brute force every byte in a block, from the last byte to the first one.
1 2 3 4 5 6 7 8 9 |
|
Start with the first block ea e0 c6 90 3e 55 b4 49
, enumerate the last byte of iv, from 0x00 to 0xFF.
1 2 3 4 5 |
|
visit secure login page with the fake cookie:
1
|
|
got the message Error has occur from decrypt..
continue trying with different iv(this can be done with a piece of script)
1 2 3 4 5 |
|
a different message showed up when trying 0x1f
as the last byte in iv.
1
|
|
BINGO! It means the padding is 0x01 now(not quite), more clearly, the last byte of the plain message is 0x01.
PS: If the second to last byte in the plain message just happen to be 0x02, then the last byte may be 0x02, too. Both 0x01 and 0x02 are valid at this situation. Just change the last 0xff in iv to any other value and try again, which will break the combination of 0x02 0x02
padding (into 0x?? 0x02
). If nothing different with 0xff(no decrypt error occuring), 0x01 is the right answer.
the last byte of intermediate value can be calculated by
1 2 |
|
and then calculate the last byte of original plain message by the original iv
1 2 |
|
The last byte of the first plain block is 0x20
!
Next byte, we need to make the plain message have a value of 0x02 in the last byte, to test the 0x02 0x02
padding. So last byte of iv must be 0x02 (+) 0x1e = 0x1c
Trying like this
1 2 3 4 5 |
|
ff ff ff ff ff ff e4 1c
will make the sense. 0xe4 (+) 0x02 (+) 0xa3 = 0x45
Finally we can get the first block:
1 2 3 4 |
|
Continue with the next block:
1
|
|
Notice that the original vector of this block is the previous enctyped block ea e0 c6 90 3e 55 b4 49
, not the iv.
After all the entire message came out:
1
|
|
We didn’t got the flag but a hint
1 2 |
|
It should be a SQL injection attack.
I dinn’t solve this until the server was shut down. TAT
]]>优酷视频:
Youtube:
硬件环境:Lego NXT 8547, iPhone 4S(需越狱)
软件依赖:LeJOS, BTStack, OpenCV2
还原效率:扫描10~15秒,计算<1秒,还原~1分钟
5年前心血来潮,为了做一个能自动拧魔方的机器人,买了一套乐高8547,按当时的收入算,简直是花了一笔巨款(默默心疼3秒),之后就开始各种折腾。折腾了两天实在受不了那个中看不中用的GUI编程套件,直接刷了LeJOS(Lego Java OS),它提供了Java Runtime,可以愉快地使用Java进行coding。
先是抱Hans Andersson大神的大腿,开始拼Tilted Twister,拼成之后发现完全无法顺利行╮(╯▽╰)╭。主要原因是颜色识别很不准,主要是魔方的橙色块和红色块,通过颜色传感器采集到的颜色值太相近了(采集到的颜色值是8bit,范围是0~255),根本无法有效区分开,外界光线的亮一点或者暗一点都会严重影响识别正确率。并且因为乐高自身的CPU和内存都弱到爆炸,大约需要30s~60s才能计算出一个平均50步的解法,再花约3-5分钟进行还原,速度还没有我自己快。
后来冒出了个想法:何不使用手机摄像头扫描,然后在手机上计算还原步骤再控制机器人还原?这样不仅可以一次性扫描9个色块,也可以利用手机强大的CPU在更短的时间内计算出步骤更少的解法,同时提高准确率与运行效率。
先把颜色传感器拆掉,原来的LEGO的执行程序就不能用了,需要自己来写,只需实现机械控制部分。
机器人有两个Motor,以LEGO的前脸为正面视角:
推(PUSH)
,可以让魔方绕X轴逆时针旋转90°,实现上、前、下、后四个面之间的翻转旋转(ROTATE)
,可以让魔方绕Y轴旋转,实现前,右,后,左四个面之间的翻转抓住(HOLD)
魔方的上面两层,然后底座旋转,可以实现拧(TWIST)
魔方的底面(PS: 魔方公式形如U2 F D' R2 D F2 B2 L2
,每个字母代表顺时针将一个面旋转90°:Up, Bottom, Front, Back, Left, Right. 字母后面跟2
表示拧两次,也就是180°,跟'
表示逆时针90°。)
因此,需要两个循环队列来保存魔方在X轴和Y轴方向上的状态:pushChain的第一个元素是朝下的面,第二个元素就是通过一次PUSH操作,会翻转到朝下的面,也就是朝前的面,以此类推。每次旋转或者翻转操作,都需要同步更新两个队列的状态
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
|
之后就可以将任意面,通过不超过两步的操作翻转到底面,例如,可以通过两次PUSH
将顶面翻转到底面,也可以通过底座顺时针旋转90°后,再PUSH
将左侧的面翻转到底面:
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 |
|
拧(TWIST)
的动作可以通过HOLD->ROTATE->RELEASE
的步骤实现
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 |
|
至此,输入公式,机器人就可以一步步操作了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
下面的视频是当时留下的一个DEMO,事先向LEGO输入了20步的还原公式,LEGO只管按照公式拧:
LEGO支持蓝牙通信,因此可以用手机做主控端,整个系统的构思如下:
手里有个Android手机(KTouch-650),还有一部iPod Touch 4,虽然Android机性能有点差,但我那时候完全不懂Android和iOS开发。好在Java技能是游刃有余的,Android开发可以快速上手,也就不得不选择Android了。蓝牙连接和还原算法都好办,虽然LeJOS官方没有提供Android与LEGO通信的SDK,但是完全可以仿照PC的SDK实现一套,将蓝牙相关的实现替换为android.bluetooth包提供的实现即可。网上已经有大神给出了源码。还原算法可以直接采用Java实现的Two-Phase算法twophase.jar。
那么问题来了,怎么检测魔方在摄像头采集的画面中的位置,或者说怎么确定采集哪些像素点的颜色?最笨的办法就是——固定位置,为此,在取景界面上绘制了一个9宫格作为参考线,方便手动对齐:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
大概长成这个样子:
手动将魔方与九宫格参考线对齐之后,点击屏幕任意位置开始取色。取色的时候需要取中心区域的多个点,然后计算平均色值,避免单个点的色值误差太大。这里遇到一个问题是Android摄像头的预览图像数据是YUV色彩空间,而不是RGB,需要先进行一次转换:
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
转换到RGB就可以正常取色,取色完毕之后就需要根据颜色来计算魔方的状态了,然后转换为U、B、D、F、R、L的形式表示。我采取的算法比较简单,先确定每个面中心块的颜色,做为该面的颜色(因为魔方不管怎么转,一个面的中心块,是不会跑到其他面的),然后拿每个块的颜色分别与六个中间块的颜色对比,计算在RGB分量上的差值,差值最小的中心块的颜色,就是当前色块的颜色,也就是说这个块还原后和这个中心块在同一个面。差值计算方式采用RGB分量的差平方之和,代码节选如下:
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 |
|
然而,实际测试下来发现,这样的算法经常会有计算错误的时候,光线比较暗的情况下,摄像头取到的24bit的橙色和红色,依然很接近,用肉眼都很难区分。
还有另外一个坑,采用twophase.jar这个lib,需要约30~60M的内存,用于构建搜索树,这在当时的Android机上(至少在我的破手机上),是不可能的事情,只好作罢,方案不得不改成:
如此繁琐,颜色识别的准确率又不美丽,我不禁开始思考人生。。。
期间考虑过换用iOS设备,但是iOS设备的蓝牙不支持RFCOMM通讯协议,也就作罢。
结果是,机器人被供了起来,5年里,跟着我搬了4次家。。。
后来慢慢接触到了iOS越狱开发,也知道了BTStack这个开源库,可以通过直接操作底层接口,让越狱的iOS设备实现RFCOMM等官方蓝牙SDK不支持的协议。看了一眼躺在书橱里吃灰的LEGO机器人,想着是时候拿出来晒晒太阳了 ^.^
先clone了BTStack的源码,编译的时候遇到很多错误,iOS部分的工程结构本身就有很多问题,还有很多符号找不到。后来chekcout了v0.9分支,发现master的工程结构跟v0.9的完全不一样,但是两个分支里iOS的文件夹别无二致,缺少的符号在v0.9分支里都有,可能是长时间没有维护iOS版本的库了把,最终使用v0.9分支成功编译。其实也可以直接在Cidya里安装BTStack,然后将libBTStack.dylib从手机里拷出来。
接下来要实现RFCOMM通讯功能,坑爹的是,BTStack给出的demo中使用的是L2CAP协议,同时,BTstackManager类中只有对底层协议的封装,没有对RFCOMM/L2CAP等高层数据传输协议进行封装,留了接口,但是方法体是空的,只好自己动手了:
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
RFCOMM部分的代码实现完毕,尝试发送纯文本数据,结果LEGO根本毫无反应o(╯□╰)o。仔细阅读了LeJOS蓝牙部分的源码,发现数据包的头部,被添加了两个字节(Big-endian)用于标识数据包的大小:
1 2 3 4 5 6 7 8 |
|
这下就好办了,发送数据之前,先处理一下数据包,同样在头部插入两个字节的长度即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
深夜3点,测试一下RFCOMM通信
真是一把辛酸泪。。。
之前Android控制器的扫描部分存在两个遗留问题,一是无法自动采集魔方的颜色,二是对颜色的识别仍然存在不准的情况。这两个问题不解决,始终是心里的一个疙瘩。
如何自动采集魔方的颜色呢?如果能检测到魔方的位置,一切就好办了。那么问题转换成如何检测摄像头的画面中是否有魔方,以及魔方在画面中的位置。我先考虑了一种方案:
怎么检测直线呢?经过一番Google,找到了基于iOS开源图像处理框架GPUImage的一种算法。事实证明,我还是too young, too naive:
不管如何调整检测参数,直线的数量都超乎我的想象,更何况,我根本想不出一种算法去判断他们是不是能组成九宫格的图案。。。
后来就到处扒图像识别相关的资料,发现了基于OpenCV的一个很有意思的Demo,可以识别图像中的正方形,新的识别方案就此诞生
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
|
是不是很机智!当然,这个算法也会有傻逼的时候:
解决类似的问题需要再检测一下每个正方形之间的间距。不过,如果放在魔方机器人的底座上进行扫描,周围应该没有什么干扰项,多增加一部分计算的代码反而会影响画面的刷新率,也就无所谓了。
扫描完魔方的54个块之后,就需要对每个块的颜色进行识别分组,之前的算法是计算每个颜色与6个基准色(也就是每个面中心块的颜色)的色差,仍然会存在不准的情况,这次我扫描记录了大量颜色数据,从中分析出以下特征:
其实这样的比较方式,在昏暗的光线下,红色与橙色仍然非常相近,但根据测试,错误率大概不到3%,还算可以接受。因此,只需要对54个颜色进行多次不同维度的排序,就可以识别出正确的颜色
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 |
|
PS: convertTable
是为了将输出的颜色顺序转换为还原算法所需的顺序。
PPS: 输出的颜色是用该颜色所属的面表示的。
Two-Phase算法,也有pure C的版本,但是这个算法内存占用奇高,且计算出的还原步骤一般都要22步,甚至更多,如果要计算20步以内(上帝之数是20,也就是说任意魔方都可以用不超过20步进行还原)的解法,要花上数分钟的时间。因此我又花了大量的时间寻找性能更好的算法,最终找到两个:
很显然,这个没有名字的算法正合我意。经过一番调整和优化,这个算法顺利地在iOS上跑了起来。
将这三部分整合起来,就是文章最开始的那个视频的样子,历经千辛万苦,终于实现了最初设计的那套方案。
Some websites can provide proxy IPs, but none of them can ensure the healthy of those proxy hosts. It’s a horrible thing to check them one by one by hand when you wanna got one. So we can crawl these websites and test every proxy IP automatically.
The project is hosted at https://github.com/xelzmm/proxy_server_crawler.
Proxy Server Crawler is a tool used to crawl public proxy servers from proxy websites. When crawled a proxy server(ip::port::type), it will test the functionality of the server automatically.
Currently supported websites:
Currently supported testing(for http proxy)
1 2 |
|
[log]
1 2 3 4 5 6 7 8 9 10 |
|
The MIT License (MIT)
]]>念大婶在博客中介绍了两种方法,用于保护代码逻辑,对抗逆向分析
如果用了第二种方式,将函数改用c实现,虽然通过class-dump
得不到有价值的信息,但通过nm
命令或者IDA/Hopper
等工具仍然能从符号表中找到这些c函数以及衍生出的一些静态变量。针对这种情况,我们还是可以通过宏定义的方式,将这些c的标识符(函数名、变量名)替换为随机字符串。
举个例子:
1 2 3 4 5 6 7 8 9 |
|
nm
检查符号表,结果如下
1 2 3 |
|
说明宏替换对于c的标识符同样有效。但是要一个个手动去define,感觉是要累死的节奏。如果能通过一个脚本,自动从源代码里把所有的标识符声明提取出来,生成一个头文件就好了。可以考虑几种方案:
很显然,前两种方案都很繁琐,不好维护。并且如果我要做一个library给第三方使用,必然要暴露一些接口不能被混淆,只有第三种方式可以灵活地选择那些需要混淆哪些不需要,而这种方案实现起来也最简单。最终实现如下:
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 |
|
使用asm label
语法的好处是,只需要将符号的声明标记出来进行替换即可, 不需要对该符号的引用进行标记和替换。如果要混淆已经完成的代码,这一点非常省时省力。
扫描源代码并生成混淆头文件的脚本:
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
|
测试一下效果:
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 |
|
如果你有点懵,可以看一下混淆的过程是怎样的
1 2 3 4 5 6 7 8 9 |
|
asm label
的语法解释,可以参考gcc的onlinedocs
字符串也是逆向分析的一大切入点,可以根据目标字符串快速定位目标代码,有针对性地进行调试、分析。在binary中隐藏字符串可以有效提升静态分析的难度,因此需要在源代码中将字符串进行加密,运行时先解密后再使用。但如果在源代码中直接写加密后的字符串,代码的可读性就会变得非常差。
但字符串无法像标识符那样,在预编译阶段直接通过几个宏就替换为加密的形式。我想了一个不是很优雅,但是很有效的方法:
decrypt("密文")
的形式decrypt
函数的实现(或者事先在源代码中写好)示例,混淆这份代码中的字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
反编译的结果
字符串混淆脚本,字符串加密选用简单的抑或,仅为示例
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
执行字符串混淆脚本,源代码变为:
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 |
|
测试一下效果
1 2 3 4 5 |
|
反编译一下,已经隐藏了字符串特征
__cstring
中也看不到原始的字符串,连混淆后的字符串也看不到
说明:
如果把字符串"Hello"
转化为char[]{'H','e','l','l','o',0}
的形式进行编译,字符串就会从__cstring
中的明文字符,变为__text
中的一段代码,可以防止被搜索到。因此如果要兼顾执行效率和混淆的效果,只需要把字符串转换成char数组的形式就可以了,不需要再添加解密的步骤。
密室惊魂Online
这个桌游是一个曾经开桌游店的同事送的,觉得这个游戏很有意思,但是一般需要5-8人,线下组局太难了。加入官方QQ群之后发现大家会在QQ群里通过建讨论组的方式来在线组局。此种方式需要一个MC主持游戏,每个人的选择和行动都是私聊给MC,MC通过PS软件绘制游戏地图,并通过各种图层来控制元素的堆放和移动,然后截图发在讨论组里。不禁感慨玩家的智慧是多么的强大。
后来我就在想,何不用程序来代替MC和PS呢?于是这个项目就诞生了。
托管地址:https://github.com/xelzmm/danmned
开发语言:nodejs
相关技术:ejs, websocket, css3
游戏截图
密室惊魂是一款运用语言和推理逻辑,结合区域行动策略的版图桌面游戏。 游戏讲述一群中毒的受害者被奸徒困在一个完美密室里,大家需要在有限的时间内互相交流合作,寻找线索破解迷题,最终突破奸徒的阻挠找到出口逃生。该款游戏的特色是将玩家的语言交流和实际行动相结合,是一个考验玩家之间合作和显示玩家智慧的新型语言推理类桌面游戏。
游戏人数: 5到9人,已支持3-4人的mini模式
游戏时长: 约60至90分钟
游戏类型: 语言推理类
网杀地址: http://msjh.aliapp.com/ (已失效) http://msjh.shinemarketing.cn
随机
,安全房间只有一个(1号-12号房间内随机)受害者
目标:解除身上的剧毒,并通过获取线索找出安全房间逃生
逃生
定义:最后一回合时,已解毒,并处于安全房间内受害者总人数-2
完成逃生,即视作受害者团体胜利奸徒
目标:阻止受害者逃生,受害者失败即为奸徒胜利
逃生
,逃生与否,不影响奸徒胜利EX受害者
目标:与受害者一起逃生或者单独逃生
只有
EX逃生时,EX单独胜利SP暗警
目标:解除身上的剧毒,并通过获取线索找出安全房间逃生
破坏
卡),如若此做
EX受害者
/SP暗警
/奸徒
,人数越多几率越大13号房间危险
一级线索卡,用于增加难度大厅危险
一级线索卡,用于增加难度毒雾
功能,用于增加难度黑色房间危险
二级线索卡,用于增加难度密室法则
依次发言70秒
,后续每回合增加10秒
15秒
无任何输入动作将自动超时结束任意发言
/xs
可以输出自己的线索标记状态over
字样,或者提交空发言
,将提前结束发言索要
或者赠予
条件(自己没有/有钥匙,同房间内有其他有/没有钥匙的玩家),可在此阶段发动索要/赠予
按钮自己发言完毕时
询问目标玩家是否同意/接受15秒
,超时自动拒绝(索要)/接受(赠予)抢钥匙
的按钮当前房间内所有玩家
都发言完毕后进行投票
谁
的钥匙,再投票抢钥匙
15秒
,超时自动弃权
点击线索区域
进行标记密室法则
依次移动,每个玩家限时30秒
自动移动
,在满足规则前提下完全随机(不会上锁)点击要去的房间
,自动计算可选路径及上锁/解锁
方案有钥匙
的前提下点击自己所在房间
,可以停留
必须移动
,不得停留在原房间玩家身份
及安全房间
,游戏结束密室法则
,决定房间功能执行者,并执行房间功能密室法则
:每个房间内,只有一个人
可以执行房间功能让过
执行权给房间内下一位玩家(若有)跳过
房间功能执行钥匙
,则按照房间L/S属性,没有钥匙的最大/小号玩家获得钥匙行动
:拆弹/升级/降级15秒
配合
还是破坏
破坏
,则行动失败
破坏
,即奸徒
与SP暗警
都破坏
,视作SP暗警
破坏了奸徒
的行动,炸弹直接被解除,游戏增加一回合获得房间功能执行权的玩家
大厅
毒雾
中毒
,经过
毒雾大厅,不会中毒绿色
变为红色
毒雾
会在第6回合发言阶段之前散去,停留不再中毒治疗
房间
解毒
(逃生的必要条件)红色
变为绿色
线索
房间
【1】级线索卡
,获得后,自动标记线索区销毁线索卡
(销毁后不能立刻获得新的线索卡)一张线索卡
(无论等级)监视
房间
不在监视房
的玩家手中的线索卡任意监视房
内的其他玩家的线索卡升级
房间 / 降级
房间
同房间内
的两张
低(高)等级线索卡合成为一张高(低)等级线索卡行动
步骤1 + 1 = 2
, 1 + 2 = 3
, 其他不可升级3 - 1 = 2
, 3 - 2 = 1
, 2 - 1 = 1
, 其他不可降级询问
与谁合成房间功能执行者
所有,原线索卡消失
拆弹
房间:两个拆弹房间
合力进行拆弹
各至少1人
,才可发起拆弹总人数
大于等于以下标准,才可发起拆弹所有玩家
,进入行动
步骤受害者
及EX受害者
只能配合
,奸徒
和SP暗警
可以破坏
增加一回合
永久破坏
,不可再次拆弹15秒
,规划下一阶段的发言/行动剩余线索卡
张数、下一轮拆弹(如果可以)所需人数下一轮
3-4人
游戏时自动开启mini模式
1、5、8、12号
房间拆弹
房间、降级
房间钥匙
及锁
功能3级线索卡
EX受害者
、SP暗警
8
逃生人数 >= 受害者总人数 - 1
个人线索区
只能自己看到
,并不与其他玩家共享自行点击线索区
标记/xs
,可以自动输出自己的线索区标记状态至发言区自动重连
,请不要关闭或者刷新游戏页面,否则游戏将直接结束。路由器(包括无线路由器),是一种连接两个以上不同的网络、具备网络间数据包转发功能的专用网络设备。它基于IP协议,工作在网络层(OSI七层模型的第3层)。路由器的主要功能是将不同的网络连接起来,让它们相互间能够进行数据交换,并将局域网内不必要的广播流量精确地发送到目标主机。有很多制造路由器的厂商,简单列举一些:Cisco(思科),Linksys,Juniper,Netgear(网件),Nortel (北电),Redback,Lucent(朗讯),3Com,HP(惠普),Dlink,Belkin(贝尔金)等。
有一些网络技能认证考试(如CCNA,CCNP,JNCIA,JNCIE)会考你区分网络设备的能力。这篇关于路由器的文章主要会解释如何辨别路由器,路由器有哪些功能(当然不包含某些厂商特有的技术)。
我们用普通家用无线宽带路由器举一个非常简单的例子
这个过程对于大部分路由器都适用。
PS: 稍微解释下,家用宽带路由器以及无线路由器同样肩负着“代理”的任务,它们会把IP数据包中的源IP改成路由器自己的IP,这一点跟运营商级别的路由器是不同的。
路由器可以把不同的网络连接起来,并且实现以下功能,希望你看过之后能在网络认证考试中正确地描述路由器:
网络(尤其是以太网)在物理层、数据链路层和网络层是通过广播方式进行通信的。网络层的广播是指通过网络层协议(一般是IP或者IPX协议)将数据流量发送到网络上所有的主机。网络广播是用来传输特定的数据包(如ARP, RARP, DHCP, IPX-SAP等)从而使网络能够正常运转。因为有部分网络设备会尝试同时传输数据因而产生冲突,所以最好能通过交换机或者路由器把一个超大集群划分成不同的广播域。
随着网络中主机数量的不断增长,广播数量也会随之增多。如果网络上涌现了足够多的广播流量,网络上的正常通信将会变得非常困难。
为了减少广播的数量,网络管理员可以将一个包含大量主机的网络集群划分成两个小的网络。广播流量将被限制在各自的网络内部,而此时路由器则承担了连接互通两个网络的『默认网关』的作用。
现如今的网络环境下,人们需要将电脑接入互联网。当你的电脑想跟另一个网络下的某一台电脑通信时,你的数据就会被发送给你的默认网关。默认网关是一台连接了和你的电脑同样网络的路由器。这台路由器作为默认网关,接收你的数据,寻找远端主机的地址并作出路由选择。根据路由的选择,默认网关会把你的数据转发给理远端主机更近的另一个网络设备。这中间可能会经过很多路由器,所以会有很多路由器参与处理你的数据包,就像消防队员救火时的水桶接力。
路由器具有将数据包从一个网络传送到另一个网络的能力。这使两个不同组织管理的不同网络之间的数据交换成为可能。他们可以在路由器之间创建一个中间网络并在这个网络上交换数据。因为路由器可以从任意接入的网络接收数据,并将其转发到其他网络,因此它也可以让不能正常通信的两个网络交换数据。技术角度来说,有了路由器,一个令牌环网络可以通过一个串行网络跟一个以太网进行通信。
路由器只有在使用诸如RIP,OSPF,EIGRP,IS-IS或者BGP等路由协议时,才会动态地学习路由并公布。否则路由器只能被手动设定,这种路由方式也被称作静态路由。
路由器是『逐跳』传送数据的,就像『烫手的山芋』一跳接一跳。如果经过一系列路由器,数据还是没有到达目标主机,而是转了一圈回到某一跳路由节点,这杯称作路由环路。数据包在路由环路中一直传递直到生命周期结束:到达TTL上限。TTL是IP数据报文头部中的一个计数器。TTL的值会随着数据包在每一跳路由传递而递减,最终如果变成0就会被丢弃。
路由器其实是特殊的计算机,因此和其他计算器有着相同的组成:
中央处理器:运行路由器操作系统,例如Juniper路由器运行着JunOS,Cisco路由器运行着Cisco IOS(Nexus OS)。操作系统管理者路由器的各个组件,并提供路由器运行所必须的功能逻辑。
闪存:存储着操作系统,类似你电脑里的硬盘。如果你的电脑使用了SSD,那么它就用了闪存。(译注:SSD是闪存组成的)
非易失性内存:这是一块额外的空间,用来存储系统的备份或者原始操作系统。路由器会从这里开始引导,并加载所有的程序。
内存:当路由器启动时,操作系统已经被加载进了内存。一旦路由器完成了启动,就开始计算自身的路由,如果配置了RIP(v1及v2),OSPF, EIGRP, IS-IS或者BGP等路由协议,也会从其他路由器学习路由。内存也用于缓存ARP表,路由表,路由距离以及其他可以加快路由转发的数据。
网络接口:路由器总是拥有许多网络接口(网卡)。操作系统中包含了可以控制这些网络接口的驱动程序。路由器会在启动的时候获知每个接口配置了什么样的网络。之后它们会从自身连接的其他路由器那里学习路由,并学习将数据包传送到某个远端网络需要通过哪个接口。
控制台:最后,很重要的一点,就是控制台。在以往的时间里,管理和配置路由器都是在每个设备上的控制台里进行,比如问题排查和错误诊断。网络认证考试中会包含非常多关于配置和排查问题的控制台命令。然而路由器的生产厂商正在迅速地淘汰设备上的独立控制台,转而建立中心化的管理系统用以管理大量的网络设备。
]]>I usually use linode to visit Google services via ssh tunnel. but recently I always got captchas even Sorry...
page.
Finally I knows that google banned ipv6 traffics from linode which they treated as robots.
disable ipv6 of linode
append lines below to /etc/sysctl.conf
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
net.ipv6.conf.lo.disable_ipv6=1
then restart network /etc/init.d/networking restart
or reboot
I’ve hidden the game name so that challengers could not find here by some searching work.
If you guys are about to cheat by this, get lost now.
You can find the game at [url]c-a-n-y-o-u-h-a-c-k.i-t
(replace the dash with nothing)
Try to figure out by yourself, if you are really really really stucked, have a sight for some hints.
password is just password
It’s a kind of pun. If you cannot guess the riddle, just answer no
.
Acturually the answer is Nitric Oxide, as known as NO
Inspect the source code, you will find the password in comment.
Fibonacci Prime
prime(n) = 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271 …
fibonacci(n) = 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144 …
1
|
|
password is javascript
Run this code in javasript console, then check the value of variable password
.
1 2 3 4 5 6 |
|
Run this code in javasript console, then check the value of variable password
.
1 2 3 4 5 6 |
|
The Salad Cipher, aka ROT13
Decryption Key
A|B|C|D|E|F|G|H|I|J|K|L|M
-------------------------
N|O|P|Q|R|S|T|U|V|W|X|Y|Z
letter above equals below, and vice versa
Try to combine some words using the numbers with T9 IME on a mobile phone.
1 ’ | 2 ABC | 3 DEF |
4 GHI | 5 JKL | 6 MNO |
7 PQRS | 8 TUV | 9 WXYZ |
Base64 decode it.
Caesar’s Square
TSDLN ILHSY OGSRE WOOFR OPOUK OAAAR RIRID
Count the number of letters, here we have 35 We can put 35 into 5 rows of 7
TSDLNIL
HSYOGSR
EWOOFRO
POUKOAA
ARRIRID
Read it, downwards from the top left, then the next column.
Morse Alphabet
ASCII
1 2 |
|
Atbash (similar with the Salad Cipher)
A|B|C|D|E|F|G|H|I|J|K|L|M
-------------------------
Z|Y|X|W|V|U|T|S|R|Q|P|O|N
letter above equals below, and vice versa
in another way
Plain: ABCDEFGHIJKLMNOPQRSTUVWXYZ
Cipher: ZYXWVUTSRQPONMLKJIHGFEDCBA
Polybius Square
\ | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | A | B | C | D | E |
2 | F | G | H | I | K |
3 | L | M | N | O | P |
4 | Q | R | S | T | U |
5 | V | W | X | Y | Z |
Each letter is then represented by its coordinates in the grid. For example, BAT
becomes 12 11 44
. Because 26 characters do not quite fit in a square, it is rounded down to the next lowest square number by combining two letters (usually I and J).
The index of a letter in the alphabt
. 0 indicates a blank.
A programming language named BrainFuck.
Read it in a human readable way. Starting with the top left T
, then H
under it, and then E
on the right side. Hints in the title clues
.
MD5 ,brute force it with the hint a-z*6
, or try cmd5.org.
Static crypto table with a reverse. the crypto table can be easily dumped.
e7 a4 90 71 36 49 aa e6 5b 3a ef 64 a0 be eb 09 f2 8c 57 ec 8f 74 1f 01 51 98
Z Y X W V U T S R Q P O N M L K J I H G F E D C B A
91 72 61 3f 69 fe 4b fa 85 fd 14 68 73 26 0f ac cc a1 4d db ab 43 46 11 08 b7
z y x w v u t s r q p o n m l k j i h g f e d c b a
d8 b0 31 07 cf 8e 45 24 0b 5a
0 9 8 7 6 5 4 3 2 1
92 35 00 c6 3d 55 96 54 7d f6 e9
) ( * & ^ % $ # @ !
cb d9 21 3e af 38 8b 4e 9e ea 0a 4c 04 58 6d b6 67 29 13 c5
? > < " : | } { + _ / . , ' ; \ ] [ = -
Braille Alphabet
Page=Admin
1 2 |
|
/robots.txt
1
|
|
Do not waste time on the form because nothing happend when you click the button.
SESSION=abf3e2d32ec32' or '1'='1' --
look around http://theurl/Content/Challenges/Web/Files6/
curl -d 'Type=admin' 'http://theurl/Content/Challenges/Web/Web7.php'
Page[]=Home
will trigger a php fatal error
, which will display the error stack including the full path of the file in the page.
File=Files9/passconfigs.php%00
1
|
|
Try to find something in the terminal
1 2 3 4 5 |
|
Then an open port of a alive host which may be the remote camera. Open it in Firefox and then successfully we can get the CCTV admin page.
Try to login with someone’s name as the password.
]]>GitHub: https://github.com/xelzmm/Love
]]>First of all, we got a secrets
link and log in or create user
form. When we create and login, the website redirect us to the ‘secrets’ page like this
Secrets
name owner actions
key admin show
nothing asdf show
we’ve got some links to see secrets owned by other users, include the admin
, or easily add a new secret ourselves.
Having a try to open the admin’s secret, we got a 500 Error Page with some error stack, which powered by the Ruby framework Sinatra
.
From the very first sight of the page, it said unauthorized
as the error message and a piece of source code was provided
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
It meant that I’m not the secret’s holder. then have a look at the whole page, and you would find some environment variable in the Rack ENV
section, partly like
1
|
|
1
|
|
1
|
|
1
|
|
we’ve got some message:
the cookie rack.session
is some way encoded of rack.session.unpacked_cookie_data
, which is totally the same as env variable rack.session, and the coder mybe Rack::Session::Cookie::Base64::Marshal
, secret (if any) maybe wroashsoxDiculReejLykUssyifabEdGhovHabno
By seeking the source code of rack, we found this https://github.com/rack/
1 2 3 4 5 6 7 8 9 10 |
|
Once the server received a request, it would confirm the validation of the cookie, reset the session if digest mismatch
1 2 3 4 |
|
Meanwhile, we knew the whole process of the session checking. thus, I’ve wrote a ruby script to figure out this stuff with this way
unpack(decode)
the cookie to origin session dataadmin
repack(encode)
the session data to cookie string format1 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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
run the script like this:
1 2 3 |
|
then modify the cookie with the new value, using any tool you like such as Firebug(for Firefox), WebInspector(for Webkit Based Browser), Fiddler(under IE7), Burpsuite(Java Based for any platform), I’d like to use the Javascript Console in Chrome:
1
|
|
refresh the page, and enjoy :)
]]>watch out for this Etdeksogav
message 1: QUVTLTI1NiBFQ0IgbW9kZSB0d2ljZSwgdHdvIGtleXM=
encrypted: THbpB4bE82Rq35khemTQ10ntxZ8sf7s2WK8ErwcdDEc=
message 2: RWFjaCBrZXkgemVybyB1bnRpbCBsYXN0IDI0IGJpdHM=
encrypted: 01YZbSrta2N+1pOeQppmPETzoT/Yqb816yGlyceuEOE=
ciphertext: s5hd0ThTkv1U44r9aRyUhaX5qJe561MZ16071nlvM9U=
看到最后的等号首先就想到了base64编码,decode之后得到
message1: AES-256 ECB mode twice, two keys
message2: Each key zero until last 24 bits
两轮AES-256加密,padding=ECB,key不一样,但是前面都是0x00,只有最后24位需要破解
密文都是2进制不可读,不贴了
题目提示了是256位(32字节的key),前29个字节都是0,需要破解两个key的后3个字节,纯暴力方式需要尝试224 * 224 = 248 ≈ 2.81e14种可能,这么大的计算量,显然是不现实的。
暴力破解,估计要用到hadoop集群了。
其实,当时忽略了一个细节,就是题目:MITM,google一下出来的都是Man-in-the-middle Attack(中间人攻击),似乎跟这个题目半毛钱关系都没有,换用wikipedia得到了我们想要的东西:
很显然,Meet-in-the-middle attack应该就是我们想找的东西了
Assume the attacker knows a set of plaintext P and ciphertext C that satisfies the following:
- C=ENCk2(ENCk1(P))
- P=DECk1(DECk2©
where ENC is the encryption function, DEC the decryption function defined as ENC-1 (inverse mapping) and k1 and k2 are two keys.
The attacker can then compute ENCk1(P) for all possible keys k1. Afterwards he can decrypt the ciphertext by computing DECk2© for each k2. Any matches between these two resulting sets are likely to reveal the correct keys. (To speed up the comparison, the ENCk1(P) set can be stored in an in-memory lookup table, then each DECk2© can be matched against the values in the lookup table to find the candidate keys)
这个模型跟题目所设的是完全一样的,思路给的很清楚了,先穷举key1,计算出明文经过所有可能的key1加密后的结果,将结果存于内存中,然后穷举key2,计算密文经过key2解密后的结果,与内存中的结果集进行比对(因为AES是对称加密,加密跟解密是用的相同的key),如果有一致的,就表明破解成功了,这样算起来,时间复杂度只有224 + 224 = 225
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 39 40 41 42 43 44 |
|
大概5分钟左右就跑完了,缓存key1的加密结果用了1.65G内存,如果内存不够,可以对key1分段跑,不过时间就要相应变长。
key1:
\x9a\xe8\x07
key2:
\xff?E
message is:
]]>This time I didn’t include sol’n