咪咕视频解析接口分析

August 22, 2021 · 分享 · 328次阅读

目标apk与md5

  • com.cmcc.cmvideo_5.9.3.10_25000599.apk
  • 81d38496ac24e05e3b9f1fc79cfb4d6d

分析记录

初步静态分析

不好,有壳,那么先脱壳一波再说

脱出来的dex可能有不能直接用jadx-gui直接打开的,不打开它便是

最终留下这些

抓包视频接口

开启抓包软件,打开一个视频进行抓包

显然比较重要的参数应该是这几个

  • sign
  • l_c
  • l_s

初步搜索,推测是这个类

  • com.cmvideo.capability.mgplayercore.net.VideoDetailsRequestHelper

用objection辅助分析

objection -g com.cmcc.cmvideo explore -P ~/.objection/plugins/
android hooking watch class com.cmvideo.capability.mgplayercore.net.VideoDetailsRequestHelper

进一步查看

android hooking watch class_method com.cmvideo.capability.mgplayercore.net.VideoDetailsRequestHelper.updateVideoInfo --dump-args --dump-backtrace --dump-return

得到如下结果

(agent) [693091] Called com.cmvideo.capability.mgplayercore.net.VideoDetailsRequestHelper.updateVideoInfo(com.cmvideo.foundation.bean.player.VideoBean, com.cmvideo.foundation.bean.player.VideoInfoBean)
(agent) [693091] Backtrace:
        com.cmvideo.capability.mgplayercore.net.VideoDetailsRequestHelper.updateVideoInfo(Native Method)
        com.cmcc.cmvideo.player.PlayHelper$2.onVideoInfoCallback(PlayHelper.java:282)
        com.cmvideo.foundation.videocache.chain.VideoInfoInvocationChain$3$1.run(VideoInfoInvocationChain.java:213)
        android.os.Handler.handleCallback(Handler.java:938)
        android.os.Handler.dispatchMessage(Handler.java:99)
        android.os.Looper.loop(Looper.java:223)
        android.app.ActivityThread.main(ActivityThread.java:7664)
        java.lang.reflect.Method.invoke(Native Method)
        com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

(agent) [693091] Arguments com.cmvideo.capability.mgplayercore.net.VideoDetailsRequestHelper.updateVideoInfo(VideoBean{mgdbId='null', id='714007259', type='null', contId='null', contName='null', epsAssetID='null', prdPackageId='null', needAuth=false, titleValue=0, endValue=0, duration=0, sTime=0, eTime=0, mediaSize=0, level='null', playName='null', urlType='null', url='null', tmpUrl='null', assertId='null', imgUrl='null', videoCoding='null', isLive=false, isStreaming=false, needClothHat=false, shareUrl='null', shareTitle='null', shareSubTitle='null', nodeId='null', goodsId='null', payType='null', payName='null', cpName='null', actor='null', programType='null', vId='null', isAdvert='null', toast='null', playLengths='null', isReserved=false, isCanReserve=false, suitMultiView=false, suitMultiViewDesc='null', suitAvs2Desc='null', suitAvs2=false, shareSwitch=false, commentSwitch=false, copyrightType=0, totalCount='null', subtitleTrackInfos=null, currentMediaFile=null, mediaFiles=null, contents=null, previewPicture=null, auth=null, star=null, urlInfos=null, keywords='null', resourceType='null', pricingStage='null', hasAudio=false, thumbViewer='null', thumbViewerPath='null', thumbViewerName='null', thumbViewerIndex=null, shieldStrategy=null, preRecord='null', albumId='null', isDirectlyRunMeWithoutByHomePage=false, index=0, copyRightObjectId='null', cutVideo='null', free=false, rid='null', code=0, reason='null', hdToast='null', hdReason='null', defaultMgdbId='null', trySeeDuration='null', totalPage='null', audioTrackInfos=null, mediaFiles4K=null, mWonderfulMomentsBeans=null, mLookTaStarsBeans=null, isLightSpot=false, shellPayType='null', shellPayContent='null', selectItemPosition=0, mDolbyUrl='null', mDolbyUrlType='null'}, [email protected])                                                             
(agent) [693091] Return Value: VideoBean{mgdbId='null', id='714007259', type='null', contId='null', contName='《我在他乡挺好的DVD版》第02集', epsAssetID='null', prdPackageId='1002581', needAuth=true, titleValue=104, endValue=4375, duration=4524, sTime=0, eTime=0, mediaSize=871268892, level='', playName='null', urlType='normal', url='http://gslbmgspvod.miguvideo.com/depository_yqv/asset/zhengshi/5103/448/823/5103448823/media/5103448823_5010320108_95.mg001.mp4.m3u8?xxx...', tmpUrl='null', assertId='5103448823', imgUrl='null', videoCoding='h265', isLive=false, isStreaming=false, needClothHat=true, shareUrl='null', shareTitle='null', shareSubTitle='null', nodeId='null', goodsId='null', payType='FREE_LIMIT', payName='限免', cpName='芒果无线增值', actor='null', programType='null', vId='null', isAdvert='2', toast='null', playLengths='5', isReserved=false, isCanReserve=false, suitMultiView=false, suitMultiViewDesc='null', suitAvs2Desc='null', suitAvs2=false, shareSwitch=false, commentSwitch=false, copyrightType=0, totalCount='null', subtitleTrackInfos=null, currentMediaFile=com.cmvid[email protected]444973e, mediaFiles=[com.cmvid[email protected]f3d799f, com.cmvid[email protected]8356aec, com.cmvid[email protected]3da9eb5], contents=null, previewPicture=null, auth=com[email protected]56ee94a, star=null, urlInfos=[com.cm[email protected]48a10bb], keywords='null', resourceType='null', pricingStage='null', hasAudio=true, thumbViewer='1', thumbViewerPath='http://img.cmvideo.cn:8080/publish/slt', thumbViewerName='thumbnail/asset/zhengshi/5103/448/823/5103448823/snapshot', thumbViewerIndex=null, shieldStrategy=null, preRecord='null', albumId='null', isDirectlyRunMeWithoutByHomePage=false, index=0, copyRightObjectId='null', cutVideo='0', free=false, rid='SUCCESS', code=200, reason='ad-skip-5-seconds', hdToast='尊敬的会员为您切换至蓝光1080p清晰度', hdReason='null', defaultMgdbId='null', trySeeDuration='0', totalPage='null', audioTrackInfos=null, mediaFiles4K=null, mWonderfulMomentsBeans=null, mLookTaStarsBeans=null, isLightSpot=false, shellPayType='null', shellPayContent='null', selectItemPosition=0, mDolbyUrl='null', mDolbyUrlType='null'}

