iOS逆向 代码注入+Hook

6,868 阅读9分钟

欢迎阅读iOS逆向系列(按序阅读食用效果更加)

写在前面

本文涉及内容无风险,但某信有检测BundId机制,建议不要大号登录

本文是建立在应用重签名的基础上

iOS逆向 应用重签名+微信重签名实战

iOS逆向 Shell脚本+脚本重签名

工具:yololib+class_dump 密码:8ujj

一、初次注入

代码注入有两种方案:通过FrameWork和dylib

1.脚本重签名

照着iOS逆向 Shell脚本+脚本重签名重签名

2.FrameWork注入

2.1 新建FrameWork

在Xcode中File->Target新增一个Framework

2.2 FrameWork中新建一个类
2.3 添加一个load方法

仅仅这样还不够,DYLD会动态加载项目中的Frameworks,但不会加载当前FrameWork

2.4 运行编译一下

保证FrameWork放到FrameWorks目录下

2.5 yololib注入动态库

建议将yololib复制粘贴到/usr/local/bin目录下,可以随时随地调用

app.sh的最后一句代码启用(注意修改FrameWork名称)

yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/XXXX.framework/XXXX"
2.6 运行

不出意外会打印 ❎❎❎❎❎❎❎❎❎❎

3.dylib注入

其实就是换了个Target

3.1 新建Library

3.2 修改dylib的BaseSDK

3.3 修改dylib的签名

修改成iPhone Developer

3.4 添加依赖

3.5 运行编译

只有加进来了才算成功了一半

3.6 修改脚本
yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/libFXHook.dylib"
3.7 运行

打印 ❎❎❎❎❎❎❎❎❎❎ (有时候会报错“image notound”,如果FrameWorks包含了dylib则重新运行就好了)

二、Method Swizzling初用

1.定义

