基于LLVM IR的防Hook方案

1. 什么是LLVM IR

當(dāng)我們點(diǎn)擊Xcode進(jìn)行編譯時,查看日志可以看到每一個編譯單元都有指定大量的編譯參數(shù),我們跳過編譯前的預(yù)處理和語法分析,使用 clang -emit-llvm XXX -S -o XXX.ll 直接導(dǎo)出查看其生成的IR(Intermediate Representation)。

也許你對于IR很陌生,但是Bitcode肯定會知道 。實(shí)際上,當(dāng)我們設(shè)置了 Enable Bitcode=YES ,進(jìn)行Archive時,Bitcode會被嵌入到鏈接后的Mach-O中,用于提交到App Store。實(shí)際上,Bitcode就是二進(jìn)制格式的IR。

非Archive編譯時,Enable Bitcode 將只增加一個編譯參數(shù) -fembed-bitcode-marker, 該參數(shù)用于在Mach-O中作為占位。因?yàn)楸镜鼐幾g調(diào)試時并不需要bitcode,去掉這個步驟可以大大加快編譯速度。

對于靜態(tài)庫等打開了Bitcode編譯,通過MachOview查看會發(fā)現(xiàn)有一個__LLVM, __bitcode 段;而全工程編譯出來對應(yīng)的是 __LLVM, __bundle 段;可以使用 segedit 命令將指定的Section導(dǎo)出:

segedit XXX.o -extract __LLVM __bitcode result.bc


2. IR文件結(jié)構(gòu)

如下,IR的結(jié)構(gòu)可分為3部分。

1.Module

可以理解為一個類文件對應(yīng)一個Module,作為一個獨(dú)立的編譯單元。其內(nèi)部包含聲明以及定義的函數(shù),全局變量等,以及架構(gòu)信息等。

2.Function

Function相當(dāng)于C里面的方法,其必須存在于Module中,內(nèi)部由參數(shù),返回類型以及多個BasicBlock組成,每個Function的起始block是一個EntryBlock,也是列表的第一個BasicBlock。

3.Basic Block

BasicBlock則是Instruction存放的地方,Instruction對應(yīng)的就是我們真正的可執(zhí)行代碼。Instruction可分為普通指令以及Terminator指令,并且BasicBlock都是以Terminator Instruction結(jié)尾,包括跳轉(zhuǎn),返回,異常等。


3. 語法格式

以下是一些基礎(chǔ)的語法,可以幫助我們大致看懂一些簡單的實(shí)現(xiàn)。

  1. 以@開頭為全局標(biāo)識符(函數(shù),全局變量);以%開頭為局部變量。
  2. %a = alloca i32, align 4 ,alloca相當(dāng)于malloc,用于內(nèi)存分配且自動釋放;i32為占有幾位,此為4個字節(jié);align字節(jié)對齊。
  3. label 嚴(yán)格的講它也是一種數(shù)據(jù)類型(type),但它可以標(biāo)識入口,相當(dāng)于代碼標(biāo)簽。
  4. 函數(shù)的聲明使用declare,函數(shù)的定義使用define。
  5. 數(shù)組類型用[count x ix]表示,其中count表示數(shù)組的大小,ix表示數(shù)組中每一個元素對應(yīng)的數(shù)據(jù)類型,比如字符串”Hello IR”表示為[9 x i8],9表示該字符串包含9個元素(末尾包含一個\0),每個元素大小為i8即c語言中的char類型大小。

接著,我們可以通過 clang -emit-llvm XXX -S -o XXX.ll 導(dǎo)出一個OC類用Sublime或其他文本編輯器打開來看看更深入的結(jié)構(gòu)。

  1. target datalayout: 該字符串指定如何在內(nèi)存中布局?jǐn)?shù)據(jù),例如:
