前言
本篇文章接着27-逆向防护(上),继续探讨逆向防护的知识点,首先给大家介绍最常用的混淆,然后重点介绍 👉🏻 如何防护fishhook,这整个过程中,如何一步步地优化我们的防护方案。
一、混淆
相信大家对混淆很熟悉,网上有很多现成的脚本可以实现代码混淆的相关功能,当然也很实用,这里不做说明。接下来给大家重点讲解下混淆需要注意的点。
1.1 核心的类名、方法名称的混淆
通常情况下,OC的项目中,我们混淆的方式通常会采用 👇🏻
脚本混淆👉🏻 统一将类名、方法名用一串随机字符串替换
但是会有个问题,我们创建类的时候,类名和文件名其实是一样的,此时如果采用脚本对核心的类名进行混淆的话,可能会将文件名也一起混淆了,这不是我们想要的,那有没有别的方式对核心类名和方法名称进行混淆呢?当然有👇🏻
利用
语法特性👉🏻 针对OC工程项目,在pch头文件中使用宏定义混淆
宏定义混淆示例
- 新建演示工程
UserInfoDemo,新建示例Model类UserInfo,添加以下代码👇🏻
@interface UserInfo : NSObject
-(BOOL)isVipWithAccount:(NSString *)account;
@end
@implementation UserInfo
-(BOOL)isVipWithAccount:(NSString *)account{
if ([account isEqualToString:@"hank"]) {
return YES;
}
return NO;
}
@end
调用的代码在ViewController.m中👇🏻
#import "ViewController.h"
#import "UserInfo.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if ([[[UserInfo alloc] init] isVipWithAccount:@"hank123"]) {
NSLog(@"是VIP");
}else{
NSLog(@"不是VIP");
}
}
@end
- 尝试
动态调试👇🏻
我们就当做没有源代码,如何定位到UserInfo类,和它的isVipWithAccount这些核心的名称?
首先我们知道,UserInfo的isVipWithAccount方法,通常在类似按钮点击这种情况下触发调用,那么我们可以针对按钮点击的事件下符号断点,在本例中对touchesBegan下符号断点👇🏻

真机运行,触发断点👇🏻

这是在系统底层UIKitCore中触发的,继续点击走断点👇🏻

来到[ViewController touchesBegan:withEvent:]这层,就是页面上触发的时机了,我们看汇编,首地址是0x100f25e28,然后image list查看工程的首地址👇🏻

工程的首地址是0x0000000100f20000,由此计算得到偏移地址是0x100f25e28 - 0x0000000100f20000 = 0x5E28
根据偏移地址0x5E28,hopper搜索Mach-O二进制文件👇🏻


类名、方法名称还有传递的入参hank123一目了然!完全明文,对于破解方来说,一下子就定位到了!
宏定义混淆
接下来,我们使用宏定义混淆。
- 新建pch头文件
PrefixHeader.pch,并且配置路径👇🏻

- 在
PrefixHeader.pch中,添加代码,开始混淆👇🏻
#ifndef PrefixHeader_pch
#define PrefixHeader_pch
#define UserInfo CJKD2534
#define isVipWithAccount KKLDIU34235
#endif /* PrefixHeader_pch */
- 重新编译项目,可以观察到👇🏻

类名和方法名称全部变色了!包括调用的地方也是👇🏻

- 在以同样的方式
动态调试查看偏移地址0x5E28👇🏻

