Le but de ce projet est de créer un client Spotify capable de connaître mes habitudes d'écoute et de sauter certaines chansons que je sauterais normalement. Je dois admettre que ce besoin vient de ma paresse. Je ne veux pas avoir à créer ou à rechercher des listes de lecture lorsque j'ai envie de quelque chose. Ce que je veux, c'est sélectionner une chanson dans ma bibliothèque et pouvoir mélanger d'autres chansons et supprimer les chansons qui ne « coulent » pas de la file d'attente.
Pour y parvenir, je dois apprendre une sorte de modèle capable d'effectuer cette tâche (peut-être plus à ce sujet dans un prochain article). Mais pour pouvoir entraîner un modèle, j'ai d'abord besoin de données pour l'entraîner.
J'ai besoin de l'historique d'écoute complet, y compris les chansons que j'ai sautées. Obtenir l’historique est facile. Alors que l'API Spotify permet uniquement d'obtenir les 50 dernières chansons jouées, nous pouvons configurer une tâche cron pour interroger à plusieurs reprises le point de terminaison. Le code complet a été publié ici : https://gist.github.com/SamL98/c1200a30cdb19103138308f72de8d198
Le plus difficile est de suivre les bennes. L'API Web Spotify ne fournit aucun point de terminaison pour cela. Auparavant, j'ai créé certains services pour contrôler la lecture à l'aide de l'API Spotify AppleScript (le reste de cet article couvrira le client MacOS Spotify). Je pourrais utiliser ces services pour suivre le contenu ignoré, mais cela revient à contourner le défi. Comment puis-je le compléter ? J'ai récemment découvert le hooking, une technique dans laquelle vous "interceptez" les appels de fonction générés à partir d'un binaire cible. Je pense que ce serait la meilleure façon de suivre les sauts.
Une table de liaison PLT ou de procédure permet à votre code de référencer une fonction externe (pensez à la libc) sans savoir où se trouve cette fonction en mémoire, vous faites simplement référence à une entrée dans le PLT. L'éditeur de liens effectue une "relocalisation" au moment de l'exécution pour chaque fonction ou symbole du PLT. L'un des avantages de cette approche est que si la fonction externe est chargée à une adresse différente, seule la relocalisation dans le PLT doit être modifiée, plutôt que chaque référence à la fonction dans le code.
Ainsi, lorsque nous créons un hook d'interposition pour printf, chaque fois que le processus que nous accrochons appelle printf, nous appellerons l'implémentation de printf au lieu de libc (notre bibliothèque personnalisée appellera généralement également le standard accompli).
Après avoir acquis quelques connaissances de base sur les hooks, nous sommes prêts à essayer d'insérer un hook dans Spotify. Mais nous devons d’abord déterminer ce que nous voulons accrocher.
Où trouver le hook
Comme mentionné précédemment, vous ne pouvez créer un hook interposé que pour une fonction externe, nous chercherons donc la fonction dans la libc ou en Objective-C exécution.
La première chose que nous devons faire est de créer une bibliothèque pour définir notre CGEventTapEnable personnalisé. Le code est le suivant :
#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 appel de fonction pour obtenir l'adresse de la fonction CGEventTapEnable de la bibliothèque réelle. Ensuite, nous appelons l’ancienne implémentation afin de ne rien casser accidentellement. Compilons notre bibliothèque comme ceci (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:
Certes, cela m'a pris du temps pour faire cela, mais je tiens à attirer votre attention pour appeler r12 sur la quatrième ligne en bas. Si vous examinez d'autres cas, vous découvrirez un modèle très similaire de registres d'appel. Cela semble être une fonction intéressante, mais comment savoir où elle se trouve ?
Ouvrons un nouvel outil : le débogueur. J'ai eu beaucoup de mal au début à essayer de déboguer Spotify. Maintenant, c'est peut-être parce que je ne suis pas très familier avec les débogueurs, mais je pense avoir trouvé une solution assez intelligente.
Nous définissons d'abord un hook sur sub_10006DE40, puis nous déclenchons un point d'arrêt dans le code. Nous pouvons le faire en exécutant l'instruction d'assemblage int 3 (par exemple le débogage comme GDB et LLDB).
Voici à quoi ressemble un hook dans le framework HookCase :
Après l'avoir ajouté à la bibliothèque de modèles HookCase, vous devez également l'ajouter au tableau user_hooks :
Ensuite, nous pouvons utiliser les modèles fournis par Makefile HookCase pour le compiler. La bibliothèque peut ensuite être insérée dans Spotify à l'aide de la commande suivante : HC_INSERT_LIBRARY=
Ensuite, nous pouvons exécuter LLDB et l'attacher au processus Spotify en cours d'exécution comme ceci :
Essayez d'appuyer sur F9 (si Spotify n'est pas la fenêtre active, elle peut ouvrir iTunes). La ligne int $3 dans le hook devrait déclencher le débogueur.
Nous pouvons maintenant entrer dans le point d'entrée sub_10006DE40. Notez que le PC sera à l'emplacement correspondant à l'adresse indiquée dans IDA (je pense que cela est dû à l'endroit où le processus se charge en mémoire). Dans mon processus actuel, l'instruction push r15 est à 0x10718ee44 :
En IDA, l'adresse de cette instruction est 0x10006DE44, ce qui nous donne un offset de 0x7121000. Dans IDA, l'adresse à laquelle l'instruction r12 est appelée est 0x10006E234. Nous pouvons ensuite ajouter le décalage à cette adresse et définir un point d'arrêt en conséquence, b -a 0x10718f234, et continuer.
Quand on clique sur l'instruction cible, on peut imprimer le contenu du registre r12 :
Il suffit de soustraire le décalage de cette adresse, et voilà, on obtient notre adresse nominale : 0x100CC2E20 .
Maintenant, accrochons cette fonction :
Ajoutez-la au tableau user_hooks, compilez, exécutez et observez : chaque fois que vous appuyez sur F9 ou cliquez sur le bouton suivant dans l'application Spotify, enregistrez notre message .
Maintenant que nous avons accroché la fonction skip,
Je posterai le reste du code, mais je ne terminerai pas l'ingénierie inverse du reste car ce post est déjà assez long.
En bref, j'ai aussi accroché la fonction précédente (ce sera un bon exercice si vous suivez ceci). Ensuite, dans les deux hooks, je vérifie d’abord si la chanson en cours est déjà à mi-chemin. Si c'est le cas, je ne fais rien, supposant que je m'ennuie simplement de la chanson plutôt que de la trouver inappropriée. Puis sur les dos (F7), je saute le dernier saut.
Je voudrais dire quelques mots sur la façon de vérifier si la chanson en cours est à mi-chemin. Mon approche initiale consistait à appeler popen puis à exécuter la commande AppleScript correspondante, mais cela ne me semble pas correct.
J'ai exécuté un dump de classe sur le binaire Spotify et j'ai trouvé deux classes : SPAppleScriptObjectModel et SPAppleScriptTrack. Ces méthodes exposent les propriétés nécessaires requises pour la position de lecture, la durée et l'ID de la piste. J'ai ensuite accroché des getters pour ces propriétés et les ai appelés en utilisant les crochets next et back (je pense que Swizzle a plus de sens, mais je n'arrive pas à le faire fonctionner).
J'utilise un fichier pour suivre les sauts, où la première ligne contient le nombre de sauts, en cas de saut, nous incrémentons ce compteur et écrivons l'ID de suivi et l'horodatage dans le fichier sur la ligne spécifiée par le compteur. Sur le bouton retour, on décrémente simplement ce compteur. De cette façon, lorsque nous appuyons sur le bouton Précédent, nous configurons simplement le fichier pour qu'il écrive de nouveaux sauts dans le fichier ayant fait l'objet d'un retour en arrière.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!