代码注入
上一篇我们已经讲到使用 shell 脚本来重签并调试别人的 APP,那么我们又是重签又是附加调试别人的 APP 是为了啥呢?是太闲了吗,当然不是…我们接下来的任务就是代码注入
App 执行哪些代码?
在开始代码注入之前,我们先了解一下一个 iOS 的 App 在运行的时候,究竟会执行哪些代码,以及我们从哪里入手注入代码
- MachO。App 的二进制文件,我们写的所有代码都会在这里,后面的文章会介绍
- Framework 可以是我们写的,也可以是第三方的代码
- 系统库
其中系统库,在非越狱手机上我们改不了。MachO 我们可以修改,但是比较麻烦,需要会写汇编。而 Framework 是我们经常接触也是最容易入手的,我们自己写一个就行了…
那么现在的问题就是我们写的 Framework 别人的 APP 怎么会执行呢?
MachO 文件里面有一个 Load Commands 的部分,DYLD(the dynamic link editor 是苹果的动态链接器,后面的文章也会介绍)会读取这个 Load Commands 里面的内容,并加载到内存中,如果我们能把我们自己的 Framework 插入到别人 APP 的 MachO 的 Load Commands 里,那么我们的代码就自然的被执行了
那么现在的问题就变成了如何把我们写的 Framework 插入到别人 App 的 MachO 文件里?
yololib 这是一个终端命令行工具,就是用来把我们写的 Framework 插入到 MachO 文件里,代码不多就 200 来行,感兴趣的可以看看源码,用法是 yololib 参数1 参数2
其中参数 1 是 MachO 文件,参数 2 是我们的 Framework 相对于 MachO 文件的路径。
下载源码之后是我们很熟悉的 Xcode 工程,只不过是 Mac 上的命令行工程而不是我们常用的 iOS 工程,修改一下 Deployment Target 到你的 Xcode 兼容的最低版本,然后编译一下就能在编译后的文件夹下找到。编译后的文件夹可以通过点击 Xcode 上的 Product -> Show Build Folder in Finder 中的 Products->Debug 找到。
为了方便我们使用这个工具,建议把它放到系统的 usr/local/bin 目录下,记得给它提升可执行权限(在终端chmod +x yololib
),这样我们任意打开一个终端都可以使用这个命令了,也方便我们使用脚本来操作。
好了,现在万事具备,只欠我们动手操作了…还记得上一篇文章讲到的内容吗,使用 shell 脚本重签 App。可以接着使用上次的工程,也可以新建一个工程来执行后面的代码注入,配置好脚本之后只需要放入一个砸壳的 app 包到根目录下就好了。我这里新建一个项目来演示后面的代码注入
代码注入的步骤
新建 Framework
在完成了上一篇 shell 脚本重签的步骤之后,我们先来新建一个 Framework,把我们的 Framework 准备好
Framework 的名字就叫 FrankyHook 这个名字无所谓,但是要记住因为在后续使用 yololib 的时候要用到,在里面新建一个类 CodeInject 这个叫什么也无所谓,继承自 NSObject 就够了,主要是需要它的 load 方法,并在 load 方法里面打印点内容
在脚本中添加代码
记得修改为你自己的 Framework 名字,要是照着我的抄的的当我没说
1 | yololib "${BUILT_PRODUCTS_DIR}/${TARGET_NAME}.app/${APP_BINARY}" "Frameworks/FrankyHook.framework/FrankyHook" |
这里面有一个小细节,就是你的脚本运行一定要放在嵌入 Frameworks 之后,不然每次脚本运行之后再嵌入 Frameworks 的话 Xcode 会结合 Framework 的 Info.plist 重新处理工程源代码下的 Info.plist 文件到编译的包中,会导致少了很多配置从而启动崩溃。
实不相瞒,做了四年多的 iOS 开发,也是到今天才知道这个地方居然可以拖动…
command + R 看控制台输出
怎么样,是不是发现我们已经可以在 WeChat 里面执行我们的代码了?接下来我们来实现两个小小的需求
需要具备的一些知识点
接下来的内容需要对 Objective-C 的 rumtime 有一定的了解,了解的同学这部分可以直接跳过,看下一部分
Method Swizzle方法交换
runtime 提供了一些 api 来让我们实现方法交换,现在假设有这么一段代码:
使用 NSURL 初始化 URL 的时候,如果 URL 中含有中文,那么初始化就会失败返回 nil。这个问题做过 iOS 开发的同学应该都遇到过,解决的办法就是对 URL 进行一次百分号编码,现在假设一种情景,你刚入职一家新公司,发现了这个问题,然后在工程里面搜索了一下,发现我的天啦,到处都是这种 URL 中夹着中文,而没有进行编码的情况,那么这个时候,你是选择一个一个的去修改呢,还是会想其他更好的办法?
使用方法交换就是更好的办法,新建一个 NSURL 的分类,在分类的 load 方法中,实现我们的方法交换
一个 OC 方法,我们可以分为两个部分,一个是方法名 SEL,一个是方法的实现 IMP。正常情况下,一个方法的名字对应着它的实现。而有时候我们通过 runtime 来交换方法的实现。就如上面的 load
方法里面的代码,默认情况下方法 one
和方法 two
的实现都是指向他们自己的 IMP 的,通过 method_exchangeImplementations()
函数交换之后,方法 one
的实现就指向了方法 two
的实现,而方法 two
的实现就指向了方法 one
的实现,当代码调用 URLWithString:
的时候,就会来到我们的 HK_URLWithString:
方法,当代码调用 HK_URLWithString:
的时候,就会执行 URLWithString:
方法
这样原来工程里的所有 URLWithString:
方法都会执行到我们的代码中去,首先调用一次原始的初始化 URL 的方法,看能否成功生成 url 如果为 nil,就表示我们可能需要对字符串 str 进行一下编码。再用编码过后的 str 初始化 url,这样就不用浪费时间精力去一个一个的去修改工程里的代码了
拦截微信的注册点击
使用 Debug View Hierarchy 查看微信的登录注册页面的视图层次结构,找到注册的按钮,查看按钮的 target 和 action
我们知道了注册按钮点击的时候,会调用 WCAccountLoginControlLogic
的 onFirstViewRegister
方法,而且我们也已经可以在我们的 Framework 中执行我们的注入代码了,那么接下来我们如何实现拦截微信的注册按钮点击呢?当然可以使用 Method Swizzle 来实现
在开始写代码实现方法交换之前,还有一个小小的问题,虽然我们根据经验可以知道这个 onFirstViewRegister
应该是个对象方法,可能没有返回值也没有参数,但这些都只是根据我们经验的猜测…关考猜测可不行,那么怎么验证我们的猜测呢?
Class-dump 和 yololib 一样,也是一个终端命令行工具,同样也可以放到 usr/local/bin 目录下可以全局使用,它的作用是可以把 MachO 文件里的头文件信息全部导出来,我们可以 cd 到工程编译生成的 APP 包里面,使用以下命令
1 | class-dump -H WeChat -o ./headers/ |
将它的头文件都导到一个 headers 的文件夹内,这个过程需要一点时间
看了下这个文件挺大的,导完之后可以把它剪切到工程根目录下,这样给我们手机也能省点空间,也确实完全没必要放在 APP 包里面
可以看到这里面有 15074 个项目,微信的头文件还真不少呢…这么大的文件夹如果我们用 Xcode 打开搜索的话,一定会十分的痛苦。这里推荐使用 sublime…更轻量一点,找起头文件来也更快,搜索一番发现如图所示
这样就确保了这个 onFirstViewRegister
方法是无返回值无参数的,可以开始编写代码实现拦截了
现在我们可以来到我们 Framework 的 CodeInject.m 文件写点代码
代码就这么点,现在 command + R 运行起来之后,再次点击微信的注册按钮试试看?
窃取用户的账号密码
上面的需求仅仅只是破坏了功能,原来的功能都没法使用了。接下来这个需求让用户在神不知鬼不觉的情况下,账号密码就被窃取了(所以市面上那些来源不明的 App 真不要下载使用,除非你也是搞逆向研究的)。那么我们在什么时候,能拿到用户的账号和密码呢?当然是点击登录按钮的时候,所以你懂的,使用 viewDebug 查看登录按钮
登录按钮点击的 target:WCAccountMainLoginViewController
,action:onNext
;
查看刚刚 class-dump 出来的头文件发现这个 onNext 方法是没有参数的,那么我们需要的账号密码在哪里呢?哈哈,有经验的同学是不是发现这两个名字是不是莫名的熟悉
1 | WCAccountTextFieldItem *_textFieldUserNameItem; |
变量名是挺熟悉的,但是这个 WCAccountTextFieldItem
我们不知道是个什么东西怎么办?头文件都在手上了还问怎么办,接着搜索啊…
WCAccountTextFieldItem
里面貌似没啥东西,那就看它父类 WCBaseTextFieldItem
在 WCBaseTextFieldItem
类里面发现了一个 WXUITextField
的东西,跟我们熟悉的 UITextField
很像了,我们猜测可能就是这个 m_textField
存放着我们想要的东西,先不着急写代码
现在我们可以再次使用 viewDebug 工具调试查看一下我们的账号和密码在哪里,并且使用 lldb 来动态调试验证我们的猜测
首先控制器的地址,可以由登录按钮的 target 获取到。获取到控制器之后,再使用 KVC 大法获取成员变量 _textFieldUserPwdItem
的地址(密码都能获取到了,账号也是一样的操作)并打印它。获取到 _textFieldUserPwdItem
的地址之后,再次使用 KVC 大法获取它的成员变量 m_textField
的地址并打印这个对象,好家伙,明文密码不就在这儿了吗!!!
接下来我们通过代码来实现需求:
这代码看起来跟拦截微信注册的代码差不太多,那么我们 command + R 运行看看结果
WTF?账号密码确实是都获取到了,但是 APP 却崩溃了?为什么会发生崩溃,崩溃信息是我们经常能够遇到的经典报错 unrecognized selector sent to instance 0x10b15b400
说是控制器 WCAccountMainLoginViewController
无法识别 FK_onNext
这个方法
我们思考一下,方法交换为什么一般推荐写在想要交换方法的类所在的分类里面?因为在想要交换方法的类的分类当中,我们会新增一个方法,用来实现我们的逻辑,也正好是因为在分类中,所以当前类自然的添加了我们新增的方法,这样交换下来就不会出现找不到方法的错误
而上面的代码,我们的本意也是希望在 WCAccountMainLoginViewController
里面新增一个方法处理我们的逻辑,并交换 onNext
方法,但现在的问题是我们在 CodeInject
这个类的 load
方法中,那么有什么办法可以解决这个问题呢?
动态添加方法
rumtime 的 api 提供了运行时动态添加方法的能力
1 | BOOL |
- 参数1: 给哪个类添加方法
- 参数2: 方法的名字
- 参数3: 方法的实现
- 参数4: 方法的参数和返回值描述,用一些特定的符号表示,也可以不写
- 参数5: BOOL值,表示是否添加成功
那么借用这个 api 我们能想到什么解决办法呢,给 WCAccountMainLoginViewController
控制器添加我们的 FK_onNext
方法,再让 FK_onNext
和 onNext
方法交换,最终的代码如下
再次 command + R 运行发现,既能成功获取到用户输入的账号密码,又能成功的去调用微信的登录逻辑了
动态替换方法
现在除了添加新方法的方式,我们还有其他的办法吗?当然有(强大的runtime) rumtime 的 api 还提供了运行时替换方法的能力
1 | IMP _Nullable |
- 参数1: 替换哪个类
- 参数2: 方法的名字
- 参数3: 方法的实现
- 参数4: 方法的参数和返回值描述,用一些特定的符号表示,也可以不写
- 返回值: IMP,原始的方法实现
如果使用这个方式,那么我们应该是将原始的 onNext
方法替换成我们的 FK_onNext
方法,那么我们如何去调用微信原始的 onNext
方法呢,在替换之前将原始的 onNext
方法的实现 IMP 记录下来,然后在我们的 FK_onNext
方法中使用这个记录的 IMP 来实现对原始 onNext
的调用,具体代码如下图:
对比方法交换的代码实现,我们发现方法替换的方式需要多一个变量 originalIMP
,用来记录原始的 onNext
方法的实现,而且还会报个警告,虽然少了一点点代码,但看起来也不是那么好理解…当然,这里使用方法替换也只是为了学习一下 runtime 提供的 api,感受一下 rumtime 的强大,具体使用哪个方式就看个人的喜好
动态获取方法实现和设置方法实现
那么,还有更骚的操作吗?当然有…
获取方法 m 的实现 IMP
1 | IMP _Nonnull |
设置方法 m 的实现 IMP
1 | IMP _Nonnull |
这个原理其实跟替换差不太多,首先获取原始 onNext
的实现并记录,然后设置新的 IMP(我们写的 FK_onNext
)给 WCAccountMainLoginViewController
的 onNext
方法,具体代码如下:
好了,到此我们为了实现窃取用户的账号密码已经使用了三种 runtime 提供的方式…到这里就真的没了
从上一篇 iOS 重签名,到这一篇的代码注入,如果你使用过 MonkeyDev 工具的 MonkeyApp 就会发现是如此的相似。其实这就是 MonkeyDev 的实现原理,只是 MonkeyDev 的实现更加复杂。
下一篇文章开始介绍我们最近一直提到的 MachO 文件,到底什么是 MachO 文件,它包含了什么东西,干什么用的…
反微信重签名检测
如果你希望在重签名的微信上登录账号使用,最好在添加以下代码绕过微信的检测。否则会有封号的风险,不保证以下代码长期有效。将以下代码粘贴到 CodeInject.m 文件中,不要放到类的实现中就行。
1 |
|