1. 程式人生 > IOS開發 >iOS 逆向 - 應用安全攻防(越獄與非越獄)

iOS 逆向 - 應用安全攻防(越獄與非越獄)

iOS 逆向篇章目錄 :

前言

逆向篇章從前導知識到工具使用和原理分析我們都已經講述完畢了,也結合了實際案例來進行鞏固 . 那麼接下來,進入到我們學習逆向的最重要的目標篇章,應用安全攻防 .

這是一個大篇章,文章如果過長會分兩篇講述 .

學習逆向最重要的就是知道如何防護,本文會列舉一些目前市面上較為常見的逆向攻擊方式應該講講如何防護,這些做法並不唯一也不一定最好,如有心得歡迎交流 .

關於防護

關於應用防護,我們首先要有幾個前提概念要清楚 .

  • 1️⃣ : 沒有絕對安全的程式 .

    沒有絕對安全的應用,我們所要做的就是盡最大可能混淆 or 浪費攻擊者的時間,加大攻擊成本 .

  • 2️⃣ : 針對檢測到除錯和注入時的操作,儘量不要做非常明顯的退出應用 or 提示 等操作 .

    • 對於一個經驗比較豐富 ( 相對防護者來說 ) 的逆向工程師,當發現除錯 / 注入程式碼工程執行時與正版應用正式執行有明顯的區別時,很容易順藤摸瓜找到相應的防護或者監測處理邏輯和技術

      .

    • ( 比如經常也有一些同學問到,為什麼重簽名了之後工程一執行就閃退,正版應用就沒問題,那麼就很容易得知很可能是利用了 ptrace 或者引入包監測 / 包名監測之類的處理 ) .

    • 相對而言目前市面上許多大廠所使用的的,監測到此行為會記錄裝置 / 賬戶等資訊進行上報和封號等措施則是在無形之中做到了較為有效的防護 .

1. 動態除錯

進攻 :

對於非越獄環境來說,重簽名進行動態除錯,yololib 修改 mach-oLoad CommandsDYLD 載入攻擊者所編寫的動態庫從而進行 hook 來完成程式碼注入是最為常見的了 . ( 這裡不會詳細講述進攻過程和效果,不熟悉的同學可以翻一翻目錄中 2,3,4

這三篇文章有詳細的操作演示 ) .

利用重簽名工程的特性,其實這種解決方案有特別多,這裡挑幾個比較有代表性的防護措施列舉一下 .

防護方式 1 : 幹掉 lldb - ptrace

利用重簽名工程執行除錯特性,我們可以通過禁止開發環境使用 lldb 來實現重簽名工程執行閃退的效果 .

lldb 的原理這裡提一點 :

  • lldb 本質上是一個 GUI 斷點和命令收集工具 + debug server 通過程式附加到當前執行程式中來實現的 .

( Xcode 自帶的 Debug - Attach to process 就是根據 DebugServer 來實現的 )

ptrace

ptrace 是 命令列工程以及 Mac OS 工程裡的 <sys/ptrace.h> 提供的一個函式,可以用來來控制程式附加管理,它可以實現禁止應用程式程式被附加的效果 . 在 iOS 中並沒有暴露出來,但是 iOS 是可以使用的 .

筆者這裡將該標頭檔案匯出,( 連結 - 密碼: iaqp ) 下載後匯入工程中就可以使用了 .

使用如下 :

/**
 arg1: ptrace要做的事情: PT_DENY_ATTACH 表示要控制的是當前程式不允許被附加
 arg2: 要操作程式的PID,0就代表自己
 arg3: 地址 取決於第一個引數要做的處理不同傳遞不同
 arg4: 資料 取決於第一個引數要做的處理不同傳遞不同
 */
ptrace(PT_DENY_ATTACH,0,0);
複製程式碼

處理後效果

程式碼新增完畢後 :

  • 執行工程,程式閃退 .
  • 從手機點開應用,應用正常 .
  • 使用Xcode 自帶的 Debug - Attach to process 發現附加失敗 .

防護手段評估

  • 1️⃣ : 越獄環境下 lldb-debug server 同樣可以防護 .
  • 2️⃣ : 這種做法比較暴力,而且影響本身正向開發,只能在正向開發時不使用,打包上架時再開啟 .
  • 3️⃣ : 這種做法效果比較明顯,很容易推斷出來使用了這個機制,一個符號斷點就能輕鬆查到 .
  • 4️⃣ : 破解起來也比較簡單,使用 fishhook 可以很輕易的 hookptrace 這個函式 .

