某音乐软件二进制网络流逆向分析-编程技术交流论坛-糯五游戏网

某音乐软件二进制网络流逆向分析

1. 前言

本文章对某音乐APP的二进制加密数据流进行分析,试图解密还原其请求和响应中的原文。
注意:本文内容仅用于学习交流,请勿用于任何违反APP用户协议之处
APP版本:13.7.0.8
工具及环境:

  1. 一台已Root的Android手机
  • 激活TrustMealready模块
  • 安装Reqable
  • 安装Frida Server
  1. 一台Windows电脑
  • 安装Reqable
  • 安装jadx-gui
  • 安装Frida环境

抓包

Reqable开启协同模式并开启抓包。
抓包结果中存在大量包含 musics.cfg结尾的请求,查看其请求体和响应体都是二进制格式。

图片[1]-某音乐软件二进制网络流逆向分析-编程技术交流论坛-糯五游戏网

请求体分析

由于该APP并没有使用任何加固,所以直接使用jadx分析该app。
先在请求头中发现特殊的M-Encoding字段,使用jadx进行全局搜索。

图片[2]-某音乐软件二进制网络流逆向分析-编程技术交流论坛-糯五游戏网
图片[3]-某音乐软件二进制网络流逆向分析-编程技术交流论坛-糯五游戏网

其中 com.tencent.qqmusicplayerprocess.network.base.Requestm210698S函数比较符合,进入查看一下。

/* renamed from: S */
    public void m210698S(byte[] bArr) {
        byte[] bArr2 = SwordSwitches.switches24;
        if (bArr2 != null && ((bArr2[519] >> 4) & 1) > 0 && SwordProxy.proxyOneArg(bArr, this, 261757).isSupported) {
            return;
        }
        if (this.f189318a.f189661t) {
            C55473c c55473c = C55473c.f184011b;
            bArr = c55473c.mo202363c(bArr);
            this.f189318a.m211137a("Content-Encoding", c55473c.mo202362b());
        }
        if (this.f189318a.m211143h() == 1) {
            byte[] m202364a = InterfaceC55471a.INSTANCE.m202364a(C55472b.f184010b.mo202363c(bArr));
            if (m202364a != null) {
                this.f189318a.m211137a("M-Encoding", Keys.API_PARAM_KEY_M1);
                bArr = m202364a;
            } else {
                this.f189318a.m211145j("M-Encoding");
            }
        }
        this.f189322e = bArr;
        this.f189330m = m210678b(bArr);
    }

该函数的入参是byte[] bArr 合理怀疑是原文转成数组了,使用Frida进行hook,查看一下入参。

Java.perform(function () {
    let Request = Java.use("com.tencent.qqmusicplayerprocess.network.base.Request");
    Request["S"].implementation = function (bArr) {        
        if (bArr) {
            let str = "";
            try {
                str = Java.use("java.lang.String").$new(bArr);
            } catch (e) {
                console.log("Error converting byte[] to string: " + e);
                str = Array.from(bArr).map(b => String.fromCharCode(b)).join('');
            }
            console.log(`bArr as string: ${str}`);
        } else {
            console.log("bArr is null or undefined");
        }

        return this["S"](bArr);
    };
});
图片[4]-某音乐软件二进制网络流逆向分析-编程技术交流论坛-糯五游戏网

结果表明该函数的入参确实是请求的原文,对该函数涉及bArr入参的调用进行分析,共两处调用。

图片[5]-某音乐软件二进制网络流逆向分析-编程技术交流论坛-糯五游戏网

这里因为两处调用处于不同的if分支,而M-Encoding字段位于第二处,于是先对C55472b.f184010b.mo202363c函数进行分析

package com.tencent.qqmusiccommon.cgi.zipper;

