hook C函數
先拿一個簡單的c函數getenv
上手。這個函數接受一個char *
類型的參數(得是null terminated string)并返回對應的環境變量。
實際上在你的可執行文件啟動時這個函數會被多次調用的。
用Xcode打開Watermark
項目。創建一個symbolic breakpoint到getenv,并添加action:
po (char *)$rdi
并勾選Automatically continue after evaluating actions。
構建并運行app在模擬器上,你可以得到類似這樣的輸出:
"DYLD_INSERT_LIBRARIES"
"NSZombiesEnabled"
"OBJC_DEBUG_POOL_ALLOCATION"
"MallocStackLogging"
"MallocStackLoggingNoCompact"
"OBJC_DEBUG_MISSING_POOLS"
"LIBDISPATCH_DEBUG_QUEUE_INVERSIONS"
"LIBDISPATCH_CONTINUATION_ALLOCATOR"
... etc ...
(注意,更優雅的打印app的所有環境變量的方式是使用DYLD_PRINT_ENV
。在Product\Manage Scheme
中添加這個到Environment variables
里面去。可以簡單地添加這個名字,不需要值,就可以了。啟動app后控制臺就可以看到所有環境變量及其對應值了。)
這里需要注意的一點是,所有這些getenv的調用都發生在你的可執行文件開始執行之前。你可以在getenv添加一個斷點然后查看調用棧來驗證(你將找不到任何main入口的蛛絲馬跡)。這意味著你不能夠修改這個函數除非你在dyld加載動態庫之前定義了getenv函數。
既然C并沒有使用方法動態分配(objc_msgsend),要hook一個函數你必須得在它被加載之前攔截它。不過C函數也并不是棘手得徹底,C函數相對容易獲取。所有你需要的只是函數的方法名,并不需要任何參數和所在動態庫的名字。
c函數的hook有很多復雜度都不一樣的方式。如果你只是想在你的可執行文件內進行hook,那要做到事情并不多。但是如果你想在main函數前hook一個方法,復雜度就提升了一個等級。
一旦你的可執行文件在main函數加載完畢,load commands里面指定的所有的動態庫也都已加載完畢。dyld以深度優先的方式遞歸加載動態庫。一旦鏈接器獲取到一個特定函數的地址,它將會用這個函數的實際內存地址替換所有這個函數的引用。
這意味著如果你想在你的app啟動前hook一個c函數,你需要創建一個動態庫并把hook邏輯放到里面去,這樣它在main函數被調用之前將會是可用的。
理論扯了一大堆,回到project中來。打開**AppDelegate.swift
并用以下替換application(_:didFinishLaunchingWithOptions:)
:
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/username/Library/Developer/CoreSimulator/Devices/3EAAC880-B34B-4966-AE34-5C617769E54B/data/Containers/Data/Application/31F483BA-D23F-4DEC-8C0A-227CF6934A75
這個就是你正使用的模擬器的HOME環境變量。
通過以下命令獲取getenv所在的動態庫:
(lldb) image lookup -s getenv
1 symbols match 'getenv' in /Users/gogleyin/Library/Developer/Xcode/DerivedData/Watermark-grqfiuuagfwxlhgqetwiuaqnqiwn/Build/Products/Debug-iphonesimulator/Watermark.app/Frameworks/HookingC.framework/HookingC:
Address: HookingC[0x0000000000000d00] (HookingC.__TEXT.__text + 0)
Summary: HookingC`getenv at getenvhook.c:30
1 symbols match 'getenv' in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//usr/lib/system/libsystem_c.dylib:
Address: libsystem_c.dylib[0x000000000005ca26] (libsystem_c.dylib.__TEXT.__text + 375174)
Summary: libsystem_c.dylib`getenv
現在假設你要這樣對getenv進行hook:邏輯不變,只是當參數為HOME時返回點不一樣的東西。
正如前面所述,你需要創建一個動態庫。getenv本將會在main可執行文件中被解析,在這之前你的app的可執行文件必須獲取到getenv的地址并改變它。
File/New/Target/Cocoa Touch Framework,設若Product Name為HookingC,Project和Embed in Application選擇與你的app對應。然后新建一個c文件叫getenvhook.c(取消勾選Also create a header file),內容如下:
#import <dlfcn.h>
#import <assert.h>
#import <stdio.h>
#import <dispatch/dispatch.h>
#import <string.h>
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");
});
if (strcmp(name, "HOME") == 0) {
return "/";
}
return real_getenv(name);
}
下面來解釋一下。dlopen
函數簽名如下:
extern void * dlopen(const char * __path, int __mode);
dlopen
接受一個char *類型的路徑,和一個整型來決定它如何加載模塊。如果成功返回一個handle(類型為void *),否則NULL。
在dlopen
返回一個對模塊的引用后,你就可以使用dlsym
來獲取對函數getenv
的引用了:
extern void * dlsym(void * __handle, const char * __symbol);
第一個參數為dlopen
返回的handle,第二個參數為要獲取的函數的名字。一切正常的話,它就會返回第二個指定的函數的地址,否則返回NULL。
需要注意的是,如果你調用了一個UIKit
方法,然后UIKit
調用了getenv
,那么新的getenv
方法并不會被調用,因為getenv
的地址在UIKit
的代碼被加載的時候已經被解析了。