該專案的目標是建立一個Spotify客戶端,讓它能夠學習我的聽曲習慣並跳過一些我通常會跳過的歌曲。不得不承認,這種需求來自於我的懶惰。我不想在當我有心情想要聽某些音樂時,創建或尋找播放清單。我希望的是在我的庫中選擇一首歌,然後可以隨機播放其他歌曲,並從隊列中刪除不“flow(節奏與旋律的流暢度)”的歌曲。
為了實現這一點,我需要學習某種能夠執行此任務的模型(在未來的帖子中可能更多)。但是為了能夠訓練一個模型,我首先需要資料來訓練它。
我需要完整的聽歌歷史記錄,包括我跳過的那些歌曲。取得歷史記錄很簡單。雖然Spotify API僅允許取得最近50首播放的歌曲,但我們可以設定一個cron job來重複輪詢該端點。完整程式碼已經發佈在此處:https://gist.github.com/SamL98/c1200a30cdb19103138308f72de8d198
最困難的部分是追蹤跳過。 Spotify Web API並沒有為此提供任何的端點。之前我使用Spotify AppleScript API創建了一些控製播放的服務(本文的其餘部分將涉及到MacOS Spotify客戶端)。我可以使用這些服務來追蹤跳過的內容,但這感覺像是在迴避挑戰。我怎麼能完成它?
我最近學習了解了有關hooking的技術,你可以在其中「攔截」從目標二進位產生的函數呼叫。我認為這將是追蹤跳過的最佳方法。
最常見的鉤子類型是interpose hook。這種類型的鉤子會覆蓋PLT中的重定位,但這究竟意味著什麼呢?
PLT或過程連結表允許你的程式碼引用外部函數(想想libc)而不知道該函數在記憶體中的位置,你只需引用PLT中的一個條目。連結器在運行時為PLT中的每個函數或符號執行「重定位」。這種方法的一個好處是,如果外部函數在不同的位址加載,則只需要更改PLT中的重定位,而不是每次對程式碼中該函數的參考。
因此,當我們為printf建立一個interpose hook時,每當我們hooking的程序呼叫printf時,我們將呼叫printf的實作而不是libc(我們的自訂函式庫通常也會呼叫標準實作) 。
在對鉤子有了一些基本的知識背景後,下面我們準備嘗試在Spotify中插入一個鉤子。但首先我們需要弄清楚我們想要hook的是什麼。
如前所述,只能為外部函數建立一個interpose hook,因此我們將在libc或Objective-C runtime中尋找函數。
在研究在哪裡hook時,我認為一個開始hooking的好地方是Spotify處理「media control keys」或我MacBook上的F7-F9。假設這些鍵的處理程序在spotify應用程式中按一下Next按鈕被呼叫時會呼叫函數。我最終在:https://github.com/nevyn/spmediakeytap上找到了SPMediaKeyTap庫。我想我可以試一試,看看Spotify是否複製並貼上了這個庫中的程式碼。在SPMediaKeyTap庫中,有一個方法startWatchingMediaKeys。我在Spotify二進位檔案上執行了strings指令,看看他們是否有這個方法,果然:
Bingo!!如果我們將Spotify二進位檔案載入到IDA(當然是免費版本)並蒐索此字串,我們就會找到相應的方法:
##如果我們查看這個函數對應的源碼,我們會發現CGEventTapCreate函數的有趣參數tapEventCallback: 如果我們回顧一下反彙編,我們可以看到sub_10010C230子程式作為tapEventCallback參數傳遞。如果我們查看這個函數的原始碼或反彙編,我們看到只呼叫了一個函式庫函數CGEventTapEnable: 讓我們嘗試hook這個函數。 我們需要做的第一件事是建立一個函式庫來定義我們的自訂CGEventTapEnable。程式碼如下:#include <corefoundation> #include <dlfcn.h> #include <stdlib.h> #include <stdio.h> void CGEventTapEnable(CFMachPortRef tap, bool enable) { typeof(CGEventTapEnable) *old_tap_enable; printf(“I'm hooked!\n”); old_tap_enable = dlsym(RTLD_NEXT, “CGEventTapEnable”); (*old_tap_enable)(tap, enable); }</stdio.h></stdlib.h></dlfcn.h></corefoundation>
gcc -fno-common -c <filename>.c gcc -dynamiclib -o <library> <filename>.o</filename></library></filename>
现在,让我们尝试在插入钩子时运行Spotify:DYLD_FORCE_FLAT_NAMESPACE=1 DYLD_INSERT_LIBRARIES=
Spotify打开正常,但Apple的系统完整性保护(SIP)没有让我们加载未签名库:(。
幸运的是,我是Apple的reasonably priced developer项目的成员,所以我可以对库进行代码签名。这个问题算是得到了解决。让我们用100美元证书签名我们的库,运行上一个命令,然后......
失败。这一点不奇怪,Apple不允许你插入使用任何旧标识签名的库,只允许使用签名原始二进制文件时使用的库。看起来我们必须要找到另一种方法来hook Spotify了。
作为补充说明,细心的读者可能会注意到我们hook的函数CGEventTapEnable,只有在media key event超时时才会被调用。因此,即使我们可以插入钩子,我们也可能不会看到任何的输出。本节的主要目的是详细说明我最初的失败(和疏忽),并作为一个学习经验。
经过一番挖掘,我发现了一个非常棒的库HookCase:https://github.com/steven-michaud/HookCase。HookCase让我们实现一种比插入钩子( patch hook)更为强大的钩子类型。
通过修改你希望hook的函数触发中断插入Patch hooks。然后,内核可以处理此中断,然后将执行转移到我们的个人代码中。对于那些感兴趣的人,我强烈建议你阅读HookCase文档,因为它更为详细。
Patch hooks不仅允许我们对外部函数的hook调用,而且允许我们hook目标二进制文件内的任何函数(因为它不依赖于PLT)。HookCase为我们提供了一个框架来插入patch和/或interpose hooks,以及内核扩展来处理patch hooks生成的中断,并运行我们的自定义代码。
既然我们已经有办法hook Spotify二进制文件中的任何函数了,那么只剩下最后一个问题......就是位置在哪?
让我们重新访问SPMediaKeyTap源码,看看如何处理媒体控制键。在回调函数中,我们可以看到如果按下F7,F8或F9(NX_KEYTYPE_PREVIOUS,NX_KEYTYPE_PLAY等),我们将执行handleAndReleaseMediaKeyEvent选择器:
然后在所述选择器中通知delegate:
让我们看看repo中的这个delegate方法:
事实证明它只是为处理keys设置了一个模板。让我们在IDA中搜索receiveMediaKeyEvent函数,并查看相应函数的图形视图:
看起来非常相似,不是吗?我们可以看到,对每种类型的键都调用了一个公共函数sub_10006FE10,只设置了一个整数参数来区分它们。让我们hook它,看看我们是否可以记录按下的键。
我们可以从反汇编中看到,sub_10006FE10获得了两个参数:1)指向SPTClientAppDelegate单例的playerDelegate属性的指针,以及2)指定发生了什么类型事件的整数(0表示暂停/播放,3表示下一个,4表示上一个)。
看看sub_10006FE10(我不会在这里包含它,但我强烈建议你自己检查一下),我们可以看到它实际上是sub_10006DE40的包装器,其中包含了大部分内容:
哇!这看起来很复杂。让我们试着把它分解一下。
从这个图的结构来看,有一个指向顶部的节点有许多outgoing edges:
正如IDA所建议的那样,这是esi(前面描述的第二个整数参数)上的switch语句。看起来Spotify的处理的不仅仅是Previous,Pause/Play和Next。让我们把关注点集中到处理Next或3 block:
不可否認,為此我花了一些時間,但我想請你注意底部第四行的call r12。如果你查看其他的一些情況,你會發現一個非常相似的呼叫暫存器的模式。這似乎是一個很好的函數,但我們如何知道它在哪裡?
讓我們開啟一個新工具:debugger(偵錯器)。我最初嘗試調試Spotify時遇到了很多麻煩。現在可能是因為我對調試器不太熟悉的原因,但我認為我想出了一個相當聰明的解決方案。
我們先在sub_10006DE40上設定一個hook,然後我們在程式碼中觸發一個斷點。我們可以透過執行彙編指令int 3來做到這一點(例如像GDB和LLDB之類的偵錯)。
以下是在HookCase框架中hook的樣子:
將此加入HookCase範本庫後,你也必須將其加入user_hooks陣列:
然後我們可以使用Makefile HookCase提供的模板來編譯它。然後可以使用以下指令將庫插入Spotify:HC_INSERT_LIBRARY=
然後我們可以執行LLDB並將其attach到正在運行的Spotify進程,如下所示:
嘗試按F9(如果Spotify不是活動窗口,它可能會打開iTunes)。鉤子中的int $3行應該觸發了調試器。
現在我們可以進入到sub_10006DE40入口點這一步。請注意,PC將位於與IDA中顯示的位址相對應的位置(我認為這是由於進程載入到記憶體的位置所導致的)。在我目前的進程中,push r15指令位於0x10718ee44:
在IDA中,該指令的位址為0x10006DE44,它給了我們一個偏移量0x7121000。在IDA中,呼叫r12指令的位址為0x10006E234。然後我們可以將偏移量加到該位址,並相應地設定一個斷點,b -a 0x10718f234,然後繼續。
當我們點擊目標指令時,我們可以列印出暫存器r12的內容:
我們要做的就是從這個位址減去偏移量,看,我們取得到了我們名義上的地址:0x100CC2E20。
現在,讓我們來hook這個函數:
將其加入到user_hooks數組,編譯,運行,並觀察:每次按F9或點選Spotify應用程式中的next按鈕,都會記錄我們的訊息。
現在我們已經hook了skip功能,
我將發布剩餘的程式碼,但我不會完成其餘部分的逆向工作,因為這篇文章已經夠長的了。
簡而言之,我也hook了previous功能(如果你照著做的話,這會是一個很好的練習)。然後,在這兩個鉤子中,我首先檢查當前的歌曲是否已經過了一半。如果是的話,我什麼都不做,假設我只是對這首歌感到厭倦,而不是覺得它不合適。然後在backs (F7),我彈出last skip。
針對如何檢查當前歌曲是否已經過了一半的方法我想說幾句。我最初的方法是實際呼叫popen,然後運行相應的AppleScript命令,但感覺這不太對。
我在Spotify二進位檔案上運行了class-dump,發現了兩個類別:SPAppleScriptObjectModel和SPAppleScriptTrack。這些方法公開了播放位置,持續時間和曲目ID所需的必要屬性。然後,我為這些屬性hook了getter,並使用next和back hooks調用它們(我認為Swizzle更合理,但我無法讓它正常工作)。
我使用一個檔案來追蹤skips,其中第一行包含跳過次數,在跳過時我們增加這個計數器,並將追蹤ID和時間戳寫入計數器指定行上的檔案。在back按鈕,我們只是減少這個計數器。這樣,當我們按下back按鈕時,我們只是將檔案設定為對已回溯檔案寫入new skips。
以上是如何逆向分析Spotify.app並hook其功能取得數據的詳細內容。更多資訊請關注PHP中文網其他相關文章!