import com.tencent.qqmusic.sword.SwordProxy;
import com.tencent.qqmusic.sword.SwordProxyResult;
import com.tencent.qqmusic.sword.SwordSwitches;
import com.tencent.qqmusicplayerprocess.network.util.encoding.C56809a;
import com.tencent.wns.http.C59898b;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.Inflater;
import kotlin.Metadata;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@Metadata(m300174bv = {}, m300175d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0010\u0012\n\u0002\b\u0005\n\u0002\u0010\u000e\n\u0002\b\u0004\bÆ\u0002\u0018\u00002\u00020\u0001B\t\b\u0002¢\u0006\u0004\b\n\u0010\u000bJ\u0014\u0010\u0004\u001a\u0004\u0018\u00010\u00022\b\u0010\u0003\u001a\u0004\u0018\u00010\u0002H\u0002J\u0014\u0010\u0006\u001a\u0004\u0018\u00010\u00022\b\u0010\u0005\u001a\u0004\u0018\u00010\u0002H\u0016J\u0014\u0010\u0007\u001a\u0004\u0018\u00010\u00022\b\u0010\u0003\u001a\u0004\u0018\u00010\u0002H\u0016J\b\u0010\t\u001a\u00020\bH\u0016¨\u0006\f"}, m300176d2 = {"Lcom/tencent/qqmusiccommon/cgi/zipper/b;", "Lcom/tencent/qqmusiccommon/cgi/zipper/a;", "", "data", "d", "requestContent", "c", "a", "", C59898b.f201894e, "<init>", "()V", "lib_release"}, m300177k = 1, m300178mv = {1, 6, 0})
/* renamed from: com.tencent.qqmusiccommon.cgi.zipper.b */
/* loaded from: classes5.dex */
public final class C55472b implements InterfaceC55471a {

    /* renamed from: b */
    @NotNull
    public static final C55472b f184010b = new C55472b();

    private C55472b() {
    }

    /* renamed from: d */
    private final byte[] m202365d(byte[] data2) {
        int inflate;
        byte[] bArr = SwordSwitches.switches24;
        if (bArr != null && ((bArr[482] >> 4) & 1) > 0) {
            SwordProxyResult proxyOneArg = SwordProxy.proxyOneArg(data2, this, 261461);
            if (proxyOneArg.isSupported) {
                return (byte[]) proxyOneArg.result;
            }
        }
        byte[] bArr2 = null;
        if (data2 == null) {
            return null;
        }
        Inflater inflater = new Inflater();
        inflater.reset();
        inflater.setInput(data2);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(data2.length);
        try {
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            try {
                byte[] bArr3 = new byte[1024];
                while (!inflater.finished() && (inflate = inflater.inflate(bArr3)) > 0) {
                    byteArrayOutputStream.write(bArr3, 0, inflate);
                }
                byte[] byteArray = byteArrayOutputStream.toByteArray();
                byteArrayOutputStream.close();
                bArr2 = byteArray;
            } catch (Exception e2) {
                e2.printStackTrace();
                byteArrayOutputStream.close();
            }
            inflater.end();
            return bArr2;
        } catch (Throwable th) {
            try {
                byteArrayOutputStream.close();
            } catch (IOException e3) {
                e3.printStackTrace();
            }
            throw th;
        }
    }

    @Override // com.tencent.qqmusiccommon.cgi.zipper.InterfaceC55471a
    @Nullable
    /* renamed from: a */
    public byte[] mo202361a(@Nullable byte[] data2) {
        byte[] bArr = SwordSwitches.switches24;
        if (bArr != null && ((bArr[480] >> 6) & 1) > 0) {
            SwordProxyResult proxyOneArg = SwordProxy.proxyOneArg(data2, this, 261447);
            if (proxyOneArg.isSupported) {
                return (byte[]) proxyOneArg.result;
            }
        }
        byte[] m202365d = m202365d(data2);
        return m202365d == null ? data2 : m202365d;
    }

    @Override // com.tencent.qqmusiccommon.cgi.zipper.InterfaceC55471a
    @NotNull
    /* renamed from: b */
    public String mo202362b() {
        return "";
    }

    @Override // com.tencent.qqmusiccommon.cgi.zipper.InterfaceC55471a
    @Nullable
    /* renamed from: c */
    public byte[] mo202363c(@Nullable byte[] requestContent) {
        byte[] bArr = SwordSwitches.switches24;
        if (bArr != null && ((bArr[479] >> 2) & 1) > 0) {
            SwordProxyResult proxyOneArg = SwordProxy.proxyOneArg(requestContent, this, 261435);
            if (proxyOneArg.isSupported) {
                return (byte[]) proxyOneArg.result;
            }
        }
        byte[] m211199b = C56809a.f189692a.m211199b(requestContent);
        return m211199b == null ? requestContent : m211199b;
    }
}

这片代码是mo202363c函数所在的C55472b类,根据包名猜测加密应该是跟压缩相关的算法。
首先看该函数,主要操作是调用了C56809a.f189692a.m211199b函数,继续进入该函数的声明。

@Nullable
    /* renamed from: b */
    public final byte[] m211199b(@Nullable byte[] data2) {
        byte[] bArr = SwordSwitches.switches24;
        if (bArr != null && ((bArr[511] >> 4) & 1) > 0) {
            SwordProxyResult proxyOneArg = SwordProxy.proxyOneArg(data2, this, 261693);
            if (proxyOneArg.isSupported) {
                return (byte[]) proxyOneArg.result;
            }
        }
        if (data2 != null) {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(data2.length);
            DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream);
            try {
                try {
                    deflaterOutputStream.write(data2, 0, data2.length);
                    deflaterOutputStream.finish();
                    deflaterOutputStream.flush();
                    byte[] byteArray = byteArrayOutputStream.toByteArray();
                    try {
                        deflaterOutputStream.close();
                        byteArrayOutputStream.close();
                    } catch (IOException unused) {
                    }
                    return byteArray;
                } catch (IOException e) {
                    C61942e.p.m148888c("MusicPrivateEncodeUtils", "compressData Ex: " + e);
                    try {
                        deflaterOutputStream.close();
                        byteArrayOutputStream.close();
                        return null;
                    } catch (IOException unused2) {
                        return null;
                    }
                }
            } catch (Throwable th) {
                try {
                    deflaterOutputStream.close();
                    byteArrayOutputStream.close();
                } catch (IOException unused3) {
                }
                throw th;
            }
        }
        return null;
    }

