目标apk与md5
- com.cmcc.cmvideo_5.9.3.10_25000599.apk
- 81d38496ac24e05e3b9f1fc79cfb4d6d
初步静态分析
不好,有壳,那么先脱壳一波再说https://github.com/hluwa/FRIDA-DEXDump
脱出来的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'}, com.cmvideo.foundation.bean.player.VideoInfoBean@22b3bf9) (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.cmvideo.foundation.bean.player.VideoInfoBean$BodyBean$MediaFilesBean@444973e, mediaFiles=[com.cmvideo.foundation.bean.player.VideoInfoBean$BodyBean$MediaFilesBean@f3d799f, com.cmvideo.foundation.bean.player.VideoInfoBean$BodyBean$MediaFilesBean@8356aec, com.cmvideo.foundation.bean.player.VideoInfoBean$BodyBean$MediaFilesBean@3da9eb5], contents=null, previewPicture=null, auth=com.cmvideo.foundation.bean.player.VideoInfoBean$BodyBean$AuthBean@56ee94a, star=null, urlInfos=[com.cmvideo.foundation.bean.player.VideoInfoBean$BodyBean$UrlInfoBean@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(com.cmcc.cmvideo.application.MGApplication@a9d9282, 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 RX@0x400052ef[libmgencryptor.so]0x52ef JNIEnv->RegisterNatives(com/cmcc/migutv/encryptor/MGEncryptor, RW@0x4000f004[libmgencryptor.so]0xf004, 1) was called from RX@0x40005305[libmgencryptor.so]0x5305 RegisterNative(com/cmcc/migutv/encryptor/MGEncryptor, getSignFromNative(Landroid/content/Context;Ljava/lang/String;)[Ljava/lang/String;, RX@0x4000a731[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参数算法如下
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.(Native Method) okhttp3.HttpUrl.uri(HttpUrl.java:379) okhttp3.internal.connection.RouteSelector.resetNextProxy(RouteSelector.java:129) okhttp3.internal.connection.RouteSelector.(RouteSelector.java:63) okhttp3.internal.connection.StreamAllocation.(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×tamp=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×tamp=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: okhttp3.Request$Builder@30d2e1e
最终经过测试确定只要请求头有appVersion
即可,这里是2500090310
那么…l_s
暂时就不研究了吧…
这个看不懂?