上一篇我们已经讲到使用 shell 脚本来重签并调试别人的 APP,那么我们又是重签又是附加调试别人的 APP 是为了啥呢?是太闲了吗,当然不是…我们接下来的任务就是代码注入

App 执行哪些代码?

在开始代码注入之前,我们先了解一下一个 iOS 的 App 在运行的时候,究竟会执行哪些代码,以及我们从哪里入手注入代码

  1. MachO。App 的二进制文件,我们写的所有代码都会在这里,后面的文章会介绍
  2. Framework 可以是我们写的,也可以是第三方的代码
  3. 系统库

其中系统库,在非越狱手机上我们改不了。MachO 我们可以修改,但是比较麻烦,需要会写汇编。而 Framework 是我们经常接触也是最容易入手的,我们自己写一个就行了…

那么现在的问题就是我们写的 Framework 别人的 APP 怎么会执行呢?

MachO 文件里面有一个 Load Commands 的部分,DYLD(the dynamic link editor 是苹果的动态链接器,后面的文章也会介绍)会读取这个 Load Commands 里面的内容,并加载到内存中,如果我们能把我们自己的 Framework 插入到别人 APP 的 MachO 的 Load Commands 里,那么我们的代码就自然的被执行了

那么现在的问题就变成了如何把我们写的 Framework 插入到别人 App 的 MachO 文件里?

yololib 这是一个终端命令行工具,就是用来把我们写的 Framework 插入到 MachO 文件里,代码不多就 200 来行,感兴趣的可以看看源码,用法是 yololib 参数1 参数2 其中参数 1 是 MachO 文件,参数 2 是我们的 Framework 相对于 MachO 文件的路径。

下载源码之后是我们很熟悉的 Xcode 工程,只不过是 Mac 上的命令行工程而不是我们常用的 iOS 工程,修改一下 Deployment Target 到你的 Xcode 兼容的最低版本,然后编译一下就能在编译后的文件夹下找到。编译后的文件夹可以通过点击 Xcode 上的 Product -> Show Build Folder in Finder 中的 Products->Debug 找到。

为了方便我们使用这个工具,建议把它放到系统的 usr/local/bin 目录下,记得给它提升可执行权限(在终端chmod +x yololib),这样我们任意打开一个终端都可以使用这个命令了,也方便我们使用脚本来操作。

好了,现在万事具备,只欠我们动手操作了…还记得上一篇文章讲到的内容吗,使用 shell 脚本重签 App。可以接着使用上次的工程,也可以新建一个工程来执行后面的代码注入,配置好脚本之后只需要放入一个砸壳的 app 包到根目录下就好了。我这里新建一个项目来演示后面的代码注入

代码注入的步骤

新建 Framework

在完成了上一篇 shell 脚本重签的步骤之后,我们先来新建一个 Framework,把我们的 Framework 准备好

image.png

Framework 的名字就叫 FrankyHook 这个名字无所谓,但是要记住因为在后续使用 yololib 的时候要用到,在里面新建一个类 CodeInject 这个叫什么也无所谓,继承自 NSObject 就够了,主要是需要它的 load 方法,并在 load 方法里面打印点内容

image.png

在脚本中添加代码

记得修改为你自己的 Framework 名字,要是照着我的抄的的当我没说

1
yololib "${BUILT_PRODUCTS_DIR}/${TARGET_NAME}.app/${APP_BINARY}" "Frameworks/FrankyHook.framework/FrankyHook"

这里面有一个小细节,就是你的脚本运行一定要放在嵌入 Frameworks 之后,不然每次脚本运行之后再嵌入 Frameworks 的话 Xcode 会结合 Framework 的 Info.plist 重新处理工程源代码下的 Info.plist 文件到编译的包中,会导致少了很多配置从而启动崩溃。

image.png

实不相瞒,做了四年多的 iOS 开发,也是到今天才知道这个地方居然可以拖动…

command + R 看控制台输出

image.png

怎么样,是不是发现我们已经可以在 WeChat 里面执行我们的代码了?接下来我们来实现两个小小的需求

