【iOS】iOS逆向系列七 - Tweak

我们以微信为例,在微信【发现】界面新增两行内容。

本文章仅供学习交流使用,禁止使用技术手段非法获益。

一、微信注入

1.1. 流程分析

  1. ssh远程登录iPhone。

    • 使用usbmuxd工具进行远程操作
  2. 运行微信,使用Reveal工具查看发现页面的层级结构,最顶层视图是MMMainTableView

  1. 然后在终端使用cycript获取微信一些信息。通过一层层拦截发现,微信的发现页面使用的是UITableView,并且数据源是在FindFriendEntryViewController中实现的。所以如果要添加新的数据到页面中,就需要重写控制器中tableView的数据源

  1. 微信脱壳,找到可执行文件。

    • 使用Clutch进行脱壳可能会失败,所以此处建议使用class-dumpdecrypted进行脱壳。
  2. 使用class-dump导出可执行文件的头文件。

  3. 在导出的头文件中找到找到FindFriendEntryViewController,并且查看tableView实现的数据源方法有哪些。

  1. 使用theos中的nic.pl新建tweak项目。

  2. 代码编写完成后,需要编辑打包安装(会自动重启SpringBoard)。

1
make && make package && make install

1.2. 编写代码

编写Tweak.xm文件注意事项:

  • class-dump导出的头文件里面的方法形参是可以自己编辑的,形参如果使用id修饰的,在使用形参时不要使用点语法。
  • 如果需要引用应用本身代码的实现,需要使用%orig(theos提供的Logos语法)。
  • Tweak.xm文件中默认是替换(%hook)已有的方法,如果需要添加新的方法,需要在方法前面添加%new
  • 可以使用宏定义。
  • 加载图片资源使用imageWithContentsOfFile:,因为后面的参数是一个绝对路径,所以把图片放到手机根路径或其他具体路径就可以。
    • 上面的操作其实不太方便,theos也考虑到这样的问题,所以如果要使用额外的资源文件,在tweak项目的根路径创建一个layout文件夹,把资源放入即可。当最后编译安装的时候,theos会自动把layout文件夹里面的内容放到手机根路径。
    • 考虑到其他应用可能会有同名的资源,所以可以在layout文件夹中再创建一个文件夹(例:layout/Library/PreferenceLoader/Preferences/DBWeChat),把资源放到DBWeChat。安装时theos会把layout里面的东西原封不动的放到手机根路径,如果有同名文件夹,会自动合并文件夹)。
    • 使用:[UIImage imageWithContentsOfFile:@"/Library/PreferenceLoader/Preferences/DBWeChat/skull.png"];
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
#define DBDefaults [NSUserDefaults standardUserDefaults]
#define DBAutoKey @"db_auto_key"
#define DBFile(path) @"/Library/PreferenceLoader/Preferences/DBWeChat" #path"

%hook FindFriendEntryViewController

- (long long)numberOfSectionsInTableView:(id)tableView {
return %orig + 1;
}

- (long long)tableView:(id)tableView numberOfRowsInSection:(long long)section {
if (section == [self numberOfSectionsInTableView:tableView] - 1) {
return 2;
}
/*
%orig;
return;
*/
// 下面的代码和上面的等价
return %orig;
}

- (id)tableView:(id)tableView cellForRowAtIndexPath:(id)indexPath {
if ([indexPath section] != [self numberOfSectionsInTableView:tableView] - 1) {
return %orig;
}

NSString *cellId = ([indexPath row] == 1) ? @"exitCellId" : @"autoCellId";
UITableViewCell *cell = cell = [tableView dequeueReusableCellWithIdentifier:cellId];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
cell.backgroundColor = [UIColor whiteColor];
cell.imageView.image = [UIImage imageWithContentsOfFile:DBFile(skull.png)];
}
if ([indexPath row] == 0) {
cell.textLabel.text = @"自动抢红包";

UISwitch *switchView = [[UISwitch alloc] init];
switchView.on = [DBDefaults boolForKey:DBAutoKey];
[switchView addTarget:self action:@selector(db_autoChange:) forControlEvents:UIControlEventValueChanged];
cell.accessoryView = switchView;
} else if ([indexPath row] == 1) {
cell.textLabel.text = @"退出微信";
}
return cell;
}

- (double)tableView:(id)tableView heightForRowAtIndexPath:(id)indexPath {
if ([indexPath section] != [self numberOfSectionsInTableView:tableView] - 1) {
return %orig;
}
return 44;
}

- (void)tableView:(id)tableView didSelectRowAtIndexPath:(id)indexPath {
if ([indexPath section] != [self numberOfSectionsInTableView:tableView] - 1) {
return %orig;
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];

if ([indexPath row] == 1) {
// exit会卡一下,建议使用abort
// exit(0);
abort();
}
}

// 新方法需要添加%new
%new
- (void)db_autoChange:(UISwitch *)switchView {
[DBDefaults setBool:switchView.isOn forKey:DBAutoKey];
[DBDefaults synchronize];
}

%end

