一: ptrace 作用
ptrace系統調從名字上看是用于進程跟蹤的,它提供了父進程可以觀察和控制其子進程執行的能力,并允許父進程檢查和替換子進程的內核鏡像(包 括寄存器)的值。其基本原理是: 當使用了ptrace跟蹤后,所有發送給被跟蹤的子進程的信號(除了SIGKILL),都會被轉發給父進程,而子進程則會被阻塞,這時子進程的狀態就會被 系統標注為TASK_TRACED。而父進程收到信號后,就可以對停止下來的子進程進行檢查和修改,然后讓子進程繼續運行 ,因而可以實現斷點調試和系統調用的跟蹤
使用ptrace,你可以在用戶層 【攔截和修改】系統調用(sys call)
注意:
被跟蹤的程序在進入或者退出某次系統調用的時候都會觸發一個SIGTRAP信號,而被父進程捕獲
Ptrace其原型為:
include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
ptrace有四個參數:
1). enum __ptrace_request request:指示了ptrace要執行的命令。
2). pid_t pid: 指示ptrace要跟蹤的進程。
3). void *addr: 指示要監控的內存地址。
4). void *data: 存放讀取出的或者要寫入的數據。
ptrace是如此的強大,以至于有很多大家所常用的工具都基于ptrace來實現,如strace和gdb
反調試舉例說明:
比如: ptrace的命令PT_DENY_ATTACH 是蘋果增加的一個 ptrace 選項,用于阻止 GDB 等調試器依附到某進程,用法如下:
ptrace(PT_DENY_ATTACH, 0, 0, 0);
void anti_gdb_debug() {
void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace");
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
dlclose(handle);
}
總結一下:ptrace被廣泛用于反調試,因為一個進程只能被ptrace一次,如果事先調用了ptrace方法,那就可以防止別人調試我們的程序.
也就是說誰先調用ptrace 誰說了算,如果我們直接在app 中寫ptrace , 那么調試的時候肯定就無法調試,因為被應用內的ptrace 搶占了
反反調試: 如果別人的的app進行了ptrace防護,那么你怎么讓他的ptrace不起作用,進而調試其他的app。由于ptrace是系統函數,那么我們可以用fishhook來hook住ptrace函數,然后讓他的app調用我們自己的ptrace函數,即寫動態庫,如果多個動態庫hook 了ptrace ,我們可以調整 Link Binary Libraries的順序加載,假設人家應用自己寫的hook ptrace動態庫肯定會在自己前面,最后的方式我們可以通過修改macho的二進制讓他的ptrace失效【不去執行ptrace】,然后進行調試.
最后:在給一種反調試的方案,這種也只能無法斷點調試ptrace 函數
我不想暴露自己的ptrace等系統方法,不想被符號斷點斷住,可以采用匯編進行調用ptrace
這樣人家就很難通過斷點的方式去調試
調試器建立調試關系的兩種方式:
用gdb調試程序[調試程序也在一個進程里],可以直接gdb ./test,也可以gdb (test的進程號)。這對應著使用ptrace建立跟蹤關系的兩種方式:
fork:利用fork+execve執行被測試的程序,子進程在執行execve之前調用ptrace(PTRACE_TRACEME),建立了與父進程(debugger 調試程序進程)的跟蹤關系。
attach: debugger可以調用ptrace(PTRACE_ATTACH,pid,…),建立自己與進程號為pid的進程間的跟蹤關系。即利用PTRACE_ATTACH,使自己變成被調試程序的父進程(用ps可以看到)。用attach建立起來的跟蹤關系,可以調用ptrace(PTRACE_DETACH,pid,…)來解除。注意attach進程時的權限問題,如一個非root權限的進程是不能attach到一個root進程上的。
第一種方式的例子:
ptrace提供了對子進程進行單步的功能,ptrace(PTRACE_SINGLESTEP, …) 會使內核在子進程的每一條指令執行前先將其阻塞,然后將控制權交給父進程
而父進程此時會使用 wait函數等待阻塞信號,然后判斷status變量來檢查子進程是被ptrace暫停掉還是已經運行結束并退出,如果狀態是ptrace暫停的,則可以獲取子進程的寄存器器狀態,
ptrace(PTRACE_GETREGS,child, NULL, ?s),獲取當前指令等,在讓ptrace控制單步執行
ptrace(PTRACE_SINGLESTEP, child,NULL, NULL);
每一步都去喚醒子進程繼續執行,并告訴內核在執行一條指令后就將其阻塞
最后讓子進程恢復
PTRACE_SYSCALL:繼續,但在下一個系統調用入口或出口處停止。
二: sysctl 作用
sysctl命令被用于在內核運行時動態地修改內核的運行參數
函數原型
int sysctl (int *name, int nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen);
Name /* 整形數組,每個數組元素代表系統參數存取路徑上的一個文件或目錄名,例如/proc/sys/kernel用CTL_KERN表示*/
oldval /* 當讀取系統參數時,用于存取系統參數值,也就是/proc/sys/下的某個文件內容*/
Newval /* 當寫系統參數時,記錄所要寫入的新值*/
反調試舉例:
當一個進程被調試的時候,該進程會有一個標記來標記自己正在被調試,所以可以通過sysctl去查看當前進程的信息,看有沒有這個標記位即可檢查當前調試狀態。
檢測到調試器就退出,或者制造崩潰,或者隱藏工程啥的,當然也可以定時去查看有沒有這個標記
三: syscall 作用
為從實現從用戶態切換到內核態,系統提供了一個系統調用函數syscall ,所有的系統調用都可以通過syscall 去實現
比如: syscall (26,31,0,0) 來調用系統函數ptrace,ptrace的系統調用函數號是26
syscall是通過軟中斷來實現從用戶態到內核態,也可以通過匯編svc調用來實現。
比如:arm 32位 #80 就是軟中斷值,r12 存放系統函數編號,arm 64位 #128 是系統中斷碼,x0存放系統函數編號
四: 重簽名防護
想自己的app不被重簽名,可以在代碼中檢測簽名信息
查看證書的application-identifier 查看embedded.mobileprovision信息security cms -D -i embedded.mobileprovision 找到<key>application-identifier</key>的value的第一部分就是
在執行代碼的時候檢查簽名是否和我們已知的簽名對比
void checkCodesign(NSString *id){
// 描述文件路徑
NSString *embeddedPath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
// 讀取application-identifier 注意描述文件的編碼要使用:NSASCIIStringEncoding
NSString *embeddedProvisioning = [NSString stringWithContentsOfFile:embeddedPath encoding:NSASCIIStringEncoding error:nil];
NSArray *embeddedProvisioningLines = [embeddedProvisioning componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
for (int i = 0; i < embeddedProvisioningLines.count; i++) {
if ([embeddedProvisioningLines[i] rangeOfString:@"application-identifier"].location != NSNotFound) {
NSInteger fromPosition = [embeddedProvisioningLines[i+1] rangeOfString:@"<string>"].location+8;
NSInteger toPosition = [embeddedProvisioningLines[i+1] rangeOfString:@"</string>"].location;
NSRange range;
range.location = fromPosition;
range.length = toPosition - fromPosition;
NSString *fullIdentifier = [embeddedProvisioningLines[i+1] substringWithRange:range];
NSArray *identifierComponents = [fullIdentifier componentsSeparatedByString:@"."];
NSString *appIdentifier = [identifierComponents firstObject];
// 對比簽名ID
if (![appIdentifier isEqual:id]) {
//exit
asm(
"mov X0,#0\n"
"mov w16,#1\n"
"svc #0x80"
);
}
break;
}
}
}
五:反反調試
這里主要針對ptrace、sysctl、syscall來反反調試,做法就很簡單了,hook函數
比如:
1: hook ptrace 函數, 遇到request = 31, 就知道程序進行了反調試,所以我們可以將request 改掉
2:hook sysctl , 看其是否在檢查進程被追蹤的這個標記TASK_TRACED,我們將其返回信息info_ptr -> kp_proc.p_flag 改掉,讓其檢查的結果是沒有設置追蹤標識
3 hook syscall , 防止ptrace 是通過syscall 的方式去調用的,
4:hook dlsym 防止通過這種方式去調用ptrace 函數
5 初始化函數
或者使用fishhook
lldb 反反調試的
通過lldb下斷點,然后修改參數,或者直接返回也可以達到反反調試的效果
為了方便直接使用facebook的chisel來增加腳本。
當遇到情況為$x0 == 31 時發生回掉,將x0 或者r0 的值改成0
六: App的防護
1:首先加強現有的密碼檢測機制的監測力度,除了密碼長度,數字、字符甚至特殊符號的混雜程度,本文著重對易受攻擊的鍵盤上特定組合碼進行檢測;
2:其次完善iOS內存保護機制,利用Objective-C對象實現內存安全擦除,保證及時對文件數據的每個字節都做到全覆蓋,防止對象被跟蹤后信息遭到泄露;
3:再次在維護程序在運行時的安全性上提出了一種貫穿程序被調試的三個階段的反調試機制:從程序開始被調試、繼續被跟蹤到最終被惡意修改均進行跟蹤測試,最終阻止被惡意修改的目標繼續的執行
針對上述安全隱患,我們的iOS應用安全防護框架需實現的任務大致如下:
防護
ObjC類名方法名等重命名為難以理解的字符
加密靜態字符串運行時解密
混淆代碼使其難于反匯編
本地存儲文件防篡改
檢測
調試狀態檢測 : 反調試 ptrace . sysctl
越獄環境檢測
ObjC的Swizzle檢測
任意函數的hook檢測
指定區域或數據段的校驗和檢測
自修復
- 自修復被篡改的數據和代碼段
此外,還需要多層的防護,通過高層保護低層的方式來保證整個防護機制不失效。 參考IBM移動終端安全防護框架解決方案:
1:越獄檢測的方法:
1》使用NSFileManager判斷設備是否安裝了如下越獄常用工具
/Applications/Cydia.app
/Library/MobileSubstrate/MobileSubstrate.dylib
/bin/bash
/usr/sbin/sshd
/etc/apt
這種方式不要寫成Bool 方式去檢查 ,容易被攻擊者hook

注意: 攻擊者可能會改變這些工具的安裝路徑,躲過你的判斷。
2》可以嘗試打開cydia應用注冊的URL scheme,后面應該是你知道某個應用URL scheme
if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"[cydia://package/com.example.package](cydia://package/com.example.package)"]]){
NSLog(@"Device is jailbroken");
}
但是不是所有的工具都會注冊URL scheme,而且攻擊者可以修改任何應用的URL scheme。
3》你可以嘗試讀取下應用列表,看看有無權限獲取:
攻擊者可能會hook NSFileManager 的方法,讓你的想法不能如愿
4》你可以回避 NSFileManager,使用stat系列函數檢測Cydia等工具:

