最近看了很多書, 學到了不少新姿勢, 原本想寫出來和大家分享一下, 但是發(fā)現(xiàn)在簡書上都有類似的資料, 而且質量都還可以, 所以就只好藏拙了.
這次突然之間想到搞搞 Mac 應用, 是因為Mac 上某個下載應用讓我很煩惱, 明明網(wǎng)速很好就是因為不是會員把下載速度弄的特別慢, 加上看了糖炒小蝦的 tweakQQ, 所以想去逆向一下, 這篇文章只是講一下前期的準備工作, 也算是對糖炒小蝦文章中一些未涉及的點進行補充.
一. 事先準備:
- 一些 Objective-C 的 runtime 知識;
- 動態(tài)注入庫
yololib
, git地址, 源代碼下載下來之后編譯之, 得到一個可執(zhí)行文件 yolokit(可以自己修改工程名換成別的名字) - 一個 Mac demo 工程, 一個 dylib 工程
二. 大致需求
這邊會寫一個 Mac demo, 里面只有一個簡單的類叫 YoloTester
, 它的 -description
方法返回的是@"YoloTest", 然后我在 AppDelegate
上面寫了 NSLog(@"hello, %@!", [YoloTester new])
, 最終輸出的應該是@"hello, YoloTest".
我的目標是通過動態(tài)注入的方式, 讓最終輸出的 log 變成@"hello, cracker".
三. 注入嘗試
1). 直接重寫類YoloTester
來覆蓋:
1.新建一個 dylib 工程:
- 重寫類:
@interface YoloTester : NSObject
@end
@implementation YoloTester
- (NSString *)description
{
return @"Cracker";
}
@end
為了方便操作, 我們copy 這個 dylib 和 yololib 執(zhí)行文件到 demo 的 可執(zhí)行文件目錄下. 然后執(zhí)行 ./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!
這樣的輸出代表我們成功注入到了可執(zhí)行文件中, 然后我們執(zhí)行./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 類, 具體使用哪一個沒有被指定, 所以最終系統(tǒng)應該是按先后順序執(zhí)行了非注入的那一個, 導致輸出的還是 Hello, YoloTest!
按道理這里我們有2條路可以走, 第一條, 換一種方式注入, 第二條既然沒有指定, 我能不能想辦法指定它? 因為第二條我沒有找到太多的資料, 但是我認為也是可以走通的, 但是我比較擔心即使走通了, 會不會把這個類其它的方法全部都不加載進來了, 這樣就有點得不償失了, 所以為了穩(wěn)妥起見, 我們還是選擇既簡單有保險的路子.
寫 category:
第一條路走不同之后, 自然就想到了用 runtime hook 方法的形式, 然后打算寫 category, 然后在+initialize
里面寫 exchangeMethod, 但是有個問題是, 在 dylib 里面這么寫, 編譯不給過, 認為你給了一個不存在的類寫 category, 即使你@class YoloTest
也不會起作用, 繼續(xù)拋棄.新增入口:
依然是打runtime的主意, 問題是, 在哪加?
我們知道, 一般我們寫代碼最早大多數(shù)情況都是在 main
函數(shù)之后執(zhí)行, 但其實有很多比 main
函數(shù)還早執(zhí)行的, 例如類的+load
(Apple 已經(jīng)不建議這么寫了, 用后面的+ initialize
)和+initialize
就要早于 main
函數(shù). 但是上面我們已經(jīng)論證了 category 是不可行的, 所以這里再介紹一種更早于main
函數(shù)的入口----__attribute__((constructor))
.
__attribute__((constructor)) void myentry(){
// do something
}
一個 C 函數(shù), 如果用__attribute__((constructor))
修飾之后, 就會在imageLoad 時期就會被執(zhí)行到(切記不要濫用, 比較影響啟動性能), 這個符號會被寫在 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 函數(shù)my_description
替換掉.
執(zhí)行./yololib YoloTest libDylib.dylib
和'./YoloTest'后輸出:
2017-03-13 22:04:54.239 YoloTest[3661:165402] Hello, Cracker!
證明我們搞定了這個簡單的小需求, 成功把代碼注入到了一個已經(jīng)編譯好的程序上了.
四. 更進一步
在某些情況下, 我們 hook 之后還想拿到原來的實現(xiàn), 這里有2種方法, 第一種是:
class_replaceMethod
會返回一個 IMP, 我們都知道, IMP 可以直接強轉為一個函數(shù)指針, 所以我們可以這樣
IMP ret = class_replaceMethod(YoloTester, @selector(description), (IMP)my_description, "@:v");
NSString *(*func)() = (NSString *(*)())ret; // 如果想在 my_description函數(shù)中執(zhí)行, 可以賦值給 static 變量, 在 my_description 里面判斷執(zhí)行即可.
另一種方法是:
我們都知道 Objective-C 的方法最終都會調用 objc_msgSend, 然后第一個參數(shù)是發(fā)消息的對象, 第二個是 SEL, 后續(xù)的則是各個參數(shù), 所以我們可以先調用class_addMethod
再method_exchangeImplementations
, 然后在 my_description中,是這樣的:
NSString * my_description(id self, SEL sel)
{
// 不需要返回值用[self performSelector:], 需要返回值用 NSInvocation
}
個人還是覺得第一種更簡單一些.
五. 后記
這里對 Mac 應用做了一個簡單的注入 dylib 介紹, 里面涉及到 runtime 的東西沒有深入展開闡述, 因為網(wǎng)上資源簡直不要太多. 后續(xù)我還會繼續(xù)深入了解一下里面的情況, 希望有一些高質量的產出可以和大家分享.
其實一開始想到逆向 Mac 應用, 我腦子里最先冒出來的是直接用 Hopper 改匯編代碼, 后面覺得太麻煩, 然后翻到了糖炒小蝦的文章覺得這是一個更加"人性化"的方法...不過最近比較迷匯編, 也看了不少書, 不知道有沒有同道中人可以一起學習進步的.