11-代码注入

1,485 阅读8分钟

前言

上篇文章10-应用重签名,我们利用CodeSign终端指令Shell脚本2种方式,成功实现了对微信app的重签名,已经能够查看微信的登录注册页面UI层级,接下来,我们想做些自己的事情,例如注入自己的代码,修改微信注册登录的流程。

一、代码注入

一般修改原始的程序,是利用代码注入的方式,注入代码就会选择利用Framework 或者Dylib等第三方库的方式注入。

1.1 FrameWork注入

我们知道,重签名app后自己的壳工程的代码就被替换掉了(替换的是MachO二级制执行文件),那么原有的工程代码并不会执行。iOS系统是通过dyld(iOS系统提供的动态链接器)加载MachO可执行文件,而加载的过程,首先就是读取MachO的Load Commands(其中包括_PAGEZERO、_TEXT、_DATA、_LINKEDIT等)👇

App的执行过程,除了执行自己的工程代码外,还依赖一些系统基础库(例如UIKit,Foudation等)和第三方的库(.framwork.a库等),而这些库最终也是一个MachO可执行文件,分析MachO文件会发现Frameworks中的库在Load Commands中以LC_LOAD_DYLIB存在,路径是Frameworks下对应库👇

image.png

只要Load Command中有对应库的LC_LOAD_DYLIBdyld就会去对应路径加载库

知道了这点,那么代码注入的过程就很明确了👇

  1. 确认目标App的MachO执行文件的Load Commands中有对应的LC_LOAD_DYLIB
  2. 将自己的代码写入动态库中

1.1.1 注入步骤

  1. 给自己的壳工程添加一个target 👉 LGHook Framework动态库

⚠️注意:这里的壳工程,用的是上篇文章10-应用重签名中的shell脚本重签名的模式。

image.png

image.png

  1. LGHook 添加LGInject类,并且重写+load方法

image.png

如果LGHook被加载进内存,则会打印log

  1. run,查看Produces.app

image.png

image.png

image.png

动态库LGHook已经在目标AppFramewroks中了,接着我们查看MachO👇

  1. 查看MachO文件

image.png

Load Commands中并没有LGHookLC_LOAD_DYLIB,所以run起来,控制台中并没有打印日志。

1.1.2 yololib手动注入

这个时候就需要使用yololib工具修改MachO文件,将LGHook真正加入到目标App的Load Commands中👇

  1. 拷贝yololib和目标App可执行文件同一目录👇(可单独新建一个文件夹)

image.png

然后执行命令👇

./yololib 目标可执行文件 要添加的Framework路径名称

例如👇

./yololib WeChat Frameworks/LGHook.framework/LGHook

image.png

再查看可执行文件Load Commands👇

image.png

此时就有LGHook了。

  1. 拿取原始.ipa包,解压,在Payload文件夹中找到.app,右键显示包内容,替换成上一步生成的二进制MachO文件,再回到Payload所在目录,输入以下指令生成.ipa
zip -ry xx.ipa Payload/

⚠️注意:这一步一定要用原始.ipa包,替换原始包里的二进制MachO文件。

  • 原始包微信8.0.2.ipa,将后缀改为.zip,解压👇

image.png

image.png

  • 右键显示包内容👇

image.png

  • 替换上一步的WeChat Mach-O👇

image.png

  • 返回到Payload所在目录,输入指令生成.ipa包👇

image.png

  1. 将上一步生成的.ipa包,放入APP文件夹中👇

image.png

cmd+shift+k先清空XCode项目的缓存,再来run👇

image.png

解决👇

image.png

修改LGHook.framework所支持的最低版本。

再次run👇

image.png

大功告成!🍺🍺🍺🍺🍺🍺🍺🍺🍺

小结

以上步骤大致为👇

  1. 新建壳工程WeChat,配置重签名脚本
  2. 通过Xcode新建LGHook.framwork(注意改支持的版本),真机运行,将库安装进入APP包
  3. 通过yololib,对WeChatMachO注入LGHook.framwork库路径。命令 👉 $./yololib MachO文件路径 库路径
  4. 使用原始微信.ipa包,将第3步生成的MachO,替换原始包里面的MachO,再次打包生成.ipa
  5. 壳工程的APP文件夹中,替换第4步生成的.ipa包,直接运行即可。

所有的Framwork加载都是由DYLD加载进入内存被执行的。注入成功的库路径会写入到MachO文件的LC_LOAD_DYLIB中。

1.2 Dylib注入

除了Framwork注入外,还能用Dylib注入。原理和Framwork相同,过程和Framwork差不多。

  1. 创建dylib库。这里选择了macOS,为了是库为动态库👇

image.png

image.png 修改Base SDKiOS👇

image.png

Code Signing Identity修改为iOS Developer👇

image.png

build Phases中添加Copy Files,增加libLGDylibHook.dylib👇

image.png

