代码注入

732 阅读12分钟

上一篇我们已经讲到使用shell脚本来重签并调试别人的APP,那么我们又是重签又是附加调试别人的APP是为了啥呢?是吃的太饱了吗,当然不是...我们接下来的任务就是代码注入

iOSAPP执行哪些代码?

在开始代码注入之前,我们先了解一下一个iOS的APP在运行的时候,究竟会执行哪些代码,以及我们从哪里入手注入代码

  1. MachO(APP的二进制文件,我们写的所有代码都会在这里,后面的文章会介绍)
  2. Framework(可以是我们写的,也可以是第三方的代码)
  3. 系统库(系统提供的)

其中系统库,在非越狱手机上我们改不了;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文件的路径;为了方便我们使用这个工具,建议把它放到系统的usr/local/bin目录下,记得给它提升可执行权限(在终端chmod +x yololib),这样我们任意打开一个终端都可以使用这个命令了,也方便我们使用脚本来操作;好了,现在完事具备,只欠我们动手操作了...还记得上一篇文章讲到的内容吗,使用shell脚本重签APP,我们可以接着来,也可以新建一个项目来执行后面的代码注入,但前提依然是完成了shell脚本重签APP的步骤;我这里新建一个项目来演示后面的代码注入

代码注入的步骤

新建Framework

在完成了上一篇shell脚本重签的步骤之后,我们先来新建一个Framework,把我们的Framework准备好 image.png Framework的名字就叫FrankyHook(这个名字无所谓,但是你要记住,因为在使用yololib的时候要用到),在里面新建一个类CodeInject(这个叫什么也无所谓,继承NSObject就够了,主要是需要load方法),并在load方法里面打印点内容 image.png

在脚本中添加代码

