>  기사  >  운영 및 유지보수  >  Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

王林
王林앞으로
2023-05-13 08:37:131014검색

Project

이 프로젝트의 목표는 나의 청취 습관을 학습하고 평소에 건너뛰는 몇 곡을 건너뛸 수 있는 Spotify 클라이언트를 구축하는 것입니다. 나는 이러한 필요성이 나의 게으름에서 나온다는 것을 인정해야 합니다. 기분이 좋을 때 재생 목록을 만들거나 찾아야 하는 것을 원하지 않습니다. 내가 원하는 것은 내 라이브러리에서 노래를 선택하고 다른 노래를 섞고 대기열에서 "흐름"이 아닌 노래를 제거할 수 있는 것입니다.

이를 달성하려면 이 작업을 수행할 수 있는 일종의 모델을 배워야 합니다(향후 게시물에서 더 자세히 설명할 수도 있음). 하지만 모델을 훈련하려면 먼저 모델을 훈련할 데이터가 필요합니다.

Data

건너뛴 노래를 포함한 전체 청취 기록이 필요합니다. 기록을 얻는 것은 쉽습니다. Spotify API는 마지막으로 재생된 50곡만 가져오도록 허용하지만, 엔드포인트를 반복적으로 폴링하도록 cron 작업을 설정할 수 있습니다. 전체 코드는 여기에 게시되었습니다: https://gist.github.com/SamL98/c1200a30cdb19103138308f72de8d198

가장 어려운 부분은 건너뛰기를 추적하는 것입니다. Spotify Web API는 이에 대한 엔드포인트를 제공하지 않습니다. 이전에는 Spotify AppleScript API를 사용하여 재생을 제어하는 ​​몇 가지 서비스를 만들었습니다(이 기사의 나머지 부분에서는 MacOS Spotify 클라이언트를 다룹니다). 건너뛴 콘텐츠를 추적하기 위해 이러한 서비스를 사용할 수 있지만 이는 과제를 회피하는 것처럼 느껴집니다. 어떻게 완료할 수 있나요?

Hooking

저는 최근 대상 바이너리에서 생성된 함수 호출을 "가로채기"할 수 있는 기술인 후킹에 대해 배웠습니다. 나는 이것이 건너뛰기를 추적하는 가장 좋은 방법이라고 생각합니다.

가장 일반적인 후크 유형은 삽입 후크입니다. 이러한 유형의 후크는 PLT의 재배치를 무시하지만 이것이 실제로 무엇을 의미합니까?

PLT 또는 프로시저 연결 테이블을 사용하면 해당 함수가 메모리의 어디에 있는지 알지 못한 채 코드에서 외부 함수(libc 등)를 참조할 수 있습니다. PLT의 항목만 참조하면 됩니다. 링커는 PLT의 각 함수 또는 기호에 대해 런타임에 "재배치"를 수행합니다. 이 접근 방식의 한 가지 이점은 외부 함수가 다른 주소에 로드되는 경우 코드의 함수에 대한 각 참조가 아니라 PLT의 재배치만 변경하면 된다는 것입니다.

따라서 printf에 대한 중간 후크를 만들 때 우리가 연결하는 프로세스가 printf를 호출할 때마다 libc 대신 printf 구현을 호출합니다(사용자 정의 라이브러리는 일반적으로 표준 구현도 호출합니다).

훅에 대한 기본 배경 지식을 갖춘 후 Spotify에 후크를 삽입해 볼 준비가 되었습니다. 하지만 먼저 우리는 무엇을 연결하고 싶은지 알아내야 합니다.

훅을 찾을 수 있는 곳

앞서 언급했듯이 외부 함수에 대한 중간 후크만 생성할 수 있으므로 libc 또는 Objective-C 런타임에서 함수를 찾습니다.

후킹할 위치를 조사하는 동안 후킹을 시작하기 좋은 곳은 Spotify 핸들 "미디어 제어 키" 또는 내 MacBook의 F7-F9일 것이라고 생각했습니다. Spotify 앱에서 다음 버튼이 호출될 때 이러한 키에 대한 핸들러가 기능을 호출한다고 가정합니다. 마침내 https://github.com/nevyn/spmediakeytap에서 SPMediaKeyTap 라이브러리를 찾았습니다. 나는 Spotify가 이 라이브러리의 코드를 복사하여 붙여넣었는지 한번 시도해 보고 싶다고 생각했습니다. SPMediaKeyTap 라이브러리에는 startWatchingMediaKeys 메서드가 있습니다. 나는 Spotify 바이너리에서 이 방법이 있는지 확인하기 위해 strings 명령을 실행했습니다.

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