看起来没有经过期望的地方

那看看没有被调用的地方的sign是怎么计算的吧

android hooking watch class_method com.cmcc.migutv.encryptor.MGEncryptor.getMiGuSign --dump-args --dump-backtrace --dump-return
(agent) [104147] Called com.cmcc.migutv.encryptor.MGEncryptor.getMiGuSign(android.content.Context, java.lang.String)
(agent) [104147] Backtrace:
        com.cmcc.migutv.encryptor.MGEncryptor.getMiGuSign(Native Method)
        com.cmvideo.foundation.videocache.processor.VideoInfoProcessor.getVideoInfoParams(VideoInfoProcessor.java:205)
        com.cmvideo.foundation.videocache.processor.VideoInfoProcessor.getVideoInfo(VideoInfoProcessor.java:102)
        com.cmvideo.foundation.videocache.processor.VideoInfoProcessor.run(VideoInfoProcessor.java:65)
        com.cmvideo.foundation.videocache.chain.VideoInfoInvocationChain.getVideoInfo(VideoInfoInvocationChain.java:58)
        com.cmvideo.foundation.videocache.CacheController.getVideoInfo(CacheController.java:138)
        com.cmcc.cmvideo.player.PlayHelper.getVideoInfo(PlayHelper.java:984)
        com.cmcc.cmvideo.player.PlayHelper.preparePlayData(PlayHelper.java:776)
        com.cmcc.cmvideo.player.PlayHelper.preparePlayData(PlayHelper.java:725)
        com.cmcc.cmvideo.playdetail.widget.MgPlayPageFragment.getPlayUrl(MgPlayPageFragment.java:3481)
        com.cmcc.cmvideo.playdetail.widget.MgPlayPageFragment.onResume(MgPlayPageFragment.java:2290)
        android.support.v4.app.Fragment.performResume(Fragment.java:2498)
        android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1501)
        android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1784)
        android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1852)
        android.support.v4.app.FragmentManagerImpl.dispatchStateChange(FragmentManager.java:3269)
        android.support.v4.app.FragmentManagerImpl.dispatchResume(FragmentManager.java:3241)
        android.support.v4.app.FragmentController.dispatchResume(FragmentController.java:223)
        android.support.v4.app.FragmentActivity.onResumeFragments(FragmentActivity.java:538)
        android.support.v4.app.FragmentActivity.onPostResume(FragmentActivity.java:527)
        android.support.v7.app.AppCompatActivity.onPostResume(AppCompatActivity.java:172)
        android.app.Activity.performResume(Activity.java:8154)
        android.app.ActivityThread.performResumeActivity(ActivityThread.java:4428)
        android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4470)
        android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)
        android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        android.os.Handler.dispatchMessage(Handler.java:106)
        android.os.Looper.loop(Looper.java:223)
        android.app.ActivityThread.main(ActivityThread.java:7664)
        java.lang.reflect.Method.invoke(Native Method)
        com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

(agent) [104147] Arguments com.cmcc.migutv.encryptor.MGEncryptor.getMiGuSign([email protected], a42002edf5fdf989cb63a07327eb804c)
(agent) [104147] Return Value: 22441061,d9e1f6772cfb42705b6a9563ec7830c8

