Cocoa Touch Framework
我們將使用 Cocoa Touch Framework來Hook iOS 中系統(tǒng)的C函數(shù),首先我們先新創(chuàng)建一個(gè)工程HooKDemo,在AppDelegate.h文件中,在 application(_:didFinishLaunchingWithOptions:)方法后面,我們寫下如下代碼:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
char *str = getenv("HOME");
NSString *envStr = [[NSString alloc] initWithUTF8String:str];
NSLog(@"HOME env: %@", envStr);
return YES;
}
這段代碼是用來獲取系統(tǒng)的環(huán)境變量 HOME的值,我們運(yùn)行工程,運(yùn)行結(jié)果如下
2020-11-20 HooKDemo[76436:7339554] HOME env: /Users/ritamashin/Library/Developer/CoreSimulator/Devices/928FDC1A-5119-406D-872E-6B605E24ED40/data/Containers/Data/Application/0F44D2B0-C3F5-42A3-8661-96DEC5578ECD
該值是我們所運(yùn)行的模擬器中的 HOME環(huán)境變量值。如果我們想HOOK getEnv函數(shù),我們需要創(chuàng)建一個(gè)依賴于HooKDemo的Framework,來改變getEnv函數(shù)的實(shí)現(xiàn)。
在Xcode中,我們通過 File -> New -> Target,并且選擇 Cocoa Touch Framework,選擇 HookingC為項(xiàng)目名,語(yǔ)言選擇Objective-C

在HookingC項(xiàng)目中,我們新建一個(gè)c語(yǔ)言文件,并命名為 getenvhook

