一般修改原始程序,会利用代码注入的方式,注入代码就会选择利用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
方式:直接给原始的方法替换IMP
method_setImplementation
方式:直接重新赋值新的IMP
窃取登录密码
案例1:
点击登录按钮,输出用户输入的密码
延用
Framwork
代码注入的案例打开
rsign.sh
文件,加入以下代码:./yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/HOOK.framework/HOOK"
真机运行项目,进入登录页,使用
Debug View Hierarchy
动态调试找到登录按钮的
Target
和Action
Target
:WCAccountMainLoginViewController
Action
: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
框架使用此方式,推荐使用