target datalayout = "e-m:o-p:32:32-Fi8-f64:32:64-v64:32:64-v128:32:128-a:0:32-n32-S32"
    // e表示小端對齊
  // m指定在輸出中進(jìn)行名字重整,以混亂的轉(zhuǎn)義字符\01為前綴的符號將直接傳遞給匯編程序,而不包含轉(zhuǎn)義字符。 m:o Mach-O mangling風(fēng)格,私有符號添加L前綴,其他符號 _前綴
  // p:32:32 32-bit的指針進(jìn)行32bit對齊
  // Fi8 指定函數(shù)指針的對齊方式,i表示函數(shù)指針的對齊與函數(shù)本身是獨(dú)立的,8則函數(shù)指針的對齊方式是函數(shù)上指定的顯式對齊方式的倍數(shù),即8倍
  // f64:32:64 double類型有32bits的ABI對齊但是優(yōu)先64Bits對齊
  // v64:32:64 64-bit vector同上
  // v128:32:128 同上
  // a:0:32 聚合類型(數(shù)組和結(jié)構(gòu)體)32位對齊
  // n32 指定目標(biāo)CPU本地整數(shù)寬度為32bits
  // S32 未指定的堆棧對齊為32bits
  1. Opaque Structure Types: 不透明結(jié)構(gòu)類型用于表示沒有指定主體的已命名結(jié)構(gòu)類型。
%0 = type opaque
  1. Attribute groups:IR中對象引用的屬性組。它們對于保持.ll文件可讀性很重要,因?yàn)樵S多功能將使用同一組屬性。在與.ll單個.c文件對應(yīng)的文件的退化情況下 ,單個屬性組將捕獲用于構(gòu)建該文件的重要命令行標(biāo)志。
attributes #2 = { nounwind readnone speculatable willreturn }
attributes #3 = { nounwind }
  1. Module Flags Metadata: 整個模塊的信息如果僅僅依靠IR是很難傳遞給LLVM的子系統(tǒng)的。llvm.module.flags 的元數(shù)據(jù)就是為了解決這個問題,這些標(biāo)志以鍵/值對的形式出現(xiàn),類似于字典,使得任何關(guān)心標(biāo)志的子系統(tǒng)都可以很容易地進(jìn)行查找。
// 三元組的第一個元素是行為標(biāo)志,指定當(dāng)多個模塊合并在一起時的行為,并且元數(shù)據(jù)是相同的ID
        1 表示Error,當(dāng)兩個值不同時發(fā)出錯誤,否則結(jié)果值為操作數(shù)
    2 表示W(wǎng)arning,如果兩個值不一致,則發(fā)出警告。結(jié)果值將是被鏈接的第一個模塊的標(biāo)志的操作數(shù),或者如果其他模塊使用max,則為max(在這種情況下,結(jié)果標(biāo)志將是max)
        。。。

!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 14, i32 0]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
// !0的ID為!"SDK Version",值為2個數(shù)組元素分別為14和0,行為則是如果出現(xiàn)兩個以上的!"SDK Version"并且他們的值不相等,則拋出error
// !1的ID為!"Objective-C Version",值為2,當(dāng)出現(xiàn)多個!"Objective-C Version"且值不同時則發(fā)出warning
      
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10}
  1. DICompileUnit:表示一個編譯單元,enums:,retainedTypes:,globals:,macros: 這些字段是一些內(nèi)部包含與編譯單元相關(guān)調(diào)試信息的元組,與代碼優(yōu)化無關(guān)(有些節(jié)點(diǎn)只有在指令引用它們時才會發(fā)出)。
!11 = distinct !DICompileUnit(language: DW_LANG_ObjC, file: !12, producer: "Apple clang version 12.0.0 (clang-1200.0.32.2)", isOptimized: false, runtimeVersion: 2, emissionKind: FullDebug, enums: !13, retainedTypes: !14, imports: !23, nameTableKind: None, sysroot: "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk", sdk: "iPhoneOS14.0.sdk")

  // DIFile節(jié)點(diǎn)表示文件
!12 = !DIFile(filename: "/Users/XXX/Desktop/TestSpeed/TestSpeed/AppDelegate.m", directory: "/Users/XXX/Desktop/bcTest/vm")