Bingo!! Spotify 바이너리를 IDA(물론 무료 버전)에 로드하고 이 문자열을 검색하면 해당 메서드를 찾을 수 있습니다.

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

이 함수에 해당하는 소스 코드를 보면 CGEventTapCreate를 찾을 수 있습니다. 함수 흥미로운 매개변수 tapEventCallback:

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

디스어셈블리를 다시 보면 sub_10010C230 서브루틴이 tapEventCallback 매개변수로 전달되는 것을 볼 수 있습니다. 이 함수의 소스 코드나 디스어셈블리를 보면 CGEventTapEnable 단 하나의 라이브러리 함수만 호출되는 것을 볼 수 있습니다.

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

이 함수를 연결해 보겠습니다.

가장 먼저 해야 할 일은 사용자 정의 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= /Applications/Spotify.app/Contents/MacOS/Spotify。点击进入:

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

Spotify打开正常,但Apple的系统完整性保护(SIP)没有让我们加载未签名库:(。

幸运的是,我是Apple的reasonably priced developer项目的成员,所以我可以对库进行代码签名。这个问题算是得到了解决。让我们用100美元证书签名我们的库,运行上一个命令,然后......

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

失败。这一点不奇怪,Apple不允许你插入使用任何旧标识签名的库,只允许使用签名原始二进制文件时使用的库。看起来我们必须要找到另一种方法来hook Spotify了。

作为补充说明,细心的读者可能会注意到我们hook的函数CGEventTapEnable,只有在media key event超时时才会被调用。因此,即使我们可以插入钩子,我们也可能不会看到任何的输出。本节的主要目的是详细说明我最初的失败(和疏忽),并作为一个学习经验。

HookCase

经过一番挖掘,我发现了一个非常棒的库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生成的中断,并运行我们的自定义代码。

寻找 sub_100CC2E20

既然我们已经有办法hook Spotify二进制文件中的任何函数了,那么只剩下最后一个问题......就是位置在哪?

让我们重新访问SPMediaKeyTap源码,看看如何处理媒体控制键。在回调函数中,我们可以看到如果按下F7,F8或F9(NX_KEYTYPE_PREVIOUS,NX_KEYTYPE_PLAY等),我们将执行handleAndReleaseMediaKeyEvent选择器:

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

然后在所述选择器中通知delegate:

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

让我们看看repo中的这个delegate方法:

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

事实证明它只是为处理keys设置了一个模板。让我们在IDA中搜索receiveMediaKeyEvent函数,并查看相应函数的图形视图:

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

看起来非常相似,不是吗?我们可以看到,对每种类型的键都调用了一个公共函数sub_10006FE10,只设置了一个整数参数来区分它们。让我们hook它,看看我们是否可以记录按下的键。

我们可以从反汇编中看到,sub_10006FE10获得了两个参数:1)指向SPTClientAppDelegate单例的playerDelegate属性的指针,以及2)指定发生了什么类型事件的整数(0表示暂停/播放,3表示下一个,4表示上一个)。

看看sub_10006FE10(我不会在这里包含它,但我强烈建议你自己检查一下),我们可以看到它实际上是sub_10006DE40的包装器,其中包含了大部分内容:

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

哇!这看起来很复杂。让我们试着把它分解一下。

从这个图的结构来看,有一个指向顶部的节点有许多outgoing edges:

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

正如IDA所建议的那样,这是esi(前面描述的第二个整数参数)上的switch语句。看起来Spotify的处理的不仅仅是Previous,Pause/Play和Next。让我们把关注点集中到处理Next或3 block:

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

물론 이 작업을 수행하는 데 시간이 좀 걸렸지만, 하단 네 번째 줄에서 r12를 호출하도록 주의를 환기시키고 싶습니다. 다른 경우를 살펴보면 레지스터 호출과 매우 유사한 패턴을 찾을 수 있습니다. 이것은 좋은 기능처럼 보이지만 그것이 어디에 있는지 어떻게 알 수 있습니까?

새 도구인 디버거를 열어 보겠습니다. 처음에는 Spotify를 디버깅하려고 할 때 많은 어려움을 겪었습니다. 아마도 제가 디버거에 익숙하지 않아서일지도 모르지만 꽤 영리한 해결책을 생각해낸 것 같습니다.

