本文为看雪论坛优秀文章
看雪论坛作者ID:xwtwho
初衷是刷抖音太多,发现不能在点赞过的视频列表中直接搜索,就想自己实现下,把这个过程做了下记录,当学习笔记了,纯技术交流用。抖音web (V16.1)
IDA 7.5
Frida 14.2.2
Gda3.86
JEB
jadx-gui
unidbg
LineageOs 17.1 (android 10)
小米8
https://blog.csdn.net/weixin_48271161/article/details/108544446在这个V15.7版本里面没找到相关的so(后来发现我看的这个版本对应的是libmetasec_ml.so)。还有其它一些,比如搜索X-Gorgon,hook HashMap,发现对这个版本都不好使了,那就从头开始了,自己找这个加密关键点。现在回过头看,发现当时就输出了libmetasec_ml.so jni记录(虽然不是直接的加密函数),但是当时可能是这个记录太多了,竟然没注意到,估计当时看到了也没留意,因为开始也不知道这个so是做什么的。https://segmentfault.com/a/1190000021095757https://blog.csdn.net/u010983881/article/details/97770544/然后根据调用关系翻代码,找Cronet相关调用,浏览代码,翻到com.ttnet.org.chromium.net.impl.CronetUrlRequest,也没发现有设置X-Gorgon头的地方(其它header倒是有设置),中间也尝试通过send反找,调用线太长,还是决定换个方式来做。https://github.com/hanpfei/chromium-net然后就是对着代码,确定认为会相关的几个函数,上IDA调试:nativeCreateRequestAdapter到这里就跟libmetasec_ml.so关联起来了。知道了调用点,就想直接调用libmetasec_ml.so方便调试,写了个程序来加载这个so,发现会异常,考虑到可能是上下文环境不全,就打算按正常流程来加载so,也调用JNI_OnLoad:void* hMetasecSo = dlopen("/data/local/tmp/libmetasec_ml.so", RTLD_LAZY); typedef jint(*FUN)(JavaVM* vm, void* res); FUN func_onload = (FUN)dlsym(hMetasecSo, "JNI_OnLoad");这个调用是需要JavaVM参数的,就准备加载libart来调用JNI_CreateJavaVM创建JavaVM,参考网上资料设置好参数: options[0].optionString = "-Djava.class.path=."; vm_args.version = JNI_VERSION_1_6; vm_args.options = options; vm_args.ignoreUnrecognized = JNI_TRUE;编译运行后,直接崩了,查看日志,提示没有设置NoSigChain。然后查看android源码,找到对应地方看了下,是在检查创建VM的选项参数,应该是没有no-sig-chain这个参数,网上搜了下,没有找到怎么设置这个参数的,然后根据代码,结合其它属性改了下代码,增加了一条:options[1].optionString = "-Xno-sig-chain";android源码里面也跳过了这个相关的,编译运行,之前的错误就没有了,但是后面还是异常退出了,后来查了下,找到下面信息:从Android N开始(SDK >= 24),通过dlopen打开系统私有库,或者lib库中依赖系统私有库,都会产生异常,甚至可能导致app崩溃。应用可以调用/vendor/etc/public.libraries.txt和/system/etc/public.libraries.txt里面的所有so库,所以往这个文件写入自己希望被调用的so,这个库就变成共用的了,任意应用就可以找到这个so库了。试了下上面的方法,包括把libart.so及相关的库放到其它用户目录下,还是不行,考虑到本来就是想还原运行环境的,就想直接上APK吧,还能省去自己创建VM,就写了个测试APK调用这个so:Didn't find class "com.bytedance.mobsec.metasec.ml.MS"现在能运行了,但是调用JNI_OnLoad会异常,准备上IDA调试,看了下流程图:带了llvm,看so文件尾部记录的是Apple LLVM version 10.0.1 (clang-1001.0.46.3)。跟了下,一些跳转都做了处理,不是很好分析,准备上unidbg (https://github.com/dqzg12300/unidbg_tools,可以用这个大佬整理的),trace代码下来分析。根据trace得到的指令流,发现有这种访问"/proc/self/exe"路径返回-1的情况,我是直接修改这个函数,直接返回1了,这个函数只是用来取e_machine字段的:后面继续trace,然后结合动态调试,发现有代码校验的地方:上面都处理后,测试apk就可以正常跑完JNI_OnLoad了,后面就是主动调用加密函数测试了,直接在jni接口中调用libmetasec_ml.so:下面插个分支,到这里的时候,在一个群里看到信息说抖音开了web,直接去看了下。2、抖音web请求参数_signature算法分析直接访问www.douyin.com,看访问参数多了个_signature,这种格式:&_signature=_02B4Z6wo00901qf0GiQAAIDAwkLkeQfbXMKn9B6AAMkm74。多拿几个比较,发现前面一段(_02B4Z6wo00901)是前缀,后面初步判断是base64格式。网络爬虫-今日头条_signature参数逆向(第一弹)_井蛙不可语于海的博客-CSDN博客_byted_acrawlerhttps://blog.csdn.net/qq_39802740/article/details/104911315主要加密算法在acrawler.js,参考这个用node跑起来了,里面的实现是一个js 虚拟机,关于虚拟机还找到这篇:StriveMario/jsvm: 给"某音"的js虚拟机写一个编译器 (github.com)https://github.com/StriveMario/jsvm不过目前版本都看起来跟这个不同了,并且本地调试算法其实也用不上,但是还是可以学习下的。然后就可以调用global.byted_acrawler.init及global.byted_acrawler.sign参数了(这个后面发现这样调用的算法流程跟浏览器跑的其实是不同的)。用浏览器调试也是确定有init ,sign函数的:后面就是直接用node调试,因为原代码的格式都是这样的:都是一句话写完逻辑,不方便调试,先整理了下代码,比如这种opCode = 3 & initCode; // initCode % 4调试中把vm的基础操作整理出来(vm_xor,vm_and等等),特别字符串连接的指令,可以加上日志,方便定位。domDetect
debuggerDetect
nodeDetect
phantomDetect
webdriverDetect
incognitoDetect
hookDetect
locationDetect
当时看了几个签名数据,就有个疑问的,相同的数据,_signature总是不同的,当时想到应该有个变量因子的,但是看https提交的参数没发现这个变量,那这样就是直接记录在_signature中了。<Buffer 7c 7e 62 a4 00 00 20 30 83 81 9d 5b 28 d0 87 e9 7c 76 e3 80 00 07 26 f8>比较多个后也没发现明显的变量因子,猜到可能是时间戳,但是没找到哪个数据段是标识的时间,那就直接开始看流程了。调试过程发现有getTime的调用,直接改为固定的,最后得到的_signature就不会变了:那就是确定跟时间戳相关了,也就是服务器可以通过这个得到时间戳或者转换过的值,来验证客户端上报的_signature是否正确了。后面算法中,主要就是参数字符串(location,user_agent,param)的处理(xor,or,and等)得到各个hash值,然后类似base64加密得到加密字符串(这里不是直接得到完整明文,再最后一次base64的方式):处理完后,最后2个字符是附加的校验字符,是对前面数据得到一个DWORD值的低字节:_02B4Z6wo00f014U9W.wAAIDB4IuloLYVYYOFPV9AAIGw_02B4Z6wo00f014U9W.wAAIDB4IuloLYVYYOFPV9AAIGwa2本以为搞完了,直接跟浏览器访问的一比较,悲剧了,竟然加密结果不同,直接上浏览器调试。前面已经整理了代码的,浏览器访问的时候js是直接下载的,直接修改DNS,把这个js定向到本地服务器:127.0.0.1 sf1-ttcdn-tos.pstatp.com这样浏览器访问的时候也是用的整理后的js了,有了前面的调试经历,这个也很快搞清楚了流程。data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAQCAYAAABQrvyxAAACfElEQVRIS9WVO2hUURCGv20Eg1bapVC0sFNJJahFRPDRWFgoRokgQbAQLFJJdNFKFAVBNMROI1oYsBITsPDRpAgkNkmjEoggqIWNIILyywwMs3MxhevjwLLs3XPnzP+a0+L/XZuAE63U/wBwNz0bBWYAfcd1DrjYJfxrgHFgT9HLGeALUAIYAS6kl54A+lxNz3cAL7sEoCq7HdgVSCsBVMiPWrWszJ8EsBK4BjwEDgInrafRbKEIQJa5BawDViVrzQKHgAUrJOsdM3ZkrdP2n1sy203sPQC2eCOAW6NiX3U2JMuWCuTCKlYpIADDwBQgaV+ERpSXPgN/BLgHXAqgnCTlRxZ0dp+b7zMANb/TAPakbHQoMARMAB8BZ6/JQsqF9uw1dp7ayfJpL/AZeAR8AK4HABGwKxObVEC1HKhAVsOiQwFnQv66b0UO2/RRM2I8B1o5mA/M6aAbwHlgH3AWWJsAqLQOj6AyAA0TAdVzkVmtDgDR/xnAehtpss5kAKPfYqxtJ+hQHX4T2GzM5Wa11fd5g5UC3nRla7f2eA6xj1GpIB/rWzK/thDHO0FqSBkBup0AvAXumMcjgEWbJqrrFnS7us/dQhVQB+VuWaoAyL+PzQIRgCykMfbebKUGtgZl/EAFOjYXAWjPfmDalPqVAiJU2aruG9VtRwAxA1EBsf4MGATehFt5DvgE9AeDekDj2GxSQMNBt61Wk4VUT5mKI1v7SwWUAdlmm7HsFrpsDOgu0Cx2O30DTgFjDSH7XY+bMvDzIs0WWs6hq80i8r6HdznvdWVPBWAF8LXhNEl9xTLw15tXjxWAA8Bxu4DeAd+BjcBu4JVNEYX8n1g/AAC2uh6gEsDjAAAAAElFTkSuQmCC整体来看,web上虽然用了JS虚拟机,跟二进制的VM比起来还是弱些了,调试环境弄好后,执行流程就都比较清晰了。被上个分支中断了下,思路都断了,又重新熟悉了下,继续开始跟这个加密算法。在测试apk中调用libmetasec_ml.so加密函数后,返回的是NULL,调试后发现会从native调用java的情况:然后参考抖音补全需要的包,里面有热更新保护相关的代码,屏蔽掉,让测试工程能跑起来就行://import com.bytedance.JProtect;
//import com.bytedance.covode.number.Covode;
//import com.meituan.robust.ChangeQuickRedirect;
//import com.meituan.robust.PatchProxy;
//import com.meituan.robust.PatchProxyResult;通过分析trace日志,过滤掉一些跳转计算流程后,发现可能的关键调用,用unidbg跑的时候返回是null:下面就是调试抖音进行验证了,修改对应指令为循环点,附加后循环处下断点://修改so的起始地址
*(unsigned int *)(dwContextAddr+0x10)=0x100000;
//修改结束地址
*(unsigned int *)(dwContextAddr+0x10)=0xFFFFFFFF;现在是自己写的程序可以跑了,后面是用app中提供签名服务,还是撸算法出来,都方便很多了。3、RSA验签过程,其它工作涉及的,之前只是使用API(//2.RSA解密密文(一般是base64解密后的字节流)//3.比较尾部的串是否跟1的一致(因为解密后的结果是包含这个hash串,不是等于)
看雪ID:xwtwho
https://bbs.pediy.com/user-home-44250.htm
*本文由看雪论坛 xwtwho 原创,转载请注明来自看雪社区