听说在 macOS 上使用自带的 codesign 来移除应用程序签名的话,会使得 MachO 文件变得不正常,于是就花了点时间来看看 codesign 相关的源代码。虽然有第三方的工具,自己也可以通过简单粗暴的改 MachO 文件来解决,但是探究问题具体出在哪里也很有趣。那么这里将以 Reeder 为例子(其实存在感不强233)讲述。
同样的,在前面我也列出目录,你可以直接跳到解决方案的部分。
- 0x01 - codesign.cpp
- 0x02 - cs_sign.cpp
- 0x03 - SecCodeSigner.cpp
- 0x04 - codesign_allocate.c
- 0x05 - writeout.c
- 0x06 - 解决方案
- 0x07 - Xcode 自带的 codesign_allocate 到底对 MachO 做了什么
0x01 - codesign.cpp
首先,既然我们是调用的 codesign --remove-signature,好在 Apple 是非常开放的,我们可以在 opensource.apple.com 获得 codesign 的源代码,codesign.cpp。不过最新的 codesign 似乎不在 security_systemkeychain 里面了。总之先从 codesign 的源代码开始看起吧。
和我们去除代码签名相关的部分则是
const struct option options[] = { ... { "remove-signature", no_argument,NULL, optRemoveSignature }, ... } int main(int argc, char *argv[]) { ... case optRemoveSignature: signerName = NULL; operation = doSign; // well, un-sign break; ... } ... switch (operation) { case doSign: prepareToSign(); break; ... } ... switch (operation) { case doSign: sign(target); break; ... } }
嗯,很清晰的结构,如果是调用的 codesign --remove-signature MachOFilePath 的话,就将 operation 记为 doSign (well, un-sign, 233),然后就是准备[去掉]签名,最后[去掉]签名。唯一的麻烦就是这里的 prepareToSign() 和 sign() 函数都不是在 codesign.cpp 里的。
0x02 - cs_sign.cpp
于是查看 codesign.cpp 所在的目录 security_systemkeychain -55202/src,发现对应的函数是在 cs_sign.cpp 里面。
里面检查了不少参数,但是我们重点关注的是
static CFMutableDictionaryRef parameters;// common signing parameters static SecCodeSignerRef signerRef;// global signer object void prepareToSign() { ... if (signer) CFDictionaryAddValue(parameters, kSecCodeSignerIdentity, signer); else flags |= kSecCSRemoveSignature; ... MacOSError::check(SecCodeSignerCreate(parameters, flags, &signerRef)); ... } // // Sign a code object. // void sign(const char *target) { ... // add target-specific signing inputs, mostly carried from a prior signature CFCopyRef<SecCodeSignerRef> currentSigner = signerRef;// the one we prepared during setup // do the deed ErrorCheck check; check(SecCodeSignerAddSignatureWithErrors(currentSigner, code, kSecCSDefaultFlags, check)); ... }
一个就是 SecCodeSignerCreate, 另一个是 SecCodeSignerAddSignatureWithErrors。
0x03 - SecCodeSigner.cpp
从命名上想必也能猜到它们是 Security.framework 中的函数,于是跳到 Security.framework 的代码里,它们都在 SecCodeSigner.cpp 里。
SecCodeSignerCreate 检查参数,然后创建了一个 SecCodeSigner 的对象,用于 CodeSigning,这里我们正好是要去掉代码签名,所以 SecCodeSigner 初始化时没有做更多的工作。
接下来在 sign() 函数中,我们关心的是 SecCodeSignerAddSignatureWithErrors。它也很简单,就直接传入刚才的 SecCodeSigner 的引用,然后让它签名。接下来便是
// // Remove any existing code signature from code // void SecCodeSigner::Signer::remove(SecCSFlags flags) { // can't remove a detached signature if (state.mDetached) MacOSError::throwMe(errSecCSNotSupported); rep = code->diskRep(); if (Universal *fat = state.mNoMachO ? NULL : rep->mainExecutableImage()) { // architecture-sensitive removal MachOEditor editor(rep->writer(), *fat, digestAlgorithms(), rep->mainExecutablePath()); editor.allocate();// create copy editor.commit();// commit change } else { // architecture-agnostic removal RefPointer<DiskRep::Writer> writer = rep->writer(); writer->remove(); writer->flush(); } }
于是又来到了
void MachOEditor::allocate() { // note that we may have a temporary file from now on (for cleanup in the error case) mTempMayExist = true; // run codesign_allocate to make room in the executable file fork(); wait(); if (!Child::succeeded()) MacOSError::throwMe(errSecCSHelperFailed); // open the new (temporary) Universal file { UidGuard guard(0); mFd.open(tempPath, O_RDWR); } mNewCode = new Universal(mFd); }
这里居然 fork() 了,于是查看 Child 类,终于看到了尽头的样子
void MachOEditor::childAction() { vector<const char *> arguments; arguments.push_back(helperName); arguments.push_back("-i"); arguments.push_back(sourcePath.c_str()); arguments.push_back("-o"); arguments.push_back(tempPath.c_str()); for (Iterator it = architecture.begin(); it != architecture.end(); ++it) { size_t size = LowLevelMemoryUtilities::alignUp(it->second->blobSize, csAlign); char *ssize;// we'll leak this (execv is coming soon) asprintf(&ssize, "%zd", size); if (const char *arch = it->first.name()) { CODESIGN_ALLOCATE_ARCH((char*)arch, (unsigned int)size); arguments.push_back("-a"); arguments.push_back(arch); } else { CODESIGN_ALLOCATE_ARCHN(it->first.cpuType(), it->first.cpuSubtype(), (unsigned int)size); arguments.push_back("-A"); char *anum; asprintf(&anum, "%d", it->first.cpuType()); arguments.push_back(anum); asprintf(&anum, "%d", it->first.cpuSubtype()); arguments.push_back(anum); } arguments.push_back(ssize); } arguments.push_back(NULL); if (mHelperOverridden) ::csops(0, CS_OPS_MARKKILL, NULL, 0);// force code integrity (void)::seteuid(0);// activate privilege if caller has it; ignore error if not execv(mHelperPath, (char * const *)&arguments[0]); }
所以用 codesign 去掉签名,主要还是让 codesign_allocate 来做工作。那么我们的 codesign --remove-signature Reeder,在这里实际会执行 codesign_allocate -i Reeder -a x86_64 0 -o Reeder.cstemp
0x04 - codesign_allocate.c
那我们来看看 codesign_allocate 吧,毕竟,前面说了这么多,还没有发现任何可能导致 load_command 中文件长度不正确的代码。codesign_allocate.c 中(我们获取的是最新的 Xcode 8.2.1 中的 codesign_allocate,它包含在 cctools - 895 里,这样可以保证我们如果改了源代码使问题解决了的话,就说明的确是这一部分的问题),和我们目前关心的问题有关系的有这一部分
... else if(strcmp(argv[i], "-a") == 0){ if(i + 2 == argc){ error("missing argument(s) to: %s option", argv[i]); usage(); } else { arch_signs = reallocate(arch_signs, (narch_signs + 1) * sizeof(struct arch_sign)); if(get_arch_from_flag(argv[i+1], &(arch_signs[narch_signs].arch_flag)) == 0){ error("unknown architecture specification flag: " "%s %s %s", argv[i], argv[i+1], argv[i+2]); arch_usage(); usage(); } arch_signs[narch_signs].datasize = strtoul(argv[i+2], &endp, 0); if(*endp != '\0') fatal("size for '-a %s %s' not a proper number", argv[i+1], argv[i+2]); if((arch_signs[narch_signs].datasize % 16) != 0) fatal("size for '-a %s %s' not a multiple of 16", argv[i+1], argv[i+2]); arch_signs[narch_signs].found = FALSE; narch_signs++; i += 2; } } ...
这里接受了 arch_signs[narch_signs].datasize 的数值,然后验证了是否 16 字节对齐。之后便是
... checkout(archs, narchs); process(archs, narchs); for(i = 0; i < narch_signs; i++){ if(arch_signs[i].found == FALSE) fatal("input file: %s does not contain a matching architecture " "for specified '-a %s %u' option", input, arch_signs[i].arch_flag.name, arch_signs[i].datasize); } writeout(archs, narchs, output, 0777, TRUE, FALSE, FALSE, FALSE, NULL); ...
在 process(archs, narchs) 中(确切的说,是它所调用的setup_code_signature(archs + i, NULL, archs[i].object))包含了大量的计算,不过厘清之后,实际上只有这一小段是可能密切相关的
/* * If this has a code signature load command reuse it and just change * the size of that data. But do not use the old data. */ if(object->code_sig_cmd != NULL){ if(object->seg_linkedit != NULL){ object->seg_linkedit->filesize += arch_signs[i].datasize - object->code_sig_cmd->datasize; if(object->seg_linkedit->filesize > object->seg_linkedit->vmsize) object->seg_linkedit->vmsize = rnd(object->seg_linkedit->filesize, get_segalign_from_flag(&arch_signs[i].arch_flag)); } else if(object->seg_linkedit64 != NULL){ object->seg_linkedit64->filesize += arch_signs[i].datasize; object->seg_linkedit64->filesize -= object->code_sig_cmd->datasize; if(object->seg_linkedit64->filesize > object->seg_linkedit64->vmsize) object->seg_linkedit64->vmsize = rnd(object->seg_linkedit64->filesize, get_segalign_from_flag(&arch_signs[i].arch_flag)); } object->code_sig_cmd->datasize = arch_signs[i].datasize; object->output_code_sig_data_size = arch_signs[i].datasize; object->output_code_sig_data = NULL; object->output_sym_info_size = rnd(object->output_sym_info_size, 16); object->output_sym_info_size += object->code_sig_cmd->datasize; }
在检查之后你会发现这一段也并没有错误,简单来说,就是从 __LINKEDIT 段的文件大小中减去原来的签名的大小,然后加上现在签名的大小。于是只能将目光转向最后一个坑了,writeout 函数。
0x05 - writeout.c
writeout 函数也是在 cctools 中,位于 libstuff。writeout 函数主要调用了 writeout_to_mem,先在内存中修改,然后写入到文件。然而这里面的计算也是相当多,但是似乎也没有计算出错的地方。
既然静态分析看起来已经不现实了(毕竟靠人力慢慢去验证这么大量的计算部分还是很累的,而且出错率也不会低),于是尝试将 codesign_allocate 编译起来调试。
进入 cctools 目录,然后二话不说就开始 make,结果报错了。
鉴于 LLVM 的坑实在太大,于是在 Makefile 中关掉了 LLVM 的 LTO 支持,再次编译,结果又报错。
OFILE_LLVM_BITCODE 没有声明,干脆在 Makefile 中定义成 0 好了。
……还是看看 codesign_allocate 需要哪些吧,有些不必要的就不编译了。
在看了 codesign_allocate 的源代码和引入的头文件之后,编译 codesign_allocate 的工作就变成了如下步骤。
0x06 - 解决方案
首先下载 cctools(这里给出的链接是 Xcode 8.2.1 中 codesign_allocate 所对应的 cctools 895),解压进入 cctools 目录下的 libstuff。
编辑 Makefile,注释掉 LTO 支持的那行
#LTO = -DLTO_SUPPORT
然后仍然是在 libstuff 目录下,编译 libstuff
$ make
编译完 libstuff 之后,回到 cctools 目录中,编译 consign_allocate
$ clang -I./include -L./libstuff -lstuff misc/codesign_allocate.c -o codesign_allocate
编译完之后,我们再来试试,这里就直接用
./codesign_allocate -i /Applications/Reeder.app/Contents/MacOS/Reeder -a x86_64 0 -o /Applications/Reeder.app/Contents/MacOS/Reeder.cstemp_2
0x07 - Xcode 自带的 codesign_allocate 到底对 MachO 做了什么
为了对比先前的 codesign_allocate,这里我还调用了
/usr/bin/codesign_allocate -i /Applications/Reeder.app/Contents/MacOS/Reeder -a x86_64 0 -o /Applications/Reeder.app/Contents/MacOS/Reeder.cstemp_1
可以看到,这里两个 codesign_allocate 出来的 MachO 大小不一样,我们刚才编译的那个生成的文件是 /Applications/Reeder.app/Contents/MacOS/Reeder.cstemp_2,长度是 5746352 字节,恰好就是这篇 post 一开头 dyld 找到的 __LINKEDIT 段所 map 的文件长度。
那么这两个到底是哪里不一样呢?我们先用 MachOView 来看看。先是原始 MachO 的信息
在 mach_header 中,可以看到原始的 MachO 文件一共有 33 个 load_command,大小为 4680 字节。
原始 MachO 的 __LINKEDIT 段文件偏移为 0x00553000 (5582848),长度是 0x0003C530 (247088),那么加在一起就是 5582848 + 247088 = 5829936,的确在上面 ls 的结果中,原始 MachO 文件的大小为 5829936 字节。
原始 MachO 的 LC_CODE_SIGNATURE 里,偏移是 0x0057AEB0 (5746352),长度是 0x00014680 (83584),相加起来也是 5829936。
我们编译的 codesign_allocate 生成的 MachO 的信息如下,
这里相比原始 MachO 文件并没有改变,仍然是一共有 33 个 load_command,大小为 4680 字节。
这边可以看到__LINKEDIT 段文件偏移还是 0x00553000 (5582848),但是长度是 0x00027EB0 (163504),加起来的话,5582848 + 163504 = 5746352。同样的,上面 ls 的结果中可以看到 Reeder.cstemp_2 的大小为 5746352 字节。
这里的 LC_CODE_SIGNATURE 还在,但是签名的长度已经被设置为 0 了。可以验证,原始 MachO 文件的长度是 5829936,其签名大小为 83584,两者相减,5829936 - 83584 = 5746352。
那么自带的 codesign_allocate 所生成的 MachO 呢?
这里 Xcode 自带的 codesign_allocate 将 load_command 的数量减少了 1,大小自然也变为了 4664 字节。(LC_CODE_SIGNATURE 占用 16 字节)
Xcode 自带的 codesign_allocate 所生成的 MachO 的 __LINKEDIT 段和我们自己编译的 codesign_allocate 所生成的一样。都是文件偏移保持 0x00553000 (5582848),但是长度变为 0x00027EB0 (163504)。
因为彻底删掉了 LC_CODE_SIGNATURE,所以也就找不到了。
到目前为止,Xcode 自带的 codesign_allocate 看上去完全没有错,但是事实上,我们知道 Xcode 自带的 codesign_allocate 所生成的实际的文件变小了,于是只好上二进制对比工具,看看到底是哪里少了那几个字节。
我们先对比原始 MachO 和我们刚才编译的 codesign_allocate 所生成的文件。
可以看到,一共有 3 处不同,按照偏移量顺序来说的话,第一处是修改了 __LINKEDIT 段的长度,第二处是 LC_CODE_SIGNATURE 所记录的 data size,第三处显然是代码签名。
那么 Xcode 自带的 codesign_allocate 所生成的 MachO 呢?
这里一共有 4 处不同,同样按照偏移量顺序来说的话,第一处是 mach_header_64 中所记录的 load_command 的数量,第二处是 mach_header_64 中所记录的 load_command 的大小,第三处是 __LINKEDIT 段记录的长度,第四处则是代码签名。
等等!为什么这里代码签名被多删掉了 4 个字节?我们编译的 codesign_allocate 删代码签名时,是从 FA DE 0C C0 开始的,而 Xcode 自带的则是 00 00 00 00 FA DE 0C C0。这多删掉的 4 个字节,恰好也是前面 dyld 抱怨实际文件大小与 __LINKEDIT 段指定的文件大小的差。
我们使用的是开源的、 Xcode 8.2.1 中 cctools 代码, 仅仅关掉了 LLVM 的 LTO 支持,就使得 codesign_allocate 在移除代码签名时正常工作了,我们可以合理的推测,之前移除代码签名之后,dyld 抱怨生成的 MachO 是不合法的元凶,就是 LLVM 的 LTO 支持。在 writeout.c 中,的的确确也有两处利用这个宏开关控制的部分。这个猜想应该是合理的,至于 LLVM 的 LTO 支持在哪里出了问题,这个暂时还不清楚。