1.用dlopen和dlsym进行Hook或执行代码
1.1 Objective-C运行时和Swift与C
- Objective-C是动态语言,当
objc_msgSend
调用时在知道要怎么执行。 - Swift和C/C++表现类似。如果不需要动态性,编译器就不会用。所以你在看Swift汇编时,汇编直接调用方法地址就可以执行。这种直接调用的方式就是
dlopen
和dlsym
真正发挥的地方了。
1.2 简单模式Hook C函数
一个简单的加水印的图片。但是我们查看Assets.xcassets
或者逆向工程师查看Assets.car
都找不到这张图片。因为它是写死在代码里面的,就像这样
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,
...}
我们先在项目里下了一个断点,并打印RDI
寄存器。
"MallocDebugReport"
"MallocErrorStop"
"MallocErrorSleep"
"MallocNanoMaxMagazines"
"_MallocNanoMaxMagazines"
"LIBDISPATCH_STRICT"
"DYLD_INSERT_LIBRARIES"
"NSZombiesEnabled"
...
在我们程序启动前getenv
就已经被执行了。如果你取消掉自动继续
选项,你就会发现调用栈里面根本没有main
函数。
因为C没有动态派发,要hook
一个函数必须要在它被加载之前拦截它。另一方面来说,C函数相对容易获取,而且你只需要获取函数方法名(不需要参数)和C函数所在的动态库的名字。
C函数的hook
有很多方式,只是复杂度不同。如果你只是想在你的可执行文件内进行hook,那还比较简单。但是如果你想在main
函数前hook
一个函数,复杂度就提升了一个等级。
一旦main
函数被执行,所有的动态库也都已加载完毕。dyld
以深度优先的方式递归加载动态库。一般来说,大多数外部函数是懒加载的,除非你用了特殊的链接配置。对于懒加载的外部函数来说,函数第一次调用时,会触发很多操作。dyld
会找到这个模块,定位到这个函数。然后把这个值保存到内存的一个特定部分(__DATA.__la_symbol_ptr)。一旦这个外部函数定下来了,以后的调用就不需要用dyld
来处理了。
如果你想在程序启动前就hook
函数,你就需要创建一个动态库来执行hook
操作,那么在main
函数执行前就已经可用了。
我们在程序启动后获取HOME
环境变量,然后进行打印。HOME
环境变量就是模拟器运行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
下面我们来hook
一下getenv
函数。先创建一个HookingC
动态库,语言选择OC。并在这个库里面创建一个getenvhook
的.h
和.c
。
在getenvhook.c
中进行替换,注释是在项目中的作用。
#import <dlfcn.h> // 引入dlopen和dlsym
#import <assert.h> // 测试包含getenv函数的库是否正确加载
#import <stdio.h> // printf
#import <dispatch/dispatch.h> // dispatch_once
#import <string.h> // strcmp
char * getenv(const char *name) {
return "YAY!";
}
//运行后后台打印
//HOME env: YAY!
如果输入其他参数的时候,想要进行原来的操作,我们需要先找一下原来函数的名字。
(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
动态库,还有一个libsystem_c.dylib
库有这个函数,它的完整地址为/usr/lib/ system/libsystem_c.dylib
。既然我们知道了函数在哪儿,下满我们来用dlopen
,函数签名如下
extern void * dlopen(const char * __path, int __mode);
dlopen
接受一个char *
类型的路径,和一个整型来决定它如何加载模块。如果成功返回一个void *
句柄,否则返回NULL
。
在dlopen
返回一个对模块的引用后,就可以使用dlsym
来获取对函数getenv
的引用了。dlsym
的函数签名如下
extern void * dlsym(void * __handle, const char * __symbol);
第一个参数为dlopen
返回的void *
句柄,第二个参数为要获取的函数的名字。成功的话,会返回第二个指定的函数的地址,否则返回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\nFake getenv: %p\n", real_getenv, getenv);
return "YAY!";
}
//Real getenv: 0x7fff5232b167
//Fake getenv: 0x106c5fd80
//HOME env: YAY!
RTLD_NOW
的意思是,不需要进行懒加载,立即加载。
由于返回函数类型是void *
,但实际我们知道函数的类型,我们来优化一下。
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
}
可以看到现在hook
只对HOME
生效,对PATH
来说是可以拿到原本数据的。
注意:如果调用了一个
UIKit
方法,然后UIKit
调用了getenv
,那么新的getenv
方法并不会被调用。因为getenv
的地址在UIKit
的代码被加载的时候已经被解析了。
如果你要修改UIKit
的getenv
,就需要操作间接符号表
的知识,并修改__DATA.__la_symbol_ptr
段中对应getenv
的函数地址了。
这部分会涉及到使用fishhook
,原理可参考fishhook x MachOView源码阅读。
1.3 困难模式Hook Swift函数
非动态的Swift代码就像C函数一样。这种方法有一些复杂的地方,使得它更难融入快速的方法中。
首先,Swift在开发中经常使用类或结构。这是一个独特的挑战,因为dlsym
只能提供一个C函数。我们需要扩展这个函数,以便Swift方法可以在获取实例方法时引用self,或者在调用类方法时引用类。当访问属于类的方法时,程序汇编代码在执行该方法时通常会引用self或类的偏移量。由于dlysm
只能提供一个C类型的函数,因此我们需要利用汇编、参数和寄存器的知识,将该C函数转换为一个Swift方法。
第二个需要担心的问题是Swift会弄乱方法的名称。在代码中看到的漂亮的名字,在模块符号表中实际上是可怕的长名字。为了通过dlysm
引用Swift方法,需要找到此方法弄乱后的正确名称。
下面我们来看看怎么操作。
HookingSwift
库中有一个CopyrightImageGenerator
类,但我们只能访问到公开的watermarkedImage
计算属性,而私有的originalImage
属性访问不了。
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
。
因为知道originalImage
是用Swift实现的,我们需要用Swift模式来进行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
函数地址是0x0000000000000f70
,但这只是在HookingSwift
库中的地址。我们继续。
(lldb) image dump symtab -m HookingSwift
...
[ 4] 51 D X Code 0x0000000000000f70 0x0000000106368f70 0x0000000000000100 0x000f0000 $s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg
...
通过0x0000000000000f70
进行搜索,我们可以看到这个方法名叫
$s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg
现在我们拿到了模块和相应的方法名,就可以利用dlopen
和dlsym
来找到函数地址了。
if let handle = dlopen("./Frameworks/HookingSwift.framework/HookingSwift", RTLD_NOW),
let sym = dlsym(handle, "$s12HookingSwift23CopyrightImageGeneratorC08originalD033_71AD57F3ABD678B113CF3AD05D01FF41LLSo7UIImageCSgvg") {
print("\(sym)")
}
//打印
0x000000010f354f70
//在上面的地址处设置一个断点,看一下对不对
(lldb) b 0x000000010f354f70
Breakpoint 1: where = HookingSwift`HookingSwift.CopyrightImageGenerator.(originalImage in _71AD57F3ABD678B113CF3AD05D01FF41).getter : Swift.Optional<__C.UIImage> at CopyrightImageGenerator.swift:42, address = 0x000000010f354f70
好了,我们找到了函数地址。那我们怎么调用它呢?幸好,我们可以用Swift的关键字typealias
来进行函数的类型转换。
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
并不需要参数,为什么这里的方法会带一个Any类型的参数呢?因为函数执行时,汇编代码会从RDI
寄存器中获取self
,所以我们需要把实例作为第一个参数传进去。否者,程序就会崩溃。 - 我们定义完类型就可以进行类型转换了。我们把
sym
指针转换成对应的函数类型。然后我们就可以通过originalImageFunction
进行调用了。 - 我们通过传入
imageGenerator
实例对象,获取原始的图像,放到originalImage
中。 -
我们把没有水印的图片放到视图中。