( 坊間傳聞早期支付寶有使用過這種方式,不過並沒有實據,全當一聽 )

提示

Cycript 本身是從正在執行的程式中讀取資料,並不是程式附加的原理,並不能通過 ptrace 防護 .

防護手段破解

破解這個也有較多方式,比如直接修改二進位制彙編程式碼 ( bl -> nop ),hookptrace 等等 .

最簡單的方式就是插入一個動態庫,在這個庫的 load 中使用 fishhook 直接把 ptrace hook 掉即可 . ( 關於 fishhook 參閱 fishHook 原理與符號表 這篇文章有從使用到原理解釋的完整內容,這裡就不再演示了 ) .

Monkeydev 中預設就已經使用了 fishhook 交換了 ptrace .

防護方式 2 : sysctl

sysctl ( system control ) 是由 <sys/sysctl.h> 提供的一個函式,它有很多作用,其中一個是可以監測當前程式有沒有被附加 . 但是因為其特性,只是監測當前時刻應用有沒有被附加 .

因此正向開發中我們往往結合定時器一起使用,或者 定時 / 定期 / 在特定時期 去使用 .

使用如下 :

#import "ViewController.h"
#import <sys/sysctl.h>
@interface ViewController ()
@end

@implementation ViewController
BOOL isDebug(){
    int name[4];             //裡面放位元組碼。查詢的資訊
    name[0] = CTL_KERN;      //核心查詢
    name[1] = KERN_PROC;     //查詢程式
    name[2] = KERN_PROC_PID; //傳遞的引數是程式的ID
    name[3] = getpid();      //獲取當前程式ID
    
    struct kinfo_proc info;  //接受查詢結果的結構體
    size_t info_size = sizeof(info);  //結構體大小
    if(sysctl(name,4,&info,&info_size,0)){
        NSLog(@"查詢失敗");
        return NO;
    }
    /**
    查詢結果看info.kp_proc.p_flag 的第12位。如果為1,表示除錯狀態。
    (info.kp_proc.p_flag & P_TRACED) 就是0x800,即可獲取第12位
    */
    return ((info.kp_proc.p_flag & P_TRACED) != 0);
}

static dispatch_source_t timer;
void debugCheck(){
    timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,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 (isDebug()) {//在這裡寫你檢測到除錯要做的操作
            NSLog(@"除錯狀態!");
        }else{
            NSLog(@"正常!");
        }
    });
    dispatch_resume(timer);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    debugCheck();
}
複製程式碼

處理後效果

這個顯示層面的效果根據你自定義的結果決定,你檢測到後決定做上報還是閃退都可以 .

防護手段評估

  • 1️⃣ : 越獄環境下 lldb-debug server 同樣可以檢測到,但同樣會影響本身正向開發 .
  • 2️⃣ : 這種做法比較靈活,可以自定義處理結果,上報很容易做到無形 .
  • 3️⃣ : 破解起來也相對簡單,使用 fishhook hooksysctl 這個函式 ( 但是因為使用者使用 sysctl 獲取到返回結果,因此這裡注意 hook 之後不能直接啥也不做,需要找到返回的 flag12 位改為 0,參考下圖 ) . 但是得益於針對使用層面效果可做到無形,也被一些公司在使用 .
  • 4️⃣ : 由於其特性,檢測當時當刻,需要針對需求去操作何時檢測 or 檢測多久的邏輯 .

( 坊間傳聞它又來了 . 傳說早期抖音團隊就是使用了這個檢測,檢測到之後直接 exit 同樣沒有有實據,全當一聽.. )

提示

這裡並不推薦檢測到有除錯之後直接 exit .

因為逆向過程中通過對 exit 新增符號斷點檢視函式呼叫棧就可以檢視到呼叫 exit 的函式地址,再減去通過 image list 指令獲取 mach-o 首地址就可以獲取到函式偏移量,然後在 Hopper 中很容易就可以拿到呼叫 exit 的函式了,那攻擊人員就能找到這個函式中你是通過 sysctl 來監測的 .

因此,我們說防護最好是不要有明顯的痕跡給攻擊者,否則就是給他們提供一個很好的線索和思路去找到你的防護邏輯 .

防護手段破解

sysctlhook 做法如下 :

同樣,Monkeydev 中預設就已經使用了 fishhook 交換了 sysctl ..

問題

