一、Tweak原理
1.1 Tweak产物.dylib
- 执行make命令时,在 .theos的隐藏目录中,编译出obj/debug目录,包含 arm64、arm64e两种架构,同时生成readbadges.dylib动态库
- 在arm64、arm64e目录下,有各自架构的readbadges.dylib,而debug目录下的readbadges.dylib,是一个胖二进制文件
file readbadges.dylib
Mach-O universal binary with 2 architectures: [arm64:Mach-O 64-bit dynamically linked shared library arm64Mach-O 64-bit dynamically linked shared library arm64] [arm64e:Mach-O 64-bit dynamically linked shared library arm64eMach-O 64-bit dynamically linked shared library arm64e]
readbadges.dylib (for architecture arm64): Mach-O 64-bit dynamically linked shared library arm64
readbadges.dylib (for architecture arm64e): Mach-O 64-bit dynamically linked shared library arm64e
Tweak的编译产物是动态库,将其注入的方式有两种:
- 修改MachO文件的Load Commands,注入 LC_LOAD_DYLIB(XXX),然后根据路径找到动态库.这种方式对程序的污染比较严重,容易被开发者检测出来
- 通过DYLD_INSERT_LIBRARIES环境变量,插入动态库
Tweak插件,使用的是方式二,因为程序没有被污染.在MachO中,并没有找到LC_LOAD_DYLIB(XXX)
1.2 Tweak packages分析
执行make package命令时,在packages目录中,生成.deb文件.每执行一次打包命令,都会生成一个新的.deb文件
.deb 格式类似于 .ipa格式
- .ipa包通过AppStore安装,将.ipa包中的App包装到设备中
- .deb包通过Cydia安装,将 .deb包中的动态库安装到设备中
- data.tar.lzma解压后就是动态库
- 执行 make install命令时,在.deb包中的动态库,会被安装到设备的 /Library/MobileSubstrate/DynamicLibraries目录中
- 以相同的名称,分别存储 .dylib和 .plist 文件
- .dylib为动态库,而 .plist,记录 .dylib所依附的App包名
二、 DYLD_INSERT_LIBRARIES防护
- 当前官方的dyld源码中关于DYLD_INSERT_LIBRARIES的解释
DYLD_INSERT_LIBRARIES是一个以冒号分隔的附加动态库列表,要在程序中指定的动态库之前加载。
相反,如果您的目标是替换通常会加载的库,请使用 DYLD_LIBRARY_PATH 或者 DYLD_FRAMEWORK_PATH 代替。
- 在早期的dyld源码中,有进程限制的判断.一旦符号条件,使用 DYLD_INSERT_LIBRARIES环境变量插入的动态库将被清空
- 打开dyld-519.2.2源码
- 搜索DYLD_INSERT_LIBRARIES
- 进入dyld.cpp文件,来到5907行
-
- DYLD_INSERT_LIBRARIES 为NULL的判断
- 这段代码的上面,来到5692行
-
- 判断进程限制, 符合条件,调用pruneEnvironmentVariables 方法,清空插入的动态库
- 一旦插入的动态库被清空,意味着越狱插件将会全部失效.如果我们找到进程限制的开启条件,并将其使用在项目中,相当于对越狱插件进行了防护
- 找到 processIsRestricted 设置为 true 的代码
-
- 判断条件有两个,分别是 issetugid 和 hasRestrictedSegment 两个函数
- issetugid函数,无法在上架的App中设置,放弃使用
- hasRestrictedSegment函数,判断主程序的MachO是否受限,可以使用
- 进入 hasRestrictedSegment 函数
-
- 传入主程序的Header
- 读取 segment, 如果为 __RESTRICT 段
- 读取 segment,如果为 __restrict 节
- 如果都存在,返回 true, 表示进程限制
- __RESTRICT 段防护
- 在项目中, 添加 __RESTRICT段, __restrict节,开启进程限制,对越狱插件进行防护
- 搭建App项目,命名: AntiTweak
- 打开ViewController.m ,写入以下代码: 点击屏幕即退出App
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
exit(0);
}
-
- 进程限制,是早期dyld源码中的逻辑,在低系统下才能生效
- 搭建 Tweak插件,附加 AntiTweak应用
- 打开 Tweak.xm 文件,写入Hook代码
#import <UIKit/UIKit.h>
%hook ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan hooked");
}
%end
-
- 运行插件项目,启动应用,开启控制台查看手机日志,touchesBegan方法被插件HOOK.点击屏幕,不会闪退.
- 为AntiTweak项目,添加 __RESTRICT段, __restrict节
- 在Build Setting的Other Linker Flags,加入以下设置
-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
-
- 编译项目,查看MachO文件
-
- 成功插入 __RESTRICT 段, __restrict节
- 低系统版本iOS手机,运行项目,点击屏幕闪退,说明插入的动态库已经被清空,越狱插件全部失效
- 这种防护手段,在早期系统中比较有效.但在iOS11及更高系统中,dyld源码发生变化,这种方式已失去作用
针对上面这种防护手段,可以通过修改MachO破解
- 在老系统的越狱设备上,遇到使用此方法防护的应用,导致我们的越狱插件无法使用,可以通过修改MachO文件破解防护
- 使用MachOView打开MachO文件
-
- 修改Data值,将 72改为73,52改为53.只在以前的数值上替换,位置不要改变
-
- 当MachO文件修改后,使用重签名安装应用,此时 __RESTRICT段和 __restrict节已经不存在了,进程限制不会启动,越狱插件可正常使用.
三、 使用dyld源码防护
如果是自己的App,我们开启了进程限制,如果禁止攻击者的肆意修改呢?
- 借鉴dyld的代码,循环读取segment和section, 如果缺少 __RESTRICT段或 __restrict节.说明我们的防护代码被人篡改
- 用刚才的AntiTweak项目,将dyld中的代码迁移到项目中,
- 打开ViewController.m文件,写入以下代码
- 导入需要用到的头文件
#import <mach-o/loader.h>
#import <mach-o/dyld.h>
-
- 添加需要用到的宏定义
#if __LP64__
#define macho_header mach_header_64
#define LC_SEGMENT_COMMAND LC_SEGMENT_64
#define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT
#define LC_ENCRYPT_COMMAND LC_ENCRYPTION_INFO
#define macho_segment_command segment_command_64
#define macho_section section_64
#else
#define macho_header mach_header
#define LC_SEGMENT_COMMAND LC_SEGMENT
#define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT_64
#define LC_ENCRYPT_COMMAND LC_ENCRYPTION_INFO_64
#define macho_segment_command segment_command
#define macho_section section
#endif
-
- 添加hasRestrictedSegment函数, 循环读取segment和section. 如果缺少 __RESTRICT段或 __restrict节,返回false
static bool hasRestrictedSegment(const struct macho_header *mh) {
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(struct macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND: {
const struct macho_segment_command *seg = (struct macho_segment_command*)cmd;
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section*const sectionsStart = (struct macho_section*)((char *)seg + sizeof(struct macho_segment_command));
const struct macho_section*const sectionsEnd = §ionsStart[seg->nsects];
for (const struct macho_section*sect=sectionsStart; sect<sectionsEnd; ++sect) {
if (strcmp(sect->sectname, "__restrict")==0) {
return true;
}
}
}
}
break;
}
cmd = (const struct load_command *)(((char *)cmd)+cmd->cmdsize);
}
return false;
}
-
- 加入 load方法作为调用防护代码的时机
// 加入load方法,调用防护代码
+ (void)load {
//获取当前运行程序的MachO
struct macho_header *mhmh = _dyld_get_image_header(0);
if (hasRestrictedSegment(mhmh)) {
NSLog(@"防护代码有效");
} else {
NSLog(@"被篡改");
}
}
- 修改 Other Linker Flags 中的配置,模拟MachO被篡改
-Wl,-sectcreate,__SESTRICT,__sestrict,/dev/null
-
- 运行项目,输出以下结果
2023-04-15 00:12:39.528419+0800 AntiTweak[47753:1566353] 被篡改
- 注意:
- 当检测到MachO被篡改,不要使用痕迹明显的代码进行防护,例如: exit(0).此类代码相当于记号,让攻击者很容易找到防护的位置和逻辑
- 高明的防护手段,应该让攻击者不易察觉,在不知不觉中被系统屏蔽封杀
四、白名单检测
进程限制的防护手段,仅低版本系统有效.对于高版本系统的防护,我们可以自制白名单进行检测.
4.1 获取正常设备上的白名单
- 延用AntiTweak项目: 整理出App依赖库的白名单
- 打开ViewController.m文件,写入以下代码:
+ (void)load {
//获取镜像文件个数
uint32_t intCount = _dyld_image_count();
for (int intIndex = 0; intIndex < intCount; intIndex++) {
// 获取指定索引的镜像名称
const char *strName = _dyld_get_image_name(intIndex);
printf("%s",strName);
}
}
- 在未越狱设备上,运行项目,遍历出所有image名称
- 打印结果,相当于一份白名单.如果App运行时,加载了白名单以外的动态库,该库很可能是被第三方注入的.将输出结果拷贝出来存放
4.2 检测越狱设备上注入的动态库
- 获取思路为: 在load方法中,循环遍历依赖的动态库.如果动态库是当前的MachO文件,或者包含在白名单中,属于合法库,直接跳过.否则,将其打印.
+ (void)load {
//获取镜像文件个数
uint32_t intCount = _dyld_image_count();
for (int intIndex = 0; intIndex < intCount; intIndex++) {
// 获取指定索引的镜像名称
const char *strName = _dyld_get_image_name(intIndex);
// 0位置是当前的 MachO文件
if (intIndex == 0 || strstr(strList, strName)) {
continue;
}
printf("注入的动态库: %s\n",strName);
}
}
-
- 在越狱设备上运行项目,输出很多白名单以外的动态库,其中包含自制的TouchHookDemo插件
使用白名单做防护,需要注意:
- 在不同系统下运行项目,整理出尽可能完善的白名单.
- 检测到白名单以外的动态库,不要直接处理.建议先收集数据,如果此动态库是因为不同系统差异造成的,将其补充到白名单中.如果确定是恶意注入,再做处理.
- 白名单列表,由服务器下发,或者将逻辑直接做到服务器
白名单写在客户端的弊端:
- 白名单的字符串,位于MachO的常量区,容易被攻击者发现并HOOK篡改.
- 当系统更新,可能会出现白名单以外的依赖库,老版本App将无法使用.
五、ptrace
5.1 ptrace概述
- App可以被lldb动态调试,因为App被设备中的debugserver附加,它会跟踪我们的应用进程(trace process), 而这一过程利用的就是ptrace 函数
- ptrace是系统内核函数,它可以决定应用能否被debugserver附加.如果我们在项目中,调用ptrace函数,将程序设置为拒绝附加,即可对lldb动态调试进行有效的防护.
- ptrace在iOS系统中,无法直接使用,需要导入头文件
- ptrace函数的定义:
int ptrace(int _request, pid _pid, caddr_t _addr, int _data);
-
- request: 请求ptrace执行的操作
- pid: 目标进程的ID
- addr: 目标进程的地址值,和request参数有关
- data: 根据request的不同而变化.如果需要向目标进程中写入数据,data存放的是需要写入的数据.如果从目标进程中读数据,data将存放返回的数据
- 上面的函数怎么来的?
- 不妨回想一下,当初编译llvm源码或者Swift源码的时候,编译出附带的lldb源码,tools中就有debugserver源码,下边是编译Swift5.3.1的源码截图
-
- 打开编译运行解决了lldb_codesign问题后,就可以得到自己编译的debugserver MachO了,并且可以直接调试源码,附加Mac端的进程
5.2 ptrace防护应用
- 搭建App项目AntiDebug
- 导入MyPtraceHeader.h头文件.
- 写入调用代码
#import "ViewController.h"
#import "MyPtraceHeader.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
-
- 使用Xcode运行项目,启动后立即退出.使用ptrace设置为拒绝附加,只能手动启动App,也就是说,用户在使用App时,不会有任何影响.一旦被debugserver附加,就会闪退.
- 如果在越狱环境,手动对App进行debugserver附加呢?
- 找到AntiDebug进程,手动对App进行debugserver附加
Holothurian6P:~ root# ps -A | grep AntiDebug
48329 ?? 0:00.23 /var/containers/Bundle/Application/3D3083F3-295D-4F55-83DB-40887FF4FF46/AntiDebug.app/AntiDebug
48336 ttys000 0:00.00 grep AntiDebug
Holothurian6P:~ root# debugserver localhost:22 -a 48329
debugserver-@(#)PROGRAM:LLDB PROJECT:lldb-900.3.87
for arm64.
Attaching to process 48329...
Segmentation fault: 11
-
- 同样附加失败,无论以何种方式,都会被ptrace函数阻止.
5.3 ptrace防护破解
- ptrace是系统内核函数,被开发者所熟知.ptrace的防护痕迹也很明显,手动运行程序正常,Xcode运行程序闪退
- 我们在逆向一款App时,遇到上述情况,第一时间就会想到ptrace防护
- 由于ptrace是系统函数,需要间接符号表,我们可以尝试下ptrace的符号断点
-
- ptrace的断点命中,确定了对方的防护手段,接下来尝试破解
- 采用AntiDebug项目,模拟应用重签名,注入动态库
- 创建 Inject动态库,创建 InjectCode类
- 在Inject动态类中,导入fishhook,导入MyPtraceHeader.h头文件
#import "InjectCode.h"
#import "MyPtraceHeader.h"
#import "fishhook.h"
@implementation InjectCode
int (*sys_ptrace)(int _request, pid_t _pid, caddr_t _addr, int _data);
int my_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data){
if (_request == PT_DENY_ATTACH) {
return 0;
}
return sys_ptrace(_request,_pid,_addr,_data);
}
+ (void)load {
struct rebinding rebPtrace;
rebPtrace.name = "ptrace";
rebPtrace.replacement = my_ptrace;
rebPtrace.replaced = (void *)&sys_ptrace;
struct rebinding rebs[] = {rebPtrace};
rebind_symbols(rebs, 1);
}
@end
- 在my_ptrace函数中,如果是 PT_DENY_ATTACH 宏定义值,直接返回,如果是其他类型,系统由特定的作用,需要执行ptrace原始函数.
- 运行项目,进入lldb动态调试,ptrace防护破解成功
六、总结
Tweak原理
- Tweak编译产物是动态库
- 打包时,将动态库打包成 .deb 格式
- 插件安装到 /Library/MobileSubstrate/DynamicLibraries目录中
- 安装.dylib和 .plist文件
- .plist记录 .dylib 所要附加的App包名
- Tweak插件使用 DYLD_INSERT_LIBRARIES方式,插入动态库
DYLD_INSERT_LIBRARIES
- 早期的dyld源码中,有进程限制的判断( processIsRestricted)
- 启动进程限制,segment 存在 __RESTRICT段,section存在 __restrict节
- 符合进程限制的条件,清空插入动态库,越狱插件失效
__RESTRICT段防护
- 在Build Setting的 Other Linker Flags中配置
- -Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
- iOS11.0及更高系统,此防护无效
修改MachO破解__RESTRICT段防护
- 使用MachOView打开MachO文件,修改Data段
- 只在以前的数值上替换,不要对其增减,位数不要改变
使用dyld源码防护
- 借鉴dyld源码,读取 segment和section.如果缺少 __RESTRICT段或 __restrict节,说明我们的防护代码被人篡改
- 检测到程序被篡改,不要使用痕迹明显的代码进行防护,容易暴露
- 尽量让攻击者在不知不觉中被系统屏蔽封杀
白名单检测
- 遍历image名称
- _dyld_image_count()
- _dyld_get_image_name()
- 在不同系统下运行项目,整理出尽可能完善的白名单
- 检测到白名单以外的动态库,不要直接处理
- 白名单列表,由服务端下发,或者将逻辑直接做到服务端
ptrace
- 可阻止App被debugserver附加
- 在 iOS系统中,无法直接使用,需要导入头文件
- ptrace函数的定义
- int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
- 破解ptrace
- 防护效果: 手动运行程序正常,Xcode运行程序闪退
- 使用ptrace符号断点试探
- 使用fishhook对ptrace函数HOOK
- 是PT_DENY_ATTACH宏定义值,直接返回.其他类型,执行原始函数