먼저 sub_10006DE40에 후크를 설정한 다음 코드에서 중단점을 트리거합니다. 이는 어셈블리 명령 int 3을 실행하여 수행할 수 있습니다(예: GDB 및 LLDB와 같은 디버깅).

HookCase 프레임워크에서 후크의 모양은 다음과 같습니다.

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

이를 HookCase 템플릿 라이브러리에 추가한 후 user_hooks 배열에도 추가해야 합니다.

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

그런 다음 제공된 템플릿을 사용할 수 있습니다. Makefile HookCase를 사용하여 컴파일합니다. 그런 다음 다음 명령을 사용하여 라이브러리를 Spotify에 삽입할 수 있습니다: HC_INSERT_LIBRARY= /Applications/Spotify.app/Contents/MacOS/Spotify.

그런 다음 LLDB를 실행하고 다음과 같이 실행 중인 Spotify 프로세스에 연결할 수 있습니다.

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

F9를 눌러보세요(Spotify가 활성 창이 아닌 경우 iTunes가 열릴 수 있습니다). 후크의 int $3 줄은 디버거를 트리거해야 합니다.

이제 sub_10006DE40 진입점에 들어갈 수 있습니다. PC는 IDA에 표시된 주소에 해당하는 위치에 있을 것입니다(프로세스가 메모리에 로드되는 위치 때문인 것 같습니다). 현재 프로세스에서 push r15 명령어는 0x10718ee44에 있습니다.

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

IDA에서 이 명령어의 주소는 0x10006DE44이며, 이는 오프셋 0x7121000을 제공합니다. IDA에서 r12 명령어가 호출되는 주소는 0x10006E234이다. 그런 다음 해당 주소에 오프셋을 추가하고 이에 따라 중단점을 설정하고(b -a 0x10718f234) 계속할 수 있습니다.

목표 명령어에 도달하면 레지스터 r12의 내용을 인쇄할 수 있습니다.

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

우리가 해야 할 일은 이 주소에서 오프셋을 빼는 것뿐입니다. 그러면 명목상 주소는 0x100CC2E20이 됩니다.

Hooking sub_100CC2E20

이제 이 함수를 연결해 보겠습니다.

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

user_hooks 배열에 추가하고 컴파일하고 실행하고 관찰하세요. F9를 누르거나 Spotify 앱에서 다음 버튼을 클릭할 때마다 메시지를 녹음하세요. .

스킵 기능을 훅 켰으니

Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법

나머지 코드는 포스팅하겠지만 이미 포스팅이 너무 길어서 나머지 코드는 리버스엔지니어링을 완료하지 않겠습니다.

간단히 이전 기능도 훅 했습니다(이것을 따라하시면 ​​좋은 연습이 될 것입니다). 그런 다음 두 후크 모두에서 현재 노래가 이미 절반 정도 진행되었는지 먼저 확인합니다. 그렇다면 나는 그 노래가 부적절하다고 생각하기보다는 단지 지루하다고 생각하여 아무것도 하지 않는 것입니다. 그런 다음 뒷면(F7)에서 마지막 건너뛰기를 팝합니다.

현재 노래가 중간쯤 왔는지 확인하는 방법에 대해 몇 마디 말씀드리고 싶습니다. 내 원래 접근 방식은 실제로 popen을 호출한 다음 해당 AppleScript 명령을 실행하는 것이었지만 이는 옳지 않은 것 같습니다.

Spotify 바이너리에서 클래스 덤프를 실행하여 SPAppleScriptObjectModel과 SPAppleScriptTrack이라는 두 가지 클래스를 찾았습니다. 이러한 메서드는 재생 위치, 기간 및 트랙 ID에 필요한 필수 속성을 노출합니다. 그런 다음 이러한 속성에 대해 getter를 연결하고 다음 및 백 후크를 사용하여 호출했습니다(Swizzle이 더 합리적이라고 생각하지만 작동하도록 할 수는 없습니다).

파일을 사용하여 건너뛰기를 추적합니다. 여기서 첫 번째 줄에는 건너뛰기 횟수가 포함됩니다. 건너뛰기에서는 이 카운터를 증가시키고 카운터에서 지정한 줄의 파일에 추적 ID와 타임스탬프를 씁니다. 뒤로 버튼에서는 이 카운터를 감소시킵니다. 이렇게 하면 뒤로 버튼을 누를 때 역추적된 파일에 새 건너뛰기를 쓰도록 파일을 설정하기만 하면 됩니다.

위 내용은 Spotify.app을 리버스 엔지니어링하고 해당 기능을 연결하여 데이터를 얻는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제