前面文章演示了如何对一个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
动态调试
其中:
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框架使用此方式,推荐使用这两个方法。
-