在OC中,SEL和IMP之间的关系,就好像一本书的“目录”。 SEL是方法编号,就像“标题”一样。 IMP是方法实现的真实地址,就像“页码”一样。 他们是一一对应的关系。 Runtime提供了交换两个SEL和IMP对应关系的函数method_exchangeImplementations(<#Method _Nonnull m1#>, <#Method _Nonnull m2#>),通过这个函数交换两个SEL和IMP对应关系的技术,我们称之为Method Swizzle(方法欺骗)

我更愿意把SEL和IMP的关系理解成书的封面和书,原先一本《三国》和《水浒》,在经过方法交换之后翻开《三国》的封面却是《水浒》的内容

2.代码演示

NSURL *url = [NSURL URLWithString:[@"www.Felix.com/好好学习" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
NSLog(@"%@",url);

比如上述代码看起来很繁琐,这时候就可以用Method Swizzling来实现

2.1 新建NSURL的分类
2.2 方法交换
#import "NSURL+FXUrl.h"
#import <objc/runtime.h>

@implementation NSURL (FXUrl)

+ (void)load {
    // 获取原来的方法
    Method URLWithString = class_getClassMethod(self, @selector(URLWithString:));
    // 获取自定义方法
    Method FXURLWithString = class_getClassMethod(self, @selector(FX_URLWithString:));
    // 交换方法
    method_exchangeImplementations(URLWithString, FXURLWithString);
}

+ (instancetype)FX_URLWithString:(NSString *)string {
    NSURL *url = [NSURL FX_URLWithString:string];
    if (!url) {
        string = [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    }
    return [NSURL FX_URLWithString:string];
}

@end
2.3 疑问点

①为什么要用分类去做方法交换呢?

下文中将会提及

②自定义FX_URLWithString中是不是递归了?

答:load方法执行顺序较早,调用FX_URLWithString时已经进行了方法交换,想调用FX_URLWithString就应该调用URLWithString

各位看官可能觉得太简单了,接下来就回到重签名项目开始重头戏

三、Hook微信——破坏微信注册

目标:点击“注册”按钮使之无效

1.获取到对象名称和方法名称

之前文章中有讲到过,选中控件就能通过地址打印对应的信息(有可能直接显示了对象名称和方法名称)

2.利用class-dump导出MachO的头文件

MachO文件在编译出来的ipa包中

3.搜索头文件查看方法声明

这里用的是sublime工具,先全局找类(找不到就找父类)再找方法

4.交换方法

在第一节注入FrameWork的代码中继续

#import "InjectCode.h"
#import <objc/runtime.h>

@implementation InjectCode

+ (void)load {
    Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountLoginControlLogic"), @selector(onFirstViewRegister));
    Method newMethod = class_getInstanceMethod(self, @selector(FX_onFirstViewRegister));
    method_exchangeImplementations(oldMethod, newMethod);
}

- (void)FX_onFirstViewRegister {
    NSLog(@"想注册吗?想注册先打钱!");
}

@end

四、Hook微信——窃取登录密码

目标:点击“登录”按钮获取到账号密码并继续登录

1.分析

打印地址去头文件列表查找声明方法

发现这是一个不带参数的方法,那么账号密码去那里获取呢?

我们可以去Viewcontroller的变量中找找线索

发现了两个可疑的实例变量_textFieldUserNameItem_textFieldUserPwdItem

查看WCAccountTextFieldItem没发现什么有实际意义的内容,那再找找父类吧

WCUITextField *m_textField这个实例变量看起来有点用

怎么判断是否是我们要找的账号密码呢?

输入账号和密码再ViewDebug调试一下

如下图所示,我们找到了写Hook代码的方向

(lldb) po 0x133800600
<WCAccountMainLoginViewController: 0x133800600>

(lldb) po [(WCAccountMainLoginViewController *)0x133800600 valueForKey:@"_textFieldUserNameItem"]
<WCAccountTextFieldItem: 0x28231f180>

(lldb) po [(WCAccountTextFieldItem *)0x28231f180 valueForKey:@"m_textField"]
<WCUITextField: 0x13090d600; baseClass = UITextField; frame = (20 0; 345 44); text = 'Felix'; opaque = NO; autoresize = W+H; tintColor = UIExtendedSRGBColorSpace 0.00784314 0.733333 0 1; gestureRecognizers = <NSArray: 0x280891650>; layer = <CALayer: 0x280612560>>

2.开始Hook

#import "InjectCode.h"
#import <objc/runtime.h>
#import "UIKit/UIKit.h"

//@interface WCAccountTextFieldItem: NSObject
//
//@end

@implementation InjectCode

+ (void)load {
    Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
    Method newMethod = class_getInstanceMethod(self, @selector(FX_onNext));
    method_exchangeImplementations(oldMethod, newMethod);
}

- (void)FX_onNext {
//    /// 声明WCAccountTextFieldItem类,为了不报错
//    WCAccountTextFieldItem *account = [self valueForKey:@"_textFieldUserNameItem"];
//    /// 导入UIKit框架
//    UITextField *accountTF = [account valueForKey:@"m_textField"];
    
    UITextField *accountTF = [[self valueForKey:@"_textFieldUserNameItem"] valueForKey:@"m_textField"];
    UITextField *passwordTF = [[self valueForKey:@"_textFieldUserNameItem"] valueForKey:@"m_textField"];
    NSLog(@"账号:“%@”\n密码:“%@”", accountTF.text, passwordTF.text);
    
    [self FX_onNext];
}

@end

正当我们满心欢喜等待神奇的一刻时,熟悉的味道来了

崩溃原因:WCAccountMainLoginViewController找不到FX_onNext的方法编号,即原工程中WCAccountMainLoginViewController没有FX_onNext声明

OC方法调用有两个隐藏参数:self(方法调用者)、cmd(方法编号),FrameWork中把onNext的imp替换成了FX_onNext,页面调用登录方法来到我们自定义的方法实现;然后给VC发送FX_onNext消息,必然是unrecognized selector sent to instance

此时此刻用分类Hook的好处就体现的淋漓尽致,直接给分类加个方法就完事了

3.解决崩溃完成Hook

3.1 class_addMethod方法

利用class_addMethod方法让原始方法可以被调用(麻烦不推荐)

#import "InjectCode.h"
#import <objc/runtime.h>
#import "UIKit/UIKit.h"

@implementation InjectCode

+ (void)load {
    /**
     * 1、给哪个类添加方法
     * 2、方法编号
     * 3、方法实现(地址)
     * 4、v代表Void @代表id类型 :代表@selecter类型(可以在帮助文档查看这个方法)
     */
    BOOL didAddMethod = class_addMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(FX_onNext), FX_onNext, "v@:");
    
    if (didAddMethod) {
        NSLog(@"添加方法成功");
        Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
        Method newMethod = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(FX_onNext));
        method_exchangeImplementations(oldMethod, newMethod);
    }
}

//方法实现IMP
void FX_onNext(id self, SEL _cmd) {
    UITextField *accountTF = [[self valueForKey:@"_textFieldUserNameItem"] valueForKey:@"m_textField"];
    UITextField *passwordTF = [[self valueForKey:@"_textFieldUserNameItem"] valueForKey:@"m_textField"];
    NSLog(@"账号:“%@” 密码:“%@”", accountTF.text, passwordTF.text);
    
    //使用原来逻辑
    [self performSelector:@selector(FX_onNext)];
}

@end
3.2 class_replaceMethod方法

保存原始方法,利用replaceMethod方法将原始方法的IMP覆盖

#import "InjectCode.h"
#import <objc/runtime.h>
#import "UIKit/UIKit.h"

@implementation InjectCode

+ (void)load {
    onNext = method_getImplementation(class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)));
    class_replaceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext), FX_onNext, "v@:");
}

