逆向 Mac 應用 Bartender

前言

本文內容僅做爲學習交流,但願你們多多支持正版軟件。swift

Emmmmm... 其實最初是準備寫一篇關於 iOS 應用的逆向筆記的,不過一直沒找到合適的目標 App 以及難度適宜的功能點來做爲寫做素材...bash

破解了 Bartender 以後我以爲對於 Bartender 的破解過程難度適中,很是適合當作素材來寫,且不管是 Mac App 仍是 iOS App,逆向的思路都是相通的,因此就寫了這篇文章~微信

國慶以前,果果放出了最新操做系統 macOS Mojave 的正式版本,相信不少小夥伴都跟我同樣在正式版發佈後緊跟着就升級了系統(此前因爲工做設備參與項目產出須要確保系統穩定性因此沒敢嚐鮮的同窗應該不僅我一我的哈)。app

升級到正式版 macOS Mojave 以後,我興致勃勃的在新系統中各處探索了一番,而後將系統切換到 Dark Mode 後打開 Xcode 心滿意足地敲(搬)起了代碼(磚)...async

嘛~ 又是一個愜意的午後,有時候人就是這麼容易知足(笑)~函數

等等!這是什麼鬼!?個人 Bartender 怎麼不能正常工做了(其實如今回想起來應該是試用期到期了)...工具

本文將以 Bartender 爲目標 App,講解如何經過靜態分析工具 Hopper 逐步分析 Bartender 的內部實現邏輯並結合動態分析等手段逐步破解 Bartender 的過程與思路~學習

索引

  • Bartender
  • Hopper
  • 逆向過程 & 思路
  • 總結

Bartender

Bartender 是一款能夠幫助咱們整理屏幕頂部菜單欄圖標的工具。ui

隨着咱們安裝的 App 不斷增多,屏幕頂部菜單欄上面的圖標也會對應不斷增長。這些 App 的圖標並不是出自一家之手,風格各異,隨着數目增多逐漸顯得雜亂不堪。spa

咱們能夠經過 Bartender 來隱藏從新排列這些惱人的小圖標,能夠將沒什麼用可是運行起來卻要顯示的 App 圖標始終隱藏,將偶爾會用的 App 圖標隱藏到 Bartender 功能按鈕後面(用到的時候能夠經過點擊 Bartender 功能按鈕切換顯隱),只顯示經常使用的或者咱們認爲好看的應用圖標。

除此以外 Bartender 還具有一些其餘更加深刻的功能(好比支持所有菜單欄條目範圍的搜索等等),毫無疑問它是一款很是棒的菜單欄圖標管理工具。

Note: 重申,Bartender 僅售 15 刀,仍是推薦各位使用正版,本文僅做爲學習交流。

Hopper

Hopper 是一款不錯的 mac OS 與 Linux 反彙編工具,同時還提供必定的反編譯能力,能夠利用它來調試咱們的程序。此外,Hopper 還支持控制流視圖模式,Python 腳本,LLDB & GDB,而且提供了 Hopper SDK 可供擴展,在 Hopper SDK 的基礎上你甚至能夠擴展本身的文件格式和 CPU 支持。

值得一提的是 Hopper 的做者是一名獨立開發者,他的平常工做環境也是在 mac OS 上,因此在 mac OS 上的 Hopper 是徹底使用 Cocoa Framework 實現的,而 Linux 版本的 Hopper 則選擇使用 Qt 5 來實現。

我的認爲 Hopper 在 mac OS 上面的運行表現很是好,不少細節(好比類型顏色區分等)都作的不錯,功能簡潔的同時快捷鍵也很好記(Hopper 提供的功能已經覆蓋了絕大多數使用場景)。

最關鍵的一點是收費良心,我的證書只要 99 刀,當之無愧的人人都買得起的逆向工具!固然若是你以爲貴,Hopper 還提供試用,試用形式相似於 Charles,每次開啓後能夠試用 30 分鐘,通常狀況下這已經夠用了。

Note: Hopper v4.4.0 支持 Mojave Dark Mode。

逆向過程 & 思路