类名、方法名称都被替换了!🍺🍺🍺🍺🍺🍺 此时想要定位到核心类名和方法名,难度就大了!
尝试符号断点,查看调用栈,也是宏定义替换后的结果,一脸懵逼😳,头疼!
由此可见,宏定义混淆相对于脚本混淆的优点在于👇🏻
代码不需要改变,项目
无污染,轻量级!
1.2 常量的混淆
细心的你会发现,入参值hank123仍然可以看到,在我们的开发场景中,也存在一些敏感信息,需要作为入参传递,但是不想被破解,那么如何解决呢?第一时间想到的就是加密,接下来我们采用AES对称加密算法,解决下面的示例👇🏻
- 首先
AES/DES对称加密的算法代码如下👇🏻
EncryptionTools.h
#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonCrypto.h>
@interface EncryptionTools : NSObject
+ (instancetype)sharedEncryptionTools;
/**
@constant kCCAlgorithmAES 高级加密标准,128位(默认)
@constant kCCAlgorithmDES 数据加密标准
*/
@property (nonatomic, assign) uint32_t algorithm;
/**
* 加密字符串并返回base64编码字符串
*
* @param string 要加密的字符串
* @param keyString 加密密钥
* @param iv 初始化向量(8个字节)
*
* @return 返回加密后的base64编码字符串
*/
- (NSString *)encryptString:(NSString *)string keyString:(NSString *)keyString iv:(NSData *)iv;
- (NSString *)encryptString:(NSString *)string;
/**
* 解密字符串
*
* @param string 加密并base64编码后的字符串
* @param keyString 解密密钥
* @param iv 初始化向量(8个字节)
*
* @return 返回解密后的字符串
*/
- (NSString *)decryptString:(NSString *)string keyString:(NSString *)keyString iv:(NSData *)iv;
- (NSString *)decryptString:(NSString *)string;
@end
EncryptionTools.m
#import "EncryptionTools.h"
@interface EncryptionTools()
@property (nonatomic, assign) int keySize;
@property (nonatomic, assign) int blockSize;
@property (nonatomic, copy, readwrite) NSString *key;
@end
@implementation EncryptionTools
+ (instancetype)sharedEncryptionTools {
static EncryptionTools *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
instance.algorithm = kCCAlgorithmAES;
});
return instance;
}
- (void)setAlgorithm:(uint32_t)algorithm {
_algorithm = algorithm;
switch (algorithm) {
case kCCAlgorithmAES:
self.keySize = kCCKeySizeAES128;
self.blockSize = kCCBlockSizeAES128;
break;
case kCCAlgorithmDES:
self.keySize = kCCKeySizeDES;
self.blockSize = kCCBlockSizeDES;
break;
default:
break;
}
}
- (NSString *)encryptString:(NSString *)string {
// 生成>=24位的key
if (self.key == nil || self.key.length == 0) {
NSMutableString *randomString = [NSMutableString stringWithCapacity:24];
for (int i = 0; i < 24; i++) {
[randomString appendFormat: @"%C", [kRandomAlphabet characterAtIndex:arc4random_uniform((u_int32_t)[kRandomAlphabet length])]];
}
self.key = randomString;
NSLog(@"=-=-DES.key = %@", self.key);
}
NSString *ivStr = @"00000000";
return [self encryptString:string keyString:self.key iv:[ivStr dataUsingEncoding:NSUTF8StringEncoding]];
}
- (NSString *)encryptString:(NSString *)string keyString:(NSString *)keyString iv:(NSData *)iv {
// 设置秘钥
NSData *keyData = [keyString dataUsingEncoding:NSUTF8StringEncoding];
uint8_t cKey[self.keySize];
bzero(cKey, sizeof(cKey));
[keyData getBytes:cKey length:self.keySize];
// 设置iv
uint8_t cIv[self.blockSize];
bzero(cIv, self.blockSize);
int option = 0;
if (iv) {
[iv getBytes:cIv length:self.blockSize];
option = kCCOptionPKCS7Padding;
} else {
option = kCCOptionPKCS7Padding | kCCOptionECBMode;
}
// 设置输出缓冲区
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
size_t bufferSize = [data length] + self.blockSize;
void *buffer = malloc(bufferSize);
// 开始加密
size_t encryptedSize = 0;
//加密解密都是它 -- CCCrypt
CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
self.algorithm,
option,
cKey,
self.keySize,
cIv,
[data bytes],
[data length],
buffer,
bufferSize,
&encryptedSize);
NSData *result = nil;
if (cryptStatus == kCCSuccess) {
result = [NSData dataWithBytesNoCopy:buffer length:encryptedSize];
} else {
free(buffer);
NSLog(@"[错误] 加密失败|状态编码: %d", cryptStatus);
}
return [result base64EncodedStringWithOptions:0];
}
- (NSString *)decryptString:(NSString *)string {
NSString *ivStr = @"00000000";
return [self decryptString:string keyString:self.key iv:[ivStr dataUsingEncoding:NSUTF8StringEncoding]];
}
- (NSString *)decryptString:(NSString *)string keyString:(NSString *)keyString iv:(NSData *)iv {
// 设置秘钥
NSData *keyData = [keyString dataUsingEncoding:NSUTF8StringEncoding];
uint8_t cKey[self.keySize];
bzero(cKey, sizeof(cKey));
[keyData getBytes:cKey length:self.keySize];
// 设置iv
uint8_t cIv[self.blockSize];
bzero(cIv, self.blockSize);
int option = 0;
if (iv) {
[iv getBytes:cIv length:self.blockSize];
option = kCCOptionPKCS7Padding;
} else {
option = kCCOptionPKCS7Padding | kCCOptionECBMode;
}
// 设置输出缓冲区
NSData *data = [[NSData alloc] initWithBase64EncodedString:string options:0];
size_t bufferSize = [data length] + self.blockSize;
void *buffer = malloc(bufferSize);
// 开始解密
size_t decryptedSize = 0;
CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
self.algorithm,
option,
cKey,
self.keySize,
cIv,
[data bytes],
[data length],
buffer,
bufferSize,
&decryptedSize);
NSData *result = nil;
if (cryptStatus == kCCSuccess) {
result = [NSData dataWithBytesNoCopy:buffer length:decryptedSize];
} else {
free(buffer);
NSLog(@"[错误] 解密失败|状态编码: %d", cryptStatus);
}
return [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding];
}
@end
- 我们使用上面的加密类
EncryptionTools进行加密,在UserInfo类中添加发送信息的方法,对发送的信息进行加密👇🏻
@interface UserInfo : NSObject
-(BOOL)isVipWithAccount:(NSString *)account;
-(void)sendWithUserInfo:(NSString *)info;
@end
NSString * const AES_KEY = @"IU**YD#$%()*";
@implementation UserInfo
-(BOOL)isVipWithAccount:(NSString *)account{
if ([account isEqualToString:@"hank"]) {
return YES;
}
return NO;
}
//给服务器一些敏感的信息
-(void)sendWithUserInfo:(NSString *)info{
NSLog(@"加密之后%@",[[EncryptionTools sharedEncryptionTools] encryptString:info keyString:AES_KEY iv:nil]);
}
@end
调用的地方,将判断账户hank123改为hank,满足发送信息的条件👇🏻
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UserInfo * user = [[UserInfo alloc] init];
if ([user isVipWithAccount:@"hank"]) {
[user sendWithUserInfo:@"some msg"];
NSLog(@"是VIP");
}else{
NSLog(@"不是VIP");
}
}
- 再增加3个宏定义,混淆加密的类名
EncryptionTools和方法名encryptString keyString👇🏻
#define EncryptionTools KKLDIU32035
#define encryptString KOIE76875
#define keyString JUIIYT8776
- 运行查看Mach-O文件👇🏻

