11 - 代码注入

前面文章演示了如何对一个App进行重签名,本章将演示对重签名之后的App进行代码注入

简介

代码注入是什么?
答:代码注入是在别人的App中添加我们的代码。

代码注入有哪些形式
答:1. 直接注入汇编代码(这种方式只能用在简单测试。在iPhone中,我们可以通过LLDB、Cycript直接注入OC原生语言进行测试,这种方式更加简单粗暴);2. 以动态库形式注入(在iPhone中,动态库指的是Framework与dylib)

代码注入原理是什么?
答:当dyld加载可执行文件时,先读取Mach-O中Header,获取Mach-O类型。然后读取Load Commands,通过读取__PAGEZERO、__TEXT、__DATA、__LINKEDIT等段,可以得到Mach-O的大小,代码段和数据段的位置,告知dyld应该如何将Mach-O加载到内存中。当dyld读取代码段时,通过读取LC_MAIN找到主程序入口。

Mach-O.png

dyld除了加载Mach-O,还要加载UIkit、Foundation等系统库,而Mach-OLoad Commands列出了App所依赖的所有的动态库(包括系统动态库和第三方动态库)。

image.png

在App包中,App所依赖的第三方动态库存放在Frameworks目录下,而系统库存在共享缓存中。

Frameworks.png

因此,如果将注入的代码包装成一个动态库,将其插入到Load Commands中,理论上动态库可以被加载,注入的代码也可以被执行。

代码注入演示

Framework注入

【步骤1】创建一个自定义Framework,名为HOOK,并新建一个自定义类Inject。

【步骤2】由类的加载中知道,当Inject类实现load方法时,load方法会在加载Mach-O的时候执行。因此,在Inject类中增加load方法,并通过打印输出信息,以方便观察自定义Framework有被成功注入到App中。

+(void)load{
   NSLog(@"\n\n\n\n\n\n\n\n\n\n");
}

【步骤3】将自定义Framework编译生成HOOK.framework

【步骤4】将HOOK.framework添加至App的Framework目录下。注意:只是添加至目录下是不行的,因为dyld是按照Mach-O进行加载的,此时Mach-OLoad Commands并没有HOOK.framework的LC_LOAD_DYLIB字段。

【步骤5】给Mach-O中的Load Commands添加关于HOOK.framework的LC_LOAD_DYLIB字段。这里使用yololib工具。

#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!

执行完成后,再查看Mach-O,发现Load Commands中新增了LC_LOAD_DYLIB(HOOK)

image.png

【步骤6】将修改的Mach-O通过Xcode下载到真机并运行,App安装成功,正常运行,同时打印注入代码。

image.png

dylib注入

【步骤1】创建dylib动态库,File->New->Project->macOS->Library。

【步骤2】修改Base SDKCode Signing Identity
Build Settings -> Base SDK -> iOS
Build Settings -> Code Signing Identity -> iOS Developer

【步骤3】给WeChat Demo项目增加libHOOK.dylib。
添加方法:1. 直接将libHOOK.dylib拷贝至Framework目录下。2. 在项目的Build Phases下新增Copy Files,选择Frameworks,点击+,选择libHOOK.dylib

image.png

image.png

【步骤4】打开HOOK.m文件,写入以下代码

+(void)load{
  NSLog(@"\n\n\n\n\n dylib \n\n\n\n\n");
}

【步骤5】之前是手动使用yololib工具将framework注入到Mach-O中,这里我们换成脚本的方法。在rsign.sh中添加以下语句:

./yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/libHOOK.dylib"

【步骤6】真机运行项目,App安装成功,正常运行,同时打印注入代码。


image.png

通过代码注入获取密码信息

以上的两种方法能在重签名的App中运行我们的代码,根据这个思路,我们试一下,能否在微信登陆的时候,获取到微信的密码。
思路:

  1. 找到登录按钮TargetAction
  2. 登陆按纽的Action换成自定义的Action

接下来就按着这个思路开始进行密码获取吧。这里使用framework注入方式。

获取WeChat登陆按纽的Target和Action

  • 真机运行项目,进入登录页,使用Debug View Hierarchy动态调试
    image.png

其中:
Target:WCAccountMainLoginViewController
Action:onNext

此时,想要打印密码框的内容,必须先找到密码框的位置,然后通过控件的属性拿到内容。这有几种方式:
【方式1】使用Debug View Hierarchy动态调试,直接找到密码框的位置,查看其属性得到密码

image.png

【方式2】使用静态分析的方法,我们已经确定登录按钮和密码框都在WCAccountMainLoginViewController中,那通过MachO导出OC中这个类的方法列表以及成员变量,这就能准确定位到密码控件,从而获取密码。而通过MachO导出OC中方法列表可通过class-dump工具。

./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文件

image.png

打开WCAccountMainLoginViewController文件,很明显_textFieldUserPwdItem将是我们要找的密码控件。

image.png