以上仅仅是提供一种思路,如果有的应用是通过数据模型控制视图的,可以通过拦截数据模型进行处理。如果有些方法确实存在,但是编译的时候tweak报错(一般是self调用时会报错),可以在tweak.xm最上面声明一个伪类,让伪类声明方法(仅仅是为了编译不报错)。

常用的Logos语法:

  • %hook、%end
    • hook一个类的开始和结束
  • %log
    • 打印⽅法调用详情
    • 可以通过Xcode -> Window -> Devices and Simulators查看⽇志
  • HBDebugLog
    • 和NSLog类似
  • %new
    • 添加一个新的方法
  • %c(className)
    • 生成⼀个Class对象,比如%c(NSObject),类似于NSStringFromClass()objc_getClass()
  • %orig
    • 函数原来的代码逻辑
  • %ctor
    • 在加载动态库时调⽤
  • %dtor
    • 在程序退出时调⽤
  • logify.pl
    • 可以将一个头⽂件快速转换成已经包含打印信息的xm文件(方法比较多,并且需要打印信息时可能需要用到该工具)
    • logify.pl xx.h > xx.xm
    • logify.pl生成的xm⽂件,有很多时候是编译不通过的,需要进行⼀些处理,最常见的有以下几种情况:
      • 删掉__weak
      • 删掉inout
      • 协议找不到时,删掉协议 或者 声明一下协议信息@protocol XXTestDelegate;
      • 类型转换错误时,替换HBLogDebug(@" = 0x%x", (unsigned int)r);HBLogDebug(@" = 0x%@", r);
      • 类名找不到时,声明一下类信息@class XXPerson; 或者 替换类名为void,⽐如将XXPerson *替换为void *
  • 如果有额外的资源文件(⽐如图⽚),放在tweak项目的layout⽂件夹中,对应着手机的根路径/

二、Tweak原理

2.1. 编译

1
make

执行make操作时,会编译Tweak代码为动态库(*.dylib),生成的动态库在tweak项目根目录/.theos/obj/debug/***.dylib

2.2. 打包

1
make package

由于动态库无法直接安装到手机中使用,所以使用make package可以把动态库生成deb文件(生成的文件在tweak项目根目录/packages/***.deb)。

如果不需要debug版本(debug版本安装包比较大,而且在Cydia中会有debug标识),需要添加debug=0标识。

1
make package debug=0

make package已经包含了编译操作,所以可以省掉make编译指令。

2.3. 安装

1
make install

越狱后的应用插件都是以deb格式通过Cydia安装到手机中的,所以我们编写的代码最终也是打包成deb,使用make install传到手机中,让Cydia自动安装。

安装流程:

  • ssh自动登录(根据环境变量THEOS_DEVICE_IPTHEOS_DEVICE_PORT自动登录)。
  • deb文件传到电脑映射的端口,并通过usbmuxd传到手机中。
  • Cydia会自动把deb解压,把dylib存放在/Library/Mobile/MobileSubstrate/DynamicLibraries/***.dylib(还会有同名的plist文件,存放的内容是插件宿主的bundleId。其实就是创建tweak项目时的***.plist文件)。
  • deb是通过Cydia里面的Cydia Substrate插件进行管理的(这个插件也是Cydia作者开发的,安装Cydia时会自动安装),

2.4. 运行

当运行APP时,Cydia Substrate会到/Library/Mobile/MobileSubstrate/DynamicLibraries/文件夹中查找*plist是否包含APP的bundleId。

如果找到对应的插件,APP会去加载对应的同名动态库。并修改APP内存中的代码逻辑,去执⾏dylib中的函数代码(每次运行APP都会加载动态库)。

所以,theos的tweak不会对APP原来的可执行⽂件进行修改,仅仅是修改了内存中的代码逻辑

三、疑问

  1. 未脱壳的APP是否⽀持tweak?

支持,因为tweak是在内存中实现的,并没有修改.app包中的可执⾏⽂件。

  1. tweak效果是否永久性的?

取决于tweak中⽤到的APP代码是否被修改过。

  1. 如果一旦更新APP,tweak会不会失效?

取决于tweak中用到的APP代码是否被修改过。

  1. 未越狱的手机是否支持tweak?

不⽀持。

  1. 能不能对Swift/C函数进⾏tweak?

可以,⽅式跟OC不一样。

  1. 能不能对游戏项⽬进行tweak?
    可以。但是游戏⼤多数是通过C++/C#编写的,而且类名、函数名会进行混淆操作。

  2. 如果需要对多个文件进行拦截,在tweak项目Makefile文件的项目名_FILES后面拼接新的文件名就行,使用空格隔开。如果需要自定义文件夹,就要填写文件的相对路径,在使用import时也是使用的相对路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Makefile
// 原来的代码
// db_wechat_FILES = Tweak.xm

// 修改后的代码
db_wechat_FILES = src/Tweak.xm src/Model/DBPerson.m

// 也可以使用通配符(但是不能对文件夹使用通配符,如下面的Model不能使用*或者**)
// db_wechat_FILES = src/*.xm src/Model/*.m

// Tweak.xm
// 在Tweak.xm中引用DBPerson
#import "Model/DBPerson.h"