上图可见,我们用Hopper查看Mach-O文件,在方法sendWithUserInfo中,看到了加密的密钥信息,这点就很危险了,这个密钥信息就是项目的核心代码,当然是不能暴露出来的。
- 接下来,我们想办法隐藏这个密钥信息,通过下面的方式👇🏻
#define KEY 0xAC
static NSString * AES_KEYINFO(){
//这种方式能够让这些字符串不进入常量区。
unsigned char key[] = {
(KEY ^ 'I'),
(KEY ^ 'U'),
(KEY ^ '&'),
(KEY ^ '*'),
(KEY ^ '('),
(KEY ^ '$'),
(KEY ^ '%'),
(KEY ^ ')'),
(KEY ^ '\0')
};
unsigned char * p = key;
while (((*p) ^= KEY) != '\0') p++;
return [NSString stringWithUTF8String:(const char *)key];
}
以上通过以字符为单位,遍历^异或固定地址KEY的方式,生成了密钥keyString。然后调用密钥的地方这么写👇🏻
//给服务器一些敏感的信息
-(void)sendWithUserInfo:(NSString *)info{
NSLog(@"加密之后%@",[[EncryptionTools sharedEncryptionTools] encryptString:info keyString:AES_KEYINFO() iv:nil]);
}
将之前的AES_KEY改为AES_KEYINFO()。
- 再次查看Mach-O文件👇🏻

在sendWithUserInfo中就能看到AES_KEYINFO,继续跟进查看👇🏻

AES_KEYINFO中的汇编,就看不到密钥信息了,证明我们成功的将之前的AES_KEY从常量区移除了!🍺🍺🍺🍺🍺🍺
小结
上述示例中,我们发现:模拟网络请求,对敏感数据进行对称加密时,存在漏洞 👇🏻
对称加密的
密钥key,可以在寄存器中读取!
我们通过符号断点 + Mach-O即动态调试+静态分析的方式,根据调用栈查汇编代码的执行流程,最终能查找到对称加密的密钥key,这点就是灾难了,必须解决。
解决措施 👇🏻
- 先
混淆方法名称,类名称 -
函数替换全局常量定义的方式 - 通过
^异或计算的方式 👉🏻 移除常量区
⚠️ 注意:
大量的流程的混淆,会导致无法上线!
如果你大量混淆了很多流程的代码,苹果在审核的时候就能检测到,会导致你App上线失败!所以我们平时只能对一些很关键的流程做混淆。
二、fishhook的防护
上篇27-逆向防护(上)中对ptrace防护做了介绍,并且通过fishhok的方式可以破解ptrace防护,我们接着这个点继续深究,如何做到 👉🏻 破解你的fishhok防护,让ptrace继续有效?
2.1 dlopen函数
之前的ptrace示例逻辑是这样👇🏻
- fishhook的代码👇🏻

- 调用的代码👇🏻

这样真机运行调试起来不会断开。
- 我们改下调用的代码👇🏻
#import "ViewController.h"
#import "MyPtraceHeader.h"
#import <dlfcn.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//通过dlopen拿到句柄
void * handle = dlopen("/usr/lib/system/libsystem_kernel.dylib", RTLD_LAZY);
//定义函数指针
int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);
ptrace_p = dlsym(handle, "ptrace");
if (ptrace_p) {
ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
}
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"正常运行!!!");
}
@end
通过dlopen的方式,通过ptrace所在的动态库的地址拿到句柄,然后dlsym的方式通过ptrace字符串构造ptrace方法并调用,达到防护的目的。
- 能否做到防护
fishhook呢? 👉🏻通过MachOView查看,Lazy表里没有ptrace符号👇🏻

所以fishhook失效,真机一运行,会自动断开!
2.2 破解dlopen
接下来,我们以破解者的身份,看看如何破解dlopen。
- Hopper查看,能否找到"ptrace"字符串👇🏻

全局搜索👇🏻

能找到,那么就能静态修改 👉🏻 可以nop,也可以将字符串改成别的值,这样就能绕过prace的调用,达到破解的目的。那么如何避免呢?接着往下看。
- 利用上面的
常量的混淆所使用的^(异或)地址的方式,提前改掉"ptrace"字符串,这样破解方者则无法找到"ptrace"字符串,代码如下👇🏻
- (void)viewDidLoad {
[super viewDidLoad];
//拼接一个 ptrace
unsigned char funcStr[] = {
('a' ^ 'p'),
('a' ^ 't'),
('a' ^ 'r'),
('a' ^ 'a'),
('a' ^ 'c'),
('a' ^ 'e'),
('a' ^ '\0'),
};
unsigned char * p = funcStr;
while (((*p) ^= 'a') != '\0') p++;
//通过dlopen拿到句柄
void * handle = dlopen("/usr/lib/system/libsystem_kernel.dylib", RTLD_LAZY);
//定义函数指针
int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);
ptrace_p = dlsym(handle, (const char *)funcStr);
if (ptrace_p) {
ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
}
}
继续Hopper查看搜索ptrace,就找不到了!
2.3 破解你的破解
既然上面能防护dlopen的破解,那接下来再破解你的这个防护!
- 下
ptrace符号断点

真机运行断住,查看调用栈👇🏻

那么调用的地址是0x1041ca5b0。
- 通过
image list获取首地址,计算偏移地址👇🏻

首地址是0x00000001041c4000,那么偏移地址 👉🏻 0x1041ca5b0 - 0x00000001041c4000 = 0x65B0
- Hopper搜索
0x65B0👇🏻


以上就是对抗fishhook!
2.4 对抗完美方案
以上2.3中是通过下ptrace符号断点,找地址,再根据地址,在Mach-O里面分析,找到ptrace的调用指令,直接nop一下,达到破解目的。但是这样的方案并不是完美的,接下来我们看看完美的方案 👇🏻
使符号断点失效,破解方无从下手!
GCD 尝试
在研究完美方案之前,我们尝试用GCD,在block中调用对抗代码,看看是什么效果。
- 尝试一:在
dispatch_after的block中执行👇🏻
- (void)viewDidLoad {
[super viewDidLoad];
//拼接一个 ptrace
unsigned char funcStr[] = {
('a' ^ 'p'),
('a' ^ 't'),
('a' ^ 'r'),
('a' ^ 'a'),
('a' ^ 'c'),
('a' ^ 'e'),
('a' ^ '\0'),
};
unsigned char * p = funcStr;
while (((*p) ^= 'a') != '\0') p++;
//通过dlopen拿到句柄
void * handle = dlopen("/usr/lib/system/libsystem_kernel.dylib", RTLD_LAZY);
//定义函数指针
int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);
ptrace_p = dlsym(handle, (const char *)funcStr);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (ptrace_p) {
ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
}
});
}
下符号断点ptrace,可以看到👇🏻

上图可见 👉🏻 可以确定是在_block_invoke中执行的ptrace。
接着查看Mach-O👇🏻
偏移地址是0x100982560 - 首地址 0x000000010097c000 = 0x6560


一样可以定位到这个blr跳转指令,所以也可以修改为nop指令,达到绕开prace的目的,结论 👉🏻 dispatch_after方式无效!
- 尝试二:改为
全局队列中执行👇🏻
- (void)viewDidLoad {
[super viewDidLoad];
antyDebug();
}
void antyDebug () {
//拼接一个 ptrace
unsigned char funcStr[] = {
('a' ^ 'p'),
('a' ^ 't'),
('a' ^ 'r'),
('a' ^ 'a'),
('a' ^ 'c'),
('a' ^ 'e'),
('a' ^ '\0'),
};
unsigned char * p = funcStr;
while (((*p) ^= 'a') != '\0') p++;
//通过dlopen拿到句柄
void * handle = dlopen("/usr/lib/system/libsystem_kernel.dylib", RTLD_LAZY);
//定义函数指针
int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);
ptrace_p = dlsym(handle, (const char *)funcStr);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),dispatch_get_global_queue(0, 0), ^{
if (ptrace_p) {
ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
}
});
}
我们再去个本地符号, buildSetting中去符号化,strip👇🏻

断点一样可以定位到👇🏻

既然能定位地址,所以还是一样可以利用hopper修改地址所对应的汇编指令,仍然无效!
- 尝试三:在
dispatch_source_t中的block执行👇🏻
既然dispatch_after不行,我们换用定时器dispatch_source_t看看👇🏻
- (void)viewDidLoad {
[super viewDidLoad];
antyDebug();
}
static dispatch_source_t timer;
void antyDebug () {
//拼接一个 ptrace
unsigned char funcStr[] = {
('a' ^ 'p'),
('a' ^ 't'),
('a' ^ 'r'),
('a' ^ 'a'),
('a' ^ 'c'),
('a' ^ 'e'),
('a' ^ '\0'),
};
unsigned char * p = funcStr;
while (((*p) ^= 'a') != '\0') p++;
//通过dlopen拿到句柄
void * handle = dlopen("/usr/lib/system/libsystem_kernel.dylib", RTLD_LAZY);
//定义函数指针
int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);
ptrace_p = dlsym(handle, (const char *)funcStr);
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
if (ptrace_p) {
ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
}
});
dispatch_resume(timer);
}
run,仍然可以定位到block_invoke的地址👇🏻

- 尝试四:仍然使用
dispatch_after👇🏻
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
antyDebug();
});
}
void antyDebug () {
//拼接一个 ptrace
unsigned char funcStr[] = {
('a' ^ 'p'),
('a' ^ 't'),
('a' ^ 'r'),
('a' ^ 'a'),
('a' ^ 'c'),
('a' ^ 'e'),
('a' ^ '\0'),
};
unsigned char * p = funcStr;
while (((*p) ^= 'a') != '\0') p++;
//通过dlopen拿到句柄
void * handle = dlopen("/usr/lib/system/libsystem_kernel.dylib", RTLD_LAZY);
//定义函数指针
int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);
ptrace_p = dlsym(handle, (const char *)funcStr);
if (ptrace_p) {
ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
}
}
但是strip去符号得注意👇🏻
- 对
HankHook动态库是去debug调试符号👇🏻

