一個QQ音樂源無損音質下載軟件逆向淺要分析
逆向一個QMD QQ音樂源下載軟件
這個Apk主要是用來下載QQ音樂的無損數字音頻文件,我為了把我iMac上的mp3音質音樂替換為flac或者HiRes無損,一個個去網上找文件。偶然間在網上發現了這個app,有些好奇怎么實現的,于是做本篇分析文章。
樣本APK:QMD 1.7.2.apk https://wwb.lanzoub.com/iX5Lr08oc3be
第一步 反射大師脫殼
對于類似于這種神奇的軟件作者總會加個殼加加固保護一下源代碼,我有種直覺這玩意應該也是加了固的,果然打開zip文件一看:

libjiagu.so赫然在目。
得,直接打開反射大師先來扒一層皮看看能不能看到里面。
反射大師脫殼過程不再贅述,直接導出內存dex即可。
第二步 JEB靜態分析
可喜可賀,2021年初還只有Jeb 3.24,坐了一年牢出來發現竟然有4.x的版本更新了,好,很有精神!
首先我們打開app開始下載高解析度音頻,看看加密如何。

我們關注一下上圖的/api/Download請求。
這個包是用來獲取QQ音樂的文件實際下載地址,我們來看看這個請求:
一個迷之數據,一個樸實無華的http請求,再無其他。
只有下面的一個http://ws.stream.qqmusic.qq.com/RS01003zLSuX07Z0LB.flac
接口關聯他。那么為了得到這個qqmusic的源數據,我們要批量下載這些音樂就需要逆向出這個迷之數據到底是什么東西,我們如何生成它。
復制代碼 隱藏代碼 接口表 獲取音樂的高解析度下載地址 /api/MusicLink/link
那么話不多說,jeb直接打開導出的dex看函數,搜索這個字符串。

直接可以看到這個搜索結果了,很好,看來不需要再去找其他的dex文件了。
tab一下看看。

關注以下函數:
復制代碼 隱藏代碼
public String getMusicLink(String arg4) {
String v4 = EncryptAndDecrypt.encryptText(arg4);
String v4_1 = new HttpManager("http://8.136.185.193/api/MusicLink/link").postDataWithResult("\"" + v4 + "\"");
Logger.e(v4_1, new Object[0]);
return v4_1;
}
v4=arg4,arg4則是一個String不足為懼,先看看這個寫的非常漂亮的Encryption函數:
復制代碼 隱藏代碼
public static String encryptText(String arg1) {
return EncryptAndDecrypt.encryptText(arg1, Cookie.getQQ());
}
public static String encryptText(String arg4, String arg5) {
if(!TextUtils.isEmpty(arg4) && !TextUtils.isEmpty(arg5)) {
int v1 = 0;
StringBuilder v5 = new StringBuilder(EncryptAndDecrypt.encryptDES(arg4, "QMD" + arg5.substring(0, 8)));
Random v4 = new Random(((long)Calendar.getInstance().get(5)));
int v0 = v4.nextInt(4) + 1;
while(v1 < v0) {
v5.insert(v4.nextInt(v5.length()), "-");
++v1;
}
return v5.toString();
}
return "";
}
他調用了encryptText(String arg4, String arg5)函數,那么我們就可知道arg5是作為密碼而存在的,arg4則是String的原文,那么Cookie.getQQ()這個代碼則就相當的可疑。
跳轉一下看看:
復制代碼 隱藏代碼
public class Cookie {
private static String Mkey;
private static String QQ;
public static String getMkey() { return Cookie.Mkey; }
public static String getQQ() { return Cookie.QQ; }
public static void setCookie(String arg0, String arg1) {
Cookie.Mkey = arg0;
Cookie.QQ = arg1;
}
}
一個靜態實體類,直接看setCookie的交叉引用看看是從decryptAndSetCookie函數設置密碼的:
復制代碼 隱藏代碼
public static boolean decryptAndSetCookie(String arg5) {
String v5 = arg5.replace("-", "").replace("|", "");
if(v5.length() >= 10 && (v5.contains("%"))) {
String[] v5_1 = v5.split("%");
String v0 = v5_1[0];
String v5_2 = EncryptAndDecrypt.decryptDES(v5_1[1], v0.substring(0, 8));
if(v5_2.length() < 8) {
v5_2 = v5_2 + "QMD";
}
Cookie.setCookie(EncryptAndDecrypt.decryptDES(v0, v5_2.substring(0, 8)), v5_2);//v5_2就是密碼,由arg5參數分解而來。 return true;
}
return false;
}
繼續跟蹤交叉引用:
復制代碼 隱藏代碼
public boolean getCookie() {
String v0 = new HttpManager("http://8.136.185.193/api/Cookies").postDataWithResult(new Gson().toJson(SystemInfoUtil.getDeviceInfo()));
return TextUtils.isEmpty(v0) ? false : EncryptAndDecrypt.decryptAndSetCookie(v0);
}
找到了,看來是從這個接口獲取的數據,但是這個接口居然是POST提交,那么我們就有必要看看這個提交的數據SystemInfoUtil.getDeviceInfo()到底是什么東西:
復制代碼 隱藏代碼
public static final DeviceInfo getDeviceInfo() {
DeviceInfo v10 = new DeviceInfo(SystemInfoUtil.getUID(), SystemInfoUtil.getSystemModel(), SystemInfoUtil.getDeviceBrand(), SystemInfoUtil.getAppVersionName(), SystemInfoUtil.getSystemVersion(), SystemInfoUtil.getAppVersionCode() + "", null, 0x40, null);
v10.setIp(EncryptAndDecrypt.encryptText(v10.getUid() + v10.getDeviceModel() + v10.getDeviceBrand() + v10.getSystemVersion() + v10.getAppVersion() + v10.getVersionCode(), "F*ckYou!"));//密碼是F*ckYou!,emmmm.... return v10;
}
獲取了設備的一些信息,然后調用了一個setIp函數,這個加密看起來像是一個接口簽名防止被抓包調用接口,提高逆向成本。