需要具备的一些知识点

接下来的内容需要对 Objective-C 的 rumtime 有一定的了解,了解的同学这部分可以直接跳过,看下一部分

Method Swizzle方法交换

runtime 提供了一些 api 来让我们实现方法交换,现在假设有这么一段代码:

image.png

使用 NSURL 初始化 URL 的时候,如果 URL 中含有中文,那么初始化就会失败返回 nil。这个问题做过 iOS 开发的同学应该都遇到过,解决的办法就是对 URL 进行一次百分号编码,现在假设一种情景,你刚入职一家新公司,发现了这个问题,然后在工程里面搜索了一下,发现我的天啦,到处都是这种 URL 中夹着中文,而没有进行编码的情况,那么这个时候,你是选择一个一个的去修改呢,还是会想其他更好的办法?

使用方法交换就是更好的办法,新建一个 NSURL 的分类,在分类的 load 方法中,实现我们的方法交换

image.png

一个 OC 方法,我们可以分为两个部分,一个是方法名 SEL,一个是方法的实现 IMP。正常情况下,一个方法的名字对应着它的实现。而有时候我们通过 runtime 来交换方法的实现。就如上面的 load 方法里面的代码,默认情况下方法 one 和方法 two 的实现都是指向他们自己的 IMP 的,通过 method_exchangeImplementations() 函数交换之后,方法 one 的实现就指向了方法 two 的实现,而方法 two 的实现就指向了方法 one 的实现,当代码调用 URLWithString: 的时候,就会来到我们的 HK_URLWithString: 方法,当代码调用 HK_URLWithString: 的时候,就会执行 URLWithString: 方法

这样原来工程里的所有 URLWithString: 方法都会执行到我们的代码中去,首先调用一次原始的初始化 URL 的方法,看能否成功生成 url 如果为 nil,就表示我们可能需要对字符串 str 进行一下编码。再用编码过后的 str 初始化 url,这样就不用浪费时间精力去一个一个的去修改工程里的代码了

拦截微信的注册点击

使用 Debug View Hierarchy 查看微信的登录注册页面的视图层次结构,找到注册的按钮,查看按钮的 target 和 action

image.png

我们知道了注册按钮点击的时候,会调用 WCAccountLoginControlLogiconFirstViewRegister 方法,而且我们也已经可以在我们的 Framework 中执行我们的注入代码了,那么接下来我们如何实现拦截微信的注册按钮点击呢?当然可以使用 Method Swizzle 来实现

在开始写代码实现方法交换之前,还有一个小小的问题,虽然我们根据经验可以知道这个 onFirstViewRegister 应该是个对象方法,可能没有返回值也没有参数,但这些都只是根据我们经验的猜测…关考猜测可不行,那么怎么验证我们的猜测呢?

Class-dumpyololib 一样,也是一个终端命令行工具,同样也可以放到 usr/local/bin 目录下可以全局使用,它的作用是可以把 MachO 文件里的头文件信息全部导出来,我们可以 cd 到工程编译生成的 APP 包里面,使用以下命令

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

将它的头文件都导到一个 headers 的文件夹内,这个过程需要一点时间

image.png

看了下这个文件挺大的,导完之后可以把它剪切到工程根目录下,这样给我们手机也能省点空间,也确实完全没必要放在 APP 包里面

image.png

可以看到这里面有 15074 个项目,微信的头文件还真不少呢…这么大的文件夹如果我们用 Xcode 打开搜索的话,一定会十分的痛苦。这里推荐使用 sublime…更轻量一点,找起头文件来也更快,搜索一番发现如图所示

image.png

这样就确保了这个 onFirstViewRegister 方法是无返回值无参数的,可以开始编写代码实现拦截了

现在我们可以来到我们 Framework 的 CodeInject.m 文件写点代码

image.png

代码就这么点,现在 command + R 运行起来之后,再次点击微信的注册按钮试试看?

image.png

窃取用户的账号密码

