前言
免责声明:
本篇博文的初衷是分享自己学习逆向分析时的个人感悟,所涉及的内容仅供学习、交流,请勿将其用于非法用途!!!任何由此引发的法律纠纷均与作者本人无关,请自行负责!!!
版权声明:
未经作者本人授权,禁止转载!!!
上篇博客
已经分析了网易云音乐的加密参数。本篇通过酷狗音乐进行逆向分析,进而加深对逆向分析流程的理解。
目标:
通过输入歌名或者歌手名,列出相应的音乐信息,然后通过选择某一项,将对应的音乐下载到本地指定目录。
工具:
Google Chrome、PyCharm
这里依旧以我最喜欢的歌手
本兮
为例,这里播放了一首
下雪的季节
,需要RMB才能听完整版的:
1. 请求分析
如果想要下载一首歌,我们首先要获取到这首歌所对应的
u
r
l
url
u
r
l
。随机选择一首歌进行播放,打开Chrome的开发者工具,刷新看一下对应的请求,找到我们想要的歌曲文件的
u
r
l
url
u
r
l
,就是下面这个:
然后找到该请求对应的
u
r
l
url
u
r
l
,分析一下该请求:
可知,获取数据的
u
r
l
url
u
r
l
为
https://wwwapi.xxxxx.com/yy/index.php?r=play/getdata&callback=jQuery1910989040365354567_1599909353609&hash=1B5C869853B6A7DD39FED655B2155207&album_id=1819925&dfid=3LfODQ2G5XMN0x1liv3DeyjX&mid=61a73ea098eb98e7c6f4fbc66cd7f367&platid=4&_=1599909353610
,请求方式为
GET
,它要提交的参数已经包含在了
u
r
l
url
u
r
l
里,而且里面有很多不是必须的参数。
经过几次刷新发现,参数
callback
和
_
的值是一直在变化的,其他参数是固定的。根据经验,
GET
请求方式的
u
r
l
url
u
r
l
一般是可以简化的,即去掉不是必须的参数后仍然可以正常得到数据,简化后的
u
r
l
url
u
r
l
为
https://wwwapi.xxxxx.com/yy/index.php?r=play/getdata&hash=1B5C869853B6A7DD39FED655B2155207
,但是通过程序访问这个
u
r
l
url
u
r
l
却是失败的,这说明,请求需要
cookie
。
综上分析可以猜测到,一首歌对应一个参数
hash
的值,而且这个参数肯定在搜索结果中,下面要做的就是找到这个
hash
。
2. 获取参数
我们来到搜索界面:
然后打开Chrome的开发者工具,刷新看一下对应的请求,找到我们想要的搜索结果列表,就是下面这个:
可以看到,我们想要的
hash
其实就是
FileHash
,而且里面还有歌名、歌手以及专辑等信息。然后找到对应的
u
r
l
url
u
r
l
,分析一下该请求:
依旧是个
GET
请求,
u
r
l
url
u
r
l
为
https://complexsearch.xxxxx.com/v2/search/song?callback=callback123&keyword=%E6%9C%AC%E5%85%AE&page=1&pagesize=30&bitrate=0&isfuzzy=0&tag=em&inputtype=0&platform=WebFilter&userid=-1&clientver=2000&iscorrection=1&privilege_filter=0&srcappid=2919&clienttime=1599910861467&mid=1599910861467&uuid=1599910861467&dfid=-&signature=51F1A4D0FBB3DE862AD5E87364E6756A
,先简单分析一下它的参数是什么意思,参数
keyword
就是我们在搜索那里输入的内容,参数
page
为页数,参数
pagesize
表示每页显示多少条信息。这里依旧是很长的一串,我尝试这简化
u
r
l
url
u
r
l
,然而并没有成功,错误信息为
"error_msg" : "Parameter Error"
、
"error_msg" : "err signature"
和
"error_msg" : "err appid(srcappid) or clientver or mid or dfid"
,可以推测出参数
signature
应该是很重要的,而且经过刷新发现参数
signature
、
clienttime
、
mid
和
uuid
每次都会发生变化,且后面三个一直相同。估计参数可能被加密了,全局搜索参数
signature
,将其定位:
果然,参数
signature
被MD5加密了,打上几个断点,然后debug看一下:
参数是20个,但是只有参数
clienttime
、
mid
和
uuid
发生变化,而且它们还相同,找一下它们来自哪里,向上定位到了它们的位置:
发现是个时间序列,由JavaScript中的
getTime()
方法生成的,它返回的是毫秒数,在Python中可以用
time
模块的
time()
方法代替。下面来模拟一下
MD5
加密,这里可以使用Python的标准库
hashlib
:
def MD5Encrypt( self, text) :
k = time. time( )
k = int( round( k * 1000) )
info = [ "NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt" , "bitrate=0" , "callback=callback123" ,
"clienttime={}" . format( k) , "clientver=2000" , "dfid=-" , "inputtype=0" ,
"iscorrection=1" , "isfuzzy=0" , "keyword={}" . format( text) , "mid={}" . format( k) ,
"page=1" , "pagesize=30"
, "platform=WebFilter" , "privilege_filter=0" ,
"srcappid=2919" , "tag=em" , "userid=-1" , "uuid={}" . format( k) , "NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt" ]
new_md5 = md5( )
info = '' . join( info)
new_md5. update( info. encode( encoding='utf-8' ) )
result = new_md5. hexdigest( )
return result. upper( )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
这个加密的结果就是参数
signature
,为了检验结果的正确性,我们将时间序列和上面的保持一致,即
clienttime = mid = uuid1599910861467
,运行结果如下:
结果是正确的,然后我们拼接成
u
r
l
url
u
r
l
即可:
我们访问拼成的
u
r
l
url
u
r
l
可以正常得到数据:
u
r
l
url
u
r
l
小常识:
+
表示空格
/
分隔目录和子目录
?
分隔实际的URL和参数
%
表示特殊字符
#
表示书签
&
表示参数间的分隔符
=
表示参数的值
3. 提取信息
两个请求的
u
r
l
url
u
r
l
我们都已经获得了,下面就是将数据从
json
格式的文本中提取出来,然后对歌曲文件
u
r
l
url
u
r
l
发起请求,将结果以二进制形式保存,后缀名为
.mp3
。不废话,直接上代码:
import time
from hashlib import md5
import json
import requests
import re
import os
class KuGouMusic ( object ) :
def __init__ ( self) :
self. headers = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36' }
def MD5Encrypt ( self, text) :
k = time. time( )
k = int ( round ( k * 1000 ) )
info = [ "NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt" , "bitrate=0" , "callback=callback123" ,
"clienttime={}" . format ( k) , "clientver=2000" , "dfid=-" , "inputtype=0" ,
"iscorrection=1" , "isfuzzy=0" , "keyword={}" . format ( text) , "mid={}" . format ( k) ,
"page=1" , "pagesize=30" , "platform=WebFilter" , "privilege_filter=0" ,
"srcappid=2919" , "tag=em" , "userid=-1" , "uuid={}" . format ( k) , "NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt" ]
new_md5 = md5( )
info = '' . join( info)
new_md5. update( info. encode( encoding= 'utf-8' ) )
signature = new_md5. hexdigest( )
url = 'https://complexsearch.kugou.com/v2/search/song?callback=callback123&keyword={0}' \
'&page=1&pagesize=30&bitrate=0&isfuzzy=0&tag=em&inputtype=0&platform=WebFilter&userid=-1' \
'&clientver=2000&iscorrection=1&privilege_filter=0&srcappid=2919&clienttime={1}&' \
'mid={2}&uuid={3}&dfid=-&signature={4}' . format ( text, k, k, k, signature. upper( ) )
return url
def get_html ( self, url) :
cookie = 'kg_mid=61a73ea098eb98e7c6f4fbc66cd7f367; kg_dfid=3LfODQ2G5XMN0x1liv3DeyjX; kg_dfid_collect=d41d8cd98f00b204e9800998ecf8427e; Hm_lvt_aedee6983d4cfc62f509129360d6bb3d=1599906321; Hm_lpvt_aedee6983d4cfc62f509129360d6bb3d=1599922649' . split(
'; ' )
cookie_dict = { }
for co in cookie:
co_list = co. split( '=' )
cookie_dict[ co_list[ 0 ] ] = co_list[ 1 ]
try :
response = requests. get( url, headers= self. headers, cookies= cookie_dict)
response. raise_for_status( )
response. encoding = 'utf-8'
return response. text
except Exception as err:
print ( err)
return
'请求异常'
def parse_text ( self, text) :
count = 0
hash_list = [ ]
print ( '{:*^80}' . format ( '搜索结果如下' ) )
print ( '{0:{5}<5}{1:{5}<15}{2:{5}<10}{3:{5}<10}{4:{5}<20}' . format ( '序号' , '歌名' , '歌手' , '时长(s)' , '专辑' , chr ( 12288 ) ) )
print ( '{:-^84}' . format ( '-' ) )
song_list = json. loads( text) [ 'data' ] [ 'lists' ]
for song in song_list:
singer_name = song[ 'SingerName' ]
pattern = re. compile ( '[</em>]' )
singer_name = re. sub( pattern, '' , singer_name)
song_name = song[ 'SongName' ]
album_name = song[ 'AlbumName' ]
duration = song[ 'Duration' ]
file_hash = song[ 'FileHash' ]
file_size = song[ 'FileSize' ]
hq_file_hash = song[ 'HQFileHash' ]
hq_file_size = song[ 'HQFileSize' ]
sq_file_hash = song[ 'SQFileHash' ]
sq_file_size = song[ 'SQFileSize' ]
mv_hash = song[ 'MvHash' ]
m4a_size = song[ 'M4aSize' ]
hash_list. append( [ file_hash, hq_file_hash, sq_file_hash] )
print ( '{0:{5}<5}{1:{5}<15}{2:{5}<10}{3:{5}<10}{4:{5}<20}' . format ( count, song_name, singer_name, duration, album_name,
chr ( 12288 ) ) )
count += 1
if count == 10 :
break
print ( '{:*^80}' . format ( '*' ) )
return hash_list
def save_file ( self, song_text) :
filepath = './download'
if not os. path. exists( filepath) :
os. mkdir( filepath)
text = json. loads( song_text) [ 'data' ]
audio_name = text[ 'audio_name' ]
author_name = text[ 'author_name' ]
album_name = text[ 'album_name' ]
img_url = text[ 'img' ]
lyrics = text[ 'lyrics' ]
play_url = text[ 'play_url' ]
response = requests. get( play_url, headers= self. headers)
with open ( os. path. join( filepath, audio_name) + '.mp3' , 'wb' ) as f:
f. write( response. content)
print ( "下载完毕!" )
if __name__ == '__main__' :
kg = KuGouMusic( )
search_info = input ( "请输入歌名或歌手: " )
search_url = kg. MD5Encrypt( search_info)
search_text = kg. get_html( search_url)
hash_list = kg. parse_text( search_text[ 12 : - 2 ] )
while True :
input_index = eval ( input ( "请输入要下载歌曲的序号(-1退出): " ) )
if input_index == - 1 :
break
download_info = hash_list[ input_index]
song_url = 'https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash={}' . format ( download_info[ 0 ] )
song_text = kg. get_html( song_url)
kg. save_file( song_text)
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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
测试结果如下:
结束语
今天分析的搜索结果接口和我以前分析出来的有点不一样,而且以前的接口还可以正常使用,没记错的话以前是不需要
cooike
的。
url = "https://songsearch.xxxxx.com/song_search_v2?keyword={}&platform=WebFilter" . format ( song_name)
与网易云音乐相比,酷狗音乐的请求相对来说简单了些,基本上没有什么加密,而且酷狗的音乐版权还贼多,我喜欢ヾ(^∀^)ノ