直接抓包看數據,可以看出來其實就是幾個字符串appand一起后加上密碼des,下面我們就開始先用python實現一下。
不過在此之前我們還要看一下EncryptAndDecrypt.encryptText函數,里面是如何處理的:
復制代碼 隱藏代碼
public static String encryptText(String arg4, String arg5) {
if(!TextUtils.isEmpty(arg4) && !TextUtils.isEmpty(arg5)) {
int v1 = 0;
StringBuilder v5 = new StringBuilder(EncryptAndDecrypt.encryptDES(arg4, ("QMD" + arg5).substring(0, 8)));
Random v4 = new Random(((long)Calendar.getInstance().get(5)));
int v0 = v4.nextInt(4) + 1;
while(v1 < v0) {
v5.insert(v4.nextInt(v5.length()), "-");
++v1;
}
return v5.toString();
}
return "";
}
清晰地看到又調用了EncryptAndDecrypt.encryptDES函數,還加上了“QMD”作為密碼前置字符串,我們繼續跟蹤:
復制代碼 隱藏代碼
public static String encryptDES(String arg5, String arg6) {
if(arg5 != null && arg6 != null) {
try {
Cipher v0 = Cipher.getInstance("DES/CBC/PKCS5Padding");
v0.init(1, new SecretKeySpec(arg6.getBytes(), "DES"), new IvParameterSpec(arg6.getBytes()));
return Base64.encodeToString(v0.doFinal(arg5.getBytes()), 0).trim();
}
catch(Exception v5) {
return v5.getMessage();
}
}
return null;
}
到這里我們已經很清晰了,密碼作為iv,加密方式為des/cbc/pkcs5padding方式填充結果,那么我們用python實現一下這個加密函數:

到這里我們就用python寫出了加密算法,接下來就可以用這個算法生成數據去請求http數據了。
第三步 測試接口訪問


于是為了下載flac,我直接寫了一個python腳本。
項目地址
https://github.com/QiuChenly/python_down_jaychou