上面的需求仅仅只是破坏了功能,原来的功能都没法使用了。接下来这个需求让用户在神不知鬼不觉的情况下,账号密码就被窃取了(所以市面上那些来源不明的 App 真不要下载使用,除非你也是搞逆向研究的)。那么我们在什么时候,能拿到用户的账号和密码呢?当然是点击登录按钮的时候,所以你懂的,使用 viewDebug 查看登录按钮

image.png

登录按钮点击的 target:WCAccountMainLoginViewController,action:onNext;

查看刚刚 class-dump 出来的头文件发现这个 onNext 方法是没有参数的,那么我们需要的账号密码在哪里呢?哈哈,有经验的同学是不是发现这两个名字是不是莫名的熟悉

1
2
WCAccountTextFieldItem *_textFieldUserNameItem;
WCAccountTextFieldItem *_textFieldUserPwdItem;

image.png

变量名是挺熟悉的,但是这个 WCAccountTextFieldItem 我们不知道是个什么东西怎么办?头文件都在手上了还问怎么办,接着搜索啊…

image.png

WCAccountTextFieldItem 里面貌似没啥东西,那就看它父类 WCBaseTextFieldItem

image.png

WCBaseTextFieldItem 类里面发现了一个 WXUITextField 的东西,跟我们熟悉的 UITextField 很像了,我们猜测可能就是这个 m_textField 存放着我们想要的东西,先不着急写代码

现在我们可以再次使用 viewDebug 工具调试查看一下我们的账号和密码在哪里,并且使用 lldb 来动态调试验证我们的猜测

image.png

首先控制器的地址,可以由登录按钮的 target 获取到。获取到控制器之后,再使用 KVC 大法获取成员变量 _textFieldUserPwdItem 的地址(密码都能获取到了,账号也是一样的操作)并打印它。获取到 _textFieldUserPwdItem 的地址之后,再次使用 KVC 大法获取它的成员变量 m_textField 的地址并打印这个对象,好家伙,明文密码不就在这儿了吗!!!

接下来我们通过代码来实现需求:

image.png

这代码看起来跟拦截微信注册的代码差不太多,那么我们 command + R 运行看看结果

image.png

WTF?账号密码确实是都获取到了,但是 APP 却崩溃了?为什么会发生崩溃,崩溃信息是我们经常能够遇到的经典报错 unrecognized selector sent to instance 0x10b15b400 说是控制器 WCAccountMainLoginViewController 无法识别 FK_onNext 这个方法

我们思考一下,方法交换为什么一般推荐写在想要交换方法的类所在的分类里面?因为在想要交换方法的类的分类当中,我们会新增一个方法,用来实现我们的逻辑,也正好是因为在分类中,所以当前类自然的添加了我们新增的方法,这样交换下来就不会出现找不到方法的错误

而上面的代码,我们的本意也是希望在 WCAccountMainLoginViewController 里面新增一个方法处理我们的逻辑,并交换 onNext 方法,但现在的问题是我们在 CodeInject 这个类的 load 方法中,那么有什么办法可以解决这个问题呢?

动态添加方法

rumtime 的 api 提供了运行时动态添加方法的能力

1
2
BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
  • 参数1: 给哪个类添加方法
  • 参数2: 方法的名字
  • 参数3: 方法的实现
  • 参数4: 方法的参数和返回值描述,用一些特定的符号表示,也可以不写
  • 参数5: BOOL值,表示是否添加成功

那么借用这个 api 我们能想到什么解决办法呢,给 WCAccountMainLoginViewController 控制器添加我们的 FK_onNext 方法,再让 FK_onNextonNext 方法交换,最终的代码如下

image.png

再次 command + R 运行发现,既能成功获取到用户输入的账号密码,又能成功的去调用微信的登录逻辑了

动态替换方法

现在除了添加新方法的方式,我们还有其他的办法吗?当然有(强大的runtime) rumtime 的 api 还提供了运行时替换方法的能力

1
2
IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
  • 参数1: 替换哪个类
  • 参数2: 方法的名字
  • 参数3: 方法的实现
  • 参数4: 方法的参数和返回值描述,用一些特定的符号表示,也可以不写
  • 返回值: IMP,原始的方法实现