!13 = !{}
!14 = !{!15}
// DICompositeType 表示由其他類型組成的類型,如結(jié)構(gòu)體,unions
!15 = !DICompositeType(tag: DW_TAG_structure_type, name: "AppDelegate", scope: !17, file: !16, line: 11, size: 32, flags: DIFlagObjcClassComplete, elements: !18, runtimeLang: DW_LANG_ObjC)
  
  // Represents a module in the programming language, for example, a Clang module, or a Fortran module.
!22 = !DIModule(scope: null, name: "UIKit", configMacros: "\22-DNS_BLOCK_ASSERTIONS=1\22 \22-DOBJC_OLD_DISPATCH_PROTOTYPES=0\22", includePath: "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/UIKit.framework")
!23 = !{!24}
// DIImportedEntity節(jié)點(diǎn)表示導(dǎo)入到編譯單元的實(shí)體
!24 = !DIImportedEntity(tag: DW_TAG_imported_declaration, scope: !11, entity: !22, file: !16, line: 9)
  
  // 編譯單元的描述符則由!llvm.dbg.cu收集,用于跟蹤全局變量,類型信息 & 導(dǎo)入的實(shí)體(聲明和namespace)
!llvm.dbg.cu = !{!11, !25, !27, !29}
  1. Automatic Linker Flags Named Metadata: 一些目標(biāo)支持在單個對象文件中嵌入標(biāo)記到鏈接器,通常,它與語言擴(kuò)展一起使用,語言擴(kuò)展允許源文件包含鏈接器命令行選項(xiàng),并通過目標(biāo)文件將這些選項(xiàng)自動傳輸?shù)芥溄悠?。這些標(biāo)志使用 !llvm.link .options 的命名元數(shù)據(jù)在IR中編碼。每個操作數(shù)都應(yīng)該是一個元數(shù)據(jù)節(jié)點(diǎn),而元數(shù)據(jù)節(jié)點(diǎn)應(yīng)該是其他元數(shù)據(jù)節(jié)點(diǎn)的列表,每個元數(shù)據(jù)節(jié)點(diǎn)應(yīng)該是定義鏈接器選項(xiàng)的元數(shù)據(jù)字符串列表。
//如下,指定了幾組linker options,鏈接iOS相關(guān)庫
!llvm.linker.options = !{!31, !32, !33, !34, !35, !36}
!31 = !{!"-framework", !"UIKit"}
!32 = !{!"-framework", !"FileProvider"}
!33 = !{!"-framework", !"UserNotifications"}
!34 = !{!"-framework", !"CoreText"}
!35 = !{!"-framework", !"QuartzCore"}
!36 = !{!"-framework", !"CoreImage"}
  1. DISubprogram:表示來自源語言的函數(shù),可以使用!dbg元數(shù)據(jù)將一個不同的DISubprogram附加到函數(shù)定義中,唯一的DISubprogram可以附加到用于call site調(diào)試信息的函數(shù)聲明中。
!48 = distinct !DISubprogram(name: "-[AppDelegate application:didFinishLaunchingWithOptions:]", scope: !17, file: !17, line: 19, type: !49, scopeLine: 19, flags: DIFlagPrototyped, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition, unit: !11, retainedNodes: !13)
 
 // DIFile節(jié)點(diǎn)表示文件
!17 = !DIFile(filename: "TestSpeed/TestSpeed/AppDelegate.m", directory: "/Users/XXX/Desktop")

  //DISubroutineType節(jié)點(diǎn)表示子例程類型,types字段引用一個元組,第一個操作數(shù)為返回類型,其次依次為形參的類型,即!50。 如果第一個參數(shù)為null,則表示函數(shù)的返回值為void
!49 = !DISubroutineType(types: !50)
!50 = !{!51, !56, !58, !61, !64}