可能大家都注意到了,上面所說這兩種解決動態除錯的方案似乎都差了點意思,破解成本也比較小 . 只要保證注入程式碼在 sysctl 檢測程式碼或者 ptrace 呼叫之前就能夠完全解決這個防護手段 .

解決上述動態除錯防護方案問題其實有很多方案,例如 :

  • 保證 sysctl 檢測程式碼或者 ptrace 的執行在 hook 注入程式碼之前 .
  • 禁用掉 fishhook .

防護方式 3 : ptrace / sysctl 優化版 - 提前執行

原理講述

根據 ld 以及 llvm 的編譯特性以及 dyld 的載入邏輯 . 實際上當我們在正向開發使用 framework 中的 load 中編寫防護程式碼時,是會比 yololib 注入的動態庫更早被執行的 .

  • 之前文章中我們也提到過,Mach-O__DATA 段後面是有空餘記憶體位置的 . yololib 新增的 hook framework 是新增在這個空缺位置中的,load commands 也會往後插入.
  • 也就是說,使用者自己引入的 framework 是會在 注入的 framework 之前被連結的 . ( 越獄環境 Insert Library 跟這個是兩個東西,後面我們會講述越獄外掛的工作原理 ) .

具體做法

那麼利用這個特性,我們可以自己新增一個 framework 在它的 load 中編寫防護程式碼 . 這些防護程式碼會比注入的更早被執行 . 也就是說,這時已經檢測到有被除錯了 . sysctl 後續雖然會被注入程式碼 hook 掉但是我們已經完成監測 .

而且由於是單獨的 framework,如果是通過符號斷點獲取函式地址去計算基於首地址偏移量還會有所不同,因為每個 framework 都是一個獨立的 mach-o,需要去找對應的檔案首地址而非主程式的首地址,分析的 mach-O 也應該是 framework 而非主程式 . 也對攻擊人員產生了額外的時間消耗 .

( 經驗豐富的逆向人員實際上也很容易忘記這一點,通常會以為自己算錯了 .. )

防護手段評估

這種方式適用於非越獄環境重簽名除錯,已經做到比較好的效果了.

攻擊人員針對在 frameworkload 中所做的防護措施,由於永遠比注入程式碼早被執行而相對更安全一些,想要攻擊也只能通過靜態彙編分析邏輯修改彙編了 . 而這種就相較於動態除錯會更加複雜和耗時 .

( 同樣,越獄外掛跟這個不是一個原理,並不能防護到,後面我們會將越獄外掛如何防護 )

提示

  • 建議不要為了防護專門開一個 framework,並且取一個很明顯的名字,這樣攻擊人員可以很輕易地看出這個 framework 就是為了防護寫的,再結合 load 方法彙編分析,發現你只做了一個 sysctl,他靜態修改也是很簡單的 . load 方法直接 ret 就行了 .
  • 可以放到一個有實際功能邏輯的庫中新增這個防護邏輯,這樣能達到更好地防護效果 .
  • 基於這個機制 ( 我們的防護程式碼可以比注入程式碼更早被執行 ),我們可以做很多處理,比如 runtime 的方法交換遮蔽,fishhook 遮蔽,這個後面我會補充一下 .

防護方式 4 : 函式地址儲存繞過 fishhook

繞過 fishhook 呼叫系統函式

我們有這樣一個設想,在我工程開始我就獲取 ptrace / sysctl 的地址,後面直接使用地址呼叫這個函式 . 實際上是可行的,利用 dlsym 這個函式 .

上述 防護方式3 在我們看來,雖然能保證防護程式碼的執行優先權,但似乎看起來仍然是治標不治本 . 那麼如何能保證我們需要用的系統函式不會被 fishhook 幹掉呢 ?

啟動優化之Clang插樁實現二進位制重排 文章中我們提到過通過符號獲取函式地址 ( dladdr 函式 ),並且使用了通過函式內部地址找到函式符號 ( dlsym 函式 ) .

使用展示

#import "MyPtraceHeader.h"
#import <dlfcn.h>
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
   
    //這裡做法是隱藏常量字串
    unsigned char str[] = {
        ('a' ^ 'p'),('a' ^ 't'),('a' ^ 'r'),('a' ^ 'a'),('a' ^ 'c'),('a' ^ 'e'),('a' ^ '\0')
    };
    unsigned char * p = str;
    while (((*p) ^= 'a') != '\0') p++;
    
    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函式指標
    ptrace_p = dlsym(handle,(const char *)str);
    if (ptrace_p) {
        //如果有值,就可以順利呼叫
        ptrace_p(PT_DENY_ATTACH,0);
    }
}
複製程式碼