上述函数找到关键字DeflaterOutputStream,主要操作就是使用Deflater对数据进行压缩。
回到最开始的m210698S函数,在使用DEFLATE压缩完数据后,又将其传入了InterfaceC55471a.INSTANCE.m202364a方法,进入该方法查看实现

public final byte[] m202364a(@Nullable byte[] source) {
            IntRange until;
            IntRange until2;
            int random;
            byte[] bArr = SwordSwitches.switches24;
            if (bArr != null && ((bArr[476] >> 4) & 1) > 0) {
                SwordProxyResult proxyOneArg = SwordProxy.proxyOneArg(source, this, 261413);
                if (proxyOneArg.isSupported) {
                    return (byte[]) proxyOneArg.result;
                }
            }
            if (source == null) {
                return null;
            }
            byte[] bArr2 = new byte[5];
            until = RangesKt___RangesKt.until(0, 5);
            Iterator<Integer> it = until.iterator();
            while (it.hasNext()) {
                int nextInt = ((IntIterator) it).nextInt();
                until2 = RangesKt___RangesKt.until(0, 100);
                random = RangesKt___RangesKt.random(until2, Random.INSTANCE);
                bArr2[nextInt] = (byte) random;
            }
            byte[] bArr3 = new byte[source.length + 5];
            System.arraycopy(bArr2, 0, bArr3, 0, 5);
            System.arraycopy(source, 0, bArr3, 5, source.length);
            return bArr3;
        }

该函数的主要操作就是随机生成5字节0-99的随机数,然后插入到入参之前。
以上就是请求体的加密方法

响应体分析

