克拉恋人会员制取证分析

篇幅有限

完整内容及源码关注公众号:ReverseCode,发送

绕过强制会员

adb install com.caratlover.apk 安装后强制支付会员费才可进主页

image-20210531003110432

脱壳

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

image-20210531002927720

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 直接绕过强制会员购买页面

image-20210531093247062

使用jadx1.2.0中同时打开多个dex,查找com.chanson.business.MainActivity

用12.8.0的frida混淆的爹妈都不认识了,还是用14.2.16版本。

绕过强制会员页面后,编辑资料填写个人详细信息。

image-20210531093539184

搭讪

通过点击发送时,调用hookEvent.js查看触发的类frida -UF -l hookEvent.js

1
[Pixel::克拉恋人]-> [WatchEvent] onClick: com.tencent.qcloud.tim.uikit.modules.chat.layout.input.InputLayout

image-20210531093736893

查看InputLayout该类的用例,该UI基本都在com.chanson.business.message.activity.ChatActivity中调用

image-20210531230429589

其中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)

image-20210531232604816

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;
}
}

}

image-20210601005958949

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整个类,查看调用链,点击发送消息时,弹窗付费

image-20210601171218877

查看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

image-20210601022535406

每次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)

image-20210601020458519

抓包

Postern配置代理,其中192.168.0.107是charles主机ip,8889是charles的socks

image-20210601021359032

配置规则

image-20210601021407826

遇到8668端口抓不到,报错SSL:Unsupported or unrecognized SSL message,修改charles的Proxy Settings

image-20210601021805962

盲猜一波是base64加密

image-20210601021901648

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)

image-20210602144812815

通过jadx查看com.chanson.common.a.j方法,其中com.chanson.common.utils.a.b将传入的jsonObject转成string后调用c方法。

image-20210602151006668

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

image-20210602154917590

大量的加密字段类似base64,尝试trace Base64。修改traceClass("android.util.Base64"),开启trace,frida -U com.caratlover -l r0tracer.js --no-pause -o base64.txt追查调用栈

image-20210602160016918

通过jadx查看com.chanson.common.a.d,其中String a2 = a.a(string, "f87210e0ed3079d8");的a方法跳转到实现发现是一个完整的标准aes加密。

image-20210602160255397

全局搜索还有AESUtils,完全自己开发的非标准的AES加密,7z x com.caratlover.apk 查看lib/armeabi-v7a下存在alicomphonenumberauthsdk-log-online-standard-release_alijtca_plus.so

image-20210602160511071

strings查看该so中的字符串,traceClass("com.mobile.auth.gatewayauth.utils.security.CheckRoot")

image-20210602161050938

对抗更新

1
2
3
adb connect 172.20.103.172  启动wifiadb
adb install com.caratlover4.1.0.apk
frida -UF -l hookEvent.js 点击马上更新按钮,触发点击时间,打印点击类

image-20210603160106003

打开jadx逐个查看脱完壳后的dex文件,新版本的jadx对加密后的dex反编译结果会rename

image-20210603162543390

查看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构造函数,点击马上更新

image-20210604213755118

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)

image-20210607212455683

找到com.chanson.business.login.presenter.PhoneLoginPresenter$a.a实现方法

image-20210607212852510

找到a方法的调用处,在switch的baseResponse.getErrorCode()的判断时调用PhoneLoginPresenter.f10498a.a,其中renamed from: com.chanson.business.g.s正是我们trace得到的类

image-20210607213046818

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));

image-20210607213548788

使用新版本的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-pausehook 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的选项,发送消息,查看调用栈

image-20210607232416746

在jadx中找到InputLayout的onClick方法

image-20210607234715239

尝试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

image-20210607235129408

通过jadx找到com.tencent.qcloud.tim.uikit.modules.message.MessageInfoUtil的buildTextMessage方法

image-20210607235326550

想办法获取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)

image-20210608000342744

主要逻辑在this.mCurrentConversation.sendMessage,进入sendMessage方法

image-20210608000653377

进入conversation.sendMessage方法

image-20210608000723461

具体流程在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().getConversationsendMessage发送消息了

image-20210608004924503

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中完整的发送消息的流程

image-20210608005229195

调用批量发送

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")
}
})
})
}

微信图片_20210608202426

文章作者: J
文章链接: http://onejane.github.io/2021/05/31/克拉恋人会员制取证分析/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