前言
以前想获取应用的 .ipa
文件可以从 iTunes
下载,macOS
上的 iTunes
升级成 12.7
后,无法再获得 .ipa
文件, 而iOS 9
后就不能从 iTools
之类的辅助工具中导出 .ipa
文件。
后面 Apple
官网又提供了一个 12.6.3 的链接
出于好奇心,想试试直接导出.ipa
文件,并重签名安装到未越狱的设备上。
环境
macOS Sierra 10.12.6
iPhone 6 Plus (11.0)
Xcode Version 9.0 (9A235)
iPhone 5s(9.3.3 已越狱)
越狱设备中导出 .ipa
SSH
到越狱设备,使用Cycript 找到xxx.app
的位置
iPhone:~ root# ps -ef | grep WeChat
501 714 1 0 0:00.00 ?? 0:29.04 /var/containers/Bundle/Application/41A6836F-CFC9-4DC1-8A9C-ED4DE09F9A66/WeChat.app/WeChat
0 807 805 0 0:00.00 ttys001 0:00.01 grep WeChat
找到后使用 tar
将 xxx.app
打包
iPhone:~ root# cd /var/containers/Bundle/Application/41A6836F-CFC9-4DC1-8A9C-ED4DE09F9A66/
iPhone:/var/containers/Bundle/Application/41A6836F-CFC9-4DC1-8A9C-ED4DE09F9A66 root# ls
WeChat.app iTunesArtwork iTunesMetadata.plist
iPhone:/var/containers/Bundle/Application/41A6836F-CFC9-4DC1-8A9C-ED4DE09F9A66 root# tar -cvf /tmp/WeChat.tar ./
./
./.com.apple.mobile_container_manager.metadata.plist
./WeChat.app/
./WeChat.app/[email protected]
...
./WeChat.app/zh_TW.lproj/network_setting.html
./iTunesArtwork
./iTunesMetadata.plist
使用 scp
发送打包文件到 mac
上
➜ ~ scp [email protected]:/tmp/WeChat.tar ~/Desktop/WeChat.tar
[email protected]'s password:
WeChat.tar 100% 97MB 1.7MB/s 00:58
重签名要求可执行文件要先脱壳
// 获得沙盒地址
iPhone:~ root# cycript -p WeChat
cy# [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]
#"file://s"
cy# exit(0)
// 获得应用地址
iPhone:~ root# ps -ef | grep WeChat
501 714 1 0 0:00.00 ?? 0:03.25 /var/containers/Bundle/Application/41A6836F-CFC9-4DC1-8A9C-ED4DE09F9A66/WeChat.app/WeChat
0 717 699 0 0:00.00 ttys000 0:00.01 grep WeChat
// DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib path/to/App
iPhone:/var/mobile/Containers/Data/Application/E7DE74DA-2FC9-4A86-A4D7-F993A88AB225/Documents root# su mobile
iPhone:~/Containers/Data/Application/E7DE74DA-2FC9-4A86-A4D7-F993A88AB225/Documents mobile$ DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/containers/Bundle/Application/41A6836F-CFC9-4DC1-8A9C-ED4DE09F9A66/WeChat.app/WeChat
mach-o decryption dumper
DISCLAIMER: This tool is only meant for security research purposes, not for application crackers.
[+] detected 64bit ARM binary in memory.
[+] offset to cryptid found: @0x100028ca8(from 0x100028000) = ca8
[+] Found encrypted data at address 00004000 of length 56131584 bytes - type 1.
[+] Opening /private/var/containers/Bundle/Application/41A6836F-CFC9-4DC1-8A9C-ED4DE09F9A66/WeChat.app/WeChat for reading.
[+] Reading header
[+] Detecting header type
[+] Executable is a plain MACH-O image
[+] Opening WeChat.decrypted for writing.
[+] Copying the not encrypted start of the file
[+] Dumping the decrypted data into the file
[+] Copying the not encrypted remainder of the file
[+] Setting the LC_ENCRYPTION_INFO->cryptid to 0 at offset ca8
[+] Closing original file
[+] Closing dump file
原版的 dumpdecrypted 无法应对有 Frameworks
的情况,不过有一个分支版本dumpdecrypted 提供了解决的方法,但是还有Plugins
,幸而还有一个分支提供了使用dumpdecryptd 对 .extension
文件进行脱壳(today
那种小插件,用这个版本脱壳尝试不成功,找了XX助手去下载脱壳的部分),详情可参考App Extension的脱壳办法。都能支持的版本也已经有了dumpdecrypted
分支
对 dumpdecrypted
有些部分的代码不理解
- 原版的
dumptofile
的函数参数是怎么来的? -
Frameworks
分支版本做了什么? -
extension
分支版本做了什么?
在这个dumpdecrypted
的Frameworks
分支版本中
__attribute__((constructor))
static void dumpexecutable() {
...
_dyld_register_func_for_add_image(&image_added);
}
__attribute__((constructor))
根据 attribute((constructor))用法解析,得到的GNU的文档 可知 dumpexecutable()
方法在 main
函数之前执行
_dyld_register_func_for_add_image
找了 github
上的 dyld
/*
* _dyld_register_func_for_add_image registers the specified function to be
* called when a new image is added (a bundle or a dynamic shared library) to
* the program. When this function is first registered it is called for once
* for each image that is currently part of the program.
*/
void
_dyld_register_func_for_add_image(
void (*func)(const struct mach_header *mh, intptr_t vmaddr_slide))
{
DYLD_LOCK_THIS_BLOCK;
typedef void (*callback_t)(const struct mach_header *mh, intptr_t vmaddr_slide);
static void (*p)(callback_t func) = NULL;
if(p == NULL)
_dyld_func_lookup("__dyld_register_func_for_add_image", (void**)&p);
p(func);
}
dyld
会负责传递 mh
和 intptr_t
参数
// usr/include/sys/_types/_intptr_t.h
typedef __darwin_intptr_t intptr_t;
// usr/include/arm/_types.h
typedef long __darwin_intptr_t;
因此 intptr_t
就是 long
类型
static void image_added(const struct mach_header *mh, intptr_t slide) {
Dl_info image_info;
int result = dladdr(mh, &image_info);
dumptofile(image_info.dli_fname, mh);
}
调用 dumptofile
函数, 在 _dyld_register_func_for_add_image
函数的注释中提到了 called when a new image is added (a bundle or a dynamic shared library)
因此可以将 framework
也一起导出。
查找 dyld
后发现在 ImageLoader.h
头文件中,有
typedef void (*Initializer)(int argc, const char* argv[], const char* envp[], const char* apple[], const ProgramVars* vars);
在 ImageLoaderMachO.cpp
中有如下的函数实现
void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
if ( fHasDashInit ) {
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_ROUTINES_COMMAND:
Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
// verify initializers are in image
if ( ! this->containsAddress((void*)func) ) {
dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
}
if ( context.verboseInit )
dyld::log("dyld: calling -init function %p in %s\n", func, this->getPath());
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}
根据函数命名知道这应该是给镜像做初始化的,里面 func
函数是 Initializer
类型的,通过 context
参数获取上下文信息,原版的 dumptofile
函数的参数列表为什么会是 (int argc, const char **argv, const char **envp, const char **apple, struct ProgramVars *pvars)
到这里就可知一二。
.extension
脱壳的 dumpdecrypted
版本与原版的区别主要是
...
strlcpy(npath, cachePath.UTF8String, sizeof(npath));
strlcat(npath, tmp, sizeof(npath));
strlcat(npath, ".decrypted", sizeof(npath));
strlcpy(buffer, npath, sizeof(buffer));
...
指明将脱壳的包导出到应用沙盒的 .../Library/Caches
目录下
/Library/MobileSubstrate/DynamicLibraries/
下的动态库会被MobileSubstrate.dylib 自动加载。
在原版的 dumpdecrypted
添加一些输出,使用 printf
时,据说在后面添加一个 \n
可以在日志中看到,但是试了似乎没效果,因此使用 NSLog
进行输出,添加
#import
同时修改 Makefile
文件, 指明要链接 Foundation
框架
-framework Foundation
make
&& ldid
签名,发送到 /Library/MobileSubstrate/DynamicLibraries/
配置好对应的 .plist
文件,启动,然后闪退,查看日志
Nov 20 09:13:29 Jasons-iPhone QQReaderUI[1237] : MS:Notice: Injecting: com.tencent.qqreaderiphone [QQReaderUI] (1290.11)
Nov 20 09:13:29 Jasons-iPhone QQReaderUI[1237] : MS:Notice: Loading: /Library/MobileSubstrate/DynamicLibraries/dumpdecrypted.dylib
Nov 20 09:13:29 Jasons-iPhone QQReaderUI[1237] : [TEST]:rpath = /private/var/containers/Bundle/Application/E276C2C9-20FE-4450-915A-86EBE07798AC/QQReaderUI.app/QQReaderUI
Nov 20 09:13:29 Jasons-iPhone QQReaderUI[1237] : [TEST]:fd = 4
Nov 20 09:13:29 Jasons-iPhone QQReaderUI[1237] : [TEST]:npath = QQReaderUI
Nov 20 09:13:29 Jasons-iPhone QQReaderUI[1237] : [TEST]:npath = QQReaderUI.decrypted
Nov 20 09:13:29 Jasons-iPhone QQReaderUI[1237] : [TEST]:outfd = -1
Nov 20 09:13:29 Jasons-iPhone QQReaderUI[1237] : [TEST]:outfd = -1
Nov 20 09:13:29 Jasons-iPhone com.apple.xpc.launchd[1] (UIKitApplication:com.tencent.qqreaderiphone[0xc2a3][1237]) : Service exited with abnormal code: 1
第一个 outfd = -1
outfd = open(npath, O_RDWR|O_CREAT|O_TRUNC, 0644);
NSLog(@"[TEST]:outfd = %d\n",outfd);
尝试在当前目录下(/Library/MobileSubstrate/DynamicLibraries/
),打开(创建)一个 QQReaderUI.decrypted
,因为权限的问题失败了。
第二个 outfd = -1
后面 dumpdecrypted.dylib
还会判断是不是在/private/var/mobile/Applications
(通过AppStore和iTunes安装的程序都在里面。),但显然不是,因此也失败了
调用 _exit(1)
闪退
找到一个比较具体的解释,排版不太好...
回到正题
将脱壳后的可执行文件同样的发送到 mac
上
➜ ~ scp [email protected]:/var/mobile/Containers/Data/Application/E7DE74DA-2FC9-4A86-A4D7-F993A88AB225/Documents/WeChat.decrypted ~/Desktop
[email protected]'s password:
WeChat.decrypted 100% 68MB 444.4KB/s 02:36
➜ ~ cd Desktop
➜ Desktop file WeChat.decrypted
WeChat.decrypted: Mach-O 64-bit executable arm64
如何判断是否已脱壳
判断二进制是否被加密可以使用 otool
工具
otool - object file displaying tool
otool可以输出app的load commands,然后通过查看cryptid这个标志位来判断app是否被加密。1代表加密了,0代表被解密了
// 直接从手机中导出的 WeChat
➜ Wechat otool -l WeChat.app/WeChat | grep -B 2 crypt
(standard input)-489- cmd LC_ENCRYPTION_INFO
(standard input)-490- cmdsize 20
(standard input):491: cryptoff 16384
(standard input):492: cryptsize 46907392
(standard input):493: cryptid 1
// 脱壳后的 WeChat
➜ Wechat otool -l WeChat.decrypted | grep -B 2 crypt
(standard input):1:WeChat.decrypted:
--
(standard input)-489- cmd LC_ENCRYPTION_INFO
(standard input)-490- cmdsize 20
(standard input):491: cryptoff 16384
(standard input):492: cryptsize 46907392
(standard input):493: cryptid 0
可以看出手机导出的只有当前架构的,而脱壳也只针对当前运行的架构,直接从 iTunes
下载的 .ipa
文件中的二进制文件大小大约是从手机导出的两倍,因为它包含了其他的架构。
了解重签名
学习一般都是从模仿开始的,看看 Xcode
是怎么签名的,创建新工程,⌘ + B
编译一下,然后查看日志
/// 签名
CodeSign /Users/Jason/Library/Developer/Xcode/DerivedData/JustForTest-ggxnkdgkdklojofwwsifymqzfwqo/Build/Products/Debug-iphoneos/JustForTest.app
/// cd 到工程目录
cd path/to/project
/// 扩展环境变量
export CODESIGN_ALLOCATE=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate
export PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin:/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
/// 签名的身份 + 描述文件说明
Signing Identity: "iPhone Developer: 开发者名 (开发者ID)"
Provisioning Profile: "iOS Team Provisioning Profile: com.coder.bk123"
(c8748c6c-9d07-41ce-8d55-ff5ca9999999)
// 使用 codesign 执行签名操作
/usr/bin/codesign --force --sign 签名值 --entitlements /Users/Jason/Library/Developer/Xcode/DerivedData/JustForTest-ggxnkdgkdklojofwwsifymqzfwqo/Build/Intermediates.noindex/JustForTest.build/Debug-iphoneos/JustForTest.build/JustForTest.app.xcent --timestamp=none /Users/Jason/Library/Developer/Xcode/DerivedData/JustForTest-ggxnkdgkdklojofwwsifymqzfwqo/Build/Products/Debug-iphoneos/JustForTest.app
--force
强制代替已存在的签名
--timestamp
时间戳
--sign
-s, --sign identity
Sign the code at the path(s) given using this identity. See SIGNING IDENTI-
TIES below.
详情可使用 man codesign
查看
打开钥匙串应用,找到开发者证书,因为我是Debug
状态下运行的,所以看开发证书,sign
的值就是指纹中的 SHA1 签名,20
个16
进制 正好是40
位
--entitlements
参数值对应的 xxx.app.xcent
文件内容如下
application-identifier
XXXXX.包名
com.apple.developer.team-identifier
XXXXX
get-task-allow
keychain-access-groups
XXXXX.包名
其中 XXXXX
是发布证书中开发者的身份ID
即 com.apple.developer.team-identifier
中的字段值
在 WWDC16 Session 401 What's New in Xcode App Signing 有如下的内容
There are three things that you need in order to sign your apps. The first thing is a signing certificate. Signing certificates establish your identity as a developer.
Provisioning profiles are all about your apps, and they grant permissions. And entitlements declare support for capabilities.
Like iCloud or Wallet. So certificates. These are issued by Apple.
And this is important because the device needs a trust chain back to Apple so it can install the app. And certificates come in two forms.
signing certificate
: 开发者身份是否可信
Provisioning profiles
: 哪些设备可安装
entitlements
: 有哪些能力,比如推送,应用间分享....
在新创建的工程 Product
目录下 Show in Finder
来查看 xxx.app
文件,可以发现里面包含一个 embedded.mobileprovision
文件,可以通过
➜ security cms -D -i embedded.mobileprovision
结果如下:
AppIDName
Resign01
ApplicationIdentifierPrefix
发布时的开发者ID
CreationDate
2017-11-15T03:42:41Z
Platform
iOS
DeveloperCertificates
签名证书
...
Entitlements
keychain-access-groups
发布时的开发者ID.*
get-task-allow
application-identifier
发布时的开发者ID.com.coder.Resign01
com.apple.developer.healthkit
com.apple.security.application-groups
group.com.coder.Resign01
com.apple.developer.team-identifier
发布时的开发者ID
aps-environment
development
ExpirationDate
2018-11-15T03:42:41Z
Name
iOS Team Provisioning Profile: com.coder.Resign01
ProvisionedDevices
允许安装的设备的UDID
....
TeamIdentifier
发布时的开发者ID
TeamName
开发者名
TimeToLive
365
UUID <-- 描述文件 -->
c8748c6c-9d07-41ce-8d55-ff5ca999999
Version
1
通过 Xcode
日志的描述文件序列号在 ~/Library/MobileDevice/Provisioning Profiles/
目录查找,然后用对比工具进行比较,发现两者是相同的,
从中可以看出,开发的情况下,签名的过程会将我们从 Apple
开发者后台下载的描述文件embedded.mobileprovision
放到包中,用于安装时的验证。
我们在 Apple
开发者后台生成证书时要求上传一个 .csr
文件,Certificate_signing_request
Before creating a CSR, the applicant first generates a key pair, keeping the private key secret. The CSR contains information identifying the applicant (such as a distinguished name in the case of an X.509 certificate) which must be signed using the applicant's private key. The CSR also contains the public key chosen by the applicant. The CSR may be accompanied by other credentials or proofs of identity required by the certificate authority, and the certificate authority may contact the applicant for further information.
创建 .csr
文件前,会用非对称加密算法生成一对密钥,分别是公钥和私钥,私钥存放在本地,而公钥与一些表明能自身身份的信息一起用于生成 .csr
文件。
Apple
开发者后台相当于一个 CA
Trusted certificates can be used to create secure connections to a server via the Internet. A certificate is essential in order to circumvent a malicious party which happens to be on the route to a target server which acts as if it were the target. Such a scenario is commonly referred to as a man-in-the-middle attack. The client uses the CA certificate to authenticate the CA signature on the server certificate, as part of the authorizations before launching a secure connection. Usually, client software—for example, browsers—include a set of trusted CA certificates. This makes sense, as many users need to trust their client software. A malicious or compromised client can skip any security check and still fool its users into believing otherwise.
关于签名的原理,请看下图
来自-iOS App 签名的原理
测试环境下(Ad Hoc)如何保证应用能正常运行?
来自 developer.apple.com
证书和 device ID
一般应该都不会有问题,都是从 Apple
开发者后台获取的,所以主要去考虑的是 bundle ID
和 entitlements
后面实践发现对 entitlements.plist
文件添加权限,删除权限,会影响到对应的embedded.mobileprovision
,但是依然可以成功安装并运行。
看了文字的说明
The app successfully launches if the app’s bundle ID matches the App ID, the signature matches the distribution certificate, and the device is in the device list of the ad hoc provisioning profile.
只提到了 3
点,包名匹配,证书匹配(--sign
对应的证书签名embedded.mobileprovision
文件中包含的证书),描述文件的设备列表包含当前设备
哪些文件需要参与签名
Nested code , 比如 .a 静态库, .frameworks (先签名)
Mach-O executables , 二进制可执行文件
Resources , 资源文件,就是 CodeResources 的部分
To apply the signature, the codesign utility adds the signature directly to the executable file.
.ipa
包里有一个 _CodeSignature
的文件夹,其中有个 CodeResources
文件
files
...
files2
...
rules
...
rules2
...
rules 和 files 是为老版本准备的,而 files2 和 rules2是为新的第二版的代码签名准备的。最主要的区别是在新版本中你无法再将某些资源文件排除在代码签名之外。+
二进制的签名会被放到可执行文件中
参考的那篇文章的链接挂了...
https://xiuchundao.me/post/code-resources-and-embedded-mobileprovision-included-in-ipa-file
Mach-O executables. ... To apply the signature, the codesign utility adds the signature directly to the executable file.
可以知道二进制文件的签名确实是直接在文件中的
总结
- 签名工具:
codesign
,需要提供 证书的SHA1
签名值 和 entitlements.plist -
embedded.mobileprovision
就是从Apple
开发者后台下载的描述文件, 同时在编译时会被拷贝到包中 - 重签名的应用要成功运行要保证包名匹配,证书匹配(
--sign
对应的证书签名embedded.mobileprovision
文件中包含的证书),描述文件的设备列表包含当前设备 - 参与签名的有内嵌的框架,资源文件,二进制可执行文件
参考
- Wiki-SHA1
- App Distribution Guide - Adding Capabilities
- iOSAppHook
- Code Signing Tasks
- Maintaining Identifiers, Devices, and Profiles
- AppDistributionGuide-Introduction
- attribute((constructor))用法解析
- How do you compile and run an objective-c file outside Xcode?