上述做法中首先使用的是常量字串的隱藏,本文第三章節有詳細講述 .

如果我們希望自己使用的系統函式不會被 hook,就可以採用這種方式 .

提示

( 實際上 MonkeyDev 同樣就已經對 dlsym 進行了 hook ),因此可以結合 防護方法3 公同使用,也就是保證獲取地址方法提前執行即可,這裡不多提了.

防護手段評估

這種防護方式間接的繞過了 fishhook,但也不是絕對的 . 很明顯,我們同樣使用了 dlopendlsym 這兩個系統函式,那他們就同樣有被 fishhook 幹掉的可能 .

不著急,後續我會繼續講如何更好地解決這個問題 .

防護方式 5 : 繞過符號斷點 syscall

syscall 是系統級別的呼叫函式的一個函式 . 例如我們希望呼叫 ptrace,但是又不希望符號斷點可以段住 ptrace . 那麼你不用匯入任何標頭檔案就可以直接使用 syscall 函式來呼叫 ptrace .

使用演示

/**
 1.引數 是函式編號 26 指的是 ptrace 函式 
 2.其他的就是引數順序. 31 指的是 ptrace 的引數 PT_DENY_ATTACH
 */
syscall(26,31,0);
複製程式碼

關於每個序號對應什麼函式,你可以匯入一下 #import <sys/syscall.h> 即可檢視 .

除了 ptrace,還有 exit 等等,當你需要繞過符號斷點就可以使用這個函式 .

當然,syscall 也是一個系統函式,因此除了彙編,你可以結合 防護方式 4防護方式 3 來解決會被 fishhook 幹掉的問題 . 而且新增 syscall 的符號斷點是同樣可以斷住的,然後可以讀暫存器檢視你是否呼叫了 ptrace,exit 等等 .

怎麼解決呢 ? 接著往下看 .

防護方式 6 : 彙編呼叫 ( 推薦版本 )

上述所寫的這麼多種防護方式,看起來都不是很完美,還是存在 fishhook 可能對各種你使用的方法進行 hook 的風險 ( 雖然你可能使用了 framework 提前執行,繞過符號斷點,使用系統級別的呼叫,但仍然會留下一些 '蛛絲馬跡' ) .

該如何解決呢 ?

我們可以通過彙編來直接呼叫 syscall .

使用演練

- (void)viewDidLoad {
    [super viewDidLoad];
    //使用匯編呼叫syscall調起ptrace
    #ifdef __arm64__
    asm volatile(
                 "mov x0,#26\n"
                 "mov x1,#31\n"
                 "mov x2,#0\n"
                 "mov x3,#0\n"
                 "mov x16,#0\n"
                 "svc #0x80\n"//這條指令就是觸發中斷(系統級別的跳轉!)
    );
    #endif
    
    //使用匯編直接呼叫 ptrace
    #ifdef __arm64__
    asm volatile(
                 "mov x0,#31\n"
                 "mov x1,#0\n"
                 "mov x2,#26\n"
                 "svc #0x80\n"
                 );
    #endif
}
複製程式碼

x16 暫存器就放呼叫 syscall 需要呼叫的函式對應編號就可以 . 當然,不同架構暫存器指令不同,例如呼叫 exit 我們可以這麼寫 :

#ifdef __arm64__
    asm volatile(
                 "mov x0,#1\n"
                 "svc #0x80\n"
                 );
#endif
#ifdef __arm__//32位下
    asm volatile(
                 "mov r0,#0\n"
                 "mov r16,#1\n"
                 "svc #80\n"
                 );
#endif
複製程式碼

防護方案評估

使用匯編指令呼叫 syscall

  • 可以防止系統函式被 fishhook 幹掉 .
  • 新增符號斷點並不能斷住 .
  • 攻擊者靜態分析也比較難以查詢 .

比較推薦這種方式 .

動態除錯其他防護方式

動態除錯除了上述防護方式以外,還有許多方案,這裡列舉幾個供大家參考 .

  • 引入動態庫監測 . 使用白名單監測自己工程當前引入三方庫,查詢是否有未知庫注入,獲取引入庫寫法如下 .
    • 注意: 由於程式本身 mach-o 這裡也能監測出來,而且是第一個,因此,迴圈應該從 1 開始,也就是剔除本身 mach-o .
    • 而且該方式同樣可以監測越獄環境 DYLD_INSERT_LIBRARIES 動態注入的外掛 .
    • 此方法可以有效地檢測到 Cycript 越獄與非越獄的除錯 .
  • Bundle ID 檢測,重簽名工程是需要修改包名的,可以以此監測 .