关于响应体的解密,尝试使用Inflater或者去除前5个字节然后使用Inflater解压缩,均失败了,看来请求体和响应体使用的加密应该不同。
仔细观察上述的C55472b类,其中的m202365d函数是对入参使用Inflater类来解压缩,Inflater通常与Deflater(压缩器)配对使用。Deflater用于压缩数据,而Inflater用于解压缩。因此这里应该是与解压相关的操作。
查看m202365d函数的用例,发现只有同类中的mo202361a函数有进行调用,继续向上追踪mo202361a函数的用例。

图片[6]-某音乐软件二进制网络流逆向分析-编程技术交流论坛-糯五游戏网

在结果中,com.tencent.qqmusicplayerprocess.network.business.C56733b.m210768a0函数有两处调用,进入查看

public static byte[] m210768a0(byte[] bArr, int i, int i2) {
        Closeable closeable;
        byte[] mo202361a;
        Closeable closeable2;
        byte[] bArr2 = SwordSwitches.switches24;
        ?? r1 = 2;
        r1 = 2;
        byte[] bArr3 = null;
        bArr3 = null;
        bArr3 = null;
        Closeable closeable3 = null;
        bArr3 = null;
        Closeable closeable4 = null;
        bArr3 = null;
        if (bArr2 != null && ((bArr2[508] >> 1) & 1) > 0) {
            SwordProxyResult proxyMoreArgs = SwordProxy.proxyMoreArgs(new Object[]{bArr, Integer.valueOf((int) i), Integer.valueOf(i2)}, null, 261666);
            if (proxyMoreArgs.isSupported) {
                return (byte[]) proxyMoreArgs.result;
            }
        }
        try {
            try {
                try {
                    try {
                        if (i2 == 1) {
                            byte[] bArr4 = new byte[bArr.length - i];
                            r1 = new DataInputStream(new ByteArrayInputStream(bArr));
                            long j = (long) i;
                            long skip = r1.skip(j);
                            if (j != skip) {
                                C61942e.p.m148888c("CgiRequest", "[decryptData] skip:" + ((int) i) + " actualSkip:" + skip);
                            }
                            int read = r1.read(bArr4);
                            i = new ByteArrayOutputStream();
                            if (read > 0) {
                                i.write(bArr4, 0, read);
                                bArr3 = C55472b.f184010b.mo202361a(i.toByteArray());
                                r1 = r1;
                                i = i;
                            }
                            mo202361a = bArr3;
                            closeable3 = r1;
                            closeable2 = i;
                            m210777m0(closeable3);
                            m210777m0(closeable2);
                            return mo202361a;
                        }
                        if (i2 == 2) {
                            byte[] bArr5 = new byte[bArr.length - i];
                            r1 = new DataInputStream(new ByteArrayInputStream(bArr));
                            long j2 = (long) i;
                            long skip2 = r1.skip(j2);
                            if (j2 != skip2) {
                                C61942e.p.m148888c("CgiRequest", "[decryptData] skip:" + ((int) i) + " actualSkip:" + skip2);
                            }
                            int read2 = r1.read(bArr5);
                            i = new ByteArrayOutputStream();
                            if (read2 > 0) {
                                i.write(bArr5, 0, read2);
                                bArr3 = C61861a.controller.brotliConfigController.mo201050a(i.toByteArray());
                                r1 = r1;
                                i = i;
                            }
                            mo202361a = bArr3;
                            closeable3 = r1;
                            closeable2 = i;
                            m210777m0(closeable3);
                            m210777m0(closeable2);
                            return mo202361a;
                        }
                        mo202361a = C55473c.f184011b.mo202361a(bArr);
                        closeable2 = null;
                        m210777m0(closeable3);
                        m210777m0(closeable2);
                        return mo202361a;
                    } catch (IOException e) {
                        e = e;
                        i = 0;
                        C61942e.p.m148889d("CgiRequest", "[decryptData] ", e);
                        m210777m0(r1);
                        m210777m0(i);
                        return bArr3;
                    } catch (Throwable th) {
                        th = th;
                        i = 0;
                        closeable4 = 2;
                        closeable = i;
                        m210777m0(closeable4);
                        m210777m0(closeable);
                        throw th;
                    }
                } catch (IOException e2) {
                    e = e2;
                    i = 0;
                    r1 = 0;
                    C61942e.p.m148889d("CgiRequest", "[decryptData] ", e);
                    m210777m0(r1);
                    m210777m0(i);
                    return bArr3;
                } catch (Throwable th2) {
                    th = th2;
                    closeable = null;
                    m210777m0(closeable4);
                    m210777m0(closeable);
                    throw th;
                }
            } catch (IOException e3) {
                e = e3;
                C61942e.p.m148889d("CgiRequest", "[decryptData] ", e);
                m210777m0(r1);
                m210777m0(i);
                return bArr3;
            }
        } catch (Throwable th3) {
            th = th3;
            closeable4 = 2;
            closeable = i;
            m210777m0(closeable4);
            m210777m0(closeable);
            throw th;
        }
    }

