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框架使用此方式,推荐使用这两个方法。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342