iOS逆向实战--032:越狱防护
Tweak原理
执行
make
命令时,在.theos
的隐藏目录中,编译出obj/debug
目录,包含arm64
、armv7
两种架构,同时生成RedDemo.dylib
动态库
在
arm64
、armv7
目录中,有各自架构的RedDemo.dylib
,而debug
目录中的RedDemo.dylib
,是一个Fat Binary
文件file RedDemo.dylib ------------------------- RedDemo.dylib: Mach-O universal binary with 2 architectures: [arm_v7:Mach-O dynamically linked shared library arm_v7] [arm64:Mach-O 64-bit dynamically linked shared library arm64] RedDemo.dylib (for architecture armv7): Mach-O dynamically linked shared library arm_v7 RedDemo.dylib (for architecture arm64): Mach-O 64-bit dynamically linked shared library arm64
Tweak
的编译产物是动态库,将其注入的方式有两种:
- 修改
MachO
文件的Load Commands
,注入LC_LOAD_DYLIB (XXX)
,然后根据路径找到动态库。这种方式对程序的污染比较严重,容易被开发者检测出来- 通过
DYLD_INSERT_LIBRARIES
环境变量,插入动态库
Tweak
插件,使用的是方式二,因为程序没有被污染。在MachO
中,并没有找到LC_LOAD_DYLIB (XXX)
执行
make package
命令时,在packages
目录中,生成.deb
文件。每执行一次打包命令,都会生成一个新的.deb
文件
.deb
格式类似于.ipa
格式
.ipa
包通过AppStore
安装,将.ipa
包中的App
安装到设备中.deb
包通过Cydia
安装,将.deb
包中的动态库安装到设备中
执行
make install
命令时,在.deb
包中的动态库,会被安装到设备的/Library/MobileSubstrate/DynamicLibraries
目录中以相同的名称,分别存储
.dylib
和.plist
文件
.dylib
为动态库,而.plist
,记录.dylib
所依附的App
包名
DYLD_INSERT_LIBRARIES
在早期的
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
段- 读取
section
,如果为__restrict
节- 如果都存在,返回
trur
,表示进程限制
__RESTRICT段防护
在项目中,添加
__RESTRICT
段,__restrict
节,开启进程限制,对越狱插件进行防护
搭建
App
项目,命名:antiTweak
打开
ViewController.m
文件,写入以下代码:- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ exit(0); }
进程限制,是早期
dyld
源码中的逻辑,在低系统下才能生效使用
iOS9.1
系统运行项目,点击屏幕就会闪退
搭建
Tweak
插件,附加antiTweak
应用打开
Tweak.x
文件,写入以下代码:#import <UIKit/UIKit.h> %hook ViewController -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"🍺🍺🍺🍺🍺"); } %end
安装插件,启动应用,
touchesBegan
方法被插件HOOK
。点击屏幕,闪退变为打印
为
antiTweak
项目,添加__RESTRICT
段,__restrict
节在
Build Setting
的Other Linker Flags
中,加入以下设置:-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
编译项目,查看
MachO
文件
- 成功插入
__RESTRICT
段,__restrict
节运行项目,点击屏幕闪退。说明插入的动态库已被清空,越狱插件全部失效
这种防护手段,在早期系统中比较有效。但在
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
方法,调用防护代码+(void)load{ 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
运行项目,输出以下结果:
antiTweak[2535:549785] 被篡改
当检测到
MachO
被篡改,不要使用痕迹明显的代码进行防护,例如:exit(0)
。此类代码相当于记号,让攻击者很容易找到防护的位置和逻辑高明的防护手段,应该让攻击者不易察觉,在不知不觉中被系统屏蔽封杀
白名单检测
进程限制的防护手段,仅低版本系统有效。对于高版本系统的防护,我们可以自制白名单进行检测
延用
antiTweak
项目整理出
App
依赖库的白名单打开
ViewController.m
文件,写入以下代码:#import "ViewController.h" #import <mach-o/loader.h> #import <mach-o/dyld.h> @implementation ViewController +(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); } } @end
在未越狱的设备上,运行项目,遍历所有
image
名称
打印结果,相当于一份白名单。如果
App
运行时,加载了白名单以外的动态库,该库很可能是被第三方注入的
检测注入的动态库
打开
ViewController.m
文件,写入以下代码:#import "ViewController.h" #import <mach-o/loader.h> #import <mach-o/dyld.h> const char* strList = "/private/var/containers/Bundle/Application/E7D8C05C-D581-463F-96AC-791B816265C6/antiTweak..."; @implementation ViewController +(void)load{ uint32_t intCount = _dyld_image_count(); for (int intIndex=0; intIndex<intCount; intIndex++) { const char* strName = _dyld_get_image_name(intIndex); if(intIndex==0 || strstr(strList, strName)){ continue; } printf("注入动态库:%s\n",strName); } } @end
在
load
方法中,循环遍历依赖的动态库。如果动态库不是当前MachO
文件,或者包含白名单中,属于合法库,直接跳过。否则,将其打印当前
MachO
文件,不需要判断,因为沙盒路径无法固定在越狱设备上运行项目,输出很多白名单以外的动态库,其中包含自制的
antiTweakDemo
插件
使用此方法进行防护,需要注意以下几点:
- 在不同系统下运行项目,整理出尽可能完善的白名单
- 检测到白名单以外的动态库,不要直接处理。这里建议先收集数据,如果此动态库是我们缺漏的,将其补充到白名单中。如果确认是恶意注入,再做处理
- 白名单列表,由服务端下发,或者将逻辑直接做到服务端
白名单写在客户端的弊端:
- 白名单的字符串,位于
MachO
的常量区,容易被攻击者发现并HOOK
- 当系统更新,可能会出现白名单以外的依赖库,老版本
App
将无法使用
ptrace
App
可以被lldb
动态调试,因为App
被设备中的debugserver
附加,它会跟踪我们的应用进程(trace process
),而这一过程利用的就是ptrace
函数
ptrace
是系统内核函数,它可以决定应用能否被debugserver
附加。如果我们在项目中,调用ptrace
函数,将程序设置为拒绝附加,即可对lldb
动态调试进行有效的防护
ptrace
在iOS
系统中,无法直接使用,需要导入头文件
ptrace
函数的定义:int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
request
:请求ptrace
执行的操作pid
:目标进程的ID
addr
:目标进程的地址值,和request
参数有关data
:根据request
的不同而变化。如果需要向目标进程中写入数据,data
存放的是需要写入的数据。如果从目标进程中读数据,data
将存放返回的数据
搭建
App
项目,命名:antiDebug
导入
MyPtraceHeader.h
头文件打开
ViewController.m
文件,写入以下代码:#import "ViewController.h" #import "MyPtraceHeader.h" @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; ptrace(PT_DENY_ATTACH, 0, 0, 0); } @end
使用
Xcode
运行项目,启动后立即退出。使用ptrace
设置为拒绝附加,只能手动启动App
也就是说,用户在使用
App
时,不会有任何影响。一旦被debugserver
附加,就会闪退
如果在越狱环境,手动对
App
进行debugserver
附加呢?找到
antiDebug
进程ps -A | grep antiDebug ------------------------- 12233 ?? 0:00.27 /var/containers/Bundle/Application/5DC00A3B-C095-46D1-9842-A3C35401DD07/antiDebug.app/antiDebug
手动对
App
进行debugserver
附加debugserver localhost:12346 -a 12233 ------------------------- debugserver-@(#)PROGRAM:LLDB PROJECT:lldb-900.3.87 for arm64. Attaching to process 12233... Segmentation fault: 11
同样附加失败,无论以何种方式,都会被
ptrace
函数阻止
破解ptrace
ptrace
是系统内核函数,被开发者所熟知。ptrace
的防护痕迹也很明显,手动运行程序正常,Xcode
运行程序闪退我们在逆向一款
App
时,遇到上述情况,第一时间就会想到ptrace
防护由于
ptrace
是系统函数,需要间接符号表,我们可以试探性的下一个ptrace
的符号断点
ptrace
的断点命中,我们确定了对方的防护手段,想要破解并非难事
延用
antiDebug
项目,模拟应用重签名,注入动态库创建
Inject
动态库,创建InjectCode
类在
Inject
动态库中,导入fishhook
,导入MyPtraceHeader.h
头文件打开
InjectCode.m
文件,写入以下代码:#import "InjectCode.h" #import "MyPtraceHeader.h" #import "fishhook.h" @implementation InjectCode +(void)load{ struct rebinding reb; reb.name="ptrace"; reb.replacement=my_ptrace; reb.replaced=(void *)&sys_ptrace; struct rebinding rebs[]={reb}; rebind_symbols(rebs, 1); } 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); } @end
在
ptrace_my
函数中,如果是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
及更高系统,此防护无效修改
MachO
破解
- 使用
MachOView
打开MachO
文件,修改Data
值- 只在以前的数值上替换,不要对其增减,位数不要改变
使用
dyld
源码防护
- 借鉴
dyld
源码,读取segment
和section
。如果缺少__RESTRICT
段或__restrict
节,说明我们的防护代码被人篡改- 检测到程序被篡改,不要使用痕迹明显的代码进行防护,容易暴露
- 尽量让攻击者在不知不觉中被系统屏蔽封杀
白名单检测
- 遍历
image
名称
◦_dyld_image_count()
◦_dyld_get_image_name(i)
- 在不同系统下运行项目,整理出尽可能完善的白名单
- 检测到白名单以外的动态库,不要直接处理
- 白名单列表,由服务端下发,或者将逻辑直接做到服务端
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
枚举值,直接返回。其他类型,执行原始函数