为了防止项目被逆向调试,需要对代码增加反调试相关功能。反调试的策略主要分为两种,一种是禁止调试,一种是监测是否被调试
1、禁止调试
ptrace简介
要想知道怎么禁止调试,需要先知道什么是ptrace
ptrace是C标准库中的一个函数,该函数是一个系统调用方法 , 可以监视进程执行 , 查看 / 更改 被监视进程的 内存 和 寄存器 情况 , 常用于断点调试。
ptrace方法如下
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
可以看到ptrace有四个参数:
enum __ptrace_request request:指定ptrace要执行的命令。
pid_t pid: 跟踪进程id。
void *addr: 进程的某个内存地址。
void *data: 存放读取出的或者要写入的数据。
第一个参数可以传入以下值
PTRACE_TRACEME, 本进程被其父进程所跟踪。其父进程应该希望跟踪子进程
PTRACE_PEEKTEXT, 从内存地址中读取一个字节,内存地址由addr给出
PTRACE_PEEKDATA, 同上
PTRACE_PEEKUSER, 可以检查用户态内存区域(USER area),从USER区域中读取一个字节,偏移量为addr
PTRACE_POKETEXT, 往内存地址中写入一个字节。内存地址由addr给出
PTRACE_POKEDATA, 往内存地址中写入一个字节。内存地址由addr给出
PTRACE_POKEUSER, 往USER区域中写入一个字节,偏移量为addr
PTRACE_GETREGS, 读取寄存器
PTRACE_GETFPREGS, 读取浮点寄存器
PTRACE_SETREGS, 设置寄存器
PTRACE_SETFPREGS, 设置浮点寄存器
PTRACE_CONT, 重新运行
PTRACE_SYSCALL, 重新运行
PTRACE_SINGLESTEP, 设置单步执行标志
PTRACE_ATTACH,追踪指定pid的进程
PTRACE_DETACH, 结束追踪
具体ptrace用法和原理可以参考下文
https://zhuanlan.zhihu.com/p/438534744
那么怎么用ptrace来实现反调试呢
其实除了上述参数,苹果增加了一个PT_DENY_ATTACH选项,这个参数用来告诉系统,阻止调试器依附。
OC中使用ptrace阻止调试器依附
在OC中,我们可以在main方法中这样写
int main(int argc, char * argv[]) {
@autoreleasepool {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
但是遗憾的是iPhone运行环境sys/ptrace.h是没有暴露的,
所以我们需要自定义ptrace方法
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import </usr/include/sys/ptrace.h>
#import <dlfcn.h>
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
int main(int argc, char * argv[]) {
@autoreleasepool {
void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
ptrace_ptr_t ptrace_ptr = (ptrace_ptr_t)dlsym(handle, "ptrace");
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
dlopen是一个计算机函数,功能是以指定模式打开指定的动态链接库文件,并返回一个句柄给dlsym的调用进程。
dlsym功能是根据动态链接库操作句柄与符号,返回符号对应的地址。
这样,拿到了ptrace,第一个参数传入PT_DENY_ATTACH,实现阻止调试器依附。
这是OC是实现方案。那么在swift中该如何使用ptrace呢?
Swift中使用ptrace阻止调试器依附
在OC中main函数如下
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
并且由于OC是完全兼容C的,所以可以直接在main方法中调用ptrace函数,但是Swift该怎么做呢?
首先,在Swift中,main函数不见了,换成了@UIApplicationMain。
@UIApplicationMain可以理解成声明了一个main函数,编译器收到这个指令会自动创建main函数
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
}
由于要在main函数中执行ptrace方法(其实不需要一定在main方法中执行ptrace,在main方法或者main方法执行之前调用ptrace都可以,比如在load中执行),我们需要自定义main函数。
新建main.swift文件。
main.swift文件内容如下
import Foundation
import UIKit
autoreleasepool {
UIApplicationMain(
CommandLine.argc,
UnsafeMutableRawPointer(CommandLine.unsafeArgv)
.bindMemory(
to: UnsafeMutablePointer<Int8>.self,
capacity: Int(CommandLine.argc)),
nil,
NSStringFromClass(AppDelegate.self)
)
}
新建C文件myPtrace
#include "myPtrace.h"
#import <sys/types.h>
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
#if !defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH 31
#endif
void disable_gdb() {
void* handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
ptrace_ptr_t ptrace_ptr = (ptrace_ptr_t)dlsym(handle, "ptrace");
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
dlclose(handle);
}
在main.swift中调用
disable_gdb()
autoreleasepool {
UIApplicationMain(
...
)
}
ptrace攻防战
由于ptrace可以被hook,所以我们可以通过提前hook来防止别人hook,新建一个framework,使这个framework在Link Binary Libraries中靠前,在framework中hook住ptrace,并调用,这样可以防止ptrace被hook。
其实,就算这样,ptrace也可能通过修改二进制导致ptrace失效。
更多相关内容可以参考下文
https://www.jianshu.com/p/9ed2de5e7497
2、监测是否被调试
实现原理
当一个进程被调试时,该进程会有一个标识(P_TRACED)来标记自己正在被调试,这时,可以使用sysctl函数获取到当前进程的相关信息,分析这些信息了解进程是否正在被调试,从而阻止调试。
sysctl监听调试
func checkTracing(){
let mib = UnsafeMutablePointer<Int32>.allocate(capacity: 4)
mib[0] = CTL_KERN//内核查看
mib[1] = KERN_PROC//进程查看
mib[2] = KERN_PROC_PID//进程ID
mib[3] = getpid()//获取pid
var size: Int = MemoryLayout<kinfo_proc>.size
var info: kinfo_proc? = nil
/* 函数的返回值若为0时,证明没有错误,其他数字为错误码。 arg1 传入一个数组,该数组中的第一个元素指定本请求定向到内核的哪个子系统。第二个及其后元素依次细化指定该系统的某个部分。 arg2 数组中的元素数目 arg3 一个结构体,指向一个供内核存放该值的缓冲区,存放进程查询结果 arg4 缓冲区的大小 arg5/arg6 为了设置某个新值,arg5参数指向一个大小为arg6参数值的缓冲区。如果不准备指定一个新值,那么arg5应为一个空指针,arg6因为0. */
sysctl(mib, 4, &info, &size, nil, 0)
//info.kp_proc.p_flag中存放的是标志位(二进制),在proc.h文件中有p_flag的宏定义,通过&运算可知对应标志位的值是否为0。(若结果值为0则对应标志位为0)。其中P_TRACED为正在跟踪调试过程。
if (info.unsafelyUnwrapped.kp_proc.p_flag & P_TRACED) > 0 {
//监听到被调试,退出程序
exit(1)
}
}
由于sysctl是系统函数,也是可以被hook的,为了防止被hook,同ptrace一样,可以在自己写的framework中定时调用该方法来防止被fishhook。
更多
但是加了这两层防护也是不安全的,因为在逆向工程中,可以通过修改macho的二进制来破解ptrace or sysctl,所以通过汇编来调用sysctl/ptrace/exit等函数可能是相对安全的方法。由于笔者水平有限,汇编相关内容在这里就不再讲解了。