前面文章演示了如何对一个App进行重签名,本章将演示对重签名之后的App进行代码注入。
简介
代码注入是什么?
答:代码注入是在别人的App中添加我们的代码。
代码注入有哪些形式?
答:1. 直接注入汇编代码(这种方式只能用在简单测试。在iPhone中,我们可以通过LLDB、Cycript直接注入OC原生语言进行测试,这种方式更加简单粗暴);2. 以动态库形式注入(在iPhone中,动态库指的是Framework与dylib)。
代码注入原理是什么?
答:当dyld加载可执行文件时,先读取Mach-O中的Header,获取Mach-O类型。然后读取Load Commands,通过读取__PAGEZERO、__TEXT、__DATA、__LINKEDIT等段,可以得到Mach-O的大小,代码段和数据段的位置,告知dyld应该如何将Mach-O加载到内存中。当dyld读取代码段时,通过读取LC_MAIN找到主程序入口。

dyld除了加载Mach-O,还要加载UIkit、Foundation等系统库,而Mach-O中Load Commands列出了App所依赖的所有的动态库(包括系统动态库和第三方动态库)。

在App包中,App所依赖的第三方动态库存放在Frameworks目录下,而系统库存在共享缓存中。

因此,如果将注入的代码包装成一个动态库,将其插入到Load Commands中,理论上动态库可以被加载,注入的代码也可以被执行。
代码注入演示
Framework注入
【步骤1】创建一个自定义Framework,名为HOOK,并新建一个自定义类Inject。
【步骤2】由类的加载中知道,当Inject类实现load方法时,load方法会在加载Mach-O的时候执行。因此,在Inject类中增加load方法,并通过打印输出信息,以方便观察自定义Framework有被成功注入到App中。
+(void)load{
NSLog(@"\n\n\n\n\n🍺🍺🍺🍺🍺\n\n\n\n\n");
}
【步骤3】将自定义Framework编译生成HOOK.framework。
【步骤4】将HOOK.framework添加至App的Framework目录下。注意:只是添加至目录下是不行的,因为dyld是按照Mach-O进行加载的,此时Mach-O中Load Commands并没有HOOK.framework的LC_LOAD_DYLIB字段。
【步骤5】给Mach-O中的Load Commands添加关于HOOK.framework的LC_LOAD_DYLIB字段。这里使用yololib工具。
#yololib指令如下:
~/yololib WeChat Frameworks/HOOK.framework/HOOK
2021-04-22 17:14:52.339 yololib[22774:33058817] dylib path @executable_path/Frameworks/HOOK.framework/HOOK
2021-04-22 17:14:52.340 yololib[22774:33058817] dylib path @executable_path/Frameworks/HOOK.framework/HOOK
Reading binary: WeChat
2021-04-22 17:14:52.341 yololib[22774:33058817] Thin 64bit binary!
2021-04-22 17:14:52.341 yololib[22774:33058817] dylib size wow 72
2021-04-22 17:14:52.341 yololib[22774:33058817] mach.ncmds 124
2021-04-22 17:14:52.341 yololib[22774:33058817] mach.ncmds 125
2021-04-22 17:14:52.341 yololib[22774:33058817] Patching mach_header..
2021-04-22 17:14:52.399 yololib[22774:33058817] Attaching dylib..
2021-04-22 17:14:52.399 yololib[22774:33058817] size 71
2021-04-22 17:14:52.399 yololib[22774:33058817] complete!
执行完成后,再查看Mach-O,发现Load Commands中新增了LC_LOAD_DYLIB(HOOK)。

【步骤6】将修改的Mach-O通过Xcode下载到真机并运行,App安装成功,正常运行,同时打印注入代码。

dylib注入
【步骤1】创建dylib动态库,File->New->Project->macOS->Library。
【步骤2】修改Base SDK和Code Signing Identity。
Build Settings -> Base SDK -> iOS
Build Settings -> Code Signing Identity -> iOS Developer
【步骤3】给WeChat Demo项目增加libHOOK.dylib。
添加方法:1. 直接将libHOOK.dylib拷贝至Framework目录下。2. 在项目的Build Phases下新增Copy Files,选择Frameworks,点击+,选择libHOOK.dylib。


