前言
本篇文章接着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
👉🏻 直接执行汇编代码
-
-