打开WCAccountTextFieldItem头文件,没有找到WCUITextField控件,但它继承自WCBaseTextFieldItem类,我们要找的控件很有可能在父类中。

image.png

打开WCBaseTextFieldItem头文件,果然找到WCUITextField控件。

image.png

找到WCUITextField控件,接下来就可以通过lldb查看WCUITextField控件的text了。

image.png

替换Action

以上是通过lldb获取到用户的密码了,接下来,我们的需求是:用户点击登陆按纽时,直接打印用户的密码。

由前面的分析知道,当应用启动的时候,会打印注入类的+load方法中的内容,说明应用启动的时候,会调用+load方法。那我们可以在+load方法,对用户登陆按纽执行的Action进行替换,使其替换成我们的方法。

  • 替换 Action
+(void)load{
    Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));

    Method newMethod = class_getInstanceMethod(self, @selector(my_onNext));
    
    method_exchangeImplementations(oldMethod, newMethod);
}
  • 打印密码
- (void)my_onNext{
    UITextField* pwd = (UITextField*)([[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]);
    NSLog(@"密码:%@", pwd.text);
}

真机运行项目,点击登录按钮,此时就能打印密码了

2021-04-28 17:16:33.791642+0800 WeChat[21348:1339926] 密码:121212

但是目前的处理不太好,我们的目的是获取了密码,但不能破坏应用程序的流程,此时就有点不好处理了。

在OC中,采用消息机制,即向某个类发送消息。在原来的运行流程:

objc_msgSend(id self, SEL op);
//self:WCAccountMainLoginViewController
//op:onNext
//SEL:onNext 对应的IMP:原登陆流程

经过方法交换之后的流程:

objc_msgSend(id self, SEL op);
//self:WCAccountMainLoginViewController
//op:onNext
//SEL:onNext 对应的IMP:my_onNext函数地址
//SEL:my_onNext对应的IMP:原登陆流程

此时在my_onNext函数中,想要继续原登陆流程,则需要向原Inject对象发送SELmy_onNext的消息, 而my_onNext函数中无法获取到原Inject对象,因此无法继续原登陆流程。

其实也并不是完全没有解决办法。以下介绍几种简单的方法:

【方法1】保存原my_onNext对应的IMP,在获取到密码之后再通过IMP执行原流程。

void my_onNext(id self, SEL _cmd){
    UITextField* pwd = (UITextField*)([[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]);
    NSLog(@"密码:%@", pwd.text);
    oldImp(self, _cmd);
}

获取并保存IMP可通过以下几种方式:

  • 通过method_getImplementation直接获取到IMP,并将其保存。
IMP (*oldImp)(id self, SEL _cmd);

+(void)load{
    Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
    oldImp = method_getImplementation(oldMethod);

    Method newMethod = class_getInstanceMethod(self, @selector(my_onNext));
    method_exchangeImplementations(oldMethod, newMethod);
}
  • 通过class_replaceMethod函数,该函数会替换原流程的方法后,将原流程的IMP返回。
IMP (*oldImp)(id self, SEL _cmd);

+(void)load{
    oldImp = class_replaceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext), my_onNext, @"v@:");
}

【方法2】在+load方法中,不使用方法交换。而使用添加方法

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

- (void)my_onNext{
    UITextField* pwd = (UITextField*)([[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]);
    NSLog(@"密码:%@", pwd.text);

    [self performSelector:@selector(my_onNext)];
}

【方法3】通过method_setImplementation函数,使得旧的SEL对应于新的IMP,并通过method_getImplementation函数保存旧的IMP。

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

+(void)load{
    Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
    oldImp = method_getImplementation(oldMethod);
    method_setImplementation(onNext, my_onNext);
}
- (void)my_onNext{
    UITextField* pwd = (UITextField*)([[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"]);
    NSLog(@"密码:%@", pwd.text);

    oldImp(self, _cmd);
}

总结

  • 代码注入的方式:

    • 汇编直接注入
    • 动态库注入(framework、dylib)
  • 动态库注入的原理

    • 自定义动态库拷贝至App的Frameworks目录下。
    • 通过yololib工具修改Mach-O,使其包含增加新增动态库的LC_LOAD_DYLIB字段。
  • 获取登陆密码案例

    • 使用Debug View Hierarchy动态调试,定位登陆密码按键触发时的TargetAction
    • 通过class-dump工具导出MachOOC的类方法列表
    • 定位到密码控件,通过valueForKey的方法获取成员变量的值。
  • Method Swizzle

    • method_exchangeImplementations交换SEL和IMP,此方法存在隐患,若两个SEL不在同一个类,想要继续走原来的流程可能会发生崩溃。
    • class_addMethod:为原来的类添加方法
    • class_replaceMethod:将新的IMP替换SEL的IMP,并将旧的IMP返回,可将函数返回保存,后续通过函数指针调用回到原流程。
    • method_getImplementationmethod_setImplementation设置IMP和获取IMP。大部分HOOK框架使用此方式,推荐使用这两个方法。

你可能感兴趣的:(11 - 代码注入)