commit
0b759e338e
@ -0,0 +1,7 @@ |
|||||||
|
* |
||||||
|
!.gitignore |
||||||
|
!agent.js |
||||||
|
!go.mod |
||||||
|
!go.sum |
||||||
|
!main.go |
||||||
|
!README.md |
@ -0,0 +1,11 @@ |
|||||||
|
# Apple Music ALAC Downloader |
||||||
|
Original script by Sorrow. Modified by me to include some fixes and improvements. |
||||||
|
|
||||||
|
## How to use |
||||||
|
1. Create a virtual device on Android Studio with a image that doesn't have Google APIs. |
||||||
|
2. Install this version of Apple Music: https://www.apkmirror.com/apk/apple/apple-music/apple-music-3-6-0-beta-release/apple-music-3-6-0-beta-4-android-apk-download/. You will also need SAI to install it: https://f-droid.org/pt_BR/packages/com.aefyr.sai.fdroid/. |
||||||
|
3. Launch Apple Music and sign in to your account. Subscription required. |
||||||
|
4. Port forward 10020 TCP: `adb forward tcp:10020 tcp:10020`. |
||||||
|
5. Start frida server. |
||||||
|
6. Start the frida agent: `frida -U -l agent.js -f com.apple.android.music`. |
||||||
|
7. Start downloading some albums: `go run main.go https://music.apple.com/us/album/whenever-you-need-somebody-2022-remaster/1624945511`. |
@ -0,0 +1,112 @@ |
|||||||
|
'use strict'; |
||||||
|
setTimeout(() => { |
||||||
|
const fairplayCert = "MIIEzjCCA7agAwIBAgIIAXAVjHFZDjgwDQYJKoZIhvcNAQEFBQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MTMwMQYDVQQDDCpBcHBsZSBLZXkgU2VydmljZXMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTIwNzI1MTgwMjU4WhcNMTQwNzI2MTgwMjU4WjAwMQswCQYDVQQGEwJVUzESMBAGA1UECgwJQXBwbGUgSW5jMQ0wCwYDVQQDDARGUFMxMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqZ9IbMt0J0dTKQN4cUlfeQRY9bcnbnP95HFv9A16Yayh4xQzRLAQqVSmisZtBK2/nawZcDmcs+XapBojRb+jDM4Dzk6/Ygdqo8LoA+BE1zipVyalGLj8Y86hTC9QHX8i05oWNCDIlmabjjWvFBoEOk+ezOAPg8c0SET38x5u+TwIDAQABo4ICHzCCAhswHQYDVR0OBBYEFPP6sfTWpOQ5Sguf5W3Y0oibbEc3MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUY+RHVMuFcVlGLIOszEQxZGcDLL4wgeIGA1UdIASB2jCB1zCB1AYJKoZIhvdjZAUBMIHGMIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuYXBwbGUuY29tL2tleXNlcnZpY2VzLmNybDAOBgNVHQ8BAf8EBAMCBSAwFAYLKoZIhvdjZAYNAQUBAf8EAgUAMBsGCyqGSIb3Y2QGDQEGAQH/BAkBAAAAAQAAAAEwKQYLKoZIhvdjZAYNAQMBAf8EFwF+bjsY57ASVFmeehD2bdu6HLGBxeC2MEEGCyqGSIb3Y2QGDQEEAQH/BC8BHrKviHJf/Se/ibc7T0/55Bt1GePzaYBVfgF3ZiNuV93z8P3qsawAqAXzzh9o5DANBgkqhkiG9w0BAQUFAAOCAQEAVGyCtuLYcYb/aPijBCtaemxuV0IokXJn3EgmwYHZynaR6HZmeGRUp9p3f8EXu6XPSekKCCQi+a86hXX9RfnGEjRdvtP+jts5MDSKuUIoaqce8cLX2dpUOZXdf3lR0IQM0kXHb5boNGBsmbTLVifqeMsexfZryGw2hE/4WDOJdGQm1gMJZU4jP1b/HSLNIUhHWAaMeWtcJTPRBucR4urAtvvtOWD88mriZNHG+veYw55b+qA36PSqDPMbku9xTY7fsMa6mxIRmwULQgi8nOk1wNhw3ZO0qUKtaCO3gSqWdloecxpxUQSZCSW7tWPkpXXwDZqegUkij9xMFS1pr37RIg=="; |
||||||
|
|
||||||
|
function newStdStringFromBuffer(content) { |
||||||
|
const size = content.byteLength; |
||||||
|
const cap = 2 ** Math.ceil(Math.log2(size + 1)); |
||||||
|
const buffer = Memory.alloc(cap); |
||||||
|
Memory.copy(buffer, content.unwrap(), size); |
||||||
|
|
||||||
|
const addr = Memory.alloc(Process.pointerSize * 3); |
||||||
|
addr.writeULong(cap | 0x1); |
||||||
|
addr.add(Process.pointerSize).writeULong(size); |
||||||
|
addr.add(Process.pointerSize * 2).writePointer(buffer); |
||||||
|
|
||||||
|
return { buffer: buffer, str: addr }; |
||||||
|
} |
||||||
|
|
||||||
|
function newStdString(content) { |
||||||
|
const size = content.length; |
||||||
|
const cap = 2 ** Math.ceil(Math.log2(size + 1)); |
||||||
|
const buffer = Memory.alloc(cap); |
||||||
|
buffer.writeUtf8String(content); |
||||||
|
|
||||||
|
const addr = Memory.alloc(Process.pointerSize * 3); |
||||||
|
addr.writeULong(cap | 0x1); |
||||||
|
addr.add(Process.pointerSize).writeULong(size); |
||||||
|
addr.add(Process.pointerSize * 2).writePointer(buffer); |
||||||
|
|
||||||
|
return { buffer: buffer, str: addr }; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const androidappmusic = Process.getModuleByName("libandroidappmusic.so"); |
||||||
|
|
||||||
|
const sessionCtrlPtr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl8instanceEv"); |
||||||
|
const sessionCtrlInstanceFunc = new NativeFunction(sessionCtrlPtr, "pointer", []); |
||||||
|
const sessionCtrlInstance = sessionCtrlInstanceFunc(); |
||||||
|
|
||||||
|
const getPersistentKeyAddr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl16getPersistentKeyERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_S8_S8_S8_S8_S8_"); |
||||||
|
const getPersistentKey = new NativeFunction(getPersistentKeyAddr, "void", Array(9).fill("pointer")); |
||||||
|
|
||||||
|
const decryptContextAddr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl14decryptContextERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEERKN11SVDecryptor15SVDecryptorTypeERKb"); |
||||||
|
const decryptContext = new NativeFunction(decryptContextAddr, "void", Array(3).fill("pointer")); |
||||||
|
|
||||||
|
const NfcRKVnxuKZy04KWbdFu71Ou = androidappmusic.getExportByName("NfcRKVnxuKZy04KWbdFu71Ou"); |
||||||
|
const decryptSample = new NativeFunction(NfcRKVnxuKZy04KWbdFu71Ou, 'ulong', ['pointer', 'uint', 'pointer', 'pointer', 'size_t']); |
||||||
|
|
||||||
|
const kdContextMap = new Map(); |
||||||
|
|
||||||
|
function getkdContext(adam, uri) { |
||||||
|
const uriStr = String.fromCharCode(...new Uint8Array(uri)) |
||||||
|
if (kdContextMap.has(uriStr)) { |
||||||
|
return kdContextMap.get(uriStr); |
||||||
|
} |
||||||
|
|
||||||
|
const defaultId = newStdStringFromBuffer(adam); |
||||||
|
const keyUri = newStdStringFromBuffer(uri); |
||||||
|
const keyFormat = newStdString("com.apple.streamingkeydelivery"); |
||||||
|
const keyFormatVer = newStdString("1"); |
||||||
|
const serverUri = newStdString("https://play.itunes.apple.com/WebObjects/MZPlay.woa/music/fps"); |
||||||
|
const protocolType = newStdString("simplified"); |
||||||
|
const fpsCert = newStdString(fairplayCert); |
||||||
|
const persistentKey = Memory.alloc(Process.pointerSize * 2); |
||||||
|
getPersistentKey(persistentKey, sessionCtrlInstance, defaultId.str, keyUri.str, keyFormat.str, keyFormatVer.str, serverUri.str, protocolType.str, fpsCert.str); |
||||||
|
|
||||||
|
const ptr = persistentKey.readPointer(); |
||||||
|
if (ptr.isNull()) return null; |
||||||
|
|
||||||
|
const svfootHillPKey = Memory.alloc(Process.pointerSize * 2); |
||||||
|
decryptContext(svfootHillPKey, sessionCtrlInstance, ptr); |
||||||
|
|
||||||
|
const ptr2 = svfootHillPKey.readPointer(); |
||||||
|
if (ptr2.isNull()) return null; |
||||||
|
|
||||||
|
const ap = ptr2.add(0x18).readPointer(); |
||||||
|
if (!ap.isNull()) kdContextMap.set(uriStr, ap); |
||||||
|
return ap; |
||||||
|
} |
||||||
|
|
||||||
|
async function handleConnection(s) { |
||||||
|
// console.log("new connection!");
|
||||||
|
while (true) { |
||||||
|
const adamSize = (await s.input.readAll(1)).unwrap().readU8(); |
||||||
|
if (adamSize === 0) |
||||||
|
break; |
||||||
|
const adam = await s.input.readAll(adamSize); |
||||||
|
const uriSize = (await s.input.readAll(1)).unwrap().readU8(); |
||||||
|
const uri = await s.input.readAll(uriSize); |
||||||
|
const kdContext = getkdContext(adam, uri); |
||||||
|
// console.log(adam, uri, kdContext)
|
||||||
|
while (true) { |
||||||
|
const size = (await s.input.readAll(4)).unwrap().readU32(); |
||||||
|
if (size === 0) |
||||||
|
break; |
||||||
|
const sample = await s.input.readAll(size); |
||||||
|
decryptSample(kdContext.readPointer(), 5, sample.unwrap(), sample.unwrap(), sample.byteLength); |
||||||
|
await s.output.writeAll(sample); |
||||||
|
} |
||||||
|
} |
||||||
|
await s.close(); |
||||||
|
} |
||||||
|
|
||||||
|
Socket.listen({ |
||||||
|
family: "ipv4", |
||||||
|
port: 10020, |
||||||
|
}).then(async function (listener) { |
||||||
|
while (true) { |
||||||
|
handleConnection(await listener.accept()); |
||||||
|
} |
||||||
|
}).catch(console.log); |
||||||
|
}, 4000); |
@ -0,0 +1,10 @@ |
|||||||
|
module main |
||||||
|
|
||||||
|
go 1.17 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/abema/go-mp4 v0.7.2 |
||||||
|
github.com/grafov/m3u8 v0.11.1 |
||||||
|
) |
||||||
|
|
||||||
|
require github.com/google/uuid v1.1.2 // indirect |
@ -0,0 +1,26 @@ |
|||||||
|
github.com/abema/go-mp4 v0.7.2 h1:ugTC8gfEmjyaDKpXs3vi2QzgJbDu9B8m6UMMIpbYbGg= |
||||||
|
github.com/abema/go-mp4 v0.7.2/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws= |
||||||
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= |
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||||
|
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= |
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= |
||||||
|
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA= |
||||||
|
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= |
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= |
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
||||||
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= |
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |
||||||
|
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= |
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
||||||
|
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I= |
||||||
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= |
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||||
|
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= |
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
Loading…
Reference in new issue