這一章節的內容會詳細的講述我我的在破解 Bartender 過程當中的想法以及中間遇到問題時解決問題的思路,以前沒有涉足逆向或者逆向經驗尚淺的同窗可能會以爲比較晦澀,這種狀況最好結合本身的實際操做反覆閱讀沒有理解的地方直到真正弄明白爲止。

相信本身,每一份努力終會有所回報!當有朝一日本身也能夠經過本身的逆向技術破解 & 定製化本身感興趣的 App 時,你會發現一切的努力都是值得的。

獲取目標二進制

Bartender 官網下載最新的 Bartender,截止本文提筆以前 Bartender 的最新版本爲 3.0.47。

將下載好的壓縮包解壓以後獲得 Bartender 3.app,將 Bartender 3.app 文件複製到本身的 Application 文件夾下。右鍵點擊 Bartender 3.app 選擇「顯示包內容」,在 Contents 目錄下找到 MacOS 目錄,裏面有咱們要的目標二進制文件 Bartender 3。

從「受權」着手

打開 Hopper,將目標二進制文件拖入 Hopper,在彈出的彈窗中選擇 OK 後等待 Hopper 分析完畢。

在左側的分欄中選擇 Proc. ,這可讓咱們查看 Hopper 分析出來的方法。分欄下面有搜索框,內部能夠經過輸入關鍵詞來過濾出咱們想要的結果。由於通常的 App 都是經過某些方法判斷是否受權的,這裏咱們先輸入 is (注意 is 前面加空格),而後觀察過濾出來的結果。

果不其然,發現裏面有三個 [xxx isLicensed] 方法,點擊方法 Hopper 會跳轉至方法處。

Note: 三處 [xxx isLicensed] 的方法內部邏輯幾乎同樣,這裏拿 [Bartender_3.AppDelegate isLicensed] 講解,其餘兩處不作贅述。

Emmmmm... 這裏的彙編代碼仍是比較簡單的,雖然我不是很瞭解 x86 的彙編指令,不過 Hopper 已經幫助咱們作了一些輔助性工做。其中開始處的 push rbp 以及結束處 pop rbp 能夠簡單理解爲入棧出棧,call sub_100067830 能夠理解爲調用地址 0x100067830 處的方法,pop 以前的 movsx eax, al 和 ARM64 中的 mov 指令相似,能夠理解爲將 al 內存儲的東西移動到 eax 寄存器中,eax 寄存器用於存儲 x86 的方法返回值

咱們能夠看出這裏調用了地址 0x100067830 處的函數,拿到結果以後又調用了 imp___stubs__$S10ObjectiveC22_convertBoolToObjCBoolyAA0eF0VSbF 方法將結果作了轉化,最後將結果賦值給 eax 寄存器用於結果返回。其中 imp___stubs__$S10ObjectiveC22_convertBoolToObjCBoolyAA0eF0VSbF 咱們能夠根據名稱推測出該方法的做用應該是將 Bool 轉化爲 Objective-C 的 BOOL 而已。

那麼關鍵信息應該在 sub_100067830 處,雙擊 sub_100067830 Hopper 會跳轉到 0x100067830 處,這樣咱們就能夠分析其中的具體實現了。不過 0x100067830 內部的實現比較複雜,跳轉過去以後發現彙編代碼很是多,還有不少跳轉... 這時候咱們能夠經過 Hopper 頂部中間靠右一點的分欄,點擊顯示爲 if(b) f(x); 的按鈕查看僞代碼。

Hopper 解析出來的僞代碼風格相似 Objective-C 代碼,能夠看到 0x100067830 內部經過 NSUserDefaults 以及其餘的邏輯實現,其中還包括其餘的形式爲 sub_xxxxxx 的方法調用,這種狀況下若是咱們繼續跳轉到這些方法的地址查看其內部實現頗有可能陷入遞歸中...

那麼這種狀況該如何處理呢?

