為了防止項(xiàng)目被逆向調(diào)試,需要對代碼增加反調(diào)試相關(guān)功能。反調(diào)試的策略主要分為兩種,一種是禁止調(diào)試,一種是監(jiān)測是否被調(diào)試
1、禁止調(diào)試
ptrace簡介
要想知道怎么禁止調(diào)試,需要先知道什么是ptrace
ptrace是C標(biāo)準(zhǔn)庫中的一個(gè)函數(shù),該函數(shù)是一個(gè)系統(tǒng)調(diào)用方法 , 可以監(jiān)視進(jìn)程執(zhí)行 , 查看 / 更改 被監(jiān)視進(jìn)程的 內(nèi)存 和 寄存器 情況 , 常用于斷點(diǎn)調(diào)試。
ptrace方法如下
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
可以看到ptrace有四個(gè)參數(shù):
enum __ptrace_request request:指定ptrace要執(zhí)行的命令。
pid_t pid: 跟蹤進(jìn)程id。
void *addr: 進(jìn)程的某個(gè)內(nèi)存地址。
void *data: 存放讀取出的或者要寫入的數(shù)據(jù)。
第一個(gè)參數(shù)可以傳入以下值
PTRACE_TRACEME, 本進(jìn)程被其父進(jìn)程所跟蹤。其父進(jìn)程應(yīng)該希望跟蹤子進(jìn)程
PTRACE_PEEKTEXT, 從內(nèi)存地址中讀取一個(gè)字節(jié),內(nèi)存地址由addr給出
PTRACE_PEEKDATA, 同上
PTRACE_PEEKUSER, 可以檢查用戶態(tài)內(nèi)存區(qū)域(USER area),從USER區(qū)域中讀取一個(gè)字節(jié),偏移量為addr
PTRACE_POKETEXT, 往內(nèi)存地址中寫入一個(gè)字節(jié)。內(nèi)存地址由addr給出
PTRACE_POKEDATA, 往內(nèi)存地址中寫入一個(gè)字節(jié)。內(nèi)存地址由addr給出
PTRACE_POKEUSER, 往USER區(qū)域中寫入一個(gè)字節(jié),偏移量為addr
PTRACE_GETREGS, 讀取寄存器
PTRACE_GETFPREGS, 讀取浮點(diǎn)寄存器
PTRACE_SETREGS, 設(shè)置寄存器
PTRACE_SETFPREGS, 設(shè)置浮點(diǎn)寄存器
PTRACE_CONT, 重新運(yùn)行
PTRACE_SYSCALL, 重新運(yùn)行
PTRACE_SINGLESTEP, 設(shè)置單步執(zhí)行標(biāo)志
PTRACE_ATTACH,追蹤指定pid的進(jìn)程
PTRACE_DETACH, 結(jié)束追蹤
具體ptrace用法和原理可以參考下文
https://zhuanlan.zhihu.com/p/438534744
那么怎么用ptrace來實(shí)現(xiàn)反調(diào)試呢
其實(shí)除了上述參數(shù),蘋果增加了一個(gè)PT_DENY_ATTACH選項(xiàng),這個(gè)參數(shù)用來告訴系統(tǒng),阻止調(diào)試器依附。
OC中使用ptrace阻止調(diào)試器依附
在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運(yùn)行環(huán)境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是一個(gè)計(jì)算機(jī)函數(shù),功能是以指定模式打開指定的動(dòng)態(tài)鏈接庫文件,并返回一個(gè)句柄給dlsym的調(diào)用進(jìn)程。
dlsym功能是根據(jù)動(dòng)態(tài)鏈接庫操作句柄與符號,返回符號對應(yīng)的地址。
這樣,拿到了ptrace,第一個(gè)參數(shù)傳入PT_DENY_ATTACH,實(shí)現(xiàn)阻止調(diào)試器依附。
這是OC是實(shí)現(xiàn)方案。那么在swift中該如何使用ptrace呢?
Swift中使用ptrace阻止調(diào)試器依附
在OC中main函數(shù)如下
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
并且由于OC是完全兼容C的,所以可以直接在main方法中調(diào)用ptrace函數(shù),但是Swift該怎么做呢?
首先,在Swift中,main函數(shù)不見了,換成了@UIApplicationMain。
@UIApplicationMain可以理解成聲明了一個(gè)main函數(shù),編譯器收到這個(gè)指令會(huì)自動(dòng)創(chuàng)建main函數(shù)
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
}
由于要在main函數(shù)中執(zhí)行ptrace方法(其實(shí)不需要一定在main方法中執(zhí)行ptrace,在main方法或者main方法執(zhí)行之前調(diào)用ptrace都可以,比如在load中執(zhí)行),我們需要自定義main函數(shù)。
新建main.swift文件。
main.swift文件內(nèi)容如下
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中調(diào)用
disable_gdb()
autoreleasepool {
UIApplicationMain(
...
)
}
ptrace攻防戰(zhàn)
由于ptrace可以被hook,所以我們可以通過提前hook來防止別人hook,新建一個(gè)framework,使這個(gè)framework在Link Binary Libraries中靠前,在framework中hook住ptrace,并調(diào)用,這樣可以防止ptrace被hook。
其實(shí),就算這樣,ptrace也可能通過修改二進(jìn)制導(dǎo)致ptrace失效。
更多相關(guān)內(nèi)容可以參考下文
http://www.itdecent.cn/p/9ed2de5e7497
2、監(jiān)測是否被調(diào)試
實(shí)現(xiàn)原理
當(dāng)一個(gè)進(jìn)程被調(diào)試時(shí),該進(jìn)程會(huì)有一個(gè)標(biāo)識(P_TRACED)來標(biāo)記自己正在被調(diào)試,這時(shí),可以使用sysctl函數(shù)獲取到當(dāng)前進(jìn)程的相關(guān)信息,分析這些信息了解進(jìn)程是否正在被調(diào)試,從而阻止調(diào)試。
sysctl監(jiān)聽調(diào)試
func checkTracing(){
let mib = UnsafeMutablePointer<Int32>.allocate(capacity: 4)
mib[0] = CTL_KERN//內(nèi)核查看
mib[1] = KERN_PROC//進(jìn)程查看
mib[2] = KERN_PROC_PID//進(jìn)程ID
mib[3] = getpid()//獲取pid
var size: Int = MemoryLayout<kinfo_proc>.size
var info: kinfo_proc? = nil
/* 函數(shù)的返回值若為0時(shí),證明沒有錯(cuò)誤,其他數(shù)字為錯(cuò)誤碼。 arg1 傳入一個(gè)數(shù)組,該數(shù)組中的第一個(gè)元素指定本請求定向到內(nèi)核的哪個(gè)子系統(tǒng)。第二個(gè)及其后元素依次細(xì)化指定該系統(tǒng)的某個(gè)部分。 arg2 數(shù)組中的元素?cái)?shù)目 arg3 一個(gè)結(jié)構(gòu)體,指向一個(gè)供內(nèi)核存放該值的緩沖區(qū),存放進(jìn)程查詢結(jié)果 arg4 緩沖區(qū)的大小 arg5/arg6 為了設(shè)置某個(gè)新值,arg5參數(shù)指向一個(gè)大小為arg6參數(shù)值的緩沖區(qū)。如果不準(zhǔn)備指定一個(gè)新值,那么arg5應(yīng)為一個(gè)空指針,arg6因?yàn)?. */
sysctl(mib, 4, &info, &size, nil, 0)
//info.kp_proc.p_flag中存放的是標(biāo)志位(二進(jìn)制),在proc.h文件中有p_flag的宏定義,通過&運(yùn)算可知對應(yīng)標(biāo)志位的值是否為0。(若結(jié)果值為0則對應(yīng)標(biāo)志位為0)。其中P_TRACED為正在跟蹤調(diào)試過程。
if (info.unsafelyUnwrapped.kp_proc.p_flag & P_TRACED) > 0 {
//監(jiān)聽到被調(diào)試,退出程序
exit(1)
}
}
由于sysctl是系統(tǒng)函數(shù),也是可以被hook的,為了防止被hook,同ptrace一樣,可以在自己寫的framework中定時(shí)調(diào)用該方法來防止被fishhook。
更多
但是加了這兩層防護(hù)也是不安全的,因?yàn)樵谀嫦蚬こ讨?,可以通過修改macho的二進(jìn)制來破解ptrace or sysctl,所以通過匯編來調(diào)用sysctl/ptrace/exit等函數(shù)可能是相對安全的方法。由于筆者水平有限,匯編相關(guān)內(nèi)容在這里就不再講解了。