1.用dlopen和dlsym進(jìn)行Hook或執(zhí)行代碼
1.1 Objective-C運(yùn)行時(shí)和Swift與C
- Objective-C是動(dòng)態(tài)語(yǔ)言,當(dāng)
objc_msgSend
調(diào)用時(shí)在知道要怎么執(zhí)行。 - Swift和C/C++表現(xiàn)類似锐想。如果不需要?jiǎng)討B(tài)性缀旁,編譯器就不會(huì)用蚪黑。所以你在看Swift匯編時(shí)抗楔,匯編直接調(diào)用方法地址就可以執(zhí)行惯雳。這種直接調(diào)用的方式就是
dlopen
和dlsym
真正發(fā)揮的地方了已添。
1.2 簡(jiǎn)單模式Hook C函數(shù)
一個(gè)簡(jiǎn)單的加水印的圖片妥箕。但是我們查看Assets.xcassets
或者逆向工程師查看Assets.car
都找不到這張圖片。因?yàn)樗菍?xiě)死在代碼里面的更舞,就像這樣
unsigned char ds_private_data_[] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x02, 0x58, 0x00, 0x00, 0x02, 0x02,
0x08, 0x06, 0x00, 0x00, 0x00, 0x13, 0x73, 0xb3, 0x4d, 0x00, 0x00, 0x00,
0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x13, 0x00, 0x00, 0x0b,
...}
我們先在項(xiàng)目里下了一個(gè)斷點(diǎn)畦幢,并打印RDI
寄存器。
"MallocDebugReport"
"MallocErrorStop"
"MallocErrorSleep"
"MallocNanoMaxMagazines"
"_MallocNanoMaxMagazines"
"LIBDISPATCH_STRICT"
"DYLD_INSERT_LIBRARIES"
"NSZombiesEnabled"
...
在我們程序啟動(dòng)前getenv
就已經(jīng)被執(zhí)行了缆蝉。如果你取消掉自動(dòng)繼續(xù)
選項(xiàng)宇葱,你就會(huì)發(fā)現(xiàn)調(diào)用棧里面根本沒(méi)有main
函數(shù)。
因?yàn)镃沒(méi)有動(dòng)態(tài)派發(fā)刊头,要hook
一個(gè)函數(shù)必須要在它被加載之前攔截它黍瞧。另一方面來(lái)說(shuō),C函數(shù)相對(duì)容易獲取原杂,而且你只需要獲取函數(shù)方法名(不需要參數(shù))和C函數(shù)所在的動(dòng)態(tài)庫(kù)的名字印颤。
C函數(shù)的hook
有很多方式,只是復(fù)雜度不同穿肄。如果你只是想在你的可執(zhí)行文件內(nèi)進(jìn)行hook年局,那還比較簡(jiǎn)單。但是如果你想在main
函數(shù)前hook
一個(gè)函數(shù)咸产,復(fù)雜度就提升了一個(gè)等級(jí)矢否。
一旦main
函數(shù)被執(zhí)行,所有的動(dòng)態(tài)庫(kù)也都已加載完畢脑溢。dyld
以深度優(yōu)先的方式遞歸加載動(dòng)態(tài)庫(kù)僵朗。一般來(lái)說(shuō),大多數(shù)外部函數(shù)是懶加載的,除非你用了特殊的鏈接配置衣迷。對(duì)于懶加載的外部函數(shù)來(lái)說(shuō)畏鼓,函數(shù)第一次調(diào)用時(shí),會(huì)觸發(fā)很多操作壶谒。dyld
會(huì)找到這個(gè)模塊云矫,定位到這個(gè)函數(shù)。然后把這個(gè)值保存到內(nèi)存的一個(gè)特定部分(__DATA.__la_symbol_ptr)汗菜。一旦這個(gè)外部函數(shù)定下來(lái)了让禀,以后的調(diào)用就不需要用dyld
來(lái)處理了。
如果你想在程序啟動(dòng)前就hook
函數(shù)陨界,你就需要?jiǎng)?chuàng)建一個(gè)動(dòng)態(tài)庫(kù)來(lái)執(zhí)行hook
操作巡揍,那么在main
函數(shù)執(zhí)行前就已經(jīng)可用了。
我們?cè)诔绦騿?dòng)后獲取HOME
環(huán)境變量菌瘪,然后進(jìn)行打印腮敌。HOME
環(huán)境變量就是模擬器運(yùn)行app的地址。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
if let cString = getenv("HOME") {
let homeEnv = String(cString: cString)
print("HOME env: \(homeEnv)")
}
return true
}
//HOME env: /Users/xxx/Library/Developer/CoreSimulator/Devices/85225EEE-8D5B-4091-A742-5BEBAE1C4906/data/Containers/Data/Application/A3CF2AF5-6FB3-43E0-B809-C36F899FC72A
下面我們來(lái)hook
一下getenv
函數(shù)俏扩。先創(chuàng)建一個(gè)HookingC
動(dòng)態(tài)庫(kù)糜工,語(yǔ)言選擇OC。并在這個(gè)庫(kù)里面創(chuàng)建一個(gè)getenvhook
的.h
和.c
录淡。
在getenvhook.c
中進(jìn)行替換捌木,注釋是在項(xiàng)目中的作用。
#import <dlfcn.h> // 引入dlopen和dlsym
#import <assert.h> // 測(cè)試包含getenv函數(shù)的庫(kù)是否正確加載
#import <stdio.h> // printf
#import <dispatch/dispatch.h> // dispatch_once
#import <string.h> // strcmp
char * getenv(const char *name) {
return "YAY!";
}
//運(yùn)行后后臺(tái)打印
//HOME env: YAY!
如果輸入其他參數(shù)的時(shí)候嫉戚,想要進(jìn)行原來(lái)的操作刨裆,我們需要先找一下原來(lái)函數(shù)的名字。
(lldb) image lookup -s getenv
1 symbols match 'getenv' in /Users/xxx/Library/Developer/Xcode/DerivedData/Watermark-eecizmuedigyaobuhjmnlfaqxfgk/Build/Products/Debug-iphonesimulator/Watermark.app/Frameworks/HookingC.framework/HookingC:
Address: HookingC[0x0000000000000f60] (HookingC.__TEXT.__text + 0)
Summary: HookingC`getenv at getenvhook.c:15
1 symbols match 'getenv' in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/system/libsystem_c.dylib:
Address: libsystem_c.dylib[0x000000000005a167] (libsystem_c.dylib.__TEXT.__text + 364823)
Summary: libsystem_c.dylib`getenv
除了我們HookingC
動(dòng)態(tài)庫(kù)彬檀,還有一個(gè)libsystem_c.dylib
庫(kù)有這個(gè)函數(shù)帆啃,它的完整地址為/usr/lib/ system/libsystem_c.dylib
。既然我們知道了函數(shù)在哪兒凤覆,下滿我們來(lái)用dlopen
链瓦,函數(shù)簽名如下
extern void * dlopen(const char * __path, int __mode);
dlopen
接受一個(gè)char *
類型的路徑,和一個(gè)整型來(lái)決定它如何加載模塊盯桦。如果成功返回一個(gè)void *
句柄,否則返回NULL
渤刃。
在dlopen
返回一個(gè)對(duì)模塊的引用后拥峦,就可以使用dlsym
來(lái)獲取對(duì)函數(shù)getenv
的引用了。dlsym
的函數(shù)簽名如下
extern void * dlsym(void * __handle, const char * __symbol);
第一個(gè)參數(shù)為dlopen
返回的void *
句柄卖子,第二個(gè)參數(shù)為要獲取的函數(shù)的名字略号。成功的話,會(huì)返回第二個(gè)指定的函數(shù)的地址,否則返回NULL
玄柠。
替換原來(lái)的函數(shù)突梦,并執(zhí)行,我們會(huì)看到兩個(gè)getenv
函數(shù)地址羽利。
char * getenv(const char *name) {
void *handle = dlopen("/usr/lib/system/libsystem_c.dylib", RTLD_NOW);
assert(handle);
void *real_getenv = dlsym(handle, "getenv");
printf("Real getenv: %p\nFake getenv: %p\n", real_getenv, getenv);
return "YAY!";
}
//Real getenv: 0x7fff5232b167
//Fake getenv: 0x106c5fd80
//HOME env: YAY!
RTLD_NOW
的意思是宫患,不需要進(jìn)行懶加載,立即加載这弧。
由于返回函數(shù)類型是void *
娃闲,但實(shí)際我們知道函數(shù)的類型,我們來(lái)優(yōu)化一下匾浪。
char * getenv(const char *name) {
static void *handle;
static char * (*real_getenv)(const char *);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
handle = dlopen("/usr/lib/system/libsystem_c.dylib", RTLD_NOW);
assert(handle);
real_getenv = dlsym(handle, "getenv");
});
//以上是利用static屬性皇帮,只獲取一次原始方法的句柄
if (strcmp(name, "HOME") == 0) {
return "/";
}
return real_getenv(name);
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
if let cString = getenv("HOME") {
let homeEnv = String(cString: cString)
print("HOME env: \(homeEnv)")
}
if let cString = getenv("PATH") {
let homeEnv = String(cString: cString)
print("PATH env: \(homeEnv)")
}
return true
}
可以看到現(xiàn)在hook
只對(duì)HOME
生效,對(duì)PATH
來(lái)說(shuō)是可以拿到原本數(shù)據(jù)的蛋辈。
注意:如果調(diào)用了一個(gè)
UIKit
方法属拾,然后UIKit
調(diào)用了getenv
,那么新的getenv
方法并不會(huì)被調(diào)用冷溶。因?yàn)?code>getenv的地址在UIKit
的代碼被加載的時(shí)候已經(jīng)被解析了渐白。
如果你要修改UIKit
的getenv
,就需要操作間接符號(hào)表
的知識(shí)挂洛,并修改__DATA.__la_symbol_ptr
段中對(duì)應(yīng)getenv
的函數(shù)地址了礼预。
這部分會(huì)涉及到使用fishhook
,原理可參考fishhook x MachOView源碼閱讀虏劲。
1.3 困難模式Hook Swift函數(shù)
非動(dòng)態(tài)的Swift代碼就像C函數(shù)一樣托酸。這種方法有一些復(fù)雜的地方,使得它更難融入快速的方法中柒巫。
首先励堡,Swift在開(kāi)發(fā)中經(jīng)常使用類或結(jié)構(gòu)。這是一個(gè)獨(dú)特的挑戰(zhàn)堡掏,因?yàn)?code>dlsym只能提供一個(gè)C函數(shù)应结。我們需要擴(kuò)展這個(gè)函數(shù),以便Swift方法可以在獲取實(shí)例方法時(shí)引用self泉唁,或者在調(diào)用類方法時(shí)引用類鹅龄。當(dāng)訪問(wèn)屬于類的方法時(shí),程序匯編代碼在執(zhí)行該方法時(shí)通常會(huì)引用self或類的偏移量亭畜。由于dlysm
只能提供一個(gè)C類型的函數(shù)扮休,因此我們需要利用匯編、參數(shù)和寄存器的知識(shí)拴鸵,將該C函數(shù)轉(zhuǎn)換為一個(gè)Swift方法玷坠。
第二個(gè)需要擔(dān)心的問(wèn)題是Swift會(huì)弄亂方法的名稱蜗搔。在代碼中看到的漂亮的名字,在模塊符號(hào)表中實(shí)際上是可怕的長(zhǎng)名字八堡。為了通過(guò)dlysm
引用Swift方法樟凄,需要找到此方法弄亂后的正確名稱。
下面我們來(lái)看看怎么操作兄渺。
HookingSwift
庫(kù)中有一個(gè)CopyrightImageGenerator
類缝龄,但我們只能訪問(wèn)到公開(kāi)的watermarkedImage
計(jì)算屬性,而私有的originalImage
屬性訪問(wèn)不了溶耘。
public class CopyrightImageGenerator {
// MARK: - Properties
private var imageData: Data? {
guard let data = ds_private_data else { return nil }
return Data(bytes: data, count: Int(ds_private_data_len))
}
private var originalImage: UIImage? {
guard let imageData = imageData else { return nil }
return UIImage(data: imageData)
}
public var watermarkedImage: UIImage? {
guard let originalImage = originalImage,
let topImage = UIImage(named: "copyright",
in: Bundle(identifier: "com.razeware.HookingSwift"),
compatibleWith: nil) else {
return nil
}
let size = originalImage.size
UIGraphicsBeginImageContext(size)
let area = CGRect(x: 0, y: 0, width: size.width, height: size.height)
originalImage.draw(in: area)
topImage.draw(in: area, blendMode: .normal, alpha: 0.50)
let mergedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return mergedImage
}
// MARK: - Initializers
public init() {}
}
在Watermark.app
包里面二拐,我們可以看到HookingSwift.framework
。
因?yàn)橹?code>originalImage是用Swift實(shí)現(xiàn)的凳兵,我們需要用Swift模式來(lái)進(jìn)行image
搜索百新。
(lldb) image lookup -rn HookingSwift.*originalImage
1 match found in /Users/xxx/Library/Developer/Xcode/DerivedData/Watermark-eecizmuedigyaobuhjmnlfaqxfgk/Build/Products/Debug-iphonesimulator/Watermark.app/Frameworks/HookingSwift.framework/HookingSwift:
Address: HookingSwift[0x0000000000000f70] (HookingSwift.__TEXT.__text + 512)
Summary: HookingSwift`HookingSwift.CopyrightImageGenerator.(originalImage in _71AD57F3ABD678B113CF3AD05D01FF41).getter : Swift.Optional<__C.UIImage> at CopyrightImageGenerator.swift:42
函數(shù)地址是0x0000000000000f70
,但這只是在HookingSwift
庫(kù)中的地址庐扫。我們繼續(xù)饭望。
(lldb) image dump symtab -m HookingSwift
...
[ 4] 51 D X Code 0x0000000000000f70 0x0000000106368f70 0x0000000000000100 0x000f0000 $s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg
...
通過(guò)0x0000000000000f70
進(jìn)行搜索,我們可以看到這個(gè)方法名叫
$s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg
現(xiàn)在我們拿到了模塊和相應(yīng)的方法名形庭,就可以利用dlopen
和dlsym
來(lái)找到函數(shù)地址了铅辞。
if let handle = dlopen("./Frameworks/HookingSwift.framework/HookingSwift", RTLD_NOW),
let sym = dlsym(handle, "$s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg") {
print("\(sym)")
}
//打印
0x000000010f354f70
//在上面的地址處設(shè)置一個(gè)斷點(diǎn),看一下對(duì)不對(duì)
(lldb) b 0x000000010f354f70
Breakpoint 1: where = HookingSwift`HookingSwift.CopyrightImageGenerator.(originalImage in _71AD57F3ABD678B113CF3AD05D01FF41).getter : Swift.Optional<__C.UIImage> at CopyrightImageGenerator.swift:42, address = 0x000000010f354f70
好了萨醒,我們找到了函數(shù)地址斟珊。那我們?cè)趺凑{(diào)用它呢?幸好富纸,我們可以用Swift的關(guān)鍵字typealias
來(lái)進(jìn)行函數(shù)的類型轉(zhuǎn)換囤踩。
let imageGenerator = CopyrightImageGenerator()
if let handle = dlopen("./Frameworks/HookingSwift.framework/HookingSwift", RTLD_NOW),
let sym = dlsym(handle, "$s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg") {
typealias privateMethodAlias = @convention(c) (Any) -> UIImage? // 1
let originalImageFunction = unsafeBitCast(sym, to: privateMethodAlias.self) // 2
let originalImage = originalImageFunction(imageGenerator) // 3
imageView.image = originalImage // 4
}
- 定義類型。Swift的方法里面
originalImage
并不需要參數(shù)晓褪,為什么這里的方法會(huì)帶一個(gè)Any類型的參數(shù)呢堵漱?因?yàn)楹瘮?shù)執(zhí)行時(shí),匯編代碼會(huì)從RDI
寄存器中獲取self
涣仿,所以我們需要把實(shí)例作為第一個(gè)參數(shù)傳進(jìn)去勤庐。否者,程序就會(huì)崩潰好港。 - 我們定義完類型就可以進(jìn)行類型轉(zhuǎn)換了愉镰。我們把
sym
指針轉(zhuǎn)換成對(duì)應(yīng)的函數(shù)類型。然后我們就可以通過(guò)originalImageFunction
進(jìn)行調(diào)用了钧汹。 - 我們通過(guò)傳入
imageGenerator
實(shí)例對(duì)象岛杀,獲取原始的圖像,放到originalImage
中崭孤。 -
我們把沒(méi)有水印的圖片放到視圖中类嗤。