【步骤4】打开HOOK.m文件,写入以下代码
+(void)load{
NSLog(@"\n\n\n\n\n🍺🍺 dylib 🍺🍺\n\n\n\n\n");
}
【步骤5】之前是手动使用yololib工具将framework注入到Mach-O中,这里我们换成脚本的方法。在rsign.sh中添加以下语句:
./yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/libHOOK.dylib"
【步骤6】真机运行项目,App安装成功,正常运行,同时打印注入代码。

通过代码注入获取密码信息
以上的两种方法能在重签名的App中运行我们的代码,根据这个思路,我们试一下,能否在微信登陆的时候,获取到微信的密码。
思路:
- 找到
登录按钮的Target和Action。 - 将
登陆按纽的Action换成自定义的Action。
接下来就按着这个思路开始进行密码获取吧。这里使用framework注入方式。
获取WeChat登陆按纽的Target和Action
- 真机运行项目,进入登录页,使用
Debug View Hierarchy动态调试
image.png
其中:
Target:WCAccountMainLoginViewController
Action:onNext
此时,想要打印密码框的内容,必须先找到密码框的位置,然后通过控件的属性拿到内容。这有几种方式:
【方式1】使用Debug View Hierarchy动态调试,直接找到密码框的位置,查看其属性得到密码

【方式2】使用静态分析的方法,我们已经确定登录按钮和密码框都在WCAccountMainLoginViewController中,那通过MachO导出OC中这个类的方法列表以及成员变量,这就能准确定位到密码控件,从而获取密码。而通过MachO导出OC中方法列表可通过class-dump工具。
./class-dump -H WeChat -o ./headers/
-------------------------
2021-04-25 13:51:24.070 class-dump[31659:33427860] Warning: Parsing instance variable type failed, ready_
2021-04-25 13:51:26.308 class-dump[31659:33427860] Warning: Parsing instance variable type failed, underlying
2021-04-25 13:51:26.308 class-dump[31659:33427860] Warning: Parsing instance variable type failed, enable
...
2021-04-25 13:52:09.288 class-dump[31659:33427860] Warning: Parsing method types failed, getKeyExtensionList:
2021-04-25 13:52:09.292 class-dump[31659:33427860] Warning: Parsing method types failed, getKeyExtensionList:
2021-04-25 13:52:09.292 class-dump[31659:33427860] Warning: Parsing method types failed, getExtensionListForSelector:
打开headers目录,列出所有导出的文件列表,找到WCAccountMainLoginViewController文件

打开WCAccountMainLoginViewController文件,很明显_textFieldUserPwdItem将是我们要找的密码控件。

打开WCAccountTextFieldItem头文件,没有找到WCUITextField控件,但它继承自WCBaseTextFieldItem类,我们要找的控件很有可能在父类中。

打开WCBaseTextFieldItem头文件,果然找到WCUITextField控件。

找到WCUITextField控件,接下来就可以通过lldb查看WCUITextField控件的text了。