上述函数虽然很长,简化分析就只有几个操作

  1. 函数接收一个字节数组bArr、一个整数i和一个整数i2作为输入参数
  2. 根据i2的值(1或2)来决定使用不同的解密方式
  • 如果i2 == 1, 使用C55472b.f184010b.mo202361a方法解密
  • 如果i2 == 2, 使用C61861a.controller.brotliConfigController.mo201050a方法解密
  • 如果i2不是1或2, 使用C55473c.f184011b.mo202361a方法解密
  1. 对于i2为1或2的情况
  • 从bArr中跳过前i个字节
  • 读取剩余的字节到一个新的字节数组
  • 将读取的字节写入ByteArrayOutputStream
  • 使用相应的解密方法处理这些字节

分析完后发现主要有三处不同的解密算法,其中C55472b.f184010b.mo202361a就是我们刚才分析的使用Deflater解压数据,第二个方法是使用C61861a.controller.brotliConfigController.mo201050a进行解密,其中有一个brotli关键词,可以大胆猜测在使用Brotli算法进行解压缩,最后一个C55473c.f184011b.mo202361a方法,进入查看一下声明。

private final byte[] m202366d(byte[] data2) {
        boolean z;
        GZIPInputStream gZIPInputStream;
        Throwable th;
        byte[] bArr = SwordSwitches.switches24;
        if (bArr != null && ((bArr[483] >> 7) & 1) > 0) {
            SwordProxyResult proxyOneArg = SwordProxy.proxyOneArg(data2, this, 261472);
            if (proxyOneArg.isSupported) {
                return (byte[]) proxyOneArg.result;
            }
        }
        if (data2 != null) {
            if (data2.length == 0) {
                z = true;
            } else {
                z = false;
            }
            if (!z) {
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data2);
                try {
                    try {
                        gZIPInputStream = new GZIPInputStream(byteArrayInputStream);
                    } catch (IOException unused) {
                        gZIPInputStream = null;
                    } catch (Throwable th2) {
                        gZIPInputStream = null;
                        th = th2;
                    }
                    try {
                        byte[] bArr2 = new byte[1024];
                        while (true) {
                            int read = gZIPInputStream.read(bArr2);
                            if (read >= 0) {
                                byteArrayOutputStream.write(bArr2, 0, read);
                            } else {
                                byte[] byteArray = byteArrayOutputStream.toByteArray();
                                gZIPInputStream.close();
                                byteArrayInputStream.close();
                                byteArrayOutputStream.close();
                                return byteArray;
                            }
                        }
                    } catch (IOException unused2) {
                        if (gZIPInputStream != null) {
                            gZIPInputStream.close();
                        }
                        byteArrayInputStream.close();
                        byteArrayOutputStream.close();
                        return null;
                    } catch (Throwable th3) {
                        th = th3;
                        if (gZIPInputStream != null) {
                            try {
                                gZIPInputStream.close();
                            } catch (IOException unused3) {
                                throw th;
                            }
                        }
                        byteArrayInputStream.close();
                        byteArrayOutputStream.close();
                        throw th;
                    }
                } catch (IOException unused4) {
                    return null;
                }
            } else {
                return data2;
            }
        } else {
            return data2;
        }
    }

    @Override // com.tencent.qqmusiccommon.cgi.zipper.InterfaceC55471a
    @Nullable
    /* renamed from: a */
    public byte[] mo202361a(@Nullable byte[] data2) {
        byte[] bArr = SwordSwitches.switches24;
        if (bArr != null && ((bArr[482] >> 7) & 1) > 0) {
            SwordProxyResult proxyOneArg = SwordProxy.proxyOneArg(data2, this, 261464);
            if (proxyOneArg.isSupported) {
                return (byte[]) proxyOneArg.result;
            }
        }
        return m202366d(data2);
    }

