18

动态库注入

 6 years ago
source link: http://saitjr.com/ios/dylib-injection.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

免越狱调试的方案中,常用的有 IPAPatchMonkeyDev ,都会将自定义的代码打成 framework,注入到 App 中,以实现调试的功能。

环境信息

optool: commit(2898b51)

dyld: commit(3f928f3)

关于动态库注入,IPAPatch 采用的是开源的 optool ,MonkeyDev 采用的是 monkeyparser。相比之下 monkeyparser 比 optool 多一些功能,但在动态库注入上,原理应该类似。除了免越狱调试这种玩法外,还可以用于 App 自动化测试、性能分析等。

在不使用 Xcode 调试的情况下,如果想要获取 premain 的耗时,可以看 Joy 的 如何精确度量 iOS App 的启动时间 。其中要记录所有动态库加载时间,所以打点的库需要第一个被加载。对于内部应用,可以用 Cocoapods,对于 ipa 产物来说,可以直接将动态库注入到第一个 LoadCommand(第一个 LC_LOAD_DYLIB )。

流程总览

注入做的事情,其实就是给 MachO 的每个架构中,添加类型为 LC_LOAD_DYLIB 的 LoadCommand。

之后补个图

optool 源码浅析

optool 实现了动态库注入与移除、移除签名,移除 ASLR 等功能,主要看动态库的部分。

基础配置

拉取源码:

# 拉取代码
git clone --depth 1 -b master [email protected]:alexzielenski/optool.git
# 拉取 optool 用来做参数解析的 FSArgumentParser
cd optool
git submodule init
git submodule update --remote

打开工程,Xcode -> Product -> Scheme -> Edit Scheme… -> Run -> Arguments 添加入参。相当于控制台直接调用,方便断点调试。

# install: 动态库注入 Action
# -t: 指定目标二进制
# -p: 指定动态库
install -t /Users/saitjr/Desktop/TTTest.app/TTTest -p /Users/saitjr/Desktop/MTHawkeye.framework/MTHawkeye

VZzAJvM.png!web

关于 -p 参数,也就是动态库路径的指定,后文再谈,这里只是为了调试 optool。目前写本地绝对路径,注入后的 App 会启动闪退。

在 optool 的 main 函数中,如果断点能正确拿到入参,就说明配置没问题了。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BOOL showHelp = NO;
        
        NSLog(@"%s", argv[1]); // 输出 install
    ...
    ...
}

读取 MachO Header

参数校验的部分直接跳过,从读取二进制,解析 header 开始:

// 读取可执行文件
NSData *originalData = [NSData dataWithContentsOfFile:executablePath];
// mutableCopy 一下,方便编辑
NSMutableData *binary = originalData.mutableCopy;
// 初始化 thin header 数组,数组长度为 4,也就是最多能包含四个架构
struct thin_header headers[4];
// 初始化 thin header 个数,即可执行文件中的架构数
uint32_t numHeaders = 0;
// 读取 header
headersFromBinary(headers, binary, &numHeaders);

thin_header 结构体是自定义的,里面直接用的系统的 mach_header ,虽然还有一个是 mach_header_64 ,但是 64 里面多的 reserved 只是保留字段,没有具体含义,所以不影响解析。

除此之外,还将 fat header 中,每个架构的 offset 逐个赋值给了 thin header,只是为了方便读取。

headersFromBinary 中,对 header 进行读取。

之后补个图

查找 LoadCommand

header 读取完后,注入需要用到的信息有:

  • 架构数
  • 每个架构的偏移量
  • 每个架构的 Load Command 数量
  • 每个架构的 Load Command 大小

然后遍历全部架构,执行 install ,也就是 insertLoadEntryIntoBinary 方法。在真正执行插入之前,会先检查当前 LoadCommand 是否已经存在,所以有个查找的操作,方法是 binaryHasLoadCommandForDylib

之后补个图


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK