动态注入 dylib到 Mac 应用
最近看了很多书, 学到了不少新姿势, 原本想写出来和大家分享一下, 但是发现在简书上都有类似的资料, 而且质量都还可以, 所以就只好藏拙了.
这次突然之间想到搞搞 Mac 应用, 是因为Mac 上某个下载应用让我很烦恼, 明明网速很好就是因为不是会员把下载速度弄的特别慢, 加上看了糖炒小虾的 tweakQQ, 所以想去逆向一下, 这篇文章只是讲一下前期的准备工作, 也算是对糖炒小虾文章中一些未涉及的点进行补充.
一. 事先准备:
- 一些 Objective-C 的 runtime 知识;
- 动态注入库
yololib
, git地址, 源代码下载下来之后编译之, 得到一个可执行文件 yolokit(可以自己修改工程名换成别的名字) - 一个 Mac demo 工程, 一个 dylib 工程
二. 大致需求
这边会写一个 Mac demo, 里面只有一个简单的类叫 YoloTester
, 它的 -description
方法返回的是@"YoloTest", 然后我在 AppDelegate
上面写了 NSLog(@"hello, %@!", [YoloTester new])
, 最终输出的应该是@"hello, YoloTest".
我的目标是通过动态注入的方式, 让最终输出的 log 变成@"hello, cracker".
三. 注入尝试
1). 直接重写类YoloTester
来覆盖:
1.新建一个 dylib 工程:
- 重写类:
@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条路可以走, 第一条, 换一种方式注入, 第二条既然没有指定, 我能不能想办法指定它? 因为第二条我没有找到太多的资料, 但是我认为也是可以走通的, 但是我比较担心即使走通了, 会不会把这个类其它的方法全部都不加载进来了, 这样就有点得不偿失了, 所以为了稳妥起见, 我们还是选择既简单有保险的路子.
-
写 category:
第一条路走不同之后, 自然就想到了用 runtime hook 方法的形式, 然后打算写 category, 然后在+initialize
里面写 exchangeMethod, 但是有个问题是, 在 dylib 里面这么写, 编译不给过, 认为你给了一个不存在的类写 category, 即使你@class YoloTest
也不会起作用, 继续抛弃. -
新增入口:
依然是打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记录, 如:
所以我们就在这里加上我们的代码试试看:
NSString * my_description()
{
return @"Cracker";
}
__attribute__((constructor)) void myentry(){
Class YoloTester = NSClassFromString(@"YoloTester");
class_replaceMethod(YoloTester, @selector(description), (IMP)my_description, "@:v");
}
上面的代码主要意思是, 把 YoloTester
的 description
方法用 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_addMethod
再method_exchangeImplementations
, 然后在 my_description中,是这样的:
NSString * my_description(id self, SEL sel)
{
// 不需要返回值用[self performSelector:], 需要返回值用 NSInvocation
}
个人还是觉得第一种更简单一些.
五. 后记
这里对 Mac 应用做了一个简单的注入 dylib 介绍, 里面涉及到 runtime 的东西没有深入展开阐述, 因为网上资源简直不要太多. 后续我还会继续深入了解一下里面的情况, 希望有一些高质量的产出可以和大家分享.
其实一开始想到逆向 Mac 应用, 我脑子里最先冒出来的是直接用 Hopper 改汇编代码, 后面觉得太麻烦, 然后翻到了糖炒小虾的文章觉得这是一个更加"人性化"的方法...不过最近比较迷汇编, 也看了不少书, 不知道有没有同道中人可以一起学习进步的.