看到这里发现非常眼熟C55472bC55473c类都是InterfaceC55471a接口的实现类,在C55473c类的mo202361a方法中又调用了m202366d方法,看到GZIPInputStream关键字就知道这里在使用gzip进行解压缩。

测试

至此,三种解压算法就清晰了,这里我们可以使用Python编写一个类似的解密函数来进行测试。

import zlib
import brotli
import gzip

def decrypt_data(bArr, i, i2):
    # 跳过前 i 个字节
    data_to_process = bArr[i:]

    # 根据 i2 选择解密方式
    if i2 == 1:
        # 使用 zlib 解压缩
        decrypted_data = zlib.decompress(data_to_process)
    elif i2 == 2:
        # 使用 Brotli 解压缩
        decrypted_data = brotli.decompress(data_to_process)
    else:
        # 使用 gzip 解压缩
        decrypted_data = gzip.decompress(data_to_process)

    return decrypted_data.decode()

将抓包结果的请求体和响应体导出为.bin二进制文件,经过测试请求体去除前5个随机字节后,使用zlib进行解压,响应体直接使用brotli进行解压。

with open("1.bin", "rb") as f:
    file_data = f.read()
result = decrypt_data(file_data, 5, 1)
print(result)


with open("2.bin", "rb") as f:
    file_data = f.read()
result = decrypt_data(file_data, 0, 2)
print(result)
图片[7]-某音乐软件二进制网络流逆向分析-编程技术交流论坛-糯五游戏网

这里请求体和响应体的数据也是成功还原了。

模拟

虽然成功解密了请求体和请求体,但是Headers中还存在sign和mask两个签名参数,这两个参数是调用JNI函数生成的,在so层实现的加密。

package com.tencent.qqmusic.modular.framework.encrypt.logic;

/* loaded from: classes4.dex */
public class MERJni {
    /* renamed from: a */
    public static void m179988a() {
        System.loadLibrary("mer");
    }

    public static native String calc(byte[] bArr, byte[] bArr2);
}

不过由于能力有限,暂时没找到解密方法,如果感兴趣的可以尝试去逆向一下libmer.so
虽然但是,如果你尝试在APP中尝试访问一个webview界面并抓包,会发现有大量的musics.cfg结尾的请求,不同之处是params多了_webcgikey、_、sign三个参数,另外请求体和响应体都是明文。

图片[8]-某音乐软件二进制网络流逆向分析-编程技术交流论坛-糯五游戏网
图片[9]-某音乐软件二进制网络流逆向分析-编程技术交流论坛-糯五游戏网

其中_webcgikey是调用的method名称,如果调用了多个module使用_分隔,_参数是请求的毫秒级时间戳,sign参数是对请求体的加密,固定以zzc开头。
关于zzc版本的sign加密算法,网上也有解密方案,具体可以参考 jixun.uk/posts/2024/qqm
那么APP的接口和Web接口使用相同的网关,是否通用呢?
经过对比APP请求体和Web请求体,有几点不同
其中请求体中都包含了comm字段,不同之处在于APP的请求体把cookies字段都写入了comm字段,web接口的请求体把cookies都写到headers里了。
APP请求体中,每个方法调用使用module名.method名作为key,而web请求使用req_0开始递增作为key,而value中都包含了modulemethodparam字段。
经过测试,将APP的请求体改为Web接口的请求格式后,也能正常发送并接收响应,至此对该APP的网络请求分析完毕。

请登录后发表评论

    没有回复内容