image.png

  1. LGDylibHook.m添加Hook代码👇
+ (void)load {
    NSLog(@"LGDylibHook Success 🍺🍺🍺🍺🍺🍺🍺🍺");
}

image.png 此时run,仍然只是Frameworks有libLGDylibHook.dylib库,MachOLoad Commands仍然没有LC_LOAD_DYLIB

1.2.1 yololib自动注入

  1. 直接在appResign.sh脚本中添加yololib修改MachO脚本的指令👇
#yololib修改MachO文件
#./yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/LGHook.framework/LGHook"
./yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/libLGDylibHook.dylib"

yololib库放在工程目录中👇

image.png

  1. 观察libLGDylibHook.dylib是否在Frameworks中👇

image.png

MachOLoad Commands👇

image.png

  1. 运行

⚠️注意:需要先编译libLGDylibHook.dylib再编译壳工程

image.png

小结

注入步骤

  1. 通过Xcode新建dylib库(注意:dylib属于MacOS,所以需要修改属性
    • Base SDKiOS
    • Code Signing Identity修改为iOS Developer
  2. 添加Target依赖,让Xcode将自定义Dylib文件打包进入APP
  3. 利用yololib进行注入

二、示例演示

接下来,我们实战一下,针对微信的注册登录页面,代码HOOK注册和登录的流程。 ###2.1 注册分析 首先是注册,我们查看注册按钮的UI层级👇

image.png

上图,打印地址可知,发现注册按钮的TargetWCAccountLoginControlLogicactiononFirstViewRegister

然后我们直接hook这个这个方法就可以拦截了注册事件👇

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

@implementation LGDylibHook

+ (void)load {
    NSLog(@"LGDylib  Hook success");
    //拦截微信注册事件
    Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountLoginControlLogic"), @selector(onFirstViewRegister));
    Method newMethod = class_getInstanceMethod(self, @selector(hook_onFirstViewRegister));
    method_exchangeImplementations(oldMethod, newMethod);
}

- (void)hook_onFirstViewRegister {
    NSLog(@"WeChat click login");
}

@end

运行看看👇

image.png

成功拦截!接下来,我们想在拦截到事件后,先执行自己的代码,然后恢复原有的流程,怎么做到呢?相信大家都能想到 👉 Method Swizzle

Method Swizzle

在OC中,SELIMP 之间的关系,就好像一本书的目录SEL 是方法编号,就像标题一样。IMP是方法实现的真实地址,就像页码一样。他们是一一对应的关系。 Runtime提供了交换两个SELIMP对应关系的函数。

image.png

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
   OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

通过这个函数交换两个SELIMP对应关系的技术称之为Method Swizzle(方法欺骗)

2.2 登录调试分析

接下来我们调试下【登录】功能👇

image.png

上图可知,view debug分析可以得到TargetWCAccountMainLoginViewControlleractiononNext。同理,【登录】事件可以拦截拿到,那么pwd怎么获取呢?

image.png

view debug可以看到pwd控件是WCUITextField并且能看到对应的text(即密码)。假如我们不借助view debug,怎么去获取密码呢?

2.2.1动态分析

第一种方式就是动态分析 👉 可以通过响应链一步步分析控件层级响应关系👇

image.png

结合presponderpviews分析。不过这种方式一般比较麻烦

2.2.2静态分析

第二种就是静态分析了 👉 使用class-dump工具导出头文件(⚠️swift文件不行)。

./class-dump -H WeChat -o ./Headers

image.png

执行完成后,所有头文件都在Headers文件夹中👇

image.png

文件过多22335个,不推荐Xcode打开。

  • 搜索WCAccountMainLoginViewController,找到onNext👇

image.png

onNext方法既没有参数也没有返回值,再看看有没其它的和密码相关的👇

image.png

我们发现了_textFieldUserPwdItem不就是密码的输入框吗。 2. 搜索WCAccountTextFieldItem

image.png

没有找到WCUITextField相关的内容,接着找父类WCBaseTextFieldItem

image.png

找到了WCUITextField类型的m_textField成员变量。这个不就是输入账号密码的控件。

  1. 调试验证

接下来我们来验证下,是否是输入密码的控件?👉 KVC控制台打印查看👇

image.png

相关指令👇

(lldb) po [(WCAccountMainLoginViewController *)0x117813000 valueForKey:@"_textFieldUserPwdItem"]
<WCAccountTextFieldItem: 0x2830f9ec0>

(lldb) po [(WCAccountTextFieldItem *)0x2830f9ec0 valueForKey:@"m_textField"]
<WCUITextField: 0x1120a4400; baseClass = UITextField; frame = (20 0; 345 44); text = 'qwer1234'; opaque = NO; autoresize = W+H; tintColor = UIExtendedSRGBColorSpace 0.027451 0.756863 0.376471 1; gestureRecognizers = <NSArray: 0x2818beca0>; layer = <CALayer: 0x281441dc0>>

(lldb) po [(WCUITextField *)0x1120a4400 text]
qwer1234

果然找到了对应的和想要的密码。🍺🍺🍺🍺🍺🍺🍺🍺

2.3 登录代码注入

⚠️注意:别用自己的常用账号去尝试,有可能被封号

最后,也是重点,注入自己的代码👇

+ (void)load {
    NSLog(@"LGDylib Hook success");
    //拦截微信登录事件
    Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
    Method newMethod = class_getInstanceMethod(self, @selector(hook_onNext));
    method_exchangeImplementations(oldMethod, newMethod);
}

- (void)hook_onNext {
    NSLog(@"WeChat click login");
    //获取密码
    UITextField *textField = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];
    NSString *pwd = [textField text];
    NSLog(@"password: %@",pwd);
}