记得红框部分的Framework名字,是你自己创建的...(要是照着我的抄的的当我没说 image.png 这里面有一个小细节,就是你的脚本运行一定要在嵌入Frameworks之前,不然每次脚本运行都把你的Framework给删掉了...(上面刚用yololib把你写的Framework路径加到MachO里面,这里脚本放在最后的话马上就把你的Framework给干掉了,会报image not found运行不起来的) image.png 实不相瞒,做了四年多的iOS开发,也是到今天才知道这个地方居然可以拖动...

command + R看控制台输出

image.png

怎么样,是不是发现我们已经可以在WeChat里面执行我们的代码了?接下来我们来实现两个小小的需求

需要具备的一些知识点

接下来的内容需要对Objective-C的rumtime有一定的了解,了解的同学这部分可以直接跳过,看下一部分
Method Swizzle方法交换
runtime提供了一些api来让我们实现方法交换,现在假设有这么一段代码: image.png 使用NSURL初始化URL的时候,如果URL中含有中文,那么初始化就会失败,返回nil;这个问题做过iOS开发的同学应该都遇到过,解决的办法就是对URL进行一次百分号编码,现在假设一种情景,你刚入职一家新公司,发现了这个问题,然后在工程里面搜索了一下,发现我的天啦,到处都是这种URL中夹着中文,而没有进行编码的情况,那么这个时候,你是选择一个一个的去修改呢,还是会想其他更好的办法?

使用方法交换就是更好的办法,新建一个NSURL的分类,在分类的load方法中,实现我们的方法交换 image.png 一个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 image.png 我们知道了注册按钮点击的时候,会调用WCAccountLoginControlLogiconFirstViewRegister方法,而且我们也已经可以在我们的Framework中执行我们的注入代码了,那么接下来我们如何实现拦截微信的注册按钮点击呢?当然可以使用Method Swizzle来实现

在开始写代码实现方法交换之前,还有一个小小的问题,虽然我们根据经验可以知道这个onFirstViewRegister应该是个对象方法,可能没有返回值,也没有参数,但这些都只是根据我们经验的猜测...咱们程序员还是应该靠谱一点,那么怎么验证我们的猜测呢?

Class-dumpyololib一样,也是一个终端命令行工具,同样也可以放到usr/local/bin目录下可以全局使用,它的作用是可以把MachO文件里的头文件信息全部导出来,我们可以cd到工程编译生成的APP包里面,使用以下命令
class-dump -H WeChat -o ./headers/
将它的头文件都导到一个headers的文件夹内,这个过程需要一点时间,耐心等待一下 image.png 看了下这个文件挺大的,导完之后可以把它剪切到工程根目录下,这样给我们手机也能省点空间,也确实完全没必要放在APP包里面 image.png 可以看到这里面有15074个项目,微信的头文件还真不少呢...这么大的文件夹如果我们用Xcode打开搜索的话,一定会十分的痛苦,这里推荐使用sublime...更轻量一点,找起头文件来也更快,搜索一番发现如图所示 image.png 这样就确保了这个onFirstViewRegister方法是无返回值无参数的,可以开始编写代码实现拦截了

现在我们可以来到我们Framework的CodeInject.m文件写点代码 image.png

代码就这么点,现在command + R运行起来之后,再次点击微信的注册按钮试试看? image.png

窃取用户的账号密码

上面的需求仅仅只是破坏了功能,原来的功能都没法使用了;接下来这个需求让用户在神不知鬼不觉的情况下,账号密码就被窃取了;那么我们在什么时候,能拿到用户的账号和密码呢,当然是点击登录按钮的时候,所以你懂的,使用viewDebug查看登录按钮 image.png 登录按钮点击的target:WCAccountMainLoginViewController,action:onNext;

查看刚刚class-dump出来的头文件发现这个onNext方法是没有参数的,那么我们需要的账号密码在哪里呢?哈哈,做过开发的同学是不是发现这两个名字是不是莫名的熟悉

WCAccountTextFieldItem *_textFieldUserNameItem;
WCAccountTextFieldItem *_textFieldUserPwdItem;

image.png

变量名是挺熟悉的,但是这个WCAccountTextFieldItem我们不知道是个什么,怎么办?头文件都在手上了,还问怎么办?接着搜啊... image.png

WCAccountTextFieldItem里面貌似没啥东西,那就看它父类WCBaseTextFieldItem image.pngWCBaseTextFieldItem类里面发现了一个WXUITextField的东西,跟我们熟悉的UITextField很像了,我们猜测可能就是这个m_textField存放着我们想要的东西,先不着急写代码

现在我们可以再次使用viewDebug工具调试查看一下我们的账号和密码在哪里,并且使用lldb来动态调试验证我们的猜测 image.png 首先控制器的地址,可以由登录按钮的target获取到;获取到控制器之后,再使用KVC大法获取成员变量_textFieldUserPwdItem的地址(密码都能获取到了,账号也是一样的操作)并打印它;获取到_textFieldUserPwdItem的地址之后,再次使用KVC大法获取它的成员变量m_textField的地址并打印这个对象,好家伙,明文密码不就在这儿了吗!!!

接下来我们通过代码来实现需求: image.png 这代码看起来跟拦截微信注册的代码差不太多,那么我们command + R运行看看结果 image.png WTF?账号密码确实是都获取到了,但是APP缺崩溃了?为什么会发生崩溃,崩溃信息是我们经常能够遇到的经典报错unrecognized selector sent to instance 0x10b15b400,说是控制器WCAccountMainLoginViewController无法识别FK_onNext这个方法;

我们仔细思考一下,方法交换为什么一般推荐写在想要交换方法的类所在的分类里面?因为在想要交换方法的类的分类当中,我们会新增一个方法,用来实现我们的逻辑,也正好是因为在分类中,所以当前类也添加了我们新增的方法,这样交换下来就不会出现找不到方法的错误;而上面的代码,我们的本意也是希望在WCAccountMainLoginViewController里面新增一个方法处理我们的逻辑,并交换onNext方法,但现在的问题是我们在CodeInject这个类的load方法中,那么有什么办法可以解决这个问题呢?

动态添加方法

rumtime的api提供了运行时动态添加方法的能力
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
参数1: 给哪个类添加方法
参数2: 方法的名字
参数3: 方法的实现
参数4: 方法的参数和返回值描述,用一些特定的符号表示,也可以不写
参数5: BOOL值,表示是否添加成功

那么借用这个api我们能想到什么解决办法呢,给WCAccountMainLoginViewController控制器添加我们的FK_onNext方法,再让FK_onNextonNext方法交换,最终的代码如下 image.png 再次command + R运行发现,既能成功获取到用户输入的账号密码,又能成功的去调用微信的登录逻辑了

动态替换方法

现在除了添加新方法的方式,我们还有其他的办法吗?当然有(强大的runtime)
rumtime的api还提供了运行时替换方法的能力
IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
参数1: 替换哪个类
参数2: 方法的名字
参数3: 方法的实现
参数4: 方法的参数和返回值描述,用一些特定的符号表示,也可以不写
返回值: IMP,原始的方法实现

如果使用这个方式,那么我们应该是将原始的onNext方法替换成我们的FK_onNext方法,那么我们如何去调用微信原始的onNext方法呢,在替换之前将原始的onNext方法的实现IMP记录下来,然后在我们的FK_onNext方法中使用这个记录的IMP来实现对原始onNext的调用,具体代码如下图: image.png 对比方法交换的代码实现,我们发现方法替换的方式需要多一个变量originalIMP,用来记录原始的onNext方法的实现,而且还会报个警告,虽然少了一点点代码,但看起来也不是那么好理解...当然,这里使用方法替换也只是为了学习一下runtime提供的api,感受一下rumtime的强大,具体使用哪个方式就看个人的喜好罢了

动态获取方法实现和设置方法实现

那么,还有更骚的操作吗?当然有...
IMP _Nonnull method_getImplementation(Method _Nonnull m)
获取方法m的实现IMP
IMP _Nonnull method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)
设置方法m的实现IMP
这个原理其实跟替换差不太多,首先获取原始onNext的实现并记录,然后设置新的IMP(我们写的FK_onNext)给WCAccountMainLoginViewControlleronNext方法,具体代码如下: image.png

好了,到此我们为了实现窃取用户的账号密码已经使用了三种runtime提供的方式...到这里就真的没了

下一篇文章开始介绍我们最近一直提到的MachO文件,到底什么是MachO文件,它包含了什么东西,干什么用的...