IMP (*onNext)(id self,SEL _cmd);

void FX_onNext(id self, SEL _cmd) {
    UITextField *accountTF = [[self valueForKey:@"_textFieldUserNameItem"] valueForKey:@"m_textField"];
    UITextField *passwordTF = [[self valueForKey:@"_textFieldUserNameItem"] valueForKey:@"m_textField"];
    NSLog(@"账号:“%@” 密码:“%@”", accountTF.text, passwordTF.text);
    
    //使用原来逻辑
    onNext(self,_cmd);
}
3.3 method_setImplementation方法

保存原始方法,利用setImplementation方法将原始方法的IMP重写

#import "InjectCode.h"
#import <objc/runtime.h>
#import "UIKit/UIKit.h"

@implementation InjectCode

+ (void)load {
    onNext = method_getImplementation(class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)));
    method_setImplementation(class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)), FX_onNext);
}

IMP (*onNext)(id self,SEL _cmd);

//方法实现IMP
void FX_onNext(id self, SEL _cmd) {
    UITextField *accountTF = [[self valueForKey:@"_textFieldUserNameItem"] valueForKey:@"m_textField"];
    UITextField *passwordTF = [[self valueForKey:@"_textFieldUserNameItem"] valueForKey:@"m_textField"];
    NSLog(@"账号:“%@” 密码:“%@”", accountTF.text, passwordTF.text);
    
    //使用原来逻辑
    onNext(self,_cmd);
}

@end

五、Runtime-API

// 1.objc_xxx 系列函数
// 函数名称     函数作用
objc_getClass     获取Class对象
objc_getMetaClass     获取MetaClass对象
objc_allocateClassPair     分配空间,创建类(仅在 创建之后,注册之前 能够添加成员变量)
objc_registerClassPair     注册一个类(注册后方可使用该类创建对象)
objc_disposeClassPair     注销某个类
objc_allocateProtocol     开辟空间创建协议
objc_registerProtocol     注册一个协议
objc_constructInstance     构造一个实例对象(ARC下无效)
objc_destructInstance     析构一个实例对象(ARC下无效)
objc_setAssociatedObject     为实例对象关联对象
objc_getAssociatedObje*ct     获取实例对象的关联对象
objc_removeAssociatedObjects     清空实例对象的所有关联对象

objc_系列函数关注于宏观使用,如类与协议的空间分配,注册,注销等操作

