(五) Hook C和Swift函数

作者: 收纳箱 | 来源:发表于2020-03-06 17:36 被阅读0次

    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 <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
      }
    
    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函数

        本文链接:https://www.haomeiwen.com/subject/upuwrhtx.html