2. 程式碼混淆

由於砸殼後應用恢復符號後使用 lldb,class-dump 或者 Mach-O 的一些檢視工具很輕易的可以看到我們的類名與方法名,而通常貫徹了程式碼規範的正向開發人員會已標準的駝峰和英文來命名,這就為攻擊人員提供了極大的便利性和可尋性 .

程式碼混淆可以在不影響正向開發人員的情況下對方法名和類名進行混淆,使生成的二進位制檔案中沒有去除符號的類和方法也變的沒有可參考性 .

使用方式

巨集定義的機制可以幫助我們很好地滿足混淆的需求 . 對於一個已經開發完成的工程我們可以很方便的來對其進行混淆 .

例如程式碼如下 :

- (void)viewDidLoad {
    [super viewDidLoad];
    LBObjectClass * objc = [LBObjectClass new];
    [objc LBObjectTestFunc];
    BOOL result = [objc checkIsVipWithkeyString:@"12311" Token:@"jfkfqwe"];
    NSLog(@"%d",result);
}

@interface LBObjectClass : NSObject
- (void)LBObjectTestFunc;
- (BOOL)checkIsVipWithkeyString:(NSString *)string Token:(NSString *)token;
@end

@implementation LBObjectClass
- (void)LBObjectTestFunc{
    NSLog(@"this is a confusion func");
}

- (BOOL)checkIsVipWithkeyString:(NSString *)string Token:(NSString *)token{
    if ([string containsString:@"111"] && (token != nil) ) return YES;
    return NO;
}
@end
複製程式碼

如上案例中我們,有一個類名叫 LBObject,他有兩個方法,一個無參無返回值,一個有參有返回值 .

如何進行混淆呢 ?

做法很簡單 . 為其定義對應的 巨集 即可 . 我這裡在 pch 檔案中新增如下 :

#define LBObject HDJSNWOIJNWPKFWD
#define LBObjectTestFunc LKNWFMWJFNJMSLW
#define checkIsVipWithkeyString IWRNWKJNDS
#define Token NFKAOWRL
複製程式碼

新增完畢後發現類名和方法名稱顏色發生了改變 . 原本程式碼無須進行任何修改 .

效果展示

MachOView 檢視混淆前 :

MachOView 檢視混淆後 :

class-dump 檢視混淆前 :

提示

class-dump 是對他人的已脫殼應用從 mach-O 中讀取標頭檔案,可以獲得類以及其方法 and 屬性的定義 . ( 使用方法參考 重籤應用除錯與程式碼修改 ) .

class-dump 檢視混淆後 :

Hopper 中檢視方法彙編實現如果做了混淆也是一樣的效果 . 這裡就不再展示了 .

巨集定義程式碼混淆方案評估

優點 :

  • 1️⃣ : 程式碼混淆可以很有效的大量增加攻擊者的分析耗時,增大幹擾性 .
  • 2️⃣ : 程式碼混淆可以很輕鬆的對已有工程進行處理 .
  • 3️⃣ : 加入混淆後對正向人員正常開發基本沒有影響 .
  • 4️⃣ : 完全可以利用巨集定義的機制,靈活處理,例如類名方法名每次執行都是隨機字串 .

缺點

  • 1️⃣ : 程式碼混淆利用巨集定義的機制,因此在預編譯階段會增加一定耗時,但對執行期間也就是使用者來說沒有影響,可以針對部分重要程式碼 or 功能增加混淆.
  • 2️⃣ : 使用巨集定義需要注意工程中有其他同樣字串產生影響 . ( 例如上述案例中我用了一個 Token 的巨集,那麼一定要注意其他使用 Token 字樣的地方,儘量使用唯一字串代替 . )

網上也有很多利用編譯器來實現的程式碼混淆的三方庫,自動實現的,思路大體上都是如此,巨集定義是手動做的 . 但也因此比較靈活和簡單,自定義程度較高 .

3. 字串常量隱藏

我們工程中的一些常量字串在逆向開發中,使用 hopper 檢視彙編時會直接以註釋的方式寫在彙編指令之後,這種註釋會給攻擊人員提供極大的線索和引導作用 .

比如如下程式碼 :

#define STRING_ENCRYPT_KEY "demo_AES_key"
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    LBObject * objc = [LBObject new];
    BOOL result = [objc checkIsVipWithkeyString:@"12311" Token:@"jfkfqwe"];
    lbCFunc(STRING_ENCRYPT_KEY);
}

