前言
本篇文章接着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 *)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
#import
@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 *)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
@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 *)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
真机运行,直接断开,连符号断点
也没断住!连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
直接执行汇编代码
-
-