可以确定和链接中的sign一致(没错是链接中的,前面没有注意到

直接访问链接结果是请求校验失败,说明请求头还是不能缺少的

MGEncryptor的反编译代码

package com.cmcc.migutv.encryptor;

import android.content.Context;
import android.text.TextUtils;
import com.meituan.robust.ChangeQuickRedirect;
import com.meituan.robust.PatchProxy;
import com.meituan.robust.PatchProxyResult;

public class MGEncryptor {
    public static ChangeQuickRedirect changeQuickRedirect;

    public native String[] getSignFromNative(Context context, String str);

    static {
        System.loadLibrary("mgencryptor");
    }

    public String[] getMiGuSign(Context context, String str) {
        PatchProxyResult proxy = PatchProxy.proxy(new Object[]{context, str}, this, changeQuickRedirect, false, 30812, new Class[]{Context.class, String.class}, String[].class);
        if (proxy.isSupported) {
            return (String[]) proxy.result;
        }
        if (context == null || TextUtils.isEmpty(str)) {
            return new String[]{"0000", "input error"};
        }
        try {
            return getSignFromNative(context, str);
        } catch (Exception e) {
            e.printStackTrace();
            return new String[]{"0000", "jni error"};
        }
    }
}

用IDA简单看一下,发现整体代码较少,下图依次是函数、字符、导入

不过今天并不打算还原算法,直接unidbg调用

拉一下最新版本的unidbg,然后做简单的so初始化载入

根据IDA直接看到的信息

可以看到有一个/proc/self/maps字符串,于是实现IOResolver

文件内容可以从内存中直接dump下来一份

同时很可能有jni交互,于是继承AbstractJni

同时导入里面有随机函数,于是用HookZz固定lrand48,只固定这一个是因为它用得最多

代码如下

package com.cmcc.migutv.encryptor;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.Symbol;
import com.github.unidbg.arm.context.EditableArm32RegisterContext;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.hook.HookContext;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.hook.hookzz.IHookZz;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.file.SimpleFileIO;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class MGEncryptor extends AbstractJni implements IOResolver {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    MGEncryptor() {
        emulator = AndroidEmulatorBuilder
                .for32Bit()
                .setProcessName("com.cmcc.cmvideo")
                .build();

        System.out.println("当前进程PID -> " + emulator.getPid());

        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

        emulator.getSyscallHandler().addIOResolver(this); // 绑定IO重定向接口
        vm = emulator.createDalvikVM();
        vm.setVerbose(true); // 设置是否打印Jni调用细节

        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/cmcc/migutv/encryptor/libmgencryptor.so"), true);
        module = dm.getModule();
        vm.setJni(this);

        IHookZz hookZz = HookZz.getInstance(emulator);
        Symbol lrand48 = module.findSymbolByName("lrand48");
        hookZz.replace(lrand48, new ReplaceCallback() {
            @Override
            public void postCall(Emulator<?> emulator, HookContext context) {
                EditableArm32RegisterContext ctx = emulator.getContext();
                System.out.println("lrand48 origin return ->" + ctx.getR0Int());
            }
        }, true);

        dm.callJNI_OnLoad(emulator);
    }

    @Override
    public FileResult resolve(Emulator emulator, String pathname, int oflags) {
        System.out.println("访问 -> " + pathname);
        if (("/proc/self/maps").equals(pathname)) {
            return FileResult.success(new SimpleFileIO(oflags, new File("unidbg-android/src/test/java/com/cmcc/migutv/encryptor/maps"), pathname));
        }
        return null;
    }

    public static void main(String[] args) {
        MGEncryptor mMGEncryptor = new MGEncryptor();
    }
}

然后运行得到下面的日志

当前进程PID -> 10097
访问 -> /dev/__properties__
访问 -> /proc/stat
访问 -> /proc/self/maps
JNIEnv->FindClass(com/cmcc/migutv/encryptor/MGEncryptor) was called from [email protected][libmgencryptor.so]0x52ef
JNIEnv->RegisterNatives(com/cmcc/migutv/encryptor/MGEncryptor, [email protected][libmgencryptor.so]0xf004, 1) was called from [email protected][libmgencryptor.so]0x5305
RegisterNative(com/cmcc/migutv/encryptor/MGEncryptor, getSignFromNative(Landroid/content/Context;Ljava/lang/String;)[Ljava/lang/String;, [email protected][libmgencryptor.so]0xa731)

可以看到getSignFromNative动态注册于0xa731,现在补充getSignFromNative的调用

public void getSignFromNative(){
    // args list
    List<Object> args = new ArrayList<>(4);
    // arg1 env
    args.add(vm.getJNIEnv());
    // arg2 jobject/jclazz 一般用不到,直接填0
    args.add(0);
    // arg3 context
    DvmObject context = vm.resolveClass("com/cmcc/cmvideo/application/MGApplication").newObject(null);
    args.add(vm.addLocalObject(context));
    // arg4 md5string
    StringObject md5string = new StringObject(vm, "a42002edf5fdf989cb63a07327eb804c");
    args.add(vm.addLocalObject(md5string));
    // call function
    Number number = module.callFunction(emulator, 0xa731, args.toArray())[0];
    System.out.println("number ->" + number);
    Object result = vm.getObject(number.intValue()).getValue();
    DvmObject[] strarr = (DvmObject[]) result;
    System.out.println("result ->" + strarr[0]);
    System.out.println("result ->" + strarr[1]);
};

报错,补充对应的jni调用

com/cmcc/cmvideo/application/MGApplication->getPackageManager()Landroid/content/pm/PackageManager;

com/cmcc/cmvideo/application/MGApplication->getPackageName()Ljava/lang/String;

成功返回结果,不过结果和hook的不一致

可以发现调用了lrand48,所以每次结果不一样

最终返回结果前有很长的一串字符串

简单测试发现最终结果就是这串字符串的md5

多次运行可以发现最终字符串构成如下

  • 传入参数 a42002edf5fdf989cb63a07327eb804c
  • 未知 25953a714f064300ae9d9d3c684dc6ae
  • 固定值 migu
  • 第二轮随机数的后六位的前四位 1484

最终返回结果的第一个字符串的构成

  • 第一个随机数的末尾两位
  • 第二个随机数的末尾六位

那么这个未知32位字符串是什么呢

根据日志可以知道最终进行md5的长字符串在0xa9fb产生

而很可惜这个地方原so没有内容

elf-dump-fix从内存中dump并修复

现在这个位置有代码了,但是不能F5看伪代码

另外通过这里的%s%s%s%s可以推测应该是进行了字符串格式化,这里是4个字符串

这和前面通过重复执行推测的结论是一致的

如果尝试在函数开始的位置创建函数,则会出现这样的错误

.text:0000AA5C: The function has undefined instruction/data at the specified address.

好在这种情况是有解的,参考

将从函数起始位置到报错位置之前的部分选中,按P创建函数

现在很容易看出来未知的字符串是怎么来的了

直接查看off_E440的数据,可以发现原来是一个固定的字符串数组

而索引值的计算是某个数对100取余数

可以确定之前的计算中的字符串所在索引是74,正好是第一个随机数对100取余

综上分析,可以得出链接中的sign参数算法如下

只是计划用unidbg调用,没想到多看了两眼直接还原了

import time
import hashlib
from random import randint

SALT_TABLE = [
    '9b49eed02d9240aeabeb782860cc6be2',
    'd34d010b674341b0b31d60118370e3e7',
    '551c102e19a74dbbbaadf82e0f603725',
    'a49d1441c2ef4ec3881ee03975ed9e64',
    'f9580a84a68b4ff9aface3fac135203a',
    'afef8c8c9ccf47bd9ff5abddffbfab06',
    '49d91578d4cb48a89c91a8f29648d884',
    '41df4cf1d6194f38ae6f8901326f27ea',
    'dd3c4050bba845acbd40d4ebd59f60f9',
    '20d530788ec54dfaa998f564fa0eed54',
    '2dd7693907354fa49e271eeba79a3c3d',
    '7ca04529cd1445e2b8b4df2edf982944',
    '8a8631b96f394283b65c8acc7b118ef6',
    '9730e9e2521d42829fcce6c47ee6e714',
    '0062ddbfd9994cdea21bcfbe8822469b',
    '-',
    'af10e55f740549e293a9d8793094557e',
    '325e13e4d0b6424a9041ce9a6e2a0936',
    '9fb2c2e3d05d4ad8855dd057111a0372',
    '9955c67d6039457e897db4fbb0e4213e',
    '383f42c488e2446c8a209826c21e07a4',
    'ac7e796c016d437e95c4904edecb5706',
    '08509a488f674143a3a565e3672cc1f3',
    '56c8088875ac4d3981021f28795ee7cb',
    '87ff10e325e44ef5a865af4a9948d0cd',
    '3503b2aaa8a849d2a2c2a157594922d9',
    '62657b2522be4905b6396f6d4a45e42d',
    '6ef4148deffd487f887ad5e77eb8b639',
    '294146dd81e04d65bb3499dc2c531227',
    '778a2ea5e7254351aa3d7b0a6ee7a6a4',
    '7ab16383308f4aef80bb91816aaa1571',
    '9ecbdf2d1fdc43b1a9ebb7b703681d1b',
    '364ba40d5cd24cb9be9df68087b9ba50',
    '20c7028e5460482987821c8c8bd44d11',
    '1053bab67a544877a6124b13a1aafd6c',
    '9b8a02b7c3c044a8bae0d22e15296088',
    'cc97fc32d3234500beea4f2a866a5788',
    'a19ef2dee5db46a18e510770002b4108',
    '992e27034ca84cfb82cdaa43e1c1e739',
    '7c228f634ce94bdbbb11c89758f60c00',
    '8c8c596dee1247d09b4d4317af1ef731',
    '1a8957f176bc4739b34d0d3331cda8f5',
    '2f6c2be2e48f49f78decf349e63265de',
    '72f1e5f5cb004912a7557d72e0f7f652',
    'fe15dabd9d984bd489c1478fd18ffe6c',
    '2b8972952cfc497d8826e8021a7d8d92',
    'bf987c51ea5c4ce78ec811d9288481d3',
    'd7b9fbea37954e329ee8608004d2da05',
    '87e9e9a543f9438490c1e59a63926f07',
    '6b558f2e86ad4696a3001ecf9fe4b21d',
    '9aeed1e21fbf4158b908aec943087eb0',
    'ac89a5dda0e143e894d435ac8995d24d',
    '2a15df822c0a4eb8a788e572afa4742c',
    '4c2c0ef25e234098bfb5f18c732a28a9',
    '1cbd413b3a61499bb810f2883303cb6a',
    '969298f797cc4265a3f2e87e4dfdc518',
    'ddac82a533ed48e98fa6b4de3a06feee',
    '3e2e6df232094bb9886b7595096e3e6b',
    '59ded02ca64c4c0ab3d5ede710371123',
    '40661ccbe2e644bc8172922b124a1710',
    'a3fa8d41ab654d56af396a54db24f7b5',
    'f92214baa3db40fabcfdb175a04ade66',
    'db8627874e774b539389f143ab1e0f8c',
    '0d6952220feb4a13a0f93e205b8d62cb',
    '5c3dd326f2da4057aa952256f45305ab',
    '750a698c5ad846f88f87aae00540505f',
    'cf73a3dd6d0c41fc9d9dbda95a4cf536',
    '008128a294ce42afa12f0ef90591aa7b',
    '729157e0c72a4f2087223661836d948f',
    'f8bbac1ddf5c4efb85cec18546a8088a',
    '173b8aa9dd664a33917e04adeff44684',
    '554ab71d3c0a41b1ab60ee7b1b758fae',
    '04a6af281972401d96674bff6fe767bb',
    'c3cb3544785f4a61a5fdf3ab78306561',
    '98a87ce236174e7ab69034aa38f659ba',
    '7f1654c5efa24dadba3703d82f97c45f',
    '5034a1b0f66a4094aabaa2a0d3bdc3b4',
    '9ff967e2f9ec40aab85a6501e8ce4d60',
    'bf0cc5b403c241c8b9323ddb2489a76e',
    '69e96fc4cf254c88a0cc92e7842ed4c8',
    '5f0920c74fef413c9a04aea044b93d7e',
    '4f6ebdd6a504445b9a38020f5a0e7aa7',
    'e00862ac9dc44ee8b3bb1e2c02162fb9',
    '9eec67c764014c139a392175e17a9998',
    '2ff76133ce0047a18219f072ab3adb09',
    '61cadeae84d449f197fad9736bf7921d',
    'f2238ccf20c84cd99336c165e8b40115',
    '25953a714f064300ae9d9d3c684dc6ae',
    '17bd953399894c17a7f96a7d9a14af9f',
    'f9098752968f478491a6d5d0dad456b9',
    '651bf4d967544a08994d1f17fd52bea8',
    '581ddc7f75be466ab3fbcc51d4ee0ffb',
    'bb9007a6f55a4a6daa5271418f0c16e5',
    '0696c78302d34b8cb9122e91c80fd935',
    'fe6afa3b778243548a36e7df3d8e7f68',
    'f5f121881568425d80b6cf2fe3f09e0d',
    '9100fcd3470f4c0f88b403f12eaaf65a',
    '3e1c91e67ff54838b28566f72478e3c6',
    '70689f17ac39440c91b4b0a82e77c58c',
    'd5deac6df499466680d6b6e74d86734c',
]


def get_sign_config(contId: str, appVersion: str = '2500090310'):
    tm = f'{time.time() * 1000 - 1000 * 1000:.0f}'
    md5string = hashlib.new('md5', f'{tm}{contId}{appVersion[:8]}'.encode('utf-8')).hexdigest()
    return tm, url_sign(md5string)


def url_sign(md5string: str):
    ''' 原算法两次随机数合并为一次 所以这里限定了下范围 '''
    salt = f'{randint(10000000, 99999999)}'
    text = f'{md5string}{SALT_TABLE[int(salt[6:]) % 100]}migu{salt[:4]}'
    sign = hashlib.new('md5', text.encode('utf-8')).hexdigest()
    return [salt, sign]


if __name__ == '__main__':
    print(get_sign_config('714725402'))
    # print(url_sign('a42002edf5fdf989cb63a07327eb804c'))

传入参数也是一个md5,根据调用栈,上一级是getVideoInfoParams

com.cmvideo.foundation.videocache.processor.VideoInfoProcessor.getVideoInfoParams(VideoInfoProcessor.java:205)

不过dump下来的dex没有找到这个类,有可能是前面去除不能打开的类的时候去掉了

好在在com.cmvideo.capability.mgplayercore.net.VideoDetailsRequestHelper里面有同样的方法

这个位置是传入字符串与时间戳与某个id相拼接

那么直接hook这个方法

android hooking watch class_method com.cmvideo.capability.mgkit.util.MD5Util.getStringMD5 --dump-args --dump-backtrace --dump-return

得到的传入参数是类似这样的

com.cmvideo.capability.mgkit.util.MD5Util.getStringMD5(162962756341271472540225000903)

构成如下

  • timestamp 1629627563412
  • contId 714725402
  • appVersion/X-UP-CLIENT-CHANNEL-ID截取前8位 25000903

请求头中的剩余校验参数后面再分析...

剩下的几个参数都是请求头里面的,构造链接很有可能会用java.net.URI,那么追踪一波

android hooking watch class_method java.net.URI.$init --dump-args --dump-return --dump-backtrace

发现确实有https://play.miguvideo.com/

(agent) [227076] Called java.net.URI.URI(java.lang.String)
(agent) [227076] Backtrace:
        java.net.URI.<init>(Native Method)
        okhttp3.HttpUrl.uri(HttpUrl.java:379)
        okhttp3.internal.connection.RouteSelector.resetNextProxy(RouteSelector.java:129)
        okhttp3.internal.connection.RouteSelector.<init>(RouteSelector.java:63)
        okhttp3.internal.connection.StreamAllocation.<init>(StreamAllocation.java:101)
        okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:113)
        okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
        okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
        com.cmvideo.capability.networkimpl.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.java:68)
        okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
        okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
        okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:257)
        okhttp3.RealCall$AsyncCall.execute(RealCall.java:201)
        okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
        java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        java.lang.Thread.run(Thread.java:923)