如果使用这个方式,那么我们应该是将原始的 onNext 方法替换成我们的 FK_onNext 方法,那么我们如何去调用微信原始的 onNext 方法呢,在替换之前将原始的 onNext 方法的实现 IMP 记录下来,然后在我们的 FK_onNext 方法中使用这个记录的 IMP 来实现对原始 onNext 的调用,具体代码如下图:

image.png

对比方法交换的代码实现,我们发现方法替换的方式需要多一个变量 originalIMP,用来记录原始的 onNext 方法的实现,而且还会报个警告,虽然少了一点点代码,但看起来也不是那么好理解…当然,这里使用方法替换也只是为了学习一下 runtime 提供的 api,感受一下 rumtime 的强大,具体使用哪个方式就看个人的喜好

动态获取方法实现和设置方法实现

那么,还有更骚的操作吗?当然有…

获取方法 m 的实现 IMP

1
2
IMP _Nonnull
method_getImplementation(Method _Nonnull m)

设置方法 m 的实现 IMP

1
2
IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)

这个原理其实跟替换差不太多,首先获取原始 onNext 的实现并记录,然后设置新的 IMP(我们写的 FK_onNext )给 WCAccountMainLoginViewControlleronNext 方法,具体代码如下:

image.png

好了,到此我们为了实现窃取用户的账号密码已经使用了三种 runtime 提供的方式…到这里就真的没了

从上一篇 iOS 重签名,到这一篇的代码注入,如果你使用过 MonkeyDev 工具的 MonkeyApp 就会发现是如此的相似。其实这就是 MonkeyDev 的实现原理,只是 MonkeyDev 的实现更加复杂。

下一篇文章开始介绍我们最近一直提到的 MachO 文件,到底什么是 MachO 文件,它包含了什么东西,干什么用的…

反微信重签名检测

如果你希望在重签名的微信上登录账号使用,最好在添加以下代码绕过微信的检测。否则会有封号的风险,不保证以下代码长期有效。将以下代码粘贴到 CodeInject.m 文件中,不要放到类的实现中就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#import <objc/runtime.h>
// 函数指针
NSUUID* (*original_advertisingIdentifier)(id, SEL);
void (*original_setBundleId)(id, SEL, NSString*);
void (*original_setClientSeqId)(id, SEL, NSString*);
void (*original_setDeviceName)(id, SEL, NSString*);
void (*original_addLogInfo_withMessage)(id, SEL,int *,const char *);
BOOL (*original_HasInstallJailbreakPluginInvalidIAPPurchase)(id, SEL);
BOOL (*original_IsJailBreak)(id, SEL);
BOOL (*original_HasInstallJailbreakPlugin)(id, SEL, id);