image.png

接下来,在这个过程中我们应该调用回原来的方法,让登录进行下去👇

- (void)hook_onNext {
    NSLog(@"WeChat click login");
    //获取密码
    UITextField *textField = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];
    NSString *pwd = [textField text];
    NSLog(@"password: %@",pwd);
    [self hook_onNext];
}

运行👇

image.png

报错:找不到方法!WCAccountMainLoginViewController中不存在hook_onNext方法。因为👇

一般情况下我们进行方法交换分类中进行,现在不是在分类中.

那有没有解决方案呢?当然有,且有3种👇

class_addMethod方式

利用AddMethod方式,让原始方法可以被调用,不至于因为找不到SEL而崩溃👇

//1.class_addMethod 方式
+ (void)load {
    //拦截微信登录事件
    Class cls = objc_getClass("WCAccountMainLoginViewController");
    Method onNext = class_getInstanceMethod(cls, @selector(onNext));
    //给WC添加新方法
    class_addMethod(cls, @selector(new_onNext), new_onNext, "v@:");
    //交换
    method_exchangeImplementations(onNext, class_getInstanceMethod(cls, @selector(new_onNext)));
}

//相当于imp
void new_onNext(id self, SEL _cmd) {
    //获取密码
    UITextField *textField = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];
    NSString *pwd = [textField text];
    NSLog(@"password: %@",pwd);
    [self performSelector:@selector(new_onNext)];
}
class_replaceMethod方式

利用class_replaceMethod,直接给原始的方法替换IMP👇

//2.class_replaceMethod 方式
+ (void)load {
    //拦截微信登录事件
    Class cls = objc_getClass("WCAccountMainLoginViewController");
    //替换
    origin_onNext = class_replaceMethod(cls, @selector(onNext), new_onNext, "v@:");
}

//原始imp
IMP (*origin_onNext)(id self, SEL _cmd);

//相当于imp
void new_onNext(id self, SEL _cmd) {
    //获取密码
    UITextField *textField = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];
    NSString *pwd = [textField text];
    NSLog(@"password: %@",pwd);
    origin_onNext(self,_cmd);
}
method_setImplementation方式

利用method_setImplementation,直接重新赋值原始的IMP👇

//3.method_setImplementation 方式
+ (void)load {
    //拦截微信登录事件
    Class cls = objc_getClass("WCAccountMainLoginViewController");
    //获取method
    Method onNext = class_getInstanceMethod(cls,@selector(onNext));
    //get
    origin_onNext = method_getImplementation(onNext);
    //set
    method_setImplementation(onNext, new_onNext);
}

//原始imp
IMP (*origin_onNext)(id self, SEL _cmd);

//相当于imp
void new_onNext(id self, SEL _cmd) {
    //获取密码
    UITextField *textField = (UITextField *)[[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];
    NSString *pwd = [textField text];
    NSLog(@"password: %@",pwd);
    origin_onNext(self,_cmd);
}

以上3种方式都可以成功回到原来的登录流程。大家可以自行验证。

Demo(包含微信.ipa原始包)

XFResignHook

⚠️注意:微信ipa包体过大,无法上传gitHub,如果有需要,请留言发邮箱或私信我。

总结

  • 代码注入:Framework(推荐)/ dylib
    • 创建Framework/dylib
    • yololib修改Macho Load Commands
      • ./yololib 要修改的可执行文件 要添加的Framework/dylib路径&名称
  • 调试分析
    • 动态调试:view debug调试控件层级结合presponderpviews
    • 静态分析:通过class-dump导出头文件分析代码逻辑
  • 代码Hook
    • + load方法中hook对应类的对应方法
    • hook方法中调用回原来的方法
      1. class_addMethod 👉🏻 让原始方法可以被调用,不至于因为找不到SEL而崩溃
      2. class_replaceMethod 👉🏻 给原始的方法替换IMP
      3. method_setImplementation 👉🏻 重新赋值原始的IMP

相关链接

yololib
class-dump官网
class-dump github
AloneMonkey/MonkeyDev