(agent) [227076] Arguments java.net.URI.URI(https://play.miguvideo.com/)

根据经验hook一下okhttp3.Request$Builder.build

android hooking watch class_method okhttp3.Request$Builder.build --dump-backtrace --dump-return

结果如下

(agent) [925216] Called okhttp3.Request$Builder.build()
(agent) [925216] Backtrace:
        okhttp3.Request$Builder.build(Native Method)
        okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93)
        okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
        okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:127)
        okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
        okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
        com.cmvideo.capability.networkimpl.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.java:68)
        okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:147)
        okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:121)
        okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:257)
        okhttp3.RealCall$AsyncCall.execute(RealCall.java:201)
        okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
        java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        java.lang.Thread.run(Thread.java:923)

(agent) [925216] Return Value: Request{method=GET, url=https://play.miguvideo.com/playurl/v1/play/playurl?chip=msmnile&salt=44076543&os=11&xavs2=true&startPlay=true&nt=4&sign=a1845b27294d7461b801286b47d37fbd&xh265=true&sessionId=************&ua=Pixel%204&dolby=false&gpu=&ott=false&hdrversion=7474174&rateType=4&isRaming=0&contId=714725402&isMultiView=true&vr=true&drm=true&timestamp=1629634666853&hdrmode=Pixel%204, tags={}}

但是这个调用栈总是好几个请求都重复出现了

经过测试,最终有一个更为明确的调用栈

(agent) [009406] Called okhttp3.Request$Builder.build()
(agent) [009406] Backtrace:
        okhttp3.Request$Builder.build(Native Method)
        com.cmvideo.capability.networkimpl.OkhttpNetworkManager.get(OkhttpNetworkManager.java:739)
        com.cmvideo.capability.network.NetworkManager2.get(NetworkManager2.java:164)
        com.cmcc.cmvideo.content.network.BaseResponseRequest.loadData(BaseResponseRequest.java:44)
        com.cmcc.cmvideo.content.network.BaseResponseObject.subscribe(BaseResponseObject.java:35)
        com.cmcc.cmvideo.content.ContentServiceImpl.getVideoInfo(ContentServiceImpl.java:157)
        com.cmvideo.foundation.videocache.processor.VideoInfoProcessor.getVideoInfo(VideoInfoProcessor.java:104)
        com.cmvideo.foundation.videocache.processor.VideoInfoProcessor.run(VideoInfoProcessor.java:65)
        com.cmvideo.foundation.videocache.chain.VideoInfoInvocationChain.getVideoInfo(VideoInfoInvocationChain.java:58)
        com.cmvideo.foundation.videocache.CacheController.getVideoInfo(CacheController.java:138)
        com.cmcc.cmvideo.player.PlayHelper.getVideoInfo(PlayHelper.java:984)
        com.cmcc.cmvideo.player.PlayHelper.preparePlayData(PlayHelper.java:776)
        com.cmcc.cmvideo.player.PlayHelper.preparePlayData(PlayHelper.java:725)
        com.cmcc.cmvideo.playdetail.widget.MgPlayPageFragment.getPlayUrl(MgPlayPageFragment.java:3481)
        com.cmcc.cmvideo.playdetail.widget.MgPlayPageFragment.onResume(MgPlayPageFragment.java:2290)
        android.support.v4.app.Fragment.performResume(Fragment.java:2498)
        android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1501)
        android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1784)
        android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1852)
        android.support.v4.app.FragmentManagerImpl.dispatchStateChange(FragmentManager.java:3269)
        android.support.v4.app.FragmentManagerImpl.dispatchResume(FragmentManager.java:3241)
        android.support.v4.app.FragmentController.dispatchResume(FragmentController.java:223)
        android.support.v4.app.FragmentActivity.onResumeFragments(FragmentActivity.java:538)
        android.support.v4.app.FragmentActivity.onPostResume(FragmentActivity.java:527)
        android.support.v7.app.AppCompatActivity.onPostResume(AppCompatActivity.java:172)
        android.app.Activity.performResume(Activity.java:8154)
        android.app.ActivityThread.performResumeActivity(ActivityThread.java:4428)
        android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4470)
        android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)
        android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        android.os.Handler.dispatchMessage(Handler.java:106)
        android.os.Looper.loop(Looper.java:223)
        android.app.ActivityThread.main(ActivityThread.java:7664)
        java.lang.reflect.Method.invoke(Native Method)
        com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