//DIDerivedType節(jié)點(diǎn)表示從其他類型(比如限定類型)派生的類型。DW_TAG_typedef用于為baseType提供一個名稱
!51 = !DIDerivedType(tag: DW_TAG_typedef, name: "BOOL", scope: !53, file: !52, line: 81, baseType: !55)
//DIBasicType節(jié)點(diǎn)表示基本類型,比如int、bool和float。標(biāo)簽:默認(rèn)為DW_TAG_base_type。
!55 = !DIBasicType(name: "signed char", size: 8, encoding: DW_ATE_signed_char)

  1. getelementptr: 用于獲取聚合數(shù)據(jù)結(jié)構(gòu)(數(shù)組或結(jié)構(gòu)體)的子元素的地址。它只執(zhí)行地址計算,不訪問內(nèi)存。該指令也可用于計算vector的地址。例如:
struct RT {
  char A;
  int B[10][20];
  char C;
};
struct ST {
  int X;
  double Y;
  struct RT Z;
};
///定義了RI ST結(jié)構(gòu)體并在foo中使用
int *foo(struct ST *s) {
  return &s[1].Z.B[5][13];
}

///在IR中表示
%struct.RT = type { i8, [10 x [20 x i32]], i8 }
%struct.ST = type { i32, double, %struct.RT }

define i32* @foo(%struct.ST* %s) nounwind uwtable readnone optsize ssp {
entry:
//第一個參數(shù)i64 1指向struct.ST類型,即%struct.ST*結(jié)構(gòu)體的一個指針
//第二個參數(shù)i32 2表示指向ST結(jié)構(gòu)體的第2個元素,即RT
//第三個參數(shù)i32 1表示指向RT的第一個元素,array B[10][20]
//最后兩個則就是取出數(shù)組的對應(yīng)下標(biāo)的值
  %arrayidx = getelementptr inbounds %struct.ST, %struct.ST* %s, i64 1, i32 2, i32 1, i64 5, i64 13
  ret i32* %arrayidx
}
//于是上面的arrayidx拆分下來等價于如下:第一步拿到struct.ST,然后取出ST位于index 2處的struct.RT,隨后struct.RT的index 1處為int二維數(shù)組,最后對B[5][13]進(jìn)行設(shè)置偏移
  %t1 = getelementptr %struct.ST, %struct.ST* %s, i32 1     
  %t2 = getelementptr %struct.ST, %struct.ST* %t1, i32 0, i32 2  
  %t3 = getelementptr %struct.RT, %struct.RT* %t2, i32 0, i32 1     
  %t4 = getelementptr [10 x [20 x i32]], [10 x [20 x i32]]* %t3, i32 0, i32 5 
  %t5 = getelementptr [20 x i32], [20 x i32]* %t4, i32 0, i32 13        
  ret i32* %t5


4. 修改OC的消息發(fā)送為直接調(diào)用

我們知道在OC中方法調(diào)用都是通過runtime進(jìn)行msgSend調(diào)用的,那么能否對一些編譯期間已經(jīng)確定了的調(diào)用規(guī)則改為直接調(diào)用的方式來避免被hook呢?

//OC代碼如下:
- (void)runTestOne {
    [self runTestTwo];
    [self runTestThree:10];
    int a = [self runTestFour];
    NSLog(@"%d", a);
}

- (void)runTestTwo {
    NSLog(@"call TestTwo");
}

- (void)runTestThree:(int)value {
    NSLog(@"call TestThree %d", value);
}

- (int)runTestFour {
    return 1;
}

//------ clang -S -fobjc-arc -emit-llvm TestIR.m -o TESTIR.ll 導(dǎo)出IR ------

; Function Attrs: nonlazybind  //禁止函數(shù)的延遲符號綁定。這可能會更快地調(diào)用函數(shù),但如果在程序啟動期間沒有調(diào)用函數(shù),則會付出額外的程序啟動時間。
  //聲明外部符號,#4對應(yīng)上面第3點(diǎn)的屬性組
