动态注入 dylib到 Mac 应用

最近看了很多书, 学到了不少新姿势, 原本想写出来和大家分享一下, 但是发现在简书上都有类似的资料, 而且质量都还可以, 所以就只好藏拙了.

这次突然之间想到搞搞 Mac 应用, 是因为Mac 上某个下载应用让我很烦恼, 明明网速很好就是因为不是会员把下载速度弄的特别慢, 加上看了糖炒小虾的 tweakQQ, 所以想去逆向一下, 这篇文章只是讲一下前期的准备工作, 也算是对糖炒小虾文章中一些未涉及的点进行补充.

一. 事先准备:

  1. 一些 Objective-C 的 runtime 知识;
  2. 动态注入库 yololib, git地址, 源代码下载下来之后编译之, 得到一个可执行文件 yolokit(可以自己修改工程名换成别的名字)
  3. 一个 Mac demo 工程, 一个 dylib 工程

二. 大致需求
这边会写一个 Mac demo, 里面只有一个简单的类叫 YoloTester, 它的 -description 方法返回的是@"YoloTest", 然后我在 AppDelegate 上面写了 NSLog(@"hello, %@!", [YoloTester new]), 最终输出的应该是@"hello, YoloTest".

我的目标是通过动态注入的方式, 让最终输出的 log 变成@"hello, cracker".

三. 注入尝试
1). 直接重写类YoloTester来覆盖:
1.新建一个 dylib 工程:

dylib_create_1.png

  1. 重写类:
@interface YoloTester : NSObject

@end

@implementation YoloTester

- (NSString *)description
{
    return @"Cracker";
}

@end

为了方便操作, 我们copy 这个 dylib 和 yololib 执行文件到 demo 的 可执行文件目录下. 然后执行 ./yololib YoloTest libDylib.dylib, 然后就会看到输入日志:

2017-03-13 21:40:58.936 yololib[2164:81591] dylib path @executable_path/libDylib.dylib
2017-03-13 21:40:58.937 yololib[2164:81591] dylib path @executable_path/libDylib.dylib
Reading binary: YoloTest

2017-03-13 21:40:58.937 yololib[2164:81591] Thin 64bit binary!
2017-03-13 21:40:58.937 yololib[2164:81591] dylib size wow 56
2017-03-13 21:40:58.937 yololib[2164:81591] mach.ncmds 20
2017-03-13 21:40:58.937 yololib[2164:81591] mach.ncmds 21
2017-03-13 21:40:58.937 yololib[2164:81591] Patching mach_header..
2017-03-13 21:40:58.937 yololib[2164:81591] Attaching dylib..

2017-03-13 21:40:58.937 yololib[2164:81591] size 55
2017-03-13 21:40:58.937 yololib[2164:81591] complete!

这样的输出代表我们成功注入到了可执行文件中, 然后我们执行./YoloTest来验证一下是否真正替换了我们的方法, 结果输出下面的日志:

objc[2221]: Class YoloTester is implemented in both /Users/ryan/Library/Developer/Xcode/DerivedData/YoloTest-fiuymriohppdctfwvyeqikbxfzgo/Build/Products/Debug/./libDylib.dylib (0x101291120) and /Users/ryan/Library/Developer/Xcode/DerivedData/YoloTest-fiuymriohppdctfwvyeqikbxfzgo/Build/Products/Debug/./YoloTest (0x10128c178). One of the two will be used. Which one is undefined.
2017-03-13 21:41:45.883 YoloTest[2221:84530] Hello, YoloTest!

也就是说, 我们注入之后, 得到了2个 YoloTester 类, 具体使用哪一个没有被指定, 所以最终系统应该是按先后顺序执行了非注入的那一个, 导致输出的还是 Hello, YoloTest!

按道理这里我们有2条路可以走, 第一条, 换一种方式注入, 第二条既然没有指定, 我能不能想办法指定它? 因为第二条我没有找到太多的资料, 但是我认为也是可以走通的, 但是我比较担心即使走通了, 会不会把这个类其它的方法全部都不加载进来了, 这样就有点得不偿失了, 所以为了稳妥起见, 我们还是选择既简单有保险的路子.

  1. 写 category:
    第一条路走不同之后, 自然就想到了用 runtime hook 方法的形式, 然后打算写 category, 然后在+initialize 里面写 exchangeMethod, 但是有个问题是, 在 dylib 里面这么写, 编译不给过, 认为你给了一个不存在的类写 category, 即使你@class YoloTest也不会起作用, 继续抛弃.

  2. 新增入口:
    依然是打runtime的主意, 问题是, 在哪加?