void lbCFunc(const char * str){
    printf("%s",str);
}
@end
複製程式碼

當採用了字串常量 or 巨集定義的做法,攻擊人員使用 hopper 檢視彙編時如下 :

( 注意 : 筆者這裡 hopper 檢視的已經是生產環境的包 )

甚至 hopper 自帶的彙編還原虛擬碼可以做到如下 :

細思極恐 ..

那麼如何做到隱藏常量字串呢 ?

其實簡單做法就是把字串常量換為一個方法,在這個方法中返回需要的字串即可,例如 :

比如很多同學的專案中,對稱性加密的 key 可能是寫在本地的,一些重要的 key / secret / token 可能也是 ( 這裡只是舉個例子,為了說明一些比較重要的常量字串,現在類似 key 這種目前普遍都是伺服器下發了 ),那麼針對這種比較明顯又比較重要的字串,我們最好是對其做一下隱藏處理的 .

簡單做法演示

#define STRING_ENCRYPT_KEY @"demo_AES_key"
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //[self uploadDataWithKey:STRING_ENCRYPT_KEY]; //使用巨集/常量字串
    [self uploadDataWithKey:AES_KEY()]; //使用函式代替字串
}

- (void)uploadDataWithKey:(NSString *)key{
    NSLog(@"%@",key);
}

static NSString * AES_KEY(){
    unsigned char key[] = {
        'd','e','m','o','_','A','E','S','k','y','\0',};
    return [NSString stringWithUTF8String:(const char *)key];
}
@end
複製程式碼

效果展示

使用常量字串 / 巨集

使用函式

可以看到已經沒有顯示的字串直接被書寫出來了,當然,配合上方法混淆會更好 .

但是可能有同學會問了,如果攻擊者靜態分析定位到我們返回 key 這個函式怎麼辦 .

函式隱藏字串升級版

#define STRING_ENCRYPT_KEY @"demo_AES_key"
#define ENCRYPT_KEY 0xAC
@interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
//    [self uploadDataWithKey:STRING_ENCRYPT_KEY]; //使用巨集/常量字串
    [self uploadDataWithKey:AES_KEY()]; //使用函式代替字串
}

- (void)uploadDataWithKey:(NSString *)key{
    NSLog(@"%@",key);
}

static NSString * AES_KEY(){
    unsigned char key[] = {
        (ENCRYPT_KEY ^ 'd'),(ENCRYPT_KEY ^ 'e'),(ENCRYPT_KEY ^ 'm'),(ENCRYPT_KEY ^ 'o'),(ENCRYPT_KEY ^ '_'),(ENCRYPT_KEY ^ 'A'),(ENCRYPT_KEY ^ 'E'),(ENCRYPT_KEY ^ 'S'),(ENCRYPT_KEY ^ '\0'),};
    unsigned char * p = key;
    while (((*p) ^= ENCRYPT_KEY) != '\0') {
        p++;
    }
    return [NSString stringWithUTF8String:(const char *)key];
}
@end
複製程式碼

採用這樣的方式,這些字元不會進入字元常量區 . 編譯器直接換算成異或結果 .

4. 越獄環境防護

在越獄環境下,最為出名的就是越獄外掛 - tweak 的使用了 . Monkey 也提供了 Xcode 的外掛可以很輕易地編寫一個自己的外掛 . ( 感興趣的同學可以閱讀一下筆者在實戰篇-釘釘打卡外掛 中有完整的逆向探索和編寫流程 ) .

外掛的工作原理

在筆者 iOS 底層 - 從頭梳理 dyld 載入流程 篇中,我們知道一個環境變數 DYLD_INSERT_LIBRARIES 的存在 . dyld 會由此標識來決定是否載入插入動態庫 .

  • 越獄環境下,Cydia 的基石:MobileSubstrate 會將 SpringBoard[FBApplicationInfo environmentVariables]函式做 hook,將環境變數 DYLD_INSERT_LIBRARIES 設定新增需要載入的外掛 ( 動態庫 ) . 而應用的二進位制包無須做任何改變,但是 dyld 在載入應用的時候就會因為 DYLD_INSERT_LIBRARIES 的機制,會去載入指定的插入庫 .

  • 外掛在開發時就需要指定需要附加的程式,因此就可以知道載入哪個應用時需要插入這個外掛 .