(agent) [009406] Return Value: Request{method=GET, url=https://play.miguvideo.com/playurl/v1/play/playurl?chip=msmnile&salt=42709724&os=11&xavs2=true&startPlay=true&nt=4&sign=ee4249aba30ebb0c6191d13e85a68d8a&xh265=true&sessionId=************&ua=Pixel%204&dolby=false&gpu=&ott=false&hdrversion=7474174&rateType=4&isRaming=0&contId=714725402&isMultiView=true&vr=true&drm=true&timestamp=1629635337448&hdrmode=Pixel%204, tags={}}

这个类又出现了

  • com.cmvideo.foundation.videocache.processor.VideoInfoProcessor

看来得拿到这个类的代码才行,又试了几次,还是没有dump下来

试一试BlackDex

然而失败了

不要紧,掏出专用脱壳机

啪的一下,很快啊就脱完了

不幸的是函数体被抽取了,而且是带偏移的抽取TAT

好在还有一些是没有被抽取的,接着又再次搜索sign关键字

然后定位到com.cmcc.cmvideo.layout.livefragment.network.RetrofitNetworkManagerEx

还是有收获的,比如SDKCEId是固定值

另外再看看请求头sign的来源

另外经过对比发现下面请求头l_c也是固定的

其中sign可以在APP目录下的app_webview/Default/Cookies找到

l_c则可以在APP目录下的files/mmkv/mmkv.default找到(sign同时也在这个文件)

这些请求头,可以知道是com.cmcc.cmvideo.foundation.network.NetworkManager.addCommonHeader添加处理的

不过hook这个方法没有看到l_s,说明应该是直接操作的存放请求头的对象做的添加

可以看到请求头放在HashMap中,那么推测l_s可能就是取了这些值然后计算出来的

(agent) [391494] Called com.cmcc.cmvideo.content.network.BaseResponseRequest.getCustomHeaders()
(agent) [391494] Backtrace:
        com.cmcc.cmvideo.content.network.BaseResponseRequest.getCustomHeaders(Native Method)
        com.cmcc.cmvideo.content.network.BaseResponseRequest.loadData(BaseResponseRequest.java:44)
        com.cmcc.cmvideo.content.network.BaseResponseRequest.loadData(Native Method)
        com.cmcc.cmvideo.content.network.BaseResponseObject.subscribe(BaseResponseObject.java:35)
        com.cmcc.cmvideo.content.ContentServiceImpl.getVideoInfo(ContentServiceImpl.java:157)
        com.cmvideo.foundation.videocache.processor.VideoInfoProcessor.getVideoInfo(VideoInfoProcessor.java:104)
        com.cmvideo.foundation.videocache.processor.VideoInfoProcessor.run(VideoInfoProcessor.java:65)
        com.cmvideo.foundation.videocache.chain.VideoInfoInvocationChain.getVideoInfo(VideoInfoInvocationChain.java:58)
        com.cmvideo.foundation.videocache.CacheController.getVideoInfo(CacheController.java:138)
        com.cmcc.cmvideo.player.PlayHelper.getVideoInfo(PlayHelper.java:984)
        com.cmcc.cmvideo.player.PlayHelper.preparePlayData(PlayHelper.java:776)
        com.cmcc.cmvideo.player.PlayHelper.preparePlayData(PlayHelper.java:725)
        com.cmcc.cmvideo.playdetail.widget.MgPlayPageFragment.getPlayUrl(MgPlayPageFragment.java:3481)
        com.cmcc.cmvideo.playdetail.widget.MgPlayPageFragment.onResume(MgPlayPageFragment.java:2290)
        android.support.v4.app.Fragment.performResume(Fragment.java:2498)
        android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1501)
        android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1784)
        android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1852)
        android.support.v4.app.FragmentManagerImpl.dispatchStateChange(FragmentManager.java:3269)
        android.support.v4.app.FragmentManagerImpl.dispatchResume(FragmentManager.java:3241)
        android.support.v4.app.FragmentController.dispatchResume(FragmentController.java:223)
        android.support.v4.app.FragmentActivity.onResumeFragments(FragmentActivity.java:538)
        android.support.v4.app.FragmentActivity.onPostResume(FragmentActivity.java:527)
        android.support.v7.app.AppCompatActivity.onPostResume(AppCompatActivity.java:172)
        android.app.Activity.performResume(Activity.java:8154)
        android.app.ActivityThread.performResumeActivity(ActivityThread.java:4428)
        android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4470)
        android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)
        android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        android.os.Handler.dispatchMessage(Handler.java:106)
        android.os.Looper.loop(Looper.java:223)
        android.app.ActivityThread.main(ActivityThread.java:7664)
        java.lang.reflect.Method.invoke(Native Method)
        com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