我们知道, 一般我们写代码最早大多数情况都是在 main 函数之后执行, 但其实有很多比 main 函数还早执行的, 例如类的+load(Apple 已经不建议这么写了, 用后面的+ initialize)和+initialize就要早于 main函数. 但是上面我们已经论证了 category 是不可行的, 所以这里再介绍一种更早于main 函数的入口----__attribute__((constructor)).

__attribute__((constructor)) void myentry(){
    // do something
}

一个 C 函数, 如果用__attribute__((constructor))修饰之后, 就会在imageLoad 时期就会被执行到(切记不要滥用, 比较影响启动性能), 这个符号会被写在 Mach-O 的 DATA 中生成一条 mod_init_func记录, 如:

mod_init.png

所以我们就在这里加上我们的代码试试看:

NSString * my_description()
{
    return @"Cracker";
}

__attribute__((constructor)) void myentry(){
    Class YoloTester = NSClassFromString(@"YoloTester");
    
    class_replaceMethod(YoloTester, @selector(description), (IMP)my_description, "@:v");
}

上面的代码主要意思是, 把 YoloTesterdescription方法用 C 函数my_description替换掉.
执行./yololib YoloTest libDylib.dylib和'./YoloTest'后输出:

2017-03-13 22:04:54.239 YoloTest[3661:165402] Hello, Cracker!

证明我们搞定了这个简单的小需求, 成功把代码注入到了一个已经编译好的程序上了.

四. 更进一步
在某些情况下, 我们 hook 之后还想拿到原来的实现, 这里有2种方法, 第一种是:
class_replaceMethod会返回一个 IMP, 我们都知道, IMP 可以直接强转为一个函数指针, 所以我们可以这样

IMP ret = class_replaceMethod(YoloTester, @selector(description), (IMP)my_description, "@:v");
NSString *(*func)() = (NSString *(*)())ret; // 如果想在 my_description函数中执行, 可以赋值给 static 变量, 在 my_description 里面判断执行即可.

另一种方法是:
我们都知道 Objective-C 的方法最终都会调用 objc_msgSend, 然后第一个参数是发消息的对象, 第二个是 SEL, 后续的则是各个参数, 所以我们可以先调用class_addMethodmethod_exchangeImplementations, 然后在 my_description中,是这样的:

NSString * my_description(id self, SEL sel)
{
// 不需要返回值用[self performSelector:], 需要返回值用 NSInvocation
}

个人还是觉得第一种更简单一些.

五. 后记
这里对 Mac 应用做了一个简单的注入 dylib 介绍, 里面涉及到 runtime 的东西没有深入展开阐述, 因为网上资源简直不要太多. 后续我还会继续深入了解一下里面的情况, 希望有一些高质量的产出可以和大家分享.

其实一开始想到逆向 Mac 应用, 我脑子里最先冒出来的是直接用 Hopper 改汇编代码, 后面觉得太麻烦, 然后翻到了糖炒小虾的文章觉得这是一个更加"人性化"的方法...不过最近比较迷汇编, 也看了不少书, 不知道有没有同道中人可以一起学习进步的.

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    萌萌的小伟哥阅读 740评论 0 9
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 27,946评论 33 466
  • 如何看待剩女: 一、提倡素质模式多样化 给“剩女”找出路,必须变革择偶模式,即改变传统的“男高女低”单一模式为多元...
    孤独的长跑者阅读 266评论 0 0
  • 故事开始于瓜阴洲一个破败的傅家园子里,这是个曾经声名显赫的大家族,后来迁徙异地,只剩一个七十多岁的老园丁和傅家的一...
    Ltt2683阅读 1,323评论 0 0
  • 樹活一張皮,人活一張臉。有時間人簡單點,或許更快樂,走入贏家發現無論說話做事都是有意義和價值,走著走著,如何在平凡...
    好彩妹阅读 126评论 0 0