网易云音乐Android版逆向分析

网易云音乐Android版逆向分析

背景

用抓包工具抓取网易云音乐网络通信数据包,发现被加密。于是便尝试对加密过程进行分析
1

JAVA层分析

1. 用JEB反编译apk文件,发现字符串被加密了,无法直接搜索字符串定位关键代码。于是写了一个Xposed插件,获取解密后的字符串。

1
2
3
4
5
6
7
8
9
10
findAndHookMethod("a.auu.a", lpparam.classLoader, "c", String.class,  new XC_MethodHook()
{

@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
XposedBridge.log( param.args[0].toString() + " -> " + param.getResult().toString());
}
});

2

2. 在解密后的字符串中搜索“params”,定位到com.netease.cloudmusic.i.f 这个类的a方法

3

3. 这个方法的作用就是把请求参数拼成一个json字符串,传给native层函数serialdata进行加密,将返回的字符串放进一个Map中。

Native层分析

1. Serialdata是com.netease.cloudmusic.utils.NeteaseMusicUtils类的native方法。在libs目录下的so库里,并没有找到函数名为***的导出函数。

4

2. 仔细看看NeteaseMusicUtils的static代码块,发现调用了UtilInterface.load(“poison”);

5

3. 跟踪UtilInterface.load, 发现load这个方法调用了UtilInterface的 native方法l.

6

4. 并没有在l方法中找到注册serialdata函数的代码.

7

5. 于是使用Cyida hook了libdvm.so中的dvmLoadNativeCode和dvmUseJNIBridge 方法。前者可以确定加载so的路径,后者可以确定jni函数的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
bool my_dvmLoadNativeCode(char* soPath, void* classLoader, char** detail)
{
LOGD("soPath:%s",soPath);
return old_dvmLoadNativeCode(soPath,classLoader,detail);
}


void my_dvmUseJNIBridge(void* method, void* func)
{
LOGD("my_dvmUseJNIBridge method.name: %s\tfunc:%.8X",*(char**)(method+16), func);
old_dvmUseJNIBridge(method,func);
}

8

9

6. serialdata主要对java层传递过来的json字符串进行转换、拼接,然后算一个MD5值放在最后,再调用AES加密函数。

10

11

12

13

14

1
2
3
4
5
6
str = 'nobody%suse%smd5forencrypt' % (uri, data)
md5 = hashlib.md5()
md5.update(str)
md5str = md5.hexdigest()
constid = '36cd479b6b5'
params = '%s-%s-%s-%s-%s' % (uri, constid, data, constid, md5str)

7. 动态调试,对AES_encrypt下断,观察参数,可以获取到加密的密钥。

15

8. 还原加密算法,封装成so,用python 调用API接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from ctypes import *
import os
import hashlib
import urllib2
import binascii
import time
import json

libcloudmusic = cdll.LoadLibrary(os.getcwd() + '/cloudmusic.so')

def decrypt(enc_data):
cloudmusic_decrypt = libcloudmusic.cloudmusic_decrypt
cloudmusic_decrypt.argtype = [c_char_p,c_char_p]
data = ' '*(len(enc_data)/2)
cloudmusic_decrypt(enc_data,data)
data = data.replace('\000', '')
return data.strip()

def encrypt(data):
cloudmusic_encrypt = libcloudmusic.cloudmusic_encrypt
cloudmusic_encrypt.argtype = [c_char_p,c_char_p]
encdata = ' ' * 2 * (len(data) + 16-len(data)%16)
cloudmusic_encrypt(data,encdata)
return encdata

def getmd5(s):
md5 = hashlib.md5()
md5.update(s)
return md5.hexdigest()

def getparams(uri, data):
s = 'nobody%suse%smd5forencrypt' % (uri, data)
md5 = getmd5(s)
constid = '36cd479b6b5'
params = '%s-%s-%s-%s-%s' % (uri, constid, data, constid, md5)
return encrypt(params)

def request(url, data):
headers = {'User-Agent':'android'}
req = urllib2.Request(url)
http = urllib2.urlopen(url,data=data)
content = http.read()
return content

def playlist_info(playlistid):
url = 'http://music.163.com/eapi/v3/playlist/detail'
uri = '/api/v3/playlist/detail'
tpl = '''{"id":" ","n":"1000","t":" ","s":"5","c":"[]",
"e_r":"true","header":{"os":"android","requestId":"1469863382839_317",
"mobilename":"MI4LTE","osver":"4.4.4","resolution":"1920x1080",
"buildver":"139278","MUSIC_U":"***","channel":"netease",
"appver":"3.5.2","deviceId":"***"}}
'''
d = json.loads(tpl)
d['id'] = playlistid
d['t'] = int(time.time())*1000
data = json.dumps(d)
params = getparams(uri, data)
postdata = 'params=%s' % (params)
raw_data = request(url, postdata)
enc_data = binascii.b2a_hex(raw_data)
dec_data = decrypt(enc_data)
print dec_data

if __name__ == '__main__':
playlist_info('374082787')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
➜  ~  python cloudmusic_api_test.py|python -m json.tool

{
"code": 200,
"playlist": {
"adType": 0,
"cloudTrackCount": 0,
"commentCount": 0,
"commentThreadId": "A_PL_0_374082787",
"coverImgId": 923589767384661,
"createTime": 1462184119385,
"creator": {
"accountStatus": 0,
"authStatus": 0,
"authority": 0,
"avatarImgId": 1418370007886995,
"avatarUrl": "http://p3.music.126.net/68DiGwbR2THQlb1szd37Iw==/1418370007886995.jpg",
"backgroundImgId": 2002210674180200,
"backgroundUrl": "http://p1.music.126.net/45Nu4EqvFqK_kQj6BkPwcw==/2002210674180200.jpg",
"birthday": 631123200000,
"city": 1004400,
"defaultAvatar": false,
"description": "",
"detailDescription": "",
"djStatus": 0,
"expertTags": null,
"followed": false,
"gender": 1,
"mutual": false,
"nickname": "zzhighest",
"province": 1000000,
"remarkName": null,
"signature": "\u4e2d\u5fc3\u85cf\u4e4b\uff0c\u4f55\u65e5\u5fd8\u4e4b\u2026",
"userId": 119900879,
"userType": 0,
"vipType": 0
......