- 对主工程是去
所有符号👇🏻

再次运行👇🏻

调用栈中就无法找到block_invoke的地址了!
但是,注意一个细节,XCode会过滤调用栈的一些信息👇🏻

取消这个选择的过滤,看看👇🏻

仍然能定位到调用的地址!
结论:
GCD的Block执行对抗代码 👉🏻 有待研究!
使符号断点失效
以上我们通过GCD的尝试,最终以失败告终。但是,我们从中也得到一个启示👇🏻
GCD的Block无效,是因为我们下了ptrace的符号断点,这个符号断点一直能断住,就能锁定地址!
顺着该思路,那能否不触发符号断点?当然能 👇🏻
执行
syscall不会触发符号断点!
代码很简单,就一句👇🏻

需引入头文件
#import <sys/syscall.h>
真机运行,直接断开,连符号断点也没断住!连fishhook都没法hook住!
syscall
/**
1、编号,你要调用哪个系统函数
2、后面都是参数!
*/
syscall(26,31,0,0,0);
- 第一个参数
26的意思👇🏻

26就是ptrace,所以真机一运行就断开,和ptrace的特点一模一样!唯一不同的是 👉🏻 不会触发符号断点!
- 能否hook
syscall
首先查看syscall是否在间接符号表中,因为fishhook就是hook间接符号表中的符号👇🏻

上图可见,有符号,那么就能使用fishhook hooksyscall,那怎么防住fishhook呢?上面我们讲过,将syscall使用dlopen dlsym移除常量区,可以做到防护fishhook,这里就无限套娃🪆了。
汇编模式
我们不想通过移除常量区去防护fishhook,说白了,就是syscall能做到防止fishhook防护ptrace,但是无法防护自己!那有没有别的方式?👇🏻
使用
汇编代码执行syscall。
- (void)viewDidLoad {
[super viewDidLoad];
// ptrace(PT_DENY_ATTACH, 0, 0, 0);
//syscall
/**
1、编号,你要调用哪个系统函数
2、后面都是参数!
*/
// syscall(26,31,0,0,0);
//相当于是调用syscall
asm volatile(
"mov x0,#26\n"
"mov x1,#31\n"
"mov x2,#0\n"
"mov x3,#0\n"
"mov x4,#0\n"
"mov x16,#0\n"//这里就是syscall的编号
"svc #0x80\n"//这条指令就是触发中断(系统级别的跳转!)
);
}
既然汇编能执行syscall,同样,也能直接执行ptrace👇🏻
//下面就是直接调用ptrace
asm volatile(
"mov x0,#31\n"
"mov x1,#0\n"
"mov x2,#0\n"
"mov x3,#0\n"
"mov x16,#26\n"//这里26就是ptrace
"svc #0x80\n"//这条指令就是触发中断(系统级别的跳转!)
);
这种汇编的模式,想要破解的话,就是只能全局搜索svc指令,通过上下文分析汇编代码,得出它所执行的功能。所以,没有绝对的防护!
总结
- 混淆
- 可使用
脚本进行统一的混淆 - 关键类名、方法名称的混淆 👉🏻
宏定义混淆 - 常量的混淆 👉🏻 移除常量区👇🏻
◦ char数组遍历
◦ ^异或固定地址
- 可使用
-
fishhook的防护-
dlopen👉🏻 传入ptrace所在动态库的地址,得到句柄 -
dlsym👉🏻 通过句柄和ptrace字符串,得到ptrace的函数调用指针,直接调用 - 破解
dlopen👉🏻常量的混淆方式破解 - 防护
破解dlopen👇🏻
◦ 下ptrace符号断点,计算出偏移地址
◦ 根据偏移地址,Hopper检索出汇编指令,改为nop,但并不完美 - 完美对抗
fishhook
◦GCD+strip去符号👉🏻 有待研究!
◦符号断点失效👇🏻-
syscall👉🏻 第一个参数26代表SYS_ptrace,1代表SYS_exit - 防护
syscall👉🏻 直接执行汇编代码
-
-