一般修改原始程序,会利用代码注入的方式,注入代码就会选择利用FrameWork或dylib等三方库的方式注入。
注入原理
当运行重签名的App时,想让它触发当前项目中的代码,例如:写在ViewController的中代码,这是不可能的。因为项目中的App在安装时会被重签名App替换掉,它们根本不是一个MachO文件。
代码注入原理:
使用
MachOView分析可执行文件从
wx8.0.2.ipa中导出MachOView,可能会出现没有权限的错误提示
解决方法,使用
MachOView菜单中的File -> Open...
选择
成功解析
当
dyld加载可执行文件时,先读取MachO中的Header,获取MachO类型。然后读取Load Commands,通过读取__PAGEZERO、__TEXT、__DATA、__LINKEDIT等段,可以得到MachO的大小,代码段和数据段的位置,告知dyld应该如何将MachO加载到内存中。当dyld读取代码段时,通过读取LC_MAIN找到程序入口。所以读取Load Commands几乎可以读取到MachO的所有信息
dyld除了加载MachO,还要加载UIkit、Foundation等系统库,以及Frameworks目录下的动态库
在
Load Commands中,列出MachO所依赖的所有系统库以及三方库
所以
dyld会加载Load Commands中包含的所有库。如果将注入的代码包装成一个动态库,将其插入到Load Commands中,理论上动态库可以被加载,注入的代码也可以被执行
Framework注入
Framwork注入流程:
- 通过
Xcode新建Framwork,将库安装进入App包- 通过
yololib注入Framwork库路径./yololib MachO文件路径 库路径
- 所有的
Framwork加载都是由dyld加载进入内存被执行的- 注入成功的库路径会写入到
MachO文件的LC_LOAD_DYLIB字段中
案例1:
使用
Framework注入代码打开自动重签名
Framework动态库
命名
HOOK
动态库中创建
Class,命名Inject
打开
Inject.m文件,写入以下代码:#import "Inject.h" @implementation Inject +(void)load{ NSLog(@"\n\n\n\n\n🍺🍺🍺🍺🍺\n\n\n\n\n"); } @end编译项目,在
App包中的Frameworks目录,可以找到HOOK.framework动态库
此时
HOOK动态库中的代码还不能被执行,因为MachO的Load Commands中,还没有插入HOOK动态库的LC_LOAD_DYLIB字段
使用
yololib给MachO注入动态库将
MachO文件,拷贝到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!查看
MachO的Load Commands,成功将HOOK动态库插入到最后,路径是注入时设置的参数2
解压缩
wx8.0.2.ipa,将MachO文件替换,然后重新打包ipa
将重新打包的
wx8.0.2.ipa,拷贝到项目中APP目录
真机运行项目,
App安装成功,正常运行,同时打印注入代码
dylib注入
dylib注入流程:
- 通过
Xcode新建dylib库(注意:dylib属于macOS,所以需要修改属性)- 添加
Target依赖,让Xcode将自定义dylib文件打包进入App包- 利用
yololib进行注入
案例1:
使用
dylib注入代码打开自动重签名
dylib动态库
命名
HOOK
在
Build Settings中,将Base SDK设置项修改为iOS
将
Code Signing Identity设置项修改为iOS Developer
切换到
App的Target,选择Build Phases,点击+,选择New Copy Files Phase
在
Copy Files的Destination中,选择Frameworks
点击
+,选择libHOOK.dylib
打开
HOOK.m文件,写入以下代码#import "HOOK.h" @implementation HOOK +(void)load{ NSLog(@"\n\n\n\n\n🍺🍺 dylib 🍺🍺\n\n\n\n\n"); } @end使用
脚本注入动态库:将yololib文件,拷贝到项目根目录
打开
rsign.sh文件,加入以下代码:./yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/libHOOK.dylib"真机运行项目,
App安装成功,正常运行,同时打印注入代码
Method Swizzle
利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。
在
OC中,SEL和IMP之间的关系,就好像一本书的“目录”
SEL是方法编号,就像“标题”一样IMP是方法实现的真实地址,就像“页码”一样- 它们是一一对应的关系
Runtime提供了交换两个SEL和IMP对应关系的函数OBJC_EXPORT void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);通过这个函数交换两个
SEL和IMP对应关系的技术,我们称之为Method Swizzle(方法欺骗)
多种
HOOK方式
class_addMethod方式:让原始方法可以被调用,不至于因为找不到SEL而崩溃class_replaceMethod方式:直接给原始的方法替换IMPmethod_setImplementation方式:直接重新赋值新的IMP
窃取登录密码
案例1:
点击登录按钮,输出用户输入的密码
延用
Framwork代码注入的案例打开
rsign.sh文件,加入以下代码:./yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/HOOK.framework/HOOK"真机运行项目,进入登录页,使用
Debug View Hierarchy动态调试找到登录按钮的
Target和Action
Target:WCAccountMainLoginViewControllerAction:onNext想要打印密码框的内容,必须先找到密码框的位置,然后通过控件的属性拿到内容
【方式一】:动态调试
找到密码框的位置,找到存储内容的
text属性
- 可以通过
响应链条,根据左侧树状图结构,对ViewController.view.subviews进行递归遍历,找到符合条件的WCUITextField为止
使用动态调试,虽然也能找到密码框的位置,但寻找的过程太过繁琐,不推荐使用
【方式二】:静态分析
目前通过动态分析,可以确定登录按钮和密码框都在
WCAccountMainLoginViewController中使用
class-dump工具,通过MachO导出OC中所有类和方法列表以及成员变量将
MachO文件,拷贝到class-dump文件的同级目录
导出
OC中所有类和方法列表./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文件,没有找到WCUITextField控件,但发现一个WCAccountTextFieldItem类型控件,名称为_textFieldUserPwdItem
打开
WCAccountTextFieldItem文件,还是没有找到WCUITextField控件,但它继承自WCBaseTextFieldItem类,我们要找的控件很有可能在父类中
打开
WCBaseTextFieldItem文件,找到WCUITextField控件
使用代码之前,可以先结合
动态调试,验证能否顺利获取到用户密码
通过
动态调试,成功获取密码框里的内容
使用代码,获取用户密码
打开
Inject文件,写入以下代码:#import "Inject.h" #import <objc/runtime.h> #import <UIKit/UIKit.h> @implementation Inject +(void)load{ Method wx_onNext = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)); Method my_onNext = class_getInstanceMethod(self, @selector(hook_onNext)); method_exchangeImplementations(wx_onNext, my_onNext); } -(void)hook_onNext{ UITextField *txtPwd = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]; NSLog(@"密码:%@", txtPwd.text); } @end真机运行项目,点击登录按钮
密码:123456成功输出用户密码
案例2:
窃取密码后,希望可以调用原始的代码逻辑
日常开发中,方法交互一般写在当前类的分类中。由于交互后
hook_onNext指向了onNext的IMP,所以代码中直接调用hook_onNext即可打开
Inject文件,修改hook_onNext方法:-(void)hook_onNext{ UITextField *txtPwd = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]; NSLog(@"密码:%@", txtPwd.text); [self hook_onNext]; }但是,
HOOK动态库中的Inject文件,和WCAccountMainLoginViewController类没有任何关联。虽然hook_onNext指向onNext的IMP,但是调用objc_msgSend函数时,给VC发送hook_onNext,程序会因为找不到SEL而崩溃
下面介绍三种方式,都可以解决上述问题
【方式一】:
class_addMethod既然
VC中没有hook_onNext方法,可以使用class_addMethod函数给VC添加一个hook_onNext,调用时不会因为找不到SEL而崩溃打开
Inject文件,写入以下代码:#import "Inject.h" #import <objc/runtime.h> #import <UIKit/UIKit.h> @implementation Inject +(void)load{ Method wx_onNext = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)); class_addMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(hook_onNext), hook_onNext, @"v@:"); Method my_onNext = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(hook_onNext)); method_exchangeImplementations(wx_onNext, my_onNext); } void hook_onNext(id self, SEL _cmd){ UITextField *txtPwd = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]; NSLog(@"密码:%@", txtPwd.text); [self performSelector:@selector(hook_onNext)]; } @end
- 之前的
hook_onNext方法,改为函数实现,增加OC方法的两个隐式参数- 使用
class_addMethod函数,给VC增加hook_onNext方法,函数名即是IMP- 将
VC下的hook_onNext和onNext进行方法交互- 在
hook_onNext函数中,如果直接调用函数,调用的不是替换后的IMP,而是hook_onNext的IMP,这样会形成递归。所以想调用原始方法,要使用objc_msgSend或performSelector方式调用真机运行项目,点击登录按钮,窃取密码后,调用原始的代码逻辑
密码:123456
【方式二】:
class_replaceMethod将
VC中onNext方法的IMP替换打开
Inject文件,写入以下代码:#import "Inject.h" #import <objc/runtime.h> #import <UIKit/UIKit.h> @implementation Inject IMP (*oldImp)(id self, SEL _cmd); +(void)load{ oldImp = class_replaceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext), hook_onNext, @"v@:"); } void hook_onNext(id self, SEL _cmd){ UITextField *txtPwd = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]; NSLog(@"密码:%@", txtPwd.text); oldImp(self, _cmd); } @end
- 声明
IMP类型的oldImp函数指针- 使用
class_replaceMethod函数,直接将onNext的IMP替换为hook_onNext的IMP,将返回的原始IMP赋值给oldImp- 在
hook_onNext函数中,想调用原始方法,直接调用oldImp并传入隐式参数即可
【方式三】:
method_setImplementation获取原始的
IMP,重新赋值新IMP打开
Inject文件,写入以下代码:#import "Inject.h" #import <objc/runtime.h> #import <UIKit/UIKit.h> @implementation Inject IMP (*oldImp)(id self, SEL _cmd); +(void)load{ Method wx_onNext = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)); oldImp = method_getImplementation(wx_onNext); method_setImplementation(wx_onNext, hook_onNext); } void hook_onNext(id self, SEL _cmd){ UITextField *txtPwd = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]; NSLog(@"密码:%@", txtPwd.text); oldImp(self, _cmd); } @end
- 使用
method_getImplementation函数,获取onNext的IMP,赋值给oldImp- 使用
method_setImplementation函数,直接赋值hook_onNext的IMP
总结
注入原理:
Xcode将注入的动态库打包进App包中MachO的Load Commands中,包含注入动态库的LC_LOAD_DYLIB字段- 由
DYLD加载注入的动态库注入方式:
Framework注入dylib注入
Framwork注入流程:
- 通过
Xcode新建Framwork,将库安装进入App包- 通过
yololib注入Framwork库路径
dylib注入流程:
- 通过
Xcode新建dylib库(注意:dylib属于macOS,所以需要修改属性)- 添加
Target依赖,让Xcode将自定义dylib文件打包进入App包- 利用
yololib进行注入案例,窃取登录密码:
- 分析思路
- 动态调试,界面入手
- 静态分析,使用
class-dump,通过MachO导出OC的类和方法列表
Method Swizzle
- 使用
method_exchangeImplementations交互SEL和IMP对应关系,此方式存在隐患,如果不在同一个类,无法调用原始方法,因为找不到SEL而崩溃- 使用
class_addMethod添加方法,不至于因为找不到SEL而崩溃。过程比较复杂,不推荐使用- 使用
class_replaceMethod替换SEL的IMP- 使用
method_getImplementation和method_setImplementation配合,直接重新赋值新的IMP。逻辑清晰,大部分HOOK框架使用此方式,推荐使用





