越獄環境的外掛就是此原理 . tweak 外掛編譯經過 .o 等中間產物其實最終會生成一個 dylib,最終打包生成一個 .deb 的包,將其修改為 zip 你會看到我們的 dylib .

Monkey 以及 theos 中會將生成的 dylib 通過 openssh 拷貝到手機 /Library/MobileSubstrate/DynamicLibraries 裡,等待 dyld 去載入附加 . 這也是為什麼外掛安裝了之後會殺掉程式重新啟動的原因 . 因為需要 dyld 再次工作將外掛動態載入進來 .

它與重簽名應用的程式碼注入,也就是通過修改應用 mach-oload commands,雖然都是通過動態庫注入,但可以說原理上完全是兩個東西 . 越獄環境外掛並不需要修改目標檔案 .

知道了越獄環境外掛的原理,那我們再來談談外掛如何防護 .

越獄環境防護方法 1 : __RESTRICT

原理分析

dyld 原始碼中,我們發現瞭如下程式碼 :

if ( gLinkContext.processIsRestricted ) {
    pruneEnvironmentVariables(envp,&apple);
    // set again because envp and apple may have changed or moved
    setContext(mainExecutableMH,argc,argv,envp,apple);
}
複製程式碼

檢視後發現,當 processIsRestrictedtrue,會刪除相應的環境變數,也就意味著 DYLD_INSERT_LIBRARIES 可以被忽略掉 . 繼續搜尋檢視到如下 :

if ( issetugid() || hasRestrictedSegment(mainExecutableMH) ) {
    gLinkContext.processIsRestricted = true;
}
複製程式碼

也就是說 hasRestrictedSegment 時,processIsRestricted 這個標識會被設定為 true .

點進去方法中,如上圖,發現其實很簡單,當我們的 mach-o 中有 __RESTRICT 段 以及 __restrict 節 時,這個函式就會返回 true . 也就意味著我們只剩下一個問題,如何給我們的應用新增 __RESTRICT 段 以及 __restrict 節 .

操作演示

操作很簡單,在 Other Linker Flags 中新增 : -Wl,-sectcreate,__RESTRICT,__restrict,/dev/null 即可 ( 這個指令不能寫錯,寫錯會直接影響越獄外掛能否注入成功 ) .

新增後編譯,檢視 mach-o .

新增完畢後,所有越獄外掛都會新增無效,筆者這裡就不演示了 .

防護手段評估

  • 該方案能有效地遮蔽越獄環境下基本所有外掛 .
  • 該方案效果比較明細,也因此比較容易被查出 .
  • 該方案存在可能因為 dyld 隨著系統更新修改了這個機制,那麼防護手段就失效的風險 .

防護手段破解

由於這個方案可以遮蔽所有的外掛,因此對於攻擊人員來說,很容易會想到是利用了 __RESTRICT 的機制來防護的 . 開啟 mach-o 直接檢視你有沒有這個段和節就能查詢到 .

而由於 dyld 載入的機制,只要段與節不叫這個名字,就不認為是受限制的程式 . 因此,只需要改一下二進位制即可 . 修改二進位制的方式很多,有專門的二進位制修改器 Synalize It!,甚至 MachOView,Hopper,IDER 等視覺化工具都可以直接修改 .

修改完畢發現如下 :
此時,重新開啟你要逆向的工程,你會發現直接閃退 . 網上各種論壇裡特別多這個問題 .. 問為什麼修改了會閃退 .

實際上我們在 應用簽名原理及重簽名 (重籤微信應用實戰) 中講過 iOS 應用簽名和驗證的原理,上述問題的根本在於 當我們修改了二進位制,那麼就需要對應用進行重簽名,否則 iOS 的驗籤中應用被修改,hash 值一定變化,那麼驗籤一定過不去 .

重簽名的過程我們就不講了,上述文章裡有詳細演示 . 下面我們講講如何防護這種做法 .

破解手段防護

fishhook 原始碼,與 阿里開源的元件化框架 BeeHive 中的啟發,我們可以自己去幹 dyld 在做的事 . 換句話說,在我們已經添加了 __RESTRICT 段和節的前提下,我們可以自己在執行時去讀 mach-o 中段和節的名稱,以達到檢查 __RESTRICT 段__restrict 節 有沒有被修改的需求 .

不知道大家這個思路理清楚了沒,解釋一下 :

為了防護我們的二進位制中 __RESTRICT 段__restrict 節 被修改掉 . 我們在執行時自己去檢查一遍 .

  • 有則代表沒有被修改 .
  • 沒有則代表已經被修改 . ( 因為我明明添加了這個段和節,卻沒有查到 )