// 2.class_xxx 系列函数
函数名称     函数作用
class_addIvar     为类添加实例变量
class_addProperty     为类添加属性
class_addMethod     为类添加方法
class_addProtocol     为类遵循协议
class_replaceMethod     替换类某方法的实现
class_getName     获取类名
class_isMetaClass     判断是否为元类
objc_getProtocol     获取某个协议
objc_copyProtocolList     拷贝在运行时中注册过的协议列表
class_getSuperclass     获取某类的父类
class_setSuperclass     设置某类的父类
class_getProperty     获取某类的属性
class_getInstanceVariable     获取实例变量
class_getClassVariable     获取类变量
class_getInstanceMethod     获取实例方法
class_getClassMethod     获取类方法
class_getMethodImplementation     获取方法的实现
class_getInstanceSize     获取类的实例的大小
class_respondsToSelector     判断类是否实现某方法
class_conformsToProtocol     判断类是否遵循某协议
class_createInstance     创建类的实例
class_copyIvarList     拷贝类的实例变量列表
class_copyMethodList     拷贝类的方法列表
class_copyProtocolList     拷贝类遵循的协议列表
class_copyPropertyList     拷贝类的属性列表

class_系列函数关注于类的内部,如实例变量,属性,方法,协议等相关问题

// 3.object_xxx 系列函数
函数名称     函数作用
object_copy     对象copy(ARC无效)
object_dispose     对象释放(ARC无效)
object_getClassName     获取对象的类名
object_getClass     获取对象的Class
object_setClass     设置对象的Class
object_getIvar     获取对象中实例变量的值
object_setIvar     设置对象中实例变量的值
object_getInstanceVariable     获取对象中实例变量的值 (ARC中无效,使用object_getIvar)
object_setInstanceVariable     设置对象中实例变量的值 (ARC中无效,使用object_setIvar)

objcet_系列函数关注于对象的角度,如实例变量

// 4.method_xxx 系列函数
函数名称     函数作用
method_getName     获取方法名
method_getImplementation     获取方法的实现
method_getTypeEncoding     获取方法的类型编码
method_getNumberOfArguments     获取方法的参数个数
method_copyReturnType     拷贝方法的返回类型
method_getReturnType     获取方法的返回类型
method_copyArgumentType     拷贝方法的参数类型
method_getArgumentType     获取方法的参数类型
method_getDescription     获取方法的描述
method_setImplementation     设置方法的实现
method_exchangeImplementations     替换方法的实现

method_系列函数关注于方法内部,如果方法的参数及返回值类型和方法的实现

// 5.property_xxx 系列函数
函数名称     函数作用
property_getName     获取属性名
property_getAttributes     获取属性的特性列表
property_copyAttributeList     拷贝属性的特性列表
property_copyAttributeValue     拷贝属性中某特性的值

property_系类函数关注与属性*内部,如属性的特性等

// 6.protocol_xxx 系列函数
函数名称     函数作用
protocol_conformsToProtocol     判断一个协议是否遵循另一个协议
protocol_isEqual     判断两个协议是否一致
protocol_getName     获取协议名称
protocol_copyPropertyList     拷贝协议的属性列表
protocol_copyProtocolList     拷贝某协议所遵循的协议列表
protocol_copyMethodDescriptionList     拷贝协议的方法列表
protocol_addProtocol     为一个协议遵循另一协议
protocol_addProperty     为协议添加属性
protocol_getProperty     获取协议中的某个属性
protocol_addMethodDescription     为协议添加方法描述
protocol_getMethodDescription     获取协议中某方法的描述

// 7.ivar_xxx 系列函数
函数名称     函数作用
ivar_getName     获取Ivar名称
ivar_getTypeEncoding     获取类型编码
ivar_getOffset     获取偏移量

// 8.sel_xxx 系列函数
函数名称     函数作用
sel_getName     获取名称
sel_getUid     注册方法
sel_registerName     注册方法
sel_isEqual     判断方法是否相等

// 9.imp_xxx 系列函数
函数名称     函数作用
imp_implementationWithBlock     通过代码块创建IMP
imp_getBlock     获取函数指针中的代码块
imp_removeBlock     移除IMP中的代码块

写在结尾

习武是为了强身健体,学习逆向是为了防护