替换Action
以上是通过lldb获取到用户的密码了,接下来,我们的需求是:用户点击登陆按纽时,直接打印用户的密码。
由前面的分析知道,当应用启动的时候,会打印注入类的+load方法中的内容,说明应用启动的时候,会调用+load方法。那我们可以在+load方法,对用户登陆按纽执行的Action进行替换,使其替换成我们的方法。
- 替换
Action
+(void)load{
Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
Method newMethod = class_getInstanceMethod(self, @selector(my_onNext));
method_exchangeImplementations(oldMethod, newMethod);
}
- 打印密码
- (void)my_onNext{
UITextField* pwd = (UITextField*)([[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]);
NSLog(@"密码:%@", pwd.text);
}
真机运行项目,点击登录按钮,此时就能打印密码了
2021-04-28 17:16:33.791642+0800 WeChat[21348:1339926] 密码:121212
但是目前的处理不太好,我们的目的是获取了密码,但不能破坏应用程序的流程,此时就有点不好处理了。
在OC中,采用消息机制,即向某个类发送消息。在原来的运行流程:
objc_msgSend(id self, SEL op);
//self:WCAccountMainLoginViewController
//op:onNext
//SEL:onNext 对应的IMP:原登陆流程
经过方法交换之后的流程:
objc_msgSend(id self, SEL op);
//self:WCAccountMainLoginViewController
//op:onNext
//SEL:onNext 对应的IMP:my_onNext函数地址
//SEL:my_onNext对应的IMP:原登陆流程
此时在my_onNext函数中,想要继续原登陆流程,则需要向原Inject对象发送SEL为my_onNext的消息, 而my_onNext函数中无法获取到原Inject对象,因此无法继续原登陆流程。
其实也并不是完全没有解决办法。以下介绍几种简单的方法:
【方法1】保存原my_onNext对应的IMP,在获取到密码之后再通过IMP执行原流程。
void my_onNext(id self, SEL _cmd){
UITextField* pwd = (UITextField*)([[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]);
NSLog(@"密码:%@", pwd.text);
oldImp(self, _cmd);
}
获取并保存IMP可通过以下几种方式:
- 通过
method_getImplementation直接获取到IMP,并将其保存。
IMP (*oldImp)(id self, SEL _cmd);
+(void)load{
Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
oldImp = method_getImplementation(oldMethod);
Method newMethod = class_getInstanceMethod(self, @selector(my_onNext));
method_exchangeImplementations(oldMethod, newMethod);
}
- 通过
class_replaceMethod函数,该函数会替换原流程的方法后,将原流程的IMP返回。
IMP (*oldImp)(id self, SEL _cmd);
+(void)load{
oldImp = class_replaceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext), my_onNext, @"v@:");
}
【方法2】在+load方法中,不使用方法交换。而使用添加方法。
+(void)load{
Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
Method newMethod = class_getInstanceMethod(self, @selector(my_onNext));
class_addMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(my_onNext), my_onNext, @"v@:");
method_exchangeImplementations(oldMethod, newMethod);
}
- (void)my_onNext{
UITextField* pwd = (UITextField*)([[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]);
NSLog(@"密码:%@", pwd.text);
[self performSelector:@selector(my_onNext)];
}
【方法3】通过method_setImplementation函数,使得旧的SEL对应于新的IMP,并通过method_getImplementation函数保存旧的IMP。
IMP (*oldImp)(id self, SEL _cmd);
+(void)load{
Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
oldImp = method_getImplementation(oldMethod);
method_setImplementation(onNext, my_onNext);
}
- (void)my_onNext{
UITextField* pwd = (UITextField*)([[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]);
NSLog(@"密码:%@", pwd.text);
oldImp(self, _cmd);
}
总结
-
代码注入的方式:
- 汇编直接注入
- 动态库注入(framework、dylib)
-
动态库注入的原理
- 将
自定义动态库拷贝至App的Frameworks目录下。 - 通过
yololib工具修改Mach-O,使其包含增加新增动态库的LC_LOAD_DYLIB字段。
- 将
-
获取登陆密码案例
- 使用
Debug View Hierarchy动态调试,定位登陆密码按键触发时的Target和Action。 - 通过
class-dump工具导出MachO中OC的类和方法列表。 - 定位到
密码控件,通过valueForKey的方法获取成员变量的值。
- 使用
-
Method Swizzle
-
method_exchangeImplementations:交换SEL和IMP,此方法存在隐患,若两个SEL不在同一个类,想要继续走原来的流程可能会发生崩溃。 -
class_addMethod:为原来的类添加方法。 -
class_replaceMethod:将新的IMP替换SEL的IMP,并将旧的IMP返回,可将函数返回保存,后续通过函数指针调用回到原流程。 -
method_getImplementation和method_setImplementation:设置IMP和获取IMP。大部分HOOK框架使用此方式,推荐使用这两个方法。
-
