Cocoa Touch Framework
我们将使用 Cocoa Touch Framework
来Hook iOS 中系统的C函数
,首先我们先新创建一个工程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;
}
这段代码是用来获取系统的环境变量
HOME
的值,我们运行工程,运行结果如下
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
该值是我们所运行的模拟器中的 HOME
环境变量值。如果我们想HOOK
getEnv
函数,我们需要创建一个依赖于HooKDemo的Framework,来改变getEnv函数的实现。
在Xcode
中,我们通过 File -> New -> Target,并且选择 Cocoa Touch Framework,选择 HookingC为项目名,语言选择Objective-C
在HookingC项目中,我们新建一个c语言
文件,并命名为 getenvhook
我们在 getenvhook.c
文件中,添加以下头文件
#import <dlfcn.h>
#import <assert.h>
#import <stdio.h>
#import <dispatch/dispatch.h>
#import <string.h>
-
dlfcn.h
中,我们主要用了两个函数dlopen
和dlsym
。 -
assert.h
中,我们主要来测试真实的getEnv
函数被真正加载。 -
stdio.h
我们主要用里面的printf
函数。 -
string.h
我们主要用里面的strcmp
函数来比较两个C字符串。
我们在 getenvhook.c
文件中,重写getenv
方法
char * getenv(const char *name) {
return "YAY!";
}
然后我们运行该工程,我们可以看到如下输出
HOME env: YAY!
我们可以得知,已经成功替换了系统的 getenv
方法,但这肯定不是我们期望的,如果我们在 getenv
函数里面增加如下代码会发生什么呢?
char * getenv(const char *name) {
return getenv(name);
return "YAY"!
}
这样的话,会造成递归循环调用,导致栈溢出
,我们现在已经替换了系统的getenv
函数,我们需要找到系统的getenv
函数,然后在调用它,我们重新启动工程,然后使工程进入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个getenv
实现,一个在HOOKingC.framework
中,一个在lib system_c.dylib
中。它的全路径为 /usr/lib/system/libsystem_c.dylib
。
现在我们知道了这个函数是从哪里加载的了,下面我们将使用dlopen
函数,去加载该函数。其方法签名如下
extern void * dlopen(const char * __path, int __mode);
-
dlopen
函数第一个参数为一个全路径
,第二个参数为一个整数
来决定了dlopen
函数以怎样的模式
去加载,如果加载成功,dlopen
将会返回一个void *
类型的数据,否则将会返回NULL
。
在 dlopen
函数加载完之后,我们使用dlsym
去得到 getenv
函数的引用,dlsym
函数的函数签名如下:
extern void * dlsym(void * __handle, const char * __symbol);
-
dlsym
的第一个参数为dlopen
函数得到的方法引用,第二个参数为方法的名字。如果成功,将返回第二个参数符号名
的内存地址
。如果失败了,则返回NULL
。
我们将我们自定义的getEnv
函数,进行改进一下。
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
函数的意思是:让该函数立即执行,不使用懒加载的方式。 - 2,我们使用
assert
函数,确保handle
不为NULL
。 - 3,我们使用
dlsym
函数,来加载get_env
的真实地址值。
我们再次运行工程,会得到类似的以下输出:
Real getenv: 0x7fff5232b167
Fake getenv: 0x107469de0
HooKDemo[86264:7770900] HOME env: YAY!
现在我们已经知道了getenv
的函数签名,接下来,我们将getenv
函数进行如下调整:
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,创建一个
static
变量handle
,因为是静态的,在函数执行完之后也不会被释放,在getenv
函数之外,我们也可以获取该值。 - 2,我们根据
getenv
函数的方法签名,定义了一个静态的real_getenv
函数指针。我们将会把它赋值到真正的getenv
方法实现中去。 - 3,使用
dispatch_once
函数,保证只执行一次。 - 4,使用
strcmp
函数,判断当前变量是否为HOME
,来更改其输出 - 5, 如果不是
HOME
变量,则调用默认的getenv
函数。
在 AppDelegate中,我们分别获取HOME
和PATH
环境变量
- (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;
}
其运行结果如下:
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
环境变量的值给替换掉,非HOME
环境变量还是调用默认的getenv
函数。
如果我们想要调用 UIKit
框架中的 getenv
函数,以我们现在的方式是不可以的,这是因为,此时UIKit
已经加载到内存中,我们自定义的getenv
函数不会得到调用。
为了能够调用UIKit
中的getenv
函数,我们需要掌握 符号表的重定向
知识,然后去改变在__DATA.__la_symbol_ptr
段中的getenv
的地址。
备注:dlopen
:打开动态库,dlsym
:获得符号名。
总结
本篇文章,我们主要探讨了使用 iOS中的framework
来HOOK
系统中的C函数
,通过 dlopen
函数得到系统中真实的函数地址,使用dlsym
函数和方法签名
对真实的地址进行符号绑定
。从而达到调用默认函数的目的。
最后附上本文代码示例 HookDemo