1、上次自己构造了一个app来调用x音的关键so,结果在一条“LDR R0, [R4,#0xC] “语句卡住了:通过ida查看得知:R4就是第三个参数,这里被当成了地址使用(java层怪不得用long类型)!第三个参数我是用frida hook得到的,换了个环境地址肯定也变了,所以这里直接”抄袭“拿过来用肯定报错,这种反调试的方法实在是秒啊!动态调试暂时卡壳,我们先来静态分析一下卡住语句的上下游:
.text:000139A4 000 F0 B5 PUSH {R4-R7,LR} .text:000139A6 014 03 AF ADD R7, SP, #0xC .text:000139A8 014 2D E9 00 0F PUSH.W {R8-R11} .text:000139AC 024 93 B0 SUB SP, SP, #0x4C .text:000139AE 070 CD E9 03 23 STRD.W R2, R3, [SP,#0x68+var_5C] .text:000139B2 070 05 46 MOV R5, R0 .text:000139B4 070 3A 48 LDR R0, =(__stack_chk_guard_ptr - 0x139BC) .text:000139B6 070 0C 46 MOV R4, R1 .text:000139B8 070 78 44 ADD R0, PC ; __stack_chk_guard_ptr .text:000139BA 070 00 68 LDR R0, [R0] ; __stack_chk_guard .text:000139BC 070 01 90 STR R0, [SP,#0x68+var_64] .text:000139BE 070 00 68 LDR R0, [R0] .text:000139C0 070 38 49 LDR R1, =(unk_99110 - 0x139CA) .text:000139C2 070 12 90 STR R0, [SP,#0x68+var_20] .text:000139C4 070 E0 68 LDR R0, [R4,#0xC] ; 自己构造的app在R4的值是第3个long参数,地址无效 .text:000139C6 070 79 44 ADD R1, PC ; unk_99110 .text:000139C8 070 10 90 STR R0, [SP,#0x68+var_28] .text:000139CA 070 08 31 ADDS R1, #8 .text:000139CC 070 04 30 ADDS R0, #4 ; rwlock .text:000139CE 070 0F 91 STR R1, [SP,#0x68+var_2C] .text:000139D0 070 F3 F7 36 EF BLX pthread_rwlock_rdlock .text:000139D4 070 11 90 STR R0, [SP,#0x68+var_24]
卡住的语句后面做了注释:从R4+C的地方取4字节数据存入R0,然后把R0存到栈上;接着把R0+4,这里ida已经识别出了是rwlock读写锁,然后就是调用pthread_rwlock_rdlock获取读写锁的读锁!这就很关键了:
(1)为什么要对数据加读锁了而不是互斥锁了?在互斥机制中,读者和写者都需要独立独占互斥量以独占共享资源;而在读写锁机制下,允许同时有多个读者读访问共享资源,只有写者才需要独占资源。相比互斥机制,读写机制由于允许多个读者同时读访问共享资源,进一步提高了多线程的并发度。这里我个人猜测:加密字段输入了长长的url+http头的很多字段,需要输出4个加密字段,如果让单线程顺序读取并计算,效率很低,影响用户体验,所以使用多线程机制协同:多个线程同时读取输入,分别计算不同的加密字段!
(2)从ida的trace记录看,前面所有的指令都没有读取栈上保存的url+http头的数据,所以前面肯定还没来得及生成那4个加密字段;从这里开始用读写锁,结合上面的分析大胆猜测:接下来要开始生成加密字段了!换个角度,这个函数附近有好些地方都调用pThread_Create函数,从这里开始计算机密字段嫌疑很大!
2、既然卡在了第三个地址参数,自然需要分析一下这个参数的来源,直接上frida,先hook h.a方法,看看这个函数在java层的调用路径:
at ms.bd.c.h.a(Native Method) at ms.bd.c.b.a() at ms.bd.c.u1$a.a(:34013379)
发现是u1$a.a在调用,在jadx静态分析u1$a.a这个方法,发现是
String[] strArr = (String[]) C85964b.m218696a(50331649, 0, C86039u1.this.f357108d, str, (String[]) arrayList.toArray(new String[0])); 这行代码调用了b.a方法,里面涉及到了long参数(已经标黄),而这个long参数是u1类的成员变量,在u1.a方法被设置了,这个函数的参数只有1个long类型,继续hook这个方法:调用栈如下:
神奇地发现:居然还是h.a在调用,形成环形了,唯一的解释:h.a参数不同,生成的结果就不同!之前hook h.a的时候确实也发现了其他参数,但当时目的是追踪这4个加密字段,完全没重视其他参数。哎,悔不当初!
接着追溯:上面调用栈中有一个方法是com.ss.android.ugc.aweme.sec.SecApiImpl.initSec,该方法部分代码如下(太多了,这里只贴一部分);
public final void initSec(Context context, String str, int i, String str2, String str3, boolean z, SecGetDataCallBack dVar) { String str4; if (!PatchProxy.proxy(new Object[]{context, str, Integer.valueOf(i), str2, str3, Byte.valueOf(z ? (byte) 1 : 0), dVar}, this, changeQuickRedirect, false, 338895).isSupported) { C51302a.m136764a(4, "Sec", "initSec"); if (!PatchProxy.proxy(new Object[]{context, str, Integer.valueOf(i), str2, str3, Byte.valueOf((byte) z), dVar}, null, DmtSec.f284302a, true, 338875).isSupported) { DmtSec.f284308g = new SecInitReceiver(DmtSec.C66800a.f284321b); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction("com.ms.init"); DmtSec.m171500a(context, DmtSec.f284308g, intentFilter); SecLogger bVar = SecLogger.f284353b; bVar.mo193744a(SecLogger.f284354c, "init language = " + str + ", aid = " + i + ", appName = " + str2 + ", channel= " + str3); long currentTimeMillis = System.currentTimeMillis(); long currentTimeMillis2 = System.currentTimeMillis(); GlobalContext.setContext(context); C30329a.C30330a aVar = new C30329a.C30330a(String.valueOf(i), "bo95dJizD1WFcV03zOuLzN5Pn1sFtVa3szqiVQmflMJTNW0p0Kpqfw8D4i0zUlfrou4kuYt/i0521YRygM83dwv/wn3DD+TMJF+QFzW9wb8Qq2/1B4jPMbObrDNdyMMukpAYqy1fLWtbLGVIPxsFsZegwQy5lsRX9h49PH/Qx8MwgYvWvH7ZTFLV28LwTWZiljQyBPaBE+TsyumEu0Y+JRkeidHFEYcVs0yRoa+xC004hugQhdPupIt6dBiWA4phsB3fNJZjFTAKGE1lPB4gzt6Qf+FmlgZBbRvT8zekxTV2HZ5dUvSutB2/0QpbHKAvWL4DRA=="); aVar.mo248346a(0); aVar.mo248347a("tk_key", "douyin");
代码里面有写死的字符串,比如tk_key、douyin,还有很长的base64编码的bo95dJizD1WFcV03zOuLzN5Pn1sFtVa3sz....,刚好在hook h.a方法的时候,有一组参数是这样的:
猜测args[3]就是在initSec这里拼凑出来的!回到u1.a方法的调用栈,分析这个栈的思路:一层一层网上追溯,看看到底是哪个地方改变了long参数的值,根据一些magic number,找到了
ms.bd.c.ml.a()调用ms.bd.c.b.a(67108866,str)函数生成了a,a被强制转成了long类型,所以这里重点关注ms.bd.c.b.a(67108866,str)这个函数;由于还有str参数不知道是啥,我们自己调用的时候也不知道怎么传参,所以这里继续hook ms.bd.c.b.a函数,看看传入了什么参数,如下:
发现另一个参数是1128,返回是-1500374080=0xFFFFFFFFA6921BC0,看着确实像个地址;数值上和hook u1.a时看着差异较大,但我能确定就是这个函数生成了long参数,原因:
(1)数值不等可能是js不制obj直接转成long导致的,需要想办法转换类型
(2)从ms.bd.c.ml.a()函数的代码分析,调用ms.bd.c.b.a的返回的值被转成了long使用!
(3)从frida打印函数调用的顺序看,打印了ms.bd.c.u1.a后立即打印ms.bd.c.h.a,说明这两个存在调用关系
既然确认ms.bd.c.b.a返回了long,这里马上用自己的app调用ms.bd.c.b.a(67108866,"1128")试试,结果如下:函数的返回值居然是null!
这里出乎意料,我hook x音app时明明有返回的呀,并不是null!又出啥问题了? 不过这次有明显的改善:自己写的app至少没崩,说明参数设置是没任何问题的,个人猜测是metasec_ml内部可能有些检测机制,发现是第三方调用就返回null!
为了查明原因,继续用ida调试,诡异的现象发生了:F7单步调试的时候遇到跳转的地址有问题,报错!
不能就这样算了啊,调式都不行了,怎么排查了?继续用ida trace,这次在同样的地方居然不报错了(F8步过也不报错,就特么F7步入报错),trace的时候遇到了和用unidbg类似的问题,总式因为mutex卡在死循环这里鬼打墙:实在没办法了,把0x72C8E这4个字节NOP掉,不让上锁!
trace又能顺利执行了!从trace的结果看:R0和R1分别从两个地方取值,然后相减得到的结果保存到R0,也就是返回值,这里是0,怪不得67108866=0x4000002和1128这组参数返回的是null;
00003667 libmetasec_ml.so:F31D0514 LDR R0, [SP,#0xC] R0=0A423869 00003667 libmetasec_ml.so:F31D0516 LDR R1, [R5] R1=0A423869 00003667 libmetasec_ml.so:F31D0518 SUBS R0, R1, R0 R0=00000000 Z=1 N=0
由于不知道原app是怎么跑的(感兴趣的小伙伴也可以用frida stalker trace原app),这里我也没法继续分析R0和R1俩个值为啥相等(因为没有原app trace的对比,我也不知道自己代码执行的路径中哪个分支走错了)!回到我们自己构造的app卡住的代码,如下:
R4就是h.a传入的第三个long函数,这里是个地址,加上0xC后取出值存放在R0,然后又存放在栈上,最后+4后得到rwlock传入pthread_rwlock_rdlock,那么问题来了:原app中b.a(0x4000002,"1128")得到的是R4的地址,又经过一系列算式得到rwlock,为啥我们不自己构造一个rwlock,然后把地址传给R4了?自己从写写个so,里面生成pthread_rwlock_t对象,然后调用pthread_rwlock_init初始化,把对象的地址保存;由于原app中涉及到R4+0xC和R0+4等“偏移”,所以我在c代码里用了结构体;为了便于查找,给rwlock周围的变量复制了0x11111111、0x22222222等数值,结果如下:
从ida调试的结果看,原来卡住的“LDR R0, [R4,#0xC]”的这样代码现在能正常执行了!R0确实指向了rwlock(从内存的值看,调用pthread_rwlock_init初始化后还是0,那初始化还有啥用了?)
继续往下走,又遇到问题:R1明显是我自己构造magic number,这里被当成地址来用了,说明我当初构造的rwlock结构体前面的数值应该是个地址!这里的偏移0xF357465A-0xF3569000=0xb65a;
往上追溯,这里的R1来自R4+4,这里也刚过上一个卡点;
既然找到原因了,继续补呗!在自己的结构体把原来的0x22222222换成地址(注意:根据单步调试自己写的app分析,这里一共有4层指针引用,每层都要自己写指针嵌套引用);补上后继续调试,特么又遇到同样的问题(如下):还是地址为0,只不过换了个地方,说明构造的地址已经通过了上次代码的执行;
往上追溯调试:偏移为0xB8C0的地方,此时R1就是我自己构造的p4指针,但是这里很鸡贼地取了p4+8作为地址,而我并没有构造p4+8,所以内存是0(p4我构造的是0x77777777);这里的p1到p4是我自定义的指针,层层递进引用(建议下载末尾的源码查看具体的定义和调用)!
继续漫漫追溯,发现这里的push把寄存器的值压栈上了,这里的偏移:0x6D970
发现R1和R2还是从栈上取得!这里得偏移:0x13A14
发现是之前R1和R2存栈上得!偏移:0xB8C0,又回到了p4+8的地方!
继续用结构体去补:貌似补上了!
又发现一个地方:0xBE4C;还好自己填的值辨识度高,这里一眼就看出了是那个字段的赋值有问题!继续补上!
继续往下:发现我们自己填的0x22222222被当成跳转的代码地址了,呵呵...... 偏移:13A42
由于是跳转到新地址,此时我也不知道应该补什么地址,只能继续查看原app的跳转地址。这里可以用frida hook,看看context;也可以直接用ida调试原app;我直接用ida,简单方便,查到了这里跳转的地址:偏移是0x14581
来到0x14581这里,发现还有大量的计算逻辑:原本是想把上面的BLX R6直接NOP掉,但是看到还有这么多代码,担心逻辑出错,想想还是算了;只能想办法得到metasec的基址,再加上0x14581的偏移来替代我之前设置的0x22222222,来保证原有的逻辑正常!
至此,so层所有的逻辑都补全了,h.a可以顺利运行完毕,没有任何,但结果却是返回0!和上面调用ms.bd.c.b.a(67108866,“1128”)得到long的结果是一样的!x音原来的app是可以正常运行的,hook的结果也是对的,但自己补全了参数得到的结果是0,直接传参调用的结果还是0,说明:(1) h.a还有其他的反调试或检测逻辑:一旦发现so被第三方调用了,直接返回0!(2)我自己补的参数可能有问题导致代码运行的逻辑出错!文章末尾是我自己构建的app,哪位能提示甚至帮忙把第三个参数构造好,站内私信我加微信发红包!
直接来硬的不行那就继续绕呗!有两种思路:
- x音原app调用ms.bd.c.b.a(67108866,“1128”)生成了第三个long参数,为啥我不能调用了?
- x音原app调用ms.bd.c.b.a生成那4个加密字段,为啥我不能调用了?唯一不确定的就是第三个long参数,直接hook原app得到后再“为我所用”呗!
自己构造app是为了反调试和主动调用,用frida rpc一样可以的嘛!为了直接用rpc执行ms.bd.c.b.a,这里彻底地做一次伸手党:先hook ms.bd.c.b.a得到第三个long参数,保存后再通过rpc主动执行ms.bd.c.b.a生成加密字段!rpc代码如下:
call.js文件:
function printStringArry(strArr){ var FastJson = Java.use('com.alibaba.fastjson.JSON'); return FastJson.toJSONString(strArr); } function convert2ArrayList(str){ var ArrayList = Java.use('java.util.ArrayList').$new(); var strObj = Java.use('java.lang.String').$new(); var strArray = new Array(); strArray = str.split(","); for(var i=0;i<strArray.length;i++){ ArrayList.add(strArray[i]); } return ArrayList; } var longParam; function getLongParam(){ Java.perform(function(){ var h_class = Java.use("ms.bd.c.h"); h_class.a.overload('int', 'int', 'long', 'java.lang.String', 'java.lang.Object').implementation = function(){ if(arguments[2]!=0){ longParam = arguments[2]; console.log(' longParam = arguments[2]:' + longParam); } return this.a(arguments[0],arguments[1],arguments[2],arguments[3],arguments[4]); } }); } function ms_bd_c_h_a(args0,args1,args2,args3,args4){ var encrypt_result; Java.perform(function() { args2 = longParam; args4 = convert2ArrayList(args4.toString()); try{ var targetClass = Java.use("ms.bd.c.h"); encrypt_result = targetClass.a(args0,args1,args2,args3,args4); }catch(e){ console.log(e.stack); } console.log("encrypt_result is :"+printStringArry(encrypt_result)); }); return encrypt_result; } setImmediate(getLongParam); rpc.exports = { encrypt: ms_bd_c_h_a };
python文件:注意,args4是post数据包的head,需要改成和url匹配的head,否则app会崩掉!
# -*- coding, utf-8 -*- import codecs import frida import time session = frida.get_usb_device().attach('抖音') #读取JS脚本 with codecs.open('./call.js', 'r', 'utf-8') as f: source = f.read() script = session.create_script(source) script.load() rpc = script.exports time.sleep(5)#给hook预留时间,让x音触发ms.bd.c.h.a函数;期间也可以手动点赞、滑动屏幕等方式触发ms.bd.c.h.a函数 args0 = 50331649 args1 = 0 args2 = 0 args3 = "https,//aweme.snssdk.com/aweme/v1/commit/item/digg/?aweme_id=7016928444202175783&type=0&channel_id=0&city=510100&activity=0&os_api=25&device_type=SM-G9750&ssmix=a&manifest_version_code=150501&dpi=280&app_name=aweme&version_name=15.5.0&ts=1633766716&cpu_support64=false&app_type=normal&ac=wifi&appTheme=light&host_abi=armeabi-v7a&channel=lephone_xh_sd_1128_0415&update_version_code=15509900&_rticket=1633766717162&device_platform=android&iid=2401771765105502&version_code=150500&cdid=5b43e48d-b149-4c8a-8863-b2d84e650be4&openudid=b83ec6a675adba6a&device_id=4380918606995591&resolution=1080*1920&device_brand=samsung&language=zh&os_version=7.1.2&aid=1128&minor_status=0&mcc_mnc=46000" args4 = ["accept-encoding","gzip","cookie","n_mh=B6WRe0yd-1qIuffF6ZWNO-CSGlW1Q-VhC0E79NrqYTg;uid_tt=b21518a30e097fbd558cdddf9bcfd3a5;uid_tt_ss=b21518a30e097fbd558cdddf9bcfd3a5;sid_tt=5a51412bfb955ee582edae8d48765690;sessionid=5a51412bfb955ee582edae8d48765690;sessionid_ss=5a51412bfb955ee582edae8d48765690;d_ticket=6b7d0d3678f21faa482196c460483d4eed15d;install_id=2542488099492919;ttreq=1$a01a1173c36665a50314924786f4704df6cdeb45;sid_guard=5a51412bfb955ee582edae8d48765690%7C1633073022%7C5184000%7CTue%2C+30-Nov-2021+07%3A23%3A42+GMT;odin_tt=0ea847f78343f7e969e78ba7b9239200535f7a60e9667c1a9bd3eff221ae71e00ec61567785d8a107011c961fae3961c835c25f4bfd09dec3446bd650115e45263f7c216cd7d33846418d3b45f505996","passport-sdk-version","18","sdk-version","2","x-ss-req-ticket","1633766717163","x-tt-dt","AAA3VJFIKPXTQ3DZW622UN5YMCIJ3AQ5BZMAI2TMBJQASG3PQIMUKVBQYQG2YCXGALRNNZBQUJ7XDNZ4FXIGI2TL5LI5XLUXSPCTZ4NJGDU4JCIL2UF6OEV25GKGINMX7ARAHHRP5IU6VITY2YJ4BCI","x-tt-token","005a51412bfb955ee582edae8d48765690009b144562dfd76dacbe19854da5bd41e9bc9c249278cf5cfd759602ab0bab22f34d19554661df3791190f9068394dd1e6f7edd268b7fa3c5a0b745f1aee15931aa0b0f43260e7297fb7d16d872614ca256-1.0.1"] result = rpc.encrypt(args0,args1,args2,args3,args4) print("five gods,",result) session.detach()
在写rpc调用时遇到的几个坑在这里分享一下,希望对大家有所帮助:
- 这里涉及到3中不同的语言:java、js、python,每种语言的类型是不一样的;java层ms.bd.c.b.a的参数是('int', 'int', 'long', 'java.lang.String', 'java.lang.Object'),但是js是弱类型,并没有long类型;python同理也没有,该怎么构造long参数了?我这里是直接在js里面hook ms.bd.c.b.a后保存了第三个参数,然后传入rpc调用;如果通过python传进来,可以用int(data)转换,在js统一识别成number类型;我想过用Java.cast、Java.use('java.lang.Long').parseLong.overload('java.lang.String').call(Java.use('java.lang.Long'), args2)等方式强转成long,但都没卵用!
- 最后一个参数类型是java.lang.Object,如果直接把key/value字符串转成string传进去,app会直接崩掉(如果x音是故意这么干的,说明反调试做的牛逼;如果不是,说明鲁棒性没考虑周全,没有对错误的参数做处理);如果直接传入["accept-encoding":"gzip","cookie":"n_mh=B6WRe0yd-".....] 这种list类型会爆下面的错误,说明python的list根本不匹配java.lang.Object
Error: a(): argument types do not match any of: .overload('int', 'int', 'long', 'java.lang.String', 'java.lang.Object')
public final Map<String, String> mo70417a(String str, Map<String, List<String>> map) { String str2; PatchProxyResult proxy = PatchProxy.proxy(new Object[]{str, map}, this, f357109a, false, 431597); if (proxy.isSupported) { return (Map) proxy.result; } HashMap hashMap = new HashMap(); if (!(str == null || map == null)) { if (str.toLowerCase().contains((String) C85985h.m218727a(16777217, 0, 0, "2412bc", new byte[]{43, 34, 86, 86})) || str.toLowerCase().contains((String) C85985h.m218727a(16777217, 0, 0, "fd1c18", new byte[]{Byte.MAX_VALUE, 114, 86, 7, 29}))) { C86055z1.m218845a().mo248383b(); ArrayList arrayList = new ArrayList(); for (Map.Entry<String, List<String>> entry : map.entrySet()) { String key = entry.getKey(); if (entry.getValue() == null || entry.getValue().size() <= 0) { str2 = null; } else { str2 = entry.getValue().get(0); } if (!(key == null || str2 == null)) { arrayList.add(key); arrayList.add(str2); } } String[] strArr = (String[]) C85964b.m218696a(50331649, 0, C86039u1.this.f357108d, str, (String[]) arrayList.toArray(new String[0])); if (strArr != null) { HashMap hashMap2 = new HashMap(); for (int i = 0; i < strArr.length; i += 2) { hashMap2.put(strArr[i], strArr[i + 1]); } return hashMap2; } } else { throw new RuntimeException((String) C85985h.m218727a(16777217, 0, 0, "3ff0e1", new byte[]{43, 112, 85, 73, 79, 53, 36, 7, 53, 101, 98, 108, 1, 80, 74, 105, 56, 83, 35, 112, 49})); } } return hashMap; }
同时,在metasec_ml偏移0x13220处发现了重要的函数:第一个参数是url
第二个参数是post包的head参数,so层0x13220的这个函数和java层的ms.bd.c.u1$a.a方法在参数上出奇一致,很有可能0x13220也参与了加密字段的生成,至少是提供了加密算法的原始数据!
自己构造app: 链接:https://pan.baidu.com/s/1JavGwlMKtcpnQkaVB2zmNw 提取码:3fd6 调用ms.bd.c.b.a时返回值是0,暂时还没生成加密字段,问题还在排查中;能帮忙解决问题的私信我加微信,我发红包!
参考:
1、https://blog.csdn.net/shaoyiju/article/details/53241808 读写锁的原理及用法
2、https://blog.csdn.net/qq_30135181/article/details/108221043 调用so中函数