篇幅有限 完整内容及源码关注公众号:ReverseCode,发送 冲
绕过强制会员 adb install com.caratlover.apk 安装后强制支付会员费才可进主页
脱壳 jadx打开发现代码很少,目测被加固,脱个衣服先。
1 2 3 4 5 6 7 8 9 10 git clone https://github.com/hluwa/FRIDA-DEXDump.git ./fs1426arm64 pyenv local 3.9.0 python main.py app保持最前端,开始脱壳 git clone https://github.com/hanbinglengyue/FART.git adb push frida_fart/lib/fart* /data/local/tmp adb shell && cp fart* /data/app && chmod 777 frida -U -f com.caratlover -l frida_fart_hook.js --no-pause 使用安卓8和安卓8.1进行脱壳 mv ../*.dex carat && adb pull /sdcard/carat
file *
查看文件格式是Dalvik dex file,但是脱完的部分dex文件用010 Editor打开时,报错,说明文件并不标准。
1 2 3 objection -g com.caratlover explore android hooking list activities android intent launch_activity com.chanson.business.MainActivity 直接绕过强制会员购买页面
使用jadx1.2.0中同时打开多个dex,查找com.chanson.business.MainActivity
用12.8.0的frida混淆的爹妈都不认识了,还是用14.2.16版本。
绕过强制会员页面后,编辑资料填写个人详细信息。
搭讪 通过点击发送时,调用hookEvent.js查看触发的类frida -UF -l hookEvent.js
1 [Pixel::克拉恋人]-> [WatchEvent] onClick: com.tencent.qcloud.tim.uikit.modules.chat.layout.input.InputLayout
查看InputLayout该类的用例,该UI基本都在com.chanson.business.message.activity.ChatActivity
中调用
其中com.chanson.business.message.activity.ChatActivity
有一段代码,判断是否vip
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private final void ja() { BasicUserInfoBean col1; BasicUserInfoBean col12; if (Ib.f9521i.m()) { MyInfoBean k = Ib.f9521i.k(); if (k == null || (col12 = k.getCol1()) == null || !col12.isVip()) { CheckTalkBean checkTalkBean = this.f10545d; if ((checkTalkBean != null ? checkTalkBean.getUnlockTime() : 0) > 0) { da(); } else { l(0); } } else { da(); } } else { MyInfoBean k2 = Ib.f9521i.k(); if (k2 == null || (col1 = k2.getCol1()) == null || !col1.isReal()) { ConfirmDialogFragment.a aVar = ConfirmDialogFragment.Companion; String string = getString(R$string.you_can_chat_after_you_have_certified); i.a((Object) string, "getString(R.string.you_c…after_you_have_certified)"); String string2 = getString(R$string.authentication_now_in_ten_seconds); i.a((Object) string2, "getString(R.string.authe…ation_now_in_ten_seconds)"); FragmentManager supportFragmentManager = getSupportFragmentManager(); i.a((Object) supportFragmentManager, "supportFragmentManager"); ConfirmDialogFragment.a.a(aVar, "", string, "", string2, true, supportFragmentManager, true, (kotlin.jvm.a.a) null, false, (kotlin.jvm.a.b) null, (String) null, 0.0f, (kotlin.jvm.a.b) null, 8064, (Object) null).a(new I(this)); return; } da(); } }
其中的isVip方法来自于com.chanson.business.model.BasicUserInfoBean
,我们尝试trace下该类,并打印类的每个域的值。
trace frida -UF -l trace.js -o traceVip.txt 对指定类的所有动静态方法及构造函数进行trace
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 function inspectObject(obj) { Java.perform(function () { const obj_class = obj.class; // var objClass = Java.use("java.lang.Object").getClass.apply(object); // obj_class =Java.use("java.lang.Class").getName.apply(objClass); const fields = obj_class.getDeclaredFields(); const methods = obj_class.getMethods(); // console.log("Inspecting " + obj.getClass().toString()); // console.log("Inspecting " + obj.class.toString()); console.log("\tFields:"); for (var i in fields) { console.log("\t\t" + fields[i].toString()); var className = obj_class.toString().trim().split(" ")[1]; // console.log("className is => ",className); var fieldName = fields[i].toString().split(className.concat(".")).pop(); console.log(fieldName + " => ", obj[fieldName].value); } // console.log("\tMethods:"); // for (var i in methods) // console.log("\t\t" + methods[i].toString()); }) } function uniqBy(array, key) { var seen = {}; return array.filter(function(item) { var k = key(item); return seen.hasOwnProperty(k) ? false : (seen[k] = true); }); } // trace a specific Java Method function traceMethod(targetClassMethod) { var delim = targetClassMethod.lastIndexOf("."); if (delim === -1) return; var targetClass = targetClassMethod.slice(0, delim) var targetMethod = targetClassMethod.slice(delim + 1, targetClassMethod.length) var hook = Java.use(targetClass); var overloadCount = hook[targetMethod].overloads.length; console.log("Tracing " + targetClassMethod + " [" + overloadCount + " overload(s)]"); for (var i = 0; i < overloadCount; i++) { hook[targetMethod].overloads[i].implementation = function() { inspectObject(this) console.warn("\n*** entered " + targetClassMethod); // print backtrace // Java.perform(function() { // var bt = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()); // console.log("\nBacktrace:\n" + bt); // }); // print args if (arguments.length) console.log(); for (var j = 0; j < arguments.length; j++) { console.log("arg[" + j + "]: " + arguments[j]); } // print retval var retval = this[targetMethod].apply(this, arguments); // rare crash (Frida bug?) console.log("\nretval: " + retval); console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); console.warn("\n*** exiting " + targetClassMethod); return retval; } } } function traceClass(targetClass) { //Java.use是新建一个对象哈,大家还记得么? var hook = Java.use(targetClass); //利用反射的方式,拿到当前类的所有方法 var methods = hook.class.getDeclaredMethods(); // var methods = hook.class.getMethods(); console.log("methods => ",methods) //建完对象之后记得将对象释放掉哈 hook.$dispose; //将方法名保存到数组中 var parsedMethods = []; methods.forEach(function(method) { parsedMethods.push(method.toString().replace(targetClass + ".", "TOKEN").match(/\sTOKEN(.*)\(/)[1]); }); //去掉一些重复的值 var targets = uniqBy(parsedMethods, JSON.stringify); // 只hook构造函数 //targets = []; targets = targets.concat("$init") console.log("targets=>",targets) //对数组中所有的方法进行hook,traceMethod也就是第一小节的内容 targets.forEach(function(targetMethod) { traceMethod(targetClass + "." + targetMethod); }); } function hook() { Java.perform(function () { console.log("start") Java.enumerateClassLoaders({ onMatch: function (loader) { try { if(loader.findClass("com.ceco.nougat.gravitybox.ModStatusbarColor$1")){ // if(loader.findClass("de.robv.android.xposed.XC_MethodHook")){ // if(loader.findClass("de.robv.android.xposed.XposedBridge")){ //if(loader.findClass("com.android.internal.statusbar.StatusBarIcon")){ console.log("Successfully found loader") console.log(loader); Java.classFactory.loader = loader ; } } catch(error){ console.log("find error:" + error) } }, onComplete: function () { console.log("end1") } }) // Java.use("de.robv.android.xposed.XposedBridge").log.overload('java.lang.String').implementation = function (str) { // console.log("entering Xposedbridge.log ",str.toString()) // return true // } //traceClass("com.ceco.nougat.gravitybox.ModStatusbarColor") // Java.use("com.roysue.xposed1.HookTest$1").afterHookedMethod.implementation = function (param){ // console.log("entering afterHookedMethod param is => ",param); // return this.afterHookedMethod(param); // } // traceClass("de.robv.android.xposed.XC_MethodHook") // Java.use("de.robv.android.xposed.XC_MethodHook$MethodHookParam").setResult.implementation = function(str){ // console.log("entersing de.robv.android.xposed.XC_MethodHook$MethodHookParam setResult => ",str) // console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); // return this.setResult(str); // } Java.enumerateLoadedClasses ({ onMatch:function(className){ if(className.toString().indexOf("gravitybox")>0 && className.toString().indexOf("$")>0 ){ console.log("found => ",className) // var interFaces = Java.use(className).class.getInterfaces(); // if(interFaces.length>0){ // console.log("interface is => "); // for(var i in interFaces){ // console.log("\t",interFaces[i].toString()) // } // } if(Java.use(className).class.getSuperclass()){ var superClass = Java.use(className).class.getSuperclass().getName(); // console.log("superClass is => ",superClass); if (superClass.indexOf("XC_MethodHook")>0){ console.log("found class is => ",className.toString()) traceClass(className); } } } },onComplete:function(){ console.log("search completed!") } }) console.log("end2") }) } function main(){ // hook() Java.perform(function(){ traceClass("com.chanson.business.model.BasicUserInfoBean") // traceClass("com.chanson.business.model.MyInfoBean"); }) } setImmediate(main)
java.lang.Throwable at com.chanson.business.model.BasicUserInfoBean.isVip(Native Method) at com.chanson.business.message.activity.ChatActivity.na(SourceFile:2) at com.chanson.business.message.activity.ChatActivity.k(SourceFile:1) at com.chanson.business.message.activity.a.run(SourceFile:1) at android.os.Handler.handleCallback(Handler.java:790) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6494) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807) at de.robv.android.xposed.XposedBridge.main(XposedBridge.java:108)
优化对应关系
frida -UF -l trace.js -o traceVip.txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 function traceMethod(targetClassMethod) { var delim = targetClassMethod.lastIndexOf("."); if (delim === -1) return; var targetClass = targetClassMethod.slice(0, delim) var targetMethod = targetClassMethod.slice(delim + 1, targetClassMethod.length) var hook = Java.use(targetClass); var overloadCount = hook[targetMethod].overloads.length; console.log("Tracing " + targetClassMethod + " [" + overloadCount + " overload(s)]"); for (var i = 0; i < overloadCount; i++) { hook[targetMethod].overloads[i].implementation = function () { var output = ""; for(var line=0;line<100;line++){ output = output.concat("=") } output = output.concat("\r\n") const Class = Java.use("java.lang.Class"); // const obj_class = Java.cast(this.getClass(), Class); const obj_class = this.class; const fields = obj_class.getDeclaredFields(); // output = output.concat("Inspecting " + this.getClass().toString()); output = output.concat("Inspecting " + this.class); output = output.concat("\r\n") output = output.concat("\tFields:"); output = output.concat("\r\n") for (var i in fields) { // console.log("\t\t" + fields[i].toString()); var className = obj_class.toString().trim().split(" ")[1]; // console.log("className is => ",className); var fieldName = fields[i].toString().split(className.concat(".")).pop(); var fieldValue = undefined; if(!(this[fieldName]===undefined)){ fieldValue = this[fieldName].value ; } output = output.concat(fieldName + " => ", fieldValue); output = output.concat("\r\n") } // inspectObject(this); output = output.concat("\n*** entered " + targetClassMethod); output = output.concat("\r\n") // print backtrace // Java.perform(function() { // var bt = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()); // console.log("\nBacktrace:\n" + bt); // }); // print args if (arguments.length) console.log(); for (var j = 0; j < arguments.length; j++) { output = output.concat("arg[" + j + "]: " + arguments[j] + " => " + JSON.stringify(arguments[j])); output = output.concat("\r\n") } output = output.concat(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); output = output.concat("\r\n"); // print retval var retval = this[targetMethod].apply(this, arguments); // rare crash (Frida bug?) output = output.concat("\nretval: " + retval + " => " + JSON.stringify(retval)); output = output.concat("\r\n") output = output.concat("\n*** exiting " + targetClassMethod); output = output.concat("\r\n") console.log(output); return retval; } } }
vip 旧版4.1.0 frida -UF -l hookCaratVip.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hookVIP(){ Java.perform(function(){ Java.use("com.chanson.business.model.BasicUserInfoBean").isVip.implementation = function(){ console.log("Calling isVIP ") return true; } }) } function main(){ console.log("Start hook") hookVIP() } setImmediate(main)
新版4.6.0 1 android hooking watch class com.chanson.business.message.activity.ChatActivity --dump-args --dump-backtrace --dump-return 当我们无法判断什么时候判断vip时,hook整个类,查看调用链,点击发送消息时,弹窗付费
查看jadx中的com.chanson.business.message.activity.ChatActivity
类,通过aa方法得知只有在被拉黑等情况,返回false则无法发送消息,我们在第一步让Z()返回false,直接进入return true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 private final boolean aa() { if (!Z()) { return true; } if (this.f10873d == null) { Hb.a(Hb.f11628c, "数据异常", 0, 2, (Object) null); return false; } else if (ga()) { return false; } else { CheckTalkBean checkTalkBean = this.f10873d; if (checkTalkBean == null) { i.a(); throw null; } else if (!checkTalkBean.getUnlock()) { ChatLayout chatLayout = (ChatLayout) k(R$id.chatLayout); i.a((Object) chatLayout, "chatLayout"); chatLayout.getInputLayout().hideSoftInput(); x.a(new RunnableC1179a(this), 100); return false; } else if (checkTalkBean.getStatus() == 3 || checkTalkBean.getStatus() == 2) { Hb.a(Hb.f11628c, "你已将对方拉黑,无法发送消息", 0, 2, (Object) null); ChatLayout chatLayout2 = (ChatLayout) k(R$id.chatLayout); i.a((Object) chatLayout2, "chatLayout"); InputLayout inputLayout = chatLayout2.getInputLayout(); i.a((Object) inputLayout, "chatLayout.inputLayout"); inputLayout.getInputText().setText(""); return false; } else if (checkTalkBean.getStatus() != 1) { return true; } else { Hb.a(Hb.f11628c, "对方已将你拉黑,无法发送消息", 0, 2, (Object) null); ChatLayout chatLayout3 = (ChatLayout) k(R$id.chatLayout); i.a((Object) chatLayout3, "chatLayout"); InputLayout inputLayout2 = chatLayout3.getInputLayout(); i.a((Object) inputLayout2, "chatLayout.inputLayout"); inputLayout2.getInputText().setText(""); return false; } } }
通过objection判断ChatActivity源码实现
1 2 3 4 objection -g com.caratlover explore -P ~/.objection/plugins android hooking search classes ChatActivity plugin wallbreaker classdump --fullname com.chanson.business.message.activity.ChatActivity android hooking watch class_method com.chanson.business.message.activity.ChatActivity.Z --dump-args --dump-backtrace --dump-return
每次Z()返回true自然进不了发送消息逻辑,主动调用Z()返回false,破解vip
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hookVIP(){ Java.perform(function(){ Java.use("com.chanson.business.message.activity.ChatActivity").Z.implementation = function(){ console.log("Calling isVIP ") return false; } }) } function main(){ console.log("Start hook") hookVIP() } setImmediate(main)
抓包 Postern配置代理,其中192.168.0.107是charles主机ip,8889是charles的socks
配置规则
遇到8668端口抓不到,报错SSL:Unsupported or unrecognized SSL message
,修改charles的Proxy Settings
盲猜一波是base64加密
1 python r0capture.py -U -f com.caratlover -v -w 2 >> capture.txt 抓包发现都被加密,类被混淆的非常厉害,虽然无法识别类的作用,我们可以有通过trace去跟踪调用返回值
找到登录包/auth/login-check
,其调用栈中at com.chanson.common.a.j.intercept(SourceFile:45)
通过jadx查看com.chanson.common.a.j
方法,其中com.chanson.common.utils.a.b
将传入的jsonObject转成string后调用c方法。
1 2 frida -U -f com.caratlover -l trace.js --no-pause -o traffic.txt 修改trace的class traceClass("com.chanson.common.utils.a.b")
Error: java.lang.ClassNotFoundException: Didn’t find class “com.chanson.common.utils.a.b” 报错是因为app启动还要时间,修改setTimeout(main, 2000);
trace登录,先打开登录界面,输入密码后frida -U com.caratlover -l r0tracer.js --no-pause -o traffic.txt
大量的加密字段类似base64,尝试trace Base64。修改traceClass("android.util.Base64")
,开启trace,frida -U com.caratlover -l r0tracer.js --no-pause -o base64.txt
追查调用栈
通过jadx查看com.chanson.common.a.d
,其中String a2 = a.a(string, "f87210e0ed3079d8");
的a方法跳转到实现发现是一个完整的标准aes加密。
全局搜索还有AESUtils,完全自己开发的非标准的AES加密,7z x com.caratlover.apk
查看lib/armeabi-v7a下存在alicomphonenumberauthsdk-log-online-standard-release_alijtca_plus.so
strings查看该so中的字符串,traceClass("com.mobile.auth.gatewayauth.utils.security.CheckRoot")
对抗更新 1 2 3 adb connect 172.20.103.172 启动wifiadb adb install com.caratlover4.1.0.apk frida -UF -l hookEvent.js 点击马上更新按钮,触发点击时间,打印点击类
打开jadx逐个查看脱完壳后的dex文件,新版本的jadx对加密后的dex反编译结果会rename
查看ConfirmDialogFragment类,其中有
1 2 3 4 public /* synthetic */ void onDestroyView() { super.onDestroyView(); g(); }
主动调用去除弹窗 frida -UF -l disableUPDATE.js 再destory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function disableUPDATE(){ Java.perform(function(){ Java.choose("com.chanson.business.widget.ConfirmDialogFragment",{ onMatch:function(ins){ // 动态方法choose onMatch找到实例进行调用 console.log("found ins => ",ins); // smali或objection看真实方法名 ins.onDestroyView() }, onComplete:function(){ console.log("Search completed!") } }) }) } function main(){ console.log("Start hook") disableUPDATE() } setImmediate(main)
![GIF 2021-6-3 16-37-29](克拉恋人会员制取证分析/GIF 2021-6-3 16-37-29.gif)
不过页面无法操作,尝试直接跳到MainActivity
1 2 objection -g com.caratlover explore -P ~/.objection/plugins android intent launch_activity com.chanson.business.MainActivity
trace frida -U -f com.caratlover -l r0trace.js --runtime=v8 --no-pause -o trace.txt 在traceClass中添加targets = [];
只hook构造函数,点击马上更新
1 2 traceClass("com.chanson.business.widget.ConfirmDialogFragment") setTimeout(main, 1000);
setImmediate是立即执行函数,setTimeout是等待毫秒后延迟执行函数 二者在attach模式下没有区别 在spawn模式下,hook系统API时如javax.crypto.Cipher建议使用setImmediate立即执行,不需要延时 在spawn模式下,hook应用自己的函数或含壳时,建议使用setImmediate并给出适当的延时(500~5000)
找到com.chanson.business.login.presenter.PhoneLoginPresenter$a.a
实现方法
找到a方法的调用处,在switch的baseResponse.getErrorCode()
的判断时调用PhoneLoginPresenter.f10498a.a
,其中renamed from: com.chanson.business.g.s
正是我们trace得到的类
1 2 traceClass("com.chanson.common.base.BaseResponse") setTimeout(main, 1000);
尝试tracecom.chanson.common.base.BaseResponse
查看getErrorCode的结果,返回10002,正巧会调用PhoneLoginPresenter.f10498a.a((Update) rVar.a(rVar.a(baseResponse.getUpdate()), Update.class));
使用新版本的apk启动时重新tracecom.chanson.common.base.BaseResponse
查看正常情况下case返回的值为10001。
1 2 3 4 5 Java.use("com.chanson.common.base.BaseResponse").getErrorCode.implementation = function(){ console.log("Calling getErrorCode ") return 10001; } setTimeout(main,2000) // 壳的切换需要时间
frida -U -f com.caratlover -l disableUPDATE.js --no-pause
hook getErrorCode直接返回10001,发现正常进入登录,登录时发现我们检测到你的账号存在异常数据,为确保你的账号安全,请重新登录
,r0capture抓包发现对版本号进行了校验,接下来将SSLOutputStream的入参改成新版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream").write.overload('[B', 'int', 'int').implementation = function (bytearry, int1, int2) { for(var i = 0; i < bytearry.length; ++i){ // Memory.writeS8(ptr.add(i), array[i]); if(bytearry[i]=='0x34'){ console.log("found 4"); if(bytearry.length - i > 4){ if(bytearry[i+1] == '0x2e' && bytearry[i+2] == '0x31' && bytearry[i+3] == '0x2e' && bytearry[i+4] == '0x30' ){ bytearry[i+2] = 50 console.log("finally change to 4.2.0!") } } // 4.1.0 字符串转16进制转 0x34 0x2e 0x31 0x2e 0x30 } } var result = this.write(bytearry, int1, int2); jhexdump(bytearry) // var trafficstring = StringClass.$new(bytearry).replace(StringClass.$new("4.1.0"),StringClass.$new("4.2.0")) // console.log("write => ",trafficstring) // Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString(); // var result = this.write(trafficstring.getBytes(), int1, int2); return result; }
批量撩妹 jadx-gui查看新版本依旧加壳
1 2 3 4 5 6 7 ./fs14216arm64 pyenv local 3.9.0 git clone https://github.com/hanbinglengyue/FART.git adb push frida_fart/lib/fart* /data/local/tmp adb shell && cp fart* /data/app && chmod 777 frida -U -f com.caratlover -l frida_fart_hook.js --no-pause 使用安卓8和安卓8.1进行脱壳 mv ../*.dex carat && adb pull /sdcard/carat
开启内存漫游
1 2 3 4 pyenv local 3.8.0 ./fs128arm64 objection -g com.caratlover explore android intent launch_activity com.chanson.business.MainActivity 直接绕过强制会员购买页面
将破解vip添加在r0trace的main中执行一次,实现trace某一个类时执行单次hook
1 2 3 4 5 6 7 8 9 10 11 12 function main() { Java.perform(function () { console.warn("r0tracer begin ... !") Java.perform(function(){ Java.use("com.chanson.business.message.activity.ChatActivity").Z.implementation = function(){ console.log("Calling isVIP ") return false; } }) }) }
frida -UF -l hookEvent.js 点击发送消息,触发com.tencent.qcloud.tim.uikit.modules.chat.layout.input.InputLayout`,并弹窗要求付费,我们尝试trace该类的同时并破解vip
1 2 3 4 5 6 7 8 9 10 11 12 13 function main() { Java.perform(function () { console.warn("r0tracer begin ... !") traceClass("com.tencent.qcloud.tim.uikit.modules.chat.layout.input.InputLayout"); Java.perform(function(){ Java.use("com.chanson.business.message.activity.ChatActivity").Z.implementation = function(){ console.log("Calling isVIP ") return false; } }) }) }
frida -UF -l r0tracer.js --no-pause > chat.txt 开启trace,只有frida12 没有runtime=v8的选项,发送消息,查看调用栈
在jadx中找到InputLayout的onClick方法
尝试traceClass("com.tencent.qcloud.tim.uikit.modules.message.MessageInfoUtil")
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function main() { Java.perform(function () { console.warn("r0tracer begin ... !") traceClass("com.tencent.qcloud.tim.uikit.modules.message.MessageInfoUtil") Java.perform(function(){ Java.use("com.chanson.business.message.activity.ChatActivity").Z.implementation = function(){ console.log("Calling isVIP ") return false; } }) }) }
frida -UF -l r0tracer.js --no-pause > chat.txt 开启trace,再次发送消息,搜索我们发送的ccccdddd
通过jadx找到com.tencent.qcloud.tim.uikit.modules.message.MessageInfoUtil
的buildTextMessage方法
想办法获取MessageInfo返回值的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function main() { Java.perform(function () { console.warn("r0tracer begin ... !") traceClass("com.tencent.qcloud.tim.uikit.modules.message.MessageInfo") Java.perform(function(){ Java.use("com.chanson.business.message.activity.ChatActivity").Z.implementation = function(){ console.log("Calling isVIP ") return false; } }) }) }
frida -UF -l r0tracer.js --no-pause > chat.txt 开启trace,再次发送消息tttttttt,搜索tttttttt
Inspecting Fields: => true => class com.tencent.qcloud.tim.uikit.modules.message.MessageInfo com.tencent.imsdk.TIMMessage TIMMessage => TIMMessage{ ConverstaionType:Invalid ConversationId: MsgId:2148258574 MsgSeq:32779 Rand:2148258574 time:1614087810 isSelf:true Status:Sending Sender:klover1_server_550179 elements:[ {Type:Text, Content:tttttttt } ] } => “<instance: com.tencent.imsdk.TIMMessage>” java.lang.String dataPath => null => null android.net.Uri dataUri => null => null com.tencent.imsdk.TIMElem element => com.tencent.imsdk.TIMTextElem@7d67029 => “<instance: com.tencent.imsdk.TIMElem, $className: com.tencent.imsdk.TIMTextElem>” java.lang.Object extra => tttttttt => “<instance: java.lang.Object, $className: java.lang.String>” java.lang.String fromUser => klover1_server_550179 => “klover1_server_550179” boolean group => false => false java.lang.String groupNameCard => null => null java.lang.String id => 70b42de0-097a-4b9c-927d-13e660ce86a6 => “70b42de0-097a-4b9c-927d-13e660ce86a6” int imgHeight => 0 => 0 int imgWidth => 0 => 0 long msgTime => 1614087810 => “1614087810” int msgType => 0 => 0 boolean peerRead => false => false boolean read => true => true boolean self => true => true int status => 1 => 1 long uniqueId => 0 => “0” int MSG_STATUS_DELETE => 274 => 274 int MSG_STATUS_DOWNLOADED => 6 => 6 int MSG_STATUS_DOWNLOADING => 4 => 4 int MSG_STATUS_NORMAL => 0 => 0 int MSG_STATUS_READ => 273 => 273 int MSG_STATUS_REVOKE => 275 => 275 int MSG_STATUS_SENDING => 1 => 1 int MSG_STATUS_SEND_FAIL => 3 => 3 int MSG_STATUS_SEND_SUCCESS => 2 => 2 int MSG_STATUS_UN_DOWNLOAD => 5 => 5 int MSG_TYPE_AUDIO => 48 => 48 int MSG_TYPE_CUSTOM => 128 => 128 int MSG_TYPE_CUSTOM_FACE => 112 => 112 int MSG_TYPE_FILE => 80 => 80 int MSG_TYPE_GROUP_CREATE => 257 => 257 int MSG_TYPE_GROUP_DELETE => 258 => 258 int MSG_TYPE_GROUP_JOIN => 259 => 259 int MSG_TYPE_GROUP_KICK => 261 => 261 int MSG_TYPE_GROUP_MODIFY_NAME => 262 => 262 int MSG_TYPE_GROUP_MODIFY_NOTICE => 263 => 263 int MSG_TYPE_GROUP_QUITE => 260 => 260 int MSG_TYPE_IMAGE => 32 => 32 int MSG_TYPE_LOCATION => 96 => 96 int MSG_TYPE_MIME => 1 => 1 int MSG_TYPE_TEXT => 0 => 0 int MSG_TYPE_TIPS => 256 => 256 int MSG_TYPE_VIDEO => 64 => 64 [native function h() { [native code] } => undefined => undefined
entered com.tencent.qcloud.tim.uikit.modules.message.MessageInfo.getTIMMessage java.lang.Throwable at com.tencent.qcloud.tim.uikit.modules.message.MessageInfo.getTIMMessage(Native Method) at com.tencent.qcloud.tim.uikit.modules.chat.base.ChatManagerKit.sendMessage (SourceFile:11)
主要逻辑在this.mCurrentConversation.sendMessage
,进入sendMessage方法
进入conversation.sendMessage
方法
具体流程在native层,使用的是腾讯云sdk ,很难抓到包,不过可以在com.tencent.qcloud.tim.uikit.modules.message.MessageInfoUtil.buildTextMessage
构造消息体
1 2 3 4 5 6 android heap search instances com.tencent.imsdk.TIMManager android hooking list class_methods com.tencent.imsdk.TIMManager android heap execute 227890024 getLoginUser 根据堆中的实例主动调用方法 android heap execute 227890024 getVersion android hooking search classes TIMConversation android hooking list class_methods com.tencent.imsdk.TIMConversation
trace单个函数在r0trace中添加
1 2 3 if(targetMethod.toString().indexOf("getConversation") < 0){ return }
查看腾讯云官方文档文档中心 > 即时通信 IM > SDK 文档 > 旧版 API 教程 > 消息收发 > 消息收发(Android) ,获取会话由 TIMManager
中的 getConversation
实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function TIMManager() { Java.perform(function () { Java.choose("com.tencent.imsdk.TIMManager", { onMatch: function (ins) { console.log("found ins => ", ins) console.log("found ins.getNetworkStatus() => ", ins.getNetworkStatus()) console.log("found ins.getSdkConfig() => ", ins.getSdkConfig()) console.log("found ins.getUserConfig() => ", ins.getUserConfig()) //看不到内容可以通过r0trace的inspectObject单独看 var output = ""; output = inspectObject(ins.getUserConfig(), output); console.log(output) }, onComplete: function () { console.log("search compeled") } }) }) }
尝试trace腾讯云sdk,frida -UF -l r0tracer.js --no-pause -o chat.txt
,重新进入聊天界面获取log中的peer,即用户id
1 2 3 4 5 6 7 8 9 10 11 12 13 function main() { Java.perform(function () { console.warn("r0tracer begin ... !") traceClass("com.tencent.imsdk.TIMManager") Java.perform(function(){ Java.use("com.chanson.business.message.activity.ChatActivity").Z.implementation = function(){ console.log("Calling isVIP ") return false; } }) }) }
有了peer就可以调用TIMManager.getInstance().getConversation
的sendMessage
发送消息了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 function TIMManager() { Java.perform(function () { Java.choose("com.tencent.imsdk.TIMManager", { onMatch: function (ins) { console.log("found ins => ", ins) console.log("found ins.getNetworkStatus() => ", ins.getNetworkStatus()) console.log("found ins.getSdkConfig() => ", ins.getSdkConfig()) // console.log("found ins.getUserConfig() => ", ins.getUserConfig()) 看不到内容可以通过r0trace的inspectObject单独看 // var output = ""; // output = inspectObject(ins.getUserConfig(), output); // console.log(output) var peer = Java.use('java.lang.String').$new("klover1_server_190249"); // 这就是peer用户id var conversation = ins.getConversation(Java.use("com.tencent.imsdk.TIMConversationType").C2C.value, peer); var msg = Java.use("com.tencent.imsdk.TIMMessage").$new(); //添加文本内容 var elem = Java.use("com.tencent.imsdk.TIMTextElem").$new(); elem.setText(Java.use("java.lang.String").$new("cpdd")); msg.addElement(elem) const callback = Java.registerClass({ // new 一个接口 name: 'callback', implements: [Java.use("com.tencent.imsdk.TIMValueCallBack")], methods: { onError(code, desc) { console.log("send message failed. code: " + code + " errmsg: " + desc); }, onSuccess(msg) {//发送消息成功 console.log("SendMsg ok" + msg); }, } }); conversation.sendMessage(msg, callback.$new()) }, onComplete: function () { console.log("search compeled") } }) }) }
以上实现了sdk中完整的发送消息的流程
调用批量发送
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 function TIMManager() { Java.perform(function () { Java.choose("com.tencent.imsdk.TIMManager", { onMatch: function (ins) { console.log("found ins => ", ins) console.log("found ins.getNetworkStatus() => ", ins.getNetworkStatus()) console.log("found ins.getSdkConfig() => ", ins.getSdkConfig()) // console.log("found ins.getUserConfig() => ", ins.getUserConfig()) 看不到内容可以通过r0trace的inspectObject单独看 // var output = ""; // output = inspectObject(ins.getUserConfig(), output); // console.log(output) console.log("found ins.getConversationList() => ", ins.getConversationList()) console.log("found ins.getConversationList() => ", ins.getConversationList().toString()) console.log("found ins.getConversationList() => ", JSON.stringify(ins.getConversationList())) var iter = ins.getConversationList().listIterator(); while (iter.hasNext()) { console.log(iter.next()); if (iter.next() != null) { var TIMConversation = Java.cast(iter.next(), Java.use("com.tencent.imsdk.TIMConversation")) console.log(TIMConversation.getPeer()); // if (TIMConversation.getPeer().toString().indexOf("209509") >= 0) { console.log("try send message...") //构造一条消息 var msg = Java.use("com.tencent.imsdk.TIMMessage").$new(); //添加文本内容 var elem = Java.use("com.tencent.imsdk.TIMTextElem").$new(); elem.setText("cpdd 你是唯一 问我是谁 codewj"); //将elem添加到消息 msg.addElement(elem) const callback = Java.registerClass({ name: 'com.tencent.imsdk.TIMValueCallBackCallback', implements: [Java.use("com.tencent.imsdk.TIMValueCallBack")], methods: { onError(i, str) { console.log("send message failed. code: " + i + " errmsg: " + str) }, onSuccess(msg) { console.log("SendMsg ok", +msg) } } }); //发送消息 TIMConversation.sendMessage(msg, callback.$new()) } } }, onComplete: function () { console.log("search compeled") } }) }) }