攻擊者可能會利用 Fishhook原理 hook了stat
5》你可以看看stat是不是出自系統庫,有沒有被攻擊者換掉
使用dladdr方法可以獲得一個函數所在的模塊.從而判斷該函數是否被替換掉
如果結果不是 /usr/lib/system/libsystem_kernel.dylib 的話,那就100%被攻擊了。
如果 libsystem_kernel.dylib 都是被攻擊者替換掉的…
也可以判斷ios 的方法在什么庫中,通過該方法驗證指定類的方法是否都來自指定模塊
建議使用inline方式編譯,像這樣以內聯函數的形式編譯,攻擊者必須修改每一處調用該函數的的地方
檢查所有方法判斷是否來之某個模塊
6》檢索一下自己的應用程序是否被鏈接了異常動態庫,列出所有已鏈接的動態庫:
通常情況下,會包含越獄機的輸出結果會包含字符串: Library/MobileSubstrate/MobileSubstrate.dylib
攻擊者可能會給MobileSubstrate改名,但是原理都是通過DYLD_INSERT_LIBRARIES注入動態庫
7》可以通過檢測當前程序運行的環境變量:
未越獄設備返回結果是null,越獄設備就各有各的精彩了,尤其是老一點的iOS版本越獄環境
上述越獄檢查總結如下:
不要用NSFileManager,這是最容易被hook掉的。
檢測方法中所用到的函數盡可能用底層的C,如文件檢測用stat函數(iPod7.0,越獄機檢測越獄常見的會安裝的文件只能檢測到此步驟,下面的檢測不出來)
再進一步,就是檢測stat是否出自系統庫
再進一步,就是檢測鏈接動態庫(盡量不要,appStore可能審核不過)
再進一步,檢測程序運行的環境變量
即使這樣還是不能完全檢查
比如: 用戶可能安裝越獄檢測繞過插件(xCon),對于越獄檢測,很大程度上都還是針對某些目錄下某個文件名字是否換了或者文件被替換了等等去檢測;
檢測代碼
- (BOOL)mgjpf_isJailbroken
{
//以下檢測的過程是越往下,越獄越高級
// /Applications/Cydia.app, /privte/var/stash
BOOL jailbroken = NO;
NSString *cydiaPath = @"/Applications/Cydia.app";
NSString *aptPath = @"/private/var/lib/apt/";
if ([[NSFileManager defaultManager] fileExistsAtPath:cydiaPath]) {
jailbroken = YES;
}
if ([[NSFileManager defaultManager] fileExistsAtPath:aptPath]) {
jailbroken = YES;
}
//可能存在hook了NSFileManager方法,此處用底層C stat去檢測
struct stat stat_info;
if (0 == stat("/Library/MobileSubstrate/MobileSubstrate.dylib", &stat_info)) {
jailbroken = YES;
}
if (0 == stat("/Applications/Cydia.app", &stat_info)) {
jailbroken = YES;
}
if (0 == stat("/var/lib/cydia/", &stat_info)) {
jailbroken = YES;
}
if (0 == stat("/var/cache/apt", &stat_info)) {
jailbroken = YES;
}
// /Library/MobileSubstrate/MobileSubstrate.dylib 最重要的越獄文件,幾乎所有的越獄機都會安裝MobileSubstrate
// /Applications/Cydia.app/ /var/lib/cydia/絕大多數越獄機都會安裝
// /var/cache/apt /var/lib/apt /etc/apt
// /bin/bash /bin/sh
// /usr/sbin/sshd /usr/libexec/ssh-keysign /etc/ssh/sshd_config
//可能存在stat也被hook了,可以看stat是不是出自系統庫,有沒有被攻擊者換掉
//這種情況出現的可能性很小
int ret;
Dl_info dylib_info;
int (*func_stat)(const char *,struct stat *) = stat;
if ((ret = dladdr(func_stat, &dylib_info))) {
NSLog(@"lib:%s",dylib_info.dli_fname); //如果不是系統庫,肯定被攻擊了
if (strcmp(dylib_info.dli_fname, "/usr/lib/system/libsystem_kernel.dylib")) { //不相等,肯定被攻擊了,相等為0
jailbroken = YES;
}
}
//還可以檢測鏈接動態庫,看下是否被鏈接了異常動態庫,但是此方法存在appStore審核不通過的情況,這里不作羅列
//通常,越獄機的輸出結果會包含字符串: Library/MobileSubstrate/MobileSubstrate.dylib——之所以用檢測鏈接動態庫的方法,是可能存在前面的方法被hook的情況。這個字符串,前面的stat已經做了
//如果攻擊者給MobileSubstrate改名,但是原理都是通過DYLD_INSERT_LIBRARIES注入動態庫
//那么可以,檢測當前程序運行的環境變量
char *env = getenv("DYLD_INSERT_LIBRARIES");
if (env != NULL) {
jailbroken = YES;
}
return jailbroken;
}