苦于不能指令被抽取...

何不大胆猜测这个值是native计算出来的,直接看jni好了

但是还是没有发现

那么考虑添加请求头可能的地方,okhttp3.Request$Builder.headers方法刚好和前面VideoInfoProcessor.getVideoInfo对应上了

(agent) [989471] Called okhttp3.Request$Builder.headers(okhttp3.Headers)
(agent) [989471] Backtrace:
        okhttp3.Request$Builder.headers(Native Method)
        com.cmvideo.capability.networkimpl.OkhttpNetworkManager.get(OkhttpNetworkManager.java:738)
        com.cmvideo.capability.network.NetworkManager2.get(NetworkManager2.java:164)
        com.cmcc.cmvideo.content.network.BaseResponseRequest.loadData(BaseResponseRequest.java:44)
        com.cmcc.cmvideo.content.network.BaseResponseObject.subscribe(BaseResponseObject.java:35)
        com.cmcc.cmvideo.content.ContentServiceImpl.getVideoInfo(ContentServiceImpl.java:157)
        com.cmvideo.foundation.videocache.processor.VideoInfoProcessor.getVideoInfo(VideoInfoProcessor.java:104)
        com.cmvideo.foundation.videocache.processor.VideoInfoProcessor.run(VideoInfoProcessor.java:65)
        com.cmvideo.foundation.videocache.chain.VideoInfoInvocationChain.getVideoInfo(VideoInfoInvocationChain.java:58)
        com.cmvideo.foundation.videocache.CacheController.getVideoInfo(CacheController.java:138)
        com.cmcc.cmvideo.player.PlayHelper.getVideoInfo(PlayHelper.java:984)
        com.cmcc.cmvideo.player.PlayHelper.preparePlayData(PlayHelper.java:776)
        com.cmcc.cmvideo.player.PlayHelper.preparePlayData(PlayHelper.java:725)
        com.cmcc.cmvideo.playdetail.widget.MgPlayPageFragment.getPlayUrl(MgPlayPageFragment.java:3481)
        com.cmcc.cmvideo.playdetail.widget.MgPlayPageFragment.onResume(MgPlayPageFragment.java:2290)
        android.support.v4.app.Fragment.performResume(Fragment.java:2498)
        android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1501)
        android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1784)
        android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1852)
        android.support.v4.app.FragmentManagerImpl.dispatchStateChange(FragmentManager.java:3269)
        android.support.v4.app.FragmentManagerImpl.dispatchResume(FragmentManager.java:3241)
        android.support.v4.app.FragmentController.dispatchResume(FragmentController.java:223)
        android.support.v4.app.FragmentActivity.onResumeFragments(FragmentActivity.java:538)
        android.support.v4.app.FragmentActivity.onPostResume(FragmentActivity.java:527)
        android.support.v7.app.AppCompatActivity.onPostResume(AppCompatActivity.java:172)
        android.app.Activity.performResume(Activity.java:8154)
        android.app.ActivityThread.performResumeActivity(ActivityThread.java:4428)
        android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4470)
        android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:52)
        android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        android.os.Handler.dispatchMessage(Handler.java:106)
        android.os.Looper.loop(Looper.java:223)
        android.app.ActivityThread.main(ActivityThread.java:7664)
        java.lang.reflect.Method.invoke(Native Method)
        com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