分析問題,咱們找到 [xxx isLicensed] 而且以爲這有可能就是 Bartender 中判斷受權與否的函數,那麼咱們只須要將三處 [xxx isLicensed] 的返回值改成 true 便可。因此這裏咱們沒有必要一步步的看其內部實現,先返回 [Bartender_3.AppDelegate isLicensed] 處。前面講過在 x86 彙編中 eax 寄存器用於存儲方法的返回值,咱們在 [Bartender_3.AppDelegate isLicensed] 按快捷鍵 option + A 插入彙編代碼 mov eax, 0x1eax 永遠賦值爲 1true 以後跟 ret 即 return 指令直接讓函數返回 true 就能夠達到咱們的目的了。

用快捷鍵 shift + command + E 導出二進制文件,覆蓋到原 Bartender 目錄中,嘗試運行。你會發現一開始是成功的,屏幕頂部的菜單欄圖標也被正常管理了,可是過了大約 10s 以後一切又變回了原樣,而且還會彈出一個試用期到期的彈窗...

重拾思路

那麼咱們剛纔修改的三處 [xxx isLicensed] 爲何沒有產生做用呢?其實它已經產生做用了,雖然咱們不能夠正常使用 Bartender,可是打開 Bartender 的 License 界面咱們能夠發現這裏的界面已經顯示咱們付過款了,儘管這並無什麼卵用就是了...

到這裏咱們彷佛沒有什麼頭緒了,由於延時方法有不少,光是憑藉這一條線索很難定位到阻止咱們破解的目標代碼位置。

逆向過程當中的思路很重要,若是遇到思路斷了的狀況不要着急也不要氣餒,咱們能夠從新運行程序,嘗試不一樣的操做並觀察操做對應的表現 & 結果。

通過反覆運行程序,我發現每次從新啓動 Bartender 均可以有大約 10s 的可用時間,若是啓動以後直接主動點擊 Bartender 的功能按鈕則會直接彈出試用期到期彈窗且頂部菜單欄圖標也會直接回到以前雜亂的樣子。

這時候個人思路從延時方法轉移到了這個 Trial ended 彈窗以及 Bartender 的功能按鈕點擊以後的對應方法上。這就是動態分析,它能夠幫助咱們從新找回思路。

按鈕響應方法

有了思路,對應的方法並不難找。咱們能夠利用 Hopper 的 Tag Scope 先把可能出現的區域找出來,再到對應的區域下的方法列表中尋找咱們的目標方法位置。

這裏我很快就找到了目標函數 -[_TtC11Bartender_311AppDelegate bartenderStatusItemClickWithSender:], 其內部調用了 sub_100029ac0(arg2); 其中 arg2 就是 sender,也就是這個 Bartender 的功能按鈕了。

