このプロジェクトの目標は、私のリスニング習慣を学習し、通常はスキップするいくつかの曲をスキップできる Spotify クライアントを構築することです。正直、この欲求は私の怠惰から来ていることを認めざるを得ません。何かをしたい気分のときにプレイリストを作成したり検索したりする必要はありません。私が望んでいるのは、ライブラリ内の曲を選択して、他の曲をシャッフルし、キューから「流れない」曲を削除できるようにすることです。
これを達成するには、このタスクを実行できる何らかのモデルを学習する必要があります (これについては、将来の投稿で詳しく説明する可能性があります)。ただし、モデルをトレーニングできるようにするには、まずモデルをトレーニングするためのデータが必要です。
スキップした曲も含め、完全な視聴履歴が必要です。履歴の取得は簡単です。 Spotify API では最後に再生された 50 曲しか取得できませんが、エンドポイントを繰り返しポーリングする cron ジョブを設定できます。完全なコードはここに掲載されています: https://gist.github.com/SamL98/c1200a30cdb19103138308f72de8d198
最も難しい部分はスキップを追跡することです。 Spotify Web API は、このためのエンドポイントを提供しません。以前、Spotify AppleScript API を使用して再生を制御するサービスをいくつか作成しました (この記事の残りの部分では MacOS Spotify クライアントについて説明します)。これらのサービスを使用して、スキップされたコンテンツを追跡することもできますが、それは課題を回避しているように感じます。どうすれば完了できますか?
私は最近、ターゲット バイナリから生成された関数呼び出しを「インターセプト」できるフックというテクニックについて学びました。これがスキップを追跡する最良の方法だと思います。
最も一般的なフック タイプはインターポーズ フックです。このタイプのフックは PLT 内の再配置をオーバーライドしますが、これは実際には何を意味するのでしょうか?
PLT またはプロシージャ リンケージ テーブルを使用すると、メモリ内の関数がどこにあるかを知らなくても、コードで外部関数 (libc を考えてください) を参照できます。PLT 内のエントリを参照するだけです。リンカーは、実行時に PLT 内の各関数またはシンボルに対して「再配置」を実行します。このアプローチの利点の 1 つは、外部関数が別のアドレスにロードされた場合、コード内の関数への各参照ではなく、PLT 内の再配置のみを変更する必要があることです。
したがって、printf の interpose フックを作成するとき、フックしているプロセスが printf を呼び出すたびに、libc の代わりに printf の実装を呼び出します (通常、カスタム ライブラリは標準実装も呼び出します)。
フックに関する基本的な背景知識を理解した後、Spotify にフックを挿入してみる準備が整いました。しかし、最初に何をフックしたいのかを理解する必要があります。
前述したように、外部関数の interpose フックのみ作成できるため、libc または Objective-C ランタイムで関数を探します。
どこにフックするかを調べたとき、Spotify ハンドルの「メディア コントロール キー」または MacBook の F7 ~ F9 からフックを開始するのが良いと考えました。 Spotify アプリで「次へ」ボタンが呼び出されたときに、これらのキーのハンドラーが関数を呼び出すとします。ついに SPMediaKeyTap ライブラリを https://github.com/nevyn/spmediakeytap で見つけました。 Spotify がこのライブラリからコードをコピーして貼り付けるかどうかを試してみようと思いました。 SPMediaKeyTap ライブラリには、startWatchingMediaKeys メソッドがあります。 Spotify バイナリに対して strings コマンドを実行して、このメソッドがあるかどうかを確認しました。そして、案の定、
Bingo!! となりました。 Spotify バイナリを IDA (もちろん無料バージョン) にロードしてこの文字列を検索すると、対応するメソッドが見つかります:
この関数に対応する Lookingソース コードでは、CGEventTapCreate 関数の興味深いパラメータ TapEventCallback が見つかります。
逆アセンブリを振り返ると、sub_10010C230 サブルーチンが次のように渡されていることがわかります。 TapEventCallback パラメータ。この関数のソース コードまたは逆アセンブリを見ると、ライブラリ関数 CGEventTapEnable が 1 つだけ呼び出されていることがわかります。
この関数をフックしてみましょう。
最初に行う必要があるのは、カスタム 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>
dlsym 関数を呼び出して、実際のライブラリ CGEventTapEnable 関数のアドレスを取得します。次に、誤って何かを壊さないように古い実装を呼び出します。次のようにライブラリをコンパイルしましょう (https://ntvalk.blogspot.com/2013/11/hooking-explained-detouring-library.html):
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:
確かに、これを行うには時間がかかりましたが、一番下の 4 行目で r12 を呼び出していることに注意してください。他のケースをいくつか見てみると、レジスタを呼び出す非常によく似たパターンが見つかるでしょう。これは素晴らしい機能のように思えますが、それがどこにあるのかをどうやって知ることができるのでしょうか?
新しいツール、デバッガーを開いてみましょう。最初は Spotify をデバッグしようとして非常に苦労しました。私がデバッガにあまり詳しくないからかもしれませんが、かなり賢い解決策を思いついたと思います。
最初に sub_10006DE40 にフックを設定し、次にコード内でブレークポイントをトリガーします。これは、アセンブリ命令 int 3 を実行することで実行できます (GDB や LLDB などのデバッグなど)。
HookCase フレームワークにおけるフックは次のようになります:
これを HookCase テンプレート ライブラリに追加した後、user_hooks 配列にも追加する必要があります。 :
# 次に、Makefile HookCase が提供するテンプレートを使用してコンパイルします。次に、コマンド HC_INSERT_LIBRARY= /Applications/Spotify.app/Contents/MacOS/Spotify を使用して、ライブラリを Spotify に挿入できます。
次に、LLDB を実行し、次のように実行中の Spotify プロセスにアタッチします。
F9 キーを押してみてください (Spotify がアクティブなウィンドウでない場合は、 iTunes が開く場合があります)。フック内の行 int $3 はデバッガーをトリガーする必要があります。
これで、sub_10006DE40 エントリ ポイントに入ることができます。 PC は IDA に示されているアドレスに対応する場所にあることに注意してください (これはプロセスがメモリにロードされる場所によるものだと思います)。現在のプロセスでは、プッシュ r15 命令は 0x10718ee44 にあります。
IDA では、この命令のアドレスは 0x10006DE44 で、オフセットは 0x7121000 になります。 IDA では、r12 命令が呼び出されるアドレスは 0x10006E234 です。次に、そのアドレスにオフセットを追加し、それに応じてブレークポイントを設定します (b -a 0x10718f234)。続行します。
ターゲット命令にヒットすると、レジスタ r12 の内容を出力できます。
私たちがしなければならないことは、このアドレスからオフセットを減算することだけです。見てください、公称アドレスは 0x100CC2E20 です。
次に、この関数をフックしましょう:
これを user_hooks 配列に追加し、コンパイル、実行し、観察します。 Spotify アプリで F9 キーを押すか、「次へ」ボタンをクリックするたびに、メッセージが記録されます。
スキップ関数をフックしたので、
残りのコードを投稿しますが、残りのリバース エンジニアリングは完了しません。この記事のせいで この記事はすでに十分に長いです。
つまり、前の関数もフックしました (これに従うと良い練習になります)。次に、両方のサビで、まず現在の曲がすでに半分まで進んでいるかどうかを確認します。もしそうなら、私は何もしていないので、その曲が不適切だと思うのではなく、単にその曲に飽きているだけだと思います。次に、背面 (F7) で最後のスキップをポップします。
現在の曲が途中かどうかを確認する方法について少しお話したいと思います。私の当初のアプローチは、実際に Popen を呼び出して、対応する AppleScript コマンドを実行することでしたが、それは正しくないと感じました。
Spotify バイナリで class-dump を実行したところ、SPAppleScriptObjectModel と SPAppleScriptTrack という 2 つのクラスが見つかりました。これらのメソッドは、再生位置、継続時間、トラック ID に必要なプロパティを公開します。次に、これらのプロパティのゲッターをフックし、next フックと back フックを使用して呼び出しました (Swizzle の方が合理的だと思いますが、機能させることができません)。
ファイルを使用してスキップを追跡します。最初の行にはスキップの数が含まれます。スキップでは、このカウンターをインクリメントし、カウンターで指定された行のファイルに追跡 ID とタイムスタンプを書き込みます。 「戻る」ボタンでは、このカウンターをデクリメントするだけです。このようにして、戻るボタンを押したときに、バックトラックされたファイルに新しいスキップを書き込むようにファイルを設定するだけです。
以上がSpotify.app をリバース エンジニアリングし、その機能をフックしてデータを取得する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。