(五) Hook C和Swift函数

1.用dlopen和dlsym进行Hook或执行代码

1.1 Objective-C运行时和Swift与C

  • Objective-C是动态语言,当objc_msgSend调用时在知道要怎么执行。
  • Swift和C/C++表现类似。如果不需要动态性,编译器就不会用。所以你在看Swift汇编时,汇编直接调用方法地址就可以执行。这种直接调用的方式就是dlopendlsym真正发挥的地方了。

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寄存器。

getenv
"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

HookingC

getenvhook.c中进行替换,注释是在项目中的作用。

#import  // 引入dlopen和dlsym
#import  // 测试包含getenv函数的库是否正确加载
#import  // printf
#import  // dispatch_once
#import  // 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
  }
HOME&PATH

可以看到现在hook只对HOME生效,对PATH来说是可以拿到原本数据的。

注意:如果调用了一个UIKit方法,然后UIKit调用了getenv,那么新的getenv方法并不会被调用。因为getenv的地址在UIKit的代码被加载的时候已经被解析了。
如果你要修改UIKitgetenv,就需要操作间接符号表的知识,并修改__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

HookingSwift

因为知道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

现在我们拿到了模块和相应的方法名,就可以利用dlopendlsym来找到函数地址了。

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
}
  1. 定义类型。Swift的方法里面originalImage并不需要参数,为什么这里的方法会带一个Any类型的参数呢?因为函数执行时,汇编代码会从RDI寄存器中获取self,所以我们需要把实例作为第一个参数传进去。否者,程序就会崩溃。
  2. 我们定义完类型就可以进行类型转换了。我们把sym指针转换成对应的函数类型。然后我们就可以通过originalImageFunction进行调用了。
  3. 我们通过传入imageGenerator实例对象,获取原始的图像,放到originalImage中。
  4. 我们把没有水印的图片放到视图中。


    去水印

你可能感兴趣的:((五) Hook C和Swift函数)