int sub_100029ac0(int arg0) {
    sub_100022840(arg0);
    rbx = **_NSApp;
    if (rbx == 0x0) goto loc_100029f44;

loc_100029ae7:
    [rbx retain];
    r14 = [[rbx currentEvent] retain];
    rdi = rbx;
    if (r14 == 0x0) goto loc_100029bef;

loc_100029b18:
    [rdi release];
    if (([r14 modifierFlags] & 0x80000) != 0x0) goto loc_100029b6e;

loc_100029b33:
    [r14 retain];
    if ((([r14 modifierFlags] & 0x40000) != 0x0) || ([r14 type] == 0x4)) goto loc_100029b66;

loc_100029bcc:
    rbx = [r14 type];
    [r14 release];
    if (rbx == 0x3) goto loc_100029b6e;

loc_100029bec:
    rdi = r14;
    goto loc_100029bef;

loc_100029bef:
    [rdi release];
    r14 = [[swift_getInitializedObjCClass(@class(NSUserDefaults)) standardUserDefaults] retain];
    if (*qword_1000e7e70 != 0xffffffffffffffff) {
            swift_once(qword_1000e7e70, sub_100069790);
    }
    rbx = *qword_1000ee1f8;
    r15 = *qword_1000ee200;
    swift_bridgeObjectRetain(rbx);
    r15 = (extension in Foundation):Swift.String._bridgeToObjectiveC() -> __ObjC.NSString(rbx, r15);
    swift_bridgeObjectRelease(rbx);
    rbx = [[r14 objectForKey:r15] retain];
    [r15 release];
    [r14 release];
    if (rbx != 0x0) {
            swift_getObjectType(rbx);
            var_50 = rbx;
    }
    else {
            intrinsic_movaps(var_40, 0x0);
            var_50 = intrinsic_movaps(var_50, 0x0);
    }
    rax = sub_10001c9a0(&var_50, &var_78);
    if (var_58 != 0x1) goto loc_100029cd8;

loc_100029ccd:
    sub_10001c2f0(&var_78);
    goto loc_100029d44;

loc_100029d44:
    if (*(int8_t *)(r13 + *objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded) == 0x1) {
            rax = sub_1000230e0(0x1);
    }
    else {
            *(int8_t *)(r13 + *objc_ivar_offset__TtC11Bartender_311AppDelegate_performDelayedClicks) = 0x1;
            rax = sub_1000215f0();
            if ((rax & 0x1) == 0x0) {
                    rbx = *objc_ivar_offset__TtC11Bartender_311AppDelegate_performDelayedClicks;
                    rax = *(int8_t *)(r13 + rbx);
                    rax = !rax & 0x1;
                    *(int8_t *)(r13 + rbx) = rax;
            }
    }
    return rax;

loc_100029cd8:
    rcx = *qword_1000e8a98;
    if (rcx == 0x0) {
            rcx = swift_getObjCClassMetadata(swift_getInitializedObjCClass(@class(NSDictionary)));
            *qword_1000e8a98 = rcx;
    }
    rax = swift_dynamicCast(&var_28, &var_78, *type metadata for Any + 0x8);
    if (rax == 0x0) goto loc_100029d44;

loc_100029d24:
    r14 = var_28;
    if ([r14 count] == 0x0) goto loc_100029d8f;

loc_100029d3c:
    [r14 release];
    goto loc_100029d44;

loc_100029d8f:
    r15 = [objc_allocWithZone(@class(NSAlert)) init];
    rbx = sub_1000a7f20("No menu items have been setup", 0x1d, 0x1, rcx, 0x6);
    r12 = (extension in Foundation):Swift.String._bridgeToObjectiveC() -> __ObjC.NSString(rbx, 0x1);
    swift_bridgeObjectRelease(rbx);
    [r15 setMessageText:r12];
    [r12 release];
    rbx = sub_1000a7f20("No menu items have been setup in Bartender Preferences, so Bartender is not doing anything yet. Would you like to open preferences now.", 0x87, 0x1, rcx, 0x6);
    r12 = (extension in Foundation):Swift.String._bridgeToObjectiveC() -> __ObjC.NSString(rbx, 0x1);
    swift_bridgeObjectRelease(rbx);
    [r15 setInformativeText:r12];
    [r12 release];
    [r15 setAlertStyle:0x1];
    rbx = sub_1000a7f20("Open Preferences", 0x10, 0x1, rcx, 0x6);
    r12 = (extension in Foundation):Swift.String._bridgeToObjectiveC() -> __ObjC.NSString(rbx, 0x1);
    swift_bridgeObjectRelease(rbx);
    rbx = [[r15 addButtonWithTitle:r12] retain];
    [r12 release];
    [rbx release];
    rbx = sub_1000a7f20("Dismiss", 0x7, 0x1, rcx, 0x6);
    r12 = (extension in Foundation):Swift.String._bridgeToObjectiveC() -> __ObjC.NSString(rbx, 0x1);
    swift_bridgeObjectRelease(rbx);
    rbx = [[r15 addButtonWithTitle:r12] retain];
    [r12 release];
    [rbx release];
    if ([r15 runModal] == 0x3e8) {
            sub_100029a10();
    }
    [r15 release];
    rax = [r14 release];
    return rax;

loc_100029b6e:
    *(int8_t *)(r13 + *objc_ivar_offset__TtC11Bartender_311AppDelegate_performDelayedClicks) = 0x0;
    rdi = r14;
    if (([rdi modifierFlags] & 0x40000) == 0x0) {
            sub_100020de0();
    }
    else {
            if (*(int8_t *)(r13 + *objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded) == 0x1) {
                    sub_1000230e0(0x1);
            }
            else {
                    sub_100020fe0(rdi);
            }
    }
    rax = [r14 release];
    return rax;

loc_100029b66:
    [r14 release];
    goto loc_100029b6e;

loc_100029f44:
    asm { ud2 };
    rax = sub_100029f46();
    return rax;
}
複製代碼