防護手段使用演示
#import <mach-o/loader.h>
#import <mach-o/dyld.h>

#if __LP64__
#define LC_SEGMENT_COMMAND        LC_SEGMENT_64
#define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT
#define LC_ENCRYPT_COMMAND        LC_ENCRYPTION_INFO
#define macho_segment_command    segment_command_64
#define macho_section            section_64
#define macho_header            mach_header_64
#else
#define macho_header            mach_header
#define LC_SEGMENT_COMMAND        LC_SEGMENT
#define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT_64
#define LC_ENCRYPT_COMMAND        LC_ENCRYPTION_INFO_64
#define macho_segment_command    segment_command
#define macho_section            section
#endif

@implementation ViewController

+(void)load
{
    //imagelist 裡第0個是我們自己的可執行檔案
    const struct mach_header * header = _dyld_get_image_header(0);
    
    if (hasRestrictedSegment(header)) {
        NSLog(@"沒問題!");
    }else{
        NSLog(@"檢測到!!");
        // 退出程式,可以上報 or 記錄 ..
        #ifdef __arm64__
            asm volatile(
                         "mov x0,#0\n"
                         "mov x16,#1\n"
                         "svc #0x80\n"
                         );
        #endif
        #ifdef __arm__//32位下
            asm volatile(
                         "mov r0,#0\n"
                         "mov r16,#1\n"
                         "svc #80\n"
                         );
        #endif
    }
}

static bool hasRestrictedSegment(const struct macho_header* mh)
{
    const uint32_t cmd_count = mh->ncmds;
    const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(struct macho_header));
    const struct load_command* cmd = cmds;
    for (uint32_t i = 0; i < cmd_count; ++i) {
        switch (cmd->cmd) {
            case LC_SEGMENT_COMMAND:
            {
                const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
                
                if (strcmp(seg->segname,"__RESTRICT") == 0) {
                    const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
                    const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
                    for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
                        if (strcmp(sect->sectname,"__restrict") == 0)
                            return true;
                    }
                }
            }
                break;
        }
        cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
    }
    
    return false;
}
@end
複製程式碼

當然,同樣為了防止我們的 hasRestrictedSegment 程式碼被 hook,你可以結合混淆,提前執行機制 ( 本文的動態除錯-防護方式 3 : ptrace / sysctl 優化版 - 提前執行) 來共同使用 .

同樣還是那句話,不建議直接退出應用,不要留下明顯的防護痕跡,否則攻擊人員首先不會懷疑自己有沒有哪裡沒有理清楚,而是會針對這個點展開分析 .

吃瓜環節

這個防護手段是從哪出來的呢 ? 不知道大家有沒有人還知道 '念茜',支付寶的逆向工程師,CSDN 裡活躍的一位前輩,14 年初就開始分享逆向知識,也很感謝這些前輩為我們鋪下了路,讓我們站在巨人的肩膀上 .

防護方法 2 : 監測環境變數

使用和原理都比較簡單,我們都講述過了 . 就是監測 DYLD_INSERT_LIBRARIES . 因此就可以檢測到有沒有越獄外掛,或者說是不是越獄環境了 ( 因為越獄環境預設就有 Mobile Substrate 等外掛了 ) .

//越獄檢測
char * dlname = getenv("DYLD_INSERT_LIBRARIES");
if (dlname) {
    NSLog(@"越獄手機,關閉部分功能");
}else{
    NSLog(@"正常手機!");
}
複製程式碼

防護方式 3 : image list

這個防護手段其實就是 我們本文 動態除錯其他防護方式 中的第一種 . 使用白名單監測自己工程當前引入三方庫,查詢是否有未知庫注入 .

由於 DYLD_INSERT_LIBRARIES 的原理和特性,其本質上也是動態庫注入,只不過是由 dyld 動態去載入實現的動態注入,無須修改二進位制檔案本身 .

我們同樣可以使用如下 :

來檢視是否有其他動態庫注入,當發現

這些庫時,你基本就可以斷定這個使用者是在越獄環境中了 .

總結

防護技巧 :

  • 單一的防護手段往往不足以保證安全,多重防護手段結合使用,能起到更好效果 .
  • 不要給防護程式碼留下明顯的痕跡,尤其在 UI 層面 ( 例如閃退,彈框等等 ) .
  • 沒有絕對安全的防護,增加攻擊者的查詢和分析花費即可達到防護的目的 .
  • 不懂進攻,何談防護 .