// hook函数
static NSUUID* hook_advertisingIdentifier(id self, SEL _cmd) {
NSUUID *advertisingIdentifier;
NSString *key = @"idfa";
NSString *idfa = [[NSUserDefaults standardUserDefaults] stringForKey:key];
if (idfa && idfa.length){
advertisingIdentifier = [[NSUUID alloc] initWithUUIDString:idfa];
} else {
advertisingIdentifier = [NSUUID UUID];
[[NSUserDefaults standardUserDefaults] setObject:advertisingIdentifier.UUIDString forKey:key];
}
return advertisingIdentifier;
}
static void hook_setBundleId(id self, SEL _cmd, NSString *bundleId) {
if ([bundleId isEqualToString:[NSBundle mainBundle].bundleIdentifier]) {
bundleId = @"com.tencent.xin";
}
original_setBundleId(self, _cmd, bundleId);
}
static void hook_setClientDeqId(id self, SEL _cmd, NSString *clientSeqId) {
NSString *key = @"clientSeqId";
NSString *clientSeqId_fist = [[NSUserDefaults standardUserDefaults] stringForKey:key];
if (!clientSeqId_fist || clientSeqId_fist.length == 0) {
clientSeqId_fist = [[NSUUID UUID].UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""];
[[NSUserDefaults standardUserDefaults] setObject:clientSeqId_fist forKey:key];
}
NSString *newClientSeqId;
if ([clientSeqId containsString:@"-"]) {
NSRange range = [clientSeqId rangeOfString:@"-"];
NSString *clientSeqId_last = [clientSeqId substringFromIndex:range.location];
newClientSeqId = [NSString stringWithFormat:@"%@%@", clientSeqId_fist, clientSeqId_last];
} else {
newClientSeqId = clientSeqId_fist;
}
original_setClientSeqId(self, _cmd, newClientSeqId);
}
static void hook_setDeviceName(id self, SEL _cmd, NSString *deviceName) {
original_setDeviceName(self, _cmd, @"iPhone");
}
static void hook_addLogInfo_withMessage(id self, SEL _cmd, int *arg1, const char *arg2) {
return;
}
static BOOL hook_original_HasInstallJailbreakPluginInvalidIAPPurchase(id self, SEL _cmd) {
return NO;
}
static BOOL hook_IsJailBreak(id self, SEL _cmd) {
return NO;
}
static BOOL hook_HasInstallJailbreakPlugin(id self, SEL _cmd, id arg1) {
return NO;
}

// 所有被 hook 的类和函数放在这里的构造函数中
__attribute__((constructor)) static void hookMethods(void) {
@autoreleasepool {
Class cls = NSClassFromString(@"ASIdentifierManager");
if (cls) {
Method method = class_getInstanceMethod(cls, @selector(advertisingIdentifier));
if (method) {
original_advertisingIdentifier = (NSUUID*(*)(id, SEL))method_getImplementation(method);
method_setImplementation(method, (IMP)hook_advertisingIdentifier);
}
}

cls = NSClassFromString(@"ManualAuthAesReqData");
if (cls) {
Method method = class_getInstanceMethod(cls, @selector(setBundleId:));
if (method) {
original_setBundleId = (void(*)(id,SEL,NSString*))method_getImplementation(method);
method_setImplementation(method, (IMP)hook_setBundleId);
}
method = class_getInstanceMethod(cls, @selector(setClientSeqId:));
if (method) {
original_setBundleId = (void(*)(id,SEL,NSString*))method_getImplementation(method);
method_setImplementation(method, (IMP)hook_setBundleId);
}
method = class_getInstanceMethod(cls, @selector(setDeviceName:));
if (method) {
original_setDeviceName = (void(*)(id,SEL,NSString*))method_getImplementation(method);
method_setImplementation(method, (IMP)hook_setDeviceName);
}
}

cls = NSClassFromString(@"MMCrashReportExtLogMgr");
if (cls) {
Method method = class_getInstanceMethod(cls, @selector(addLogInfo:withMessage:));
if (method) {
original_addLogInfo_withMessage = (void(*)(id,SEL,int*,const char*))method_getImplementation(method);
method_setImplementation(method, (IMP)hook_addLogInfo_withMessage);
}
}

cls = NSClassFromString(@"JailBreakHelper");
if (cls) {
Method method = class_getInstanceMethod(cls, @selector(HasInstallJailbreakPluginInvalidIAPPurchase));
if (method) {
original_HasInstallJailbreakPluginInvalidIAPPurchase = (BOOL(*)(id,SEL))method_getImplementation(method);
method_setImplementation(method, (IMP)hook_original_HasInstallJailbreakPluginInvalidIAPPurchase);
}
method = class_getInstanceMethod(cls, @selector(IsJailBreak));
if (method) {
original_IsJailBreak = (BOOL(*)(id,SEL))method_getImplementation(method);
method_setImplementation(method, (IMP)hook_IsJailBreak);
}
method = class_getInstanceMethod(cls, @selector(HasInstallJailbreakPlugin:));
if (method) {
original_HasInstallJailbreakPlugin = (BOOL(*)(id,SEL,id))method_getImplementation(method);
method_setImplementation(method, (IMP)hook_HasInstallJailbreakPlugin);
}
}
}
}