篇幅有限
完整内容及源码关注公众号:ReverseCode,发送 冲
绕过强制会员
adb install com.caratlover.apk 安装后强制支付会员费才可进主页
脱壳
jadx打开发现代码很少,目测被加固,脱个衣服先。
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打开时,报错,说明文件并不标准。
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
[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
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
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
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
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
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
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源码实现
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
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加密
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方法。
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")
对抗更新
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类,其中有
public /* synthetic */ void onDestroyView() {
super.onDestroyView();
g();
}
主动调用去除弹窗
frida -UF -l disableUPDATE.js 再destory
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)
不过页面无法操作,尝试直接跳到MainActivity
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构造函数,点击马上更新
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得到的类
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。
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的入参改成新版本
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查看新版本依旧加壳
./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
开启内存漫游
pyenv local 3.8.0
./fs128arm64
objection -g com.caratlover explore
android intent launch_activity com.chanson.business.MainActivity 直接绕过强制会员购买页面
将破解vip添加在r0trace的main中执行一次,实现trace某一个类时执行单次hook
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
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")
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返回值的内容
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
构造消息体
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中添加
if(targetMethod.toString().indexOf("getConversation") < 0){
return
}
查看腾讯云官方文档文档中心 > 即时通信 IM > SDK 文档 > 旧版 API 教程 > 消息收发 > 消息收发(Android),获取会话由 TIMManager
中的 getConversation
实现。
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
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
发送消息了
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中完整的发送消息的流程
调用批量发送
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")
}
})
})
}
本文由博客群发一文多发等运营工具平台 OpenWrite 发布