(agent) [989471] Return Value: [email protected]

...

最终经过测试确定只要请求头有appVersion即可,这里是2500090310

那么...l_s暂时就不研究了吧...

ENJOY 5

none

最后编辑于2个月前

添加新评论

avatar

未末

118

文章数

260

评论数

7

分类

新鲜出炉の评论

获取CSDN学院m3u8解密的key
路人甲
路人甲2021-10-05

请问这个方法失效了么?获取到的是空串

获取CSDN学院m3u8解密的key
DK爱梦游
DK爱梦游2021-09-29

大佬求教,51cto的KEY怎么获取?

XstreamDL-CLI BUG修复记录
poohboy
poohboy2021-09-25

大佬,我想问一下,iqiyi的m3u8是不是没法获取?我只找到了一个dash链接,然后手动下载了里面的m4s,但用nilaoda的那个解密工具解不了,老提示获取kid失败

XstreamDL-CLI BUG修复记录
Andist
Andist2021-09-17

en……实在不好意思,代理我知道怎么启用了。我只勾选了自定义代理,但没有填写proxy参数,我太愚钝了对不起!!!

XstreamDL-CLI BUG修复记录
Andist
Andist2021-09-17

感谢您开发的这款软件,对于第一次下载mpd的小白而言很友好! 这段时间用下来就是有时候下载直连的海外视频流时可能因为网络状况不佳,会有下载不完整的问题,下载完进度没到100%,但是也合并解密了,不知道能否增加下载不完整在最后输出报错信息的功能呢? 以及我想请教一下如何让下载器使用小飞机的代理呢?我尝试在“使用自定义代理”的选项上勾选,但是好像命令行中没有变化? (另外我猜您图中的样本是在下载CP+上的用九柑仔店是吗?我也很喜欢这部剧,是我心目中排名第一的台剧哈哈)