PS: 爲了便於讀者結合後面分析部分的內容快速定位(Command + F),上面的僞代碼沒有使用截圖形式展現。

其中很醒目的是 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded 咱們按照以前的方法,將僞代碼先切回彙編模式,找到對應的彙編代碼處。

這是一段明顯的 if 語句彙編代碼,看下面的 mov edi, 0x1 這一小節就是指 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEndedtrue 以後執行的代碼,表示要是試用期到期就執行 0x1000230e0 處的方法。咱們記下這個地址以後把這兩處的彙編代碼經過上文插入彙編代碼的方式修改一下,將這個 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded 直接替換爲 0x0false

在逆向工程中,切忌不能夠冒進,時值今日幾乎全部應用都會採起措施來增長其逆向難度。這時候千萬不要想着一步到位,應該在適量修改以後嘗試導出二進制,用動態分析的方法驗證一下結果。由於咱們這時候不是正向開發者,在沒有見到上下文的狀況下修改代碼極可能會把程序改爲一個不可用的狀態(好比正常功能損壞或者頻繁 Crash),因此最好步步爲營。

這裏咱們導出修改以後的二進制文件,按照 Bartender 的原路徑覆蓋以前的二進制文件驗證一下結果。我在這個階段運行時發現若是正常開啓 Bartender 仍是會有一個 10s 左右的可用時長,以後依然會彈出試用期到期彈窗,而且程序變爲不可用狀態;而若是重啓 Bartender 在試用期彈窗彈出以前點擊功能按鈕則能夠正常切換,可是再次點擊按鈕卻切換不回來了,而且程序運行 10s 左右仍會彈出試用期到期彈窗,可是菜單欄上面的圖標不會變失效,只是切不回去而已。

功能破解

到目前爲止若是不在意功能僅僅想要隱藏菜單欄的圖標已是能夠湊合用了,可是這顯然不是咱們想要的最終結果。

經過上面運行程序後觀察到的狀況我推測在 -[_TtC11Bartender_311AppDelegate bartenderStatusItemClickWithSender:] 內部切換回來的邏輯中仍然有地方對是否到期作了判斷,咱們上面只是成功修改了切換過去的邏輯,那麼切換回來的邏輯在哪呢?

按邏輯推測,正向切換的時候是使用 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded 作判斷,反向切換應該同理纔對,咱們去追蹤 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded 的使用,最終發現 sub_10001f870 中使用了 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEndedsub_10001f870sub_100029a10 調用,sub_100029a10 又被 sub_100029ac0 調用,sub_100029ac0 就是上文在 -[_TtC11Bartender_311AppDelegate bartenderStatusItemClickWithSender:] 中被調用的函數,這不只知足了被 Bartender 功能按鈕所引用的條件,同時還對 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded 有所引用,因此我用插入彙編的方式將 sub_10001f870 中關於 objc_ivar_offset__TtC11Bartender_311AppDelegate_trialEnded 的使用改成了 0x0,即 false

嘛~ 導出二進制覆蓋,發現此次的 Bartender 已經能夠正常使用功能了,不過試用期到期的彈窗問題依然存在,儘管它並不影響使用,但我仍是沒法接受這樣一個半成品的狀態。

完美破解

還記得上文中得出的 0x1000230e0 嗎,若是試用期到期則會執行 0x1000230e0 地址處的方法,咱們經過快捷鍵 G 跳轉到 0x1000230e0 地址,看一下里面的實現邏輯。

void sub_1000230e0(int arg0) {
    r14 = arg0;
    r15 = r13 + *objc_ivar_offset__TtC11Bartender_311AppDelegate_trialOverWindow;
    rbx = swift_unknownWeakLoadStrong(r15);
    if (rbx != 0x0) {
            [rbx center];
            [rbx release];
            rbx = **_NSApp;
            if (rbx != 0x0) {
                    [rbx retain];
                    [rbx activateIgnoringOtherApps:sign_extend_64($S10ObjectiveC22_convertBoolToObjCBoolyAA0eF0VSbF(r14 & 0xff))];
                    [rbx release];
                    rbx = swift_unknownWeakLoadStrong(r15);
                    if (rbx != 0x0) {
                            [rbx makeKeyAndOrderFront:0x0];
                            [rbx release];
                    }
                    else {
                            asm { ud2 };
                            sub_100023199();
                    }
            }
            else {
                    asm { ud2 };
                    loc_100023195();
            }
    }
    else {
            asm { ud2 };
            loc_100023191();
    }
    return;
}
複製代碼