declare i8* @objc_msgSend(i8*, i8*, ...) #4 

  //noinline:不內(nèi)聯(lián)調(diào)用 optnone:跳過optimization pass ssp: 開啟堆棧保護(hù)
; Function Attrs: noinline optnone ssp
  //名字重整,以轉(zhuǎn)義字符\01為前綴
  //%0則表示上2中的不透明結(jié)構(gòu)類型,也是就msg_send的第一個參數(shù),id self
  //#1為類型組
  //!dbg !80 將元數(shù)據(jù)!80使用!dbg附加到方法中,!80則是上面提到的DISubprogram對象
  define internal void @"\01-[TestIR runTestOne]"(%0* %0, i8* %1) #1 {
  %3 = alloca %0*, align 8
  %4 = alloca i8*, align 8
  %5 = alloca i32, align 4
  store %0* %0, %0** %3, align 8
  store i8* %1, i8** %4, align 8
  %6 = load %0*, %0** %3, align 8
  // OBJC_SELECTOR_REFERENCES_.2 即為sel,sel是通過OBJC_METH_VAR_NAME_獲取到方法的字符串
  %7 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !9
  //將%0*類型的%6 轉(zhuǎn)換為i8*
  %8 = bitcast %0* %6 to i8*
  // i8* (i8*, i8*, ...) 的objc_msgSend方法, 轉(zhuǎn)成void (i8*, i8*) 再進(jìn)行傳參調(diào)用
  call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %8, i8* %7)
  %9 = load %0*, %0** %3, align 8
  %10 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.2, align 8, !invariant.load !9
  %11 = bitcast %0* %9 to i8*
  // 轉(zhuǎn)成 void (i8*, i8*, i32) 即增加一個入?yún)?  call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*, i32)*)(i8* %11, i8* %10, i32 10)
  %12 = load %0*, %0** %3, align 8
  %13 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.4, align 8, !invariant.load !9
  %14 = bitcast %0* %12 to i8*
  // 轉(zhuǎn)成 i32  (i8*, i8*) 返回值為i32
  %15 = call i32 bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i32 (i8*, i8*)*)(i8* %14, i8* %13)
  store i32 %15, i32* %5, align 4
  %16 = load i32, i32* %5, align 4
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 %16)
  ret void
}

可以看到,調(diào)用OC方法,即內(nèi)部都是通過objc_msgSend或其他幾個衍生方法來實(shí)現(xiàn)的,i8* @objc_msgSend(i8*, i8*, ...)這是一個帶變參的C函數(shù),第一個參數(shù)表示指向類實(shí)例的指針,第二個參數(shù)表示方法的選擇子,其余則為可變參數(shù)列表。換言之,該函數(shù)通過向Objective-C運(yùn)行時傳遞消息來間接調(diào)用,然后通過提供的入?yún)碚业秸_調(diào)用的真正函數(shù)。

嘗試直接在IR中修改為直接call真正的調(diào)用方法:

//第1處的msgSend調(diào)用
call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*)*)(i8* %8, i8* %7)
//替換為:
call void @"\01-[TestIR runTestTwo]"(%0* %6, i8* %7)
  
//第2處的msgSend調(diào)用
  call void bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to void (i8*, i8*, i32)*)(i8* %11, i8* %10, i32 10)
//替換為:
call void @"\01-[TestIR runTestThree:]"(%0* %9, i8* %10, i32 10)

//第3處的msgSend調(diào)用
%15 = call i32 bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i32 (i8*, i8*)*)(i8* %14, i8* %13)
//替換為:
%15 = call i32 @"\01-[TestIR runTestFour]"(%0* %12, i8* %13)

修改完成執(zhí)行 llc -filetype=obj TESTIR.ll 生成目標(biāo)文件,然后通過gcc生成可執(zhí)行文件,最終執(zhí)行如下:

./a.out 
a.out[4491:1939294] call TestTwo
a.out[4491:1939294] call TestThree param: 10
a.out[4491:1939294] 1

可以看到,此種直接調(diào)用的方案對于明確指定的方法調(diào)用是可行的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容