我們?cè)?getenvhook.c文件中,添加以下頭文件
#import <dlfcn.h>
#import <assert.h>
#import <stdio.h>
#import <dispatch/dispatch.h>
#import <string.h>
-
dlfcn.h中,我們主要用了兩個(gè)函數(shù)dlopen和dlsym。 -
assert.h中,我們主要來測(cè)試真實(shí)的getEnv函數(shù)被真正加載。 -
stdio.h我們主要用里面的printf函數(shù)。 -
string.h我們主要用里面的strcmp函數(shù)來比較兩個(gè)C字符串。
我們?cè)?getenvhook.c文件中,重寫getenv方法
char * getenv(const char *name) {
return "YAY!";
}
然后我們運(yùn)行該工程,我們可以看到如下輸出
HOME env: YAY!
我們可以得知,已經(jīng)成功替換了系統(tǒng)的 getenv方法,但這肯定不是我們期望的,如果我們?cè)?getenv函數(shù)里面增加如下代碼會(huì)發(fā)生什么呢?
char * getenv(const char *name) {
return getenv(name);
return "YAY"!
}
這樣的話,會(huì)造成遞歸循環(huán)調(diào)用,導(dǎo)致棧溢出,我們現(xiàn)在已經(jīng)替換了系統(tǒng)的getenv函數(shù),我們需要找到系統(tǒng)的getenv函數(shù),然后在調(diào)用它,我們重新啟動(dòng)工程,然后使工程進(jìn)入lldb模式,在鏡像文件中查找getenv方法:
(lldb) image lookup -s getenv
1 symbols match 'getenv' in /Users/ritamashin/Library/Developer/Xcode/DerivedData/HooKDemo-gosjkkcckyouwldjttqgdrtoaovl/Build/Products/Debug-iphonesimulator/HooKDemo.app/Frameworks/HOOKingC.framework/HOOKingC:
Address: HOOKingC[0x0000000000000f60] (HOOKingC.__TEXT.__text + 0)
Summary: HOOKingC`getenv at getenvhook.c:12
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
1 symbols match 'getenv' in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/AppleAccount.framework/AppleAccount:
我們一共找到3個(gè)getenv實(shí)現(xiàn),一個(gè)在HOOKingC.framework中,一個(gè)在lib system_c.dylib中。它的全路徑為 /usr/lib/system/libsystem_c.dylib。
現(xiàn)在我們知道了這個(gè)函數(shù)是從哪里加載的了,下面我們將使用dlopen函數(shù),去加載該函數(shù)。其方法簽名如下
extern void * dlopen(const char * __path, int __mode);
-
dlopen函數(shù)第一個(gè)參數(shù)為一個(gè)全路徑,第二個(gè)參數(shù)為一個(gè)整數(shù)來決定了dlopen函數(shù)以怎樣的模式去加載,如果加載成功,dlopen將會(huì)返回一個(gè)void *類型的數(shù)據(jù),否則將會(huì)返回NULL。
在 dlopen函數(shù)加載完之后,我們使用dlsym去得到 getenv函數(shù)的引用,dlsym函數(shù)的函數(shù)簽名如下:
extern void * dlsym(void * __handle, const char * __symbol);
-
dlsym的第一個(gè)參數(shù)為dlopen函數(shù)得到的方法引用,第二個(gè)參數(shù)為方法的名字。如果成功,將返回第二個(gè)參數(shù)符號(hào)名的內(nèi)存地址。如果失敗了,則返回NULL。
我們將我們自定義的getEnv函數(shù),進(jìn)行改進(jìn)一下。
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\n Fake getenv: %p\n", real_getenv, getenv);
return "YAY!";
}
- 1,我們使用
RTLD_NOW模式使用dlopen函數(shù)的意思是:讓該函數(shù)立即執(zhí)行,不使用懶加載的方式。 - 2,我們使用
assert函數(shù),確保handle不為NULL。 - 3,我們使用
dlsym函數(shù),來加載get_env的真實(shí)地址值。
我們?cè)俅芜\(yùn)行工程,會(huì)得到類似的以下輸出:
Real getenv: 0x7fff5232b167
Fake getenv: 0x107469de0
HooKDemo[86264:7770900] HOME env: YAY!
現(xiàn)在我們已經(jīng)知道了getenv的函數(shù)簽名,接下來,我們將getenv函數(shù)進(jìn)行如下調(diào)整:
char * getenv(const char *name) {
static void *handle; // 1
static char * (*real_getenv)(const char *); // 2
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ // 3
handle = dlopen("/usr/lib/system/libsystem_c.dylib",
RTLD_NOW);
assert(handle);
real_getenv = dlsym(handle, "getenv");
});
if (strcmp(name, "HOME") == 0) { // 4
return "/WOOT";
}
return real_getenv(name); // 5
}.
- 1,創(chuàng)建一個(gè)
static變量handle,因?yàn)槭庆o態(tài)的,在函數(shù)執(zhí)行完之后也不會(huì)被釋放,在getenv函數(shù)之外,我們也可以獲取該值。 - 2,我們根據(jù)
getenv函數(shù)的方法簽名,定義了一個(gè)靜態(tài)的real_getenv函數(shù)指針。我們將會(huì)把它賦值到真正的getenv方法實(shí)現(xiàn)中去。 - 3,使用
dispatch_once函數(shù),保證只執(zhí)行一次。 - 4,使用
strcmp函數(shù),判斷當(dāng)前變量是否為HOME,來更改其輸出 - 5, 如果不是
HOME變量,則調(diào)用默認(rèn)的getenv函數(shù)。
在 AppDelegate中,我們分別獲取HOME和PATH環(huán)境變量
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
char *str = getenv("HOME");
NSString *envStr = [[NSString alloc] initWithUTF8String:str];
NSLog(@"HOME env: %@", envStr);
char *pathchar = getenv("PATH");
NSString *pathStr = [[NSString alloc] initWithUTF8String:pathchar];
NSLog(@"HOME env: %@", pathStr);
return YES;
}
其運(yùn)行結(jié)果如下:
2020-11-21 14:38:03.325656+0800 HooKDemo[86696:7792671] HOME env: /WOOT
2020-11-21 14:38:03.325885+0800 HooKDemo[86696:7792671] HOME env: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/bin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/bin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/sbin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/sbin:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/local/bin
這樣我們就成功的將HOME環(huán)境變量的值給替換掉,非HOME環(huán)境變量還是調(diào)用默認(rèn)的getenv函數(shù)。
如果我們想要調(diào)用 UIKit框架中的 getenv函數(shù),以我們現(xiàn)在的方式是不可以的,這是因?yàn)?,此時(shí)UIKit已經(jīng)加載到內(nèi)存中,我們自定義的getenv函數(shù)不會(huì)得到調(diào)用。
為了能夠調(diào)用UIKit中的getenv函數(shù),我們需要掌握 符號(hào)表的重定向知識(shí),然后去改變?cè)?code>__DATA.__la_symbol_ptr段中的getenv的地址。
備注:dlopen:打開動(dòng)態(tài)庫(kù),dlsym:獲得符號(hào)名。
總結(jié)
本篇文章,我們主要探討了使用 iOS中的framework來HOOK系統(tǒng)中的C函數(shù),通過 dlopen函數(shù)得到系統(tǒng)中真實(shí)的函數(shù)地址,使用dlsym函數(shù)和方法簽名對(duì)真實(shí)的地址進(jìn)行符號(hào)綁定。從而達(dá)到調(diào)用默認(rèn)函數(shù)的目的。
最后附上本文代碼示例 HookDemo