經過上面的僞代碼,咱們能夠初步判斷這個 0x1000230e0 內部就是彈出試用期到期彈窗的方法。接着咱們經過快捷鍵 X 查看關於 0x1000230e0 的引用,能夠發現有三處調用,一個一個看下去發現第一個 sub_100022840 中的調用最像是延時調用,由於其中有 Hopper 反編譯出來的 Dispatch 相關的僞代碼。

$Ss10SetAlgebraPyxqd__cs8SequenceRd__7ElementQyd__ADRtzlufCTj(&var_A0, r13);
    swift_release(*__swiftEmptyArrayStorage);
    (extension in Dispatch):__ObjC.OS_dispatch_queueasyncAfterdeadlineqosflags.execute(Dispatch.DispatchTime, Dispatch.DispatchQoS, Dispatch.DispatchWorkItemFlags, @convention(block) () -> ()) -> ()(var_40, var_68, var_B0, var_30);
    (*(var_D0 + 0x8))(var_B0, var_C8);
    (*(var_C0 + 0x8))(var_68, var_B8);
    _Block_release(var_30);
    swift_release(var_D8);
    (var_38)(var_40, var_70, rdx);
    [var_A8 release];
    sub_1000230e0(0x0);
    rbx = var_48;
    goto loc_100022df5;
複製代碼

切到彙編模式,找到對應的彙編代碼。

因爲 sub_1000230e0(0x0); 是在 Dispatch 中調用的,考慮到修改後程序的穩定性,這裏經過 Hopper 的 Modify 菜單中提供的 NOP Region 填平 call sub_1000230e0 彙編代碼。

老規矩,導出二進制文件覆蓋 Bartender 中的二進制後重啓 Bartender 驗收成果。

清爽~ 此次運行 Bartender 發現不但能夠正常使用功能,以前煩人的試用期到期彈窗也被咱們成功幹掉了。

總結

  • 文章簡單介紹了本次要破解的目標 Mac 應用 Bartender,若是各位同窗尚未找到合適的頂部菜單欄圖標管理工具不妨試着使用 Bartender。
  • 文章介紹了 maxOS 與 iOS 逆向工程中主流的靜態分析工具 Hopper,從文章後面破解 Bartender 的實戰過程當中就能夠看出 Hopper 對於咱們逆向過程的幫助有多麼大。
  • 文章最後詳細講述了我在破解 Bartender 過程當中的經歷,從初始常規思路到不起做用思路被截斷再到經過動態分析重拾思路...一直到最後的完美破解中間經歷了許多關鍵節點,但願對你們有所幫助。

每一次逆向的過程都是未知的,有的時候可能會很順利(直接 mov eax, 0x1 + ret 就搞定),有的時候可能會很曲折,有的時候可能還會以失敗收尾。我寫這篇文章主要是想與你們交流在逆向過程當中的常規方法以及遇到困難時的一些解決思路,其實不管是 Bartender 仍是其餘應用,不管是 Mac 應用仍是 iOS 應用,逆向的思路都是相通的,願各位同窗往後能夠觸類旁通。

若是有任何問題歡迎在文章下方留言或在個人微博 @Lision 聯繫我,真心但願個人文章能夠爲你帶來價值~


補充~ 我建了一個技術交流微信羣,想在裏面認識更多的朋友!若是各位同窗對文章有什麼疑問或者工做之中遇到一些小問題均可以在羣裏找到我或者其餘羣友交流討論,期待你的加入喲~

Emmmmm..因爲微信羣人數過百致使不能夠掃碼入羣,因此請掃描上面的二維碼關注公衆號進羣。

相關文章
相關標籤/搜索