vx逆向分析随笔

另外有Android逆向可以一起交流学习呀。点击联系我,一个人闷搞挺没劲的。

@app_version = 7.0.20

@app_date = 2020.11.19

本来是想分析vx8的,但是电脑性能不够……反编译工具直接卡死

vx Log日志

当进行分析时,发现反复调用了ae.i,ae.d方法

image-20210814171654155

来到com.tencent.mm.sdk.platformtools.ae;查看

image-20210814171854620

同时找到了setConsoleLogOpen方法,猜测这是对控制台log全局控制的方法

image-20210814172452010

对该方法向上追溯,得到

1
2
3
4
5
com.tencent.mm.sdk.platformtools.ae$a;setConsoleLogOpen
com.tencent.mm.sdk.platformtools.ae;setConsoleLogOpen
com.tencent.mm.xlog.app.XLogSetup;keep_setupXLog
com.tencent.mm.xlog.app.XLogSetup;realSetupXlog
com.tencent.mm.plugin.account.ui.SimpleLoginUI;onCreate/com.tencent.mm.ui.LauncherUI;onResume

其中设置log的是keep_setupXLog的p5参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void keep_setupXLog(boolean p0,String p1,String p2,Integer p3,Boolean p4,Boolean p5,String p6){
// ...
XLogSetup.cachePath = p1; // 缓存目录:/data/user/0/com.tencent.mm/files/xlog
XLogSetup.logPath = p2; // log目录:/storage/emulated/0/Android/data/com.tencent.mm/MicroMsg/xlog
XLogSetup.toolsLevel = p3; // 工具版本:null
XLogSetup.appendIsSync = p4;
XLogSetup.isLogcatOpen = p5; // 是否开启日志:false
XLogSetup.nameprefix = p6; // 前缀:MM
if (!p0) {
// ...
}else if(XLogSetup.setup){
// ...
}else {
// ...
ae.setConsoleLogOpen(XLogSetup.isLogcatOpen.booleanValue());
// ...
}
return;
}

于是开启log只需要给p5赋值为true

frida脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
Java.perform(function() {
/*
* 开启vx全局日志
*/
var clazz = Java.use('com.tencent.mm.xlog.app.XLogSetup');
clazz.keep_setupXLog.overload('boolean', 'java.lang.String', 'java.lang.String', 'java.lang.Integer', 'java.lang.Boolean', 'java.lang.Boolean', 'java.lang.String').implementation = function(p0,p1,p2,p3,p4,p5,p6) {

//console.log("arguments",arguments[5]);
arguments[5] = Java.use("java.lang.Boolean").TRUE.value;
console.log("已开启vx Log打印");
return clazz.keep_setupXLog.apply(this, arguments);
}
});

vx发送消息

使用DDMSStart Method Profiling功能,同时点击【发送】键

然后停止记录,搜索onClick关键字,定位到类com.tencent.mm.pluginsdk.ui.chat.ChatFooter$7

其中sstr为输入的字符串内容

image-20210815161220040

尝试hook ChatFooter.a方法

1
2
3
4
5
6
7
Java.perform(function() {
var clazz = Java.use('com.tencent.mm.pluginsdk.ui.chat.ChatFooter');
clazz.a.overload('com.tencent.mm.pluginsdk.ui.chat.ChatFooter', 'java.lang.String').implementation = function() {
console.log("Send Message",arguments[1]);
return clazz.a.apply(this, arguments);
}
});

可以得到输出

1
2
3
4
Send Message 1
Send Message 2
Send Message [微笑]
Send Message 😂

vx聊天窗口/修改内容

定位

使用DDMSDump View Hierarchy for UI Automator来定位控件ID

image-20210815003423908

查看顶层activity

1
adb shell dumpsys activity top

再找控件ID,最终定位到com.tencent.mm.ui.chatting.view.MMChattingListView

image-20210815003515022

分析

MMChattingListView

这是一个ListView

发现了List的Adapter

image-20210815004245534

交叉引用查看是哪里调用了这个方法

跟到com.tencent.mm.ui.chatting.ChattingUIFragmentfTJ方法

ChattingUIFragment

image-20210815004604975

发现这个Fragment对Adapter进行了设置,同时存放在了this.Lrn字段

其中this.Lrr类型为MMChattingListViewthis.Lro的类型为ListView

也可以得到Adapter的类型为com.tencent.mm.ui.chatting.a.a,同时继承了BaseAdapter

Adapter

根据其重写的getCount方法,得到数据源是this.LtF字段

image-20210815010612094

this.LtF字段的定义是

1
public SparseArray LtF;

使用frida hook一下,查看一下SparseArray每个元素的类型

随便hook了一个Fragment的zt方法(因为看Log日志每次操作都会执行)

获得Fragment的Lrn字段也就是Adapter

再直接调用getCount()getItem()获取到数据源的单个数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java.perform(function() {

var clazz = Java.use('com.tencent.mm.ui.chatting.ChattingUIFragment');
clazz.zt.implementation = function() {
console.log("this.Lrn.value",this.Lrn.value);
var adapter = Java.cast(this.Lrn.value,Java.use("com.tencent.mm.ui.chatting.a.a"))
console.log("adapter",adapter);
console.log("adapter.getCount",adapter.getCount());
console.log("LtF",adapter.LtF.value);
var adapter_size = adapter.getCount();
var msg = adapter.getItem(0);
console.log("msg",msg);
return clazz.zt.apply(this, arguments);
}
});

此时手机界面为

img

获得输出为

1
2
3
4
5
this.Lrn.value com.tencent.mm.ui.chatting.a.a@f945978
adapter com.tencent.mm.ui.chatting.a.a@f945978
adapter.getCount 5
LtF {0=com.tencent.mm.storage.bz@cc74ce8, 1=com.tencent.mm.storage.bz@dba9f01, 2=com.tencent.mm.storage.bz@ae31a6, 3=com.tencent.mm.storage.bz@9afe7e7, 4=com.tencent.mm.storage.bz@c384f94}
msg com.tencent.mm.storage.bz@cc74ce8

可以知道每个数据的类型是com.tencent.mm.storage.bz

每条消息 storage.bz

该类的继承关系为

1
2
3
4
com.tencent.mm.storage.bz
com.tencent.mm.ah.aa
com.tencent.mm.g.c.ej
com.tencent.mm.sdk.e.c

查看个方法,发现里面有如下字段

image-20210815143427853

这些字段是ej类的

image-20210815144136894

同时bz还有5个子类

bz$a

image-20210815152148504

bz$b,大概是有关位置的

image-20210815152234412

bz$c

image-20210815152302867

bz$d,可以看到nickname, cityCode, CountryCode字段

image-20210815152614851

脚本实现

最终选择hook的是Adapter的notifyDataSetChanged方法,这样可以对最后的消息进行查看以及修改

修改一下消息内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java.perform(function() {
var clazz = Java.use('com.tencent.mm.ui.chatting.a.a');
clazz.notifyDataSetChanged.implementation = function() {
console.log("notifyDataSetChanged");
var data = this.LtF.value;
var data_size = data.size();
console.log("data_size",data_size);
for(var i=0;i<data_size;++i){
console.log("Message"+i,Java.cast(data.get(i),Java.use("com.tencent.mm.storage.bz")).field_content.value);
}
Java.cast(data.get(5),Java.use("com.tencent.mm.storage.bz")).field_content.value = Java.use("java.lang.String").$new("heheheheh");
return clazz.notifyDataSetChanged.apply(this, arguments);
}
});

运行脚本之前的界面

img

可以得到以下输出

1
2
3
4
5
6
7
8
9
notifyDataSetChanged
data_size 7
Message0 1
Message1 2
Message2 3
Message3 4
Message4 5
Message5 哈哈哈哈哈哈哈哈哈哈哈哈
Message6 6

同时界面被修改为了

img

当然只能修改本地数据(虽然没什么卵用)

同时json输出该消息的数据结构为

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
// com.tencent.mm.storage.bz
{
"KzF": false,
"KzG": false,
"Zq": false,
"__hadSetcreateTime": false,
"__hadSettype": false,
"dvP": "",
"eBD": false,
"eBO": false,
"eBf": false,
"eEH": 0,
"eEI": "",
"eEL": false,
"eEM": false,
"eEN": false,
"eEO": false,
"eEP": false,
"eEQ": false,
"eER": false,
"eES": false,
"eEf": false,
"eEg": false,
"eEh": false,
"eEi": false,
"eEj": false,
"eEq": false,
"ewj": true,
"ewq": false,
"exe": true,
"fdE": false,
"fdF": false,
"fdI": "",
"fdJ": 0,
"fdK": 0,
"fdL": 0,
"fdM": 1,
"fdN": 0,
"fdO": 0,
"fdP": "",
"fdQ": "",
"fdR": "",
"fdS": 0,
"fdT": [],
"fdU": "",
"fdV": "",
"fdW": 0,
"fdX": 0,
"field_bizChatId": -1,
"field_bizClientMsgId": "",
"field_content": "", // 聊天内容
"field_createTime": 1629009756000, // 聊天时间戳
"field_flag": 0,
"field_isSend": 0,
"field_isShowTimer": 0,
"field_lvbuffer": [0, 0, 125],
"field_msgId": 00, // 消息id
"field_msgSeq": 000000000,
"field_msgSvrId": 4746617000000000000,
"field_status": 0,
"field_talker": "wxid_00000000000000", // 对话的微信用户ida
"field_talkerId": 00,
"field_type": 1,
"systemRowid": -1
}

其中接收到的消息比发送的消息要多了几个字段(普通文本消息)

类总结

因此可以分析到聊天窗口UI以及数据的类为

描述 类名
Fragment com.tencent.mm.ui.chatting.ChattingUIFragment
ListView com.tencent.mm.ui.chatting.view.MMChattingListView
Adapter com.tencent.mm.ui.chatting.a.a
单条消息类 com.tencent.mm.storage.bz

vx投骰子

分析

先用DDMS的记录功能找到点击事件为com.tencent.mm.emoji.panel.a.q$1.onClick()

image-20210816123125656

随后进入了glG.a方法,也就是com.tencent.mm.emoji.panel.a.d.a()方法

image-20210816123331547

frida调试arg14.type一直等于0

刚开始没咋看代码,然后去了v0_2.A(v6)方法,然后找了三四层。😕后来发现里面的有的方法不止是投骰子的时候才被调用才发觉找错了。

然后frida查看了一下v1.getGroup(),发现我添加的自定义表情是81,而骰子和石头剪刀布都是18;同时EmojiGroupInfo.Qbd也为18

然后分析.getProvider().p(v1)方法,交叉引用有多个,分别是com.tencent.mm.ca.a.p()com.tencent.mm.plugin.emoji.e.f.p()

frida打印调用信息发现com.tencent.mm.ca.a.p()先被调用

image-20210816124809022

frida调试发现进入了getEmojiMgr().p(arg9)方法,也就是com.tencent.mm.plugin.emoji.e.f.p()

image-20210816130822813

Cursor是数据库的游标,通过aec方法来查询相应内容

image-20210816132210776

然后v0.moveToPosition(v1)让游标v0移动到v1位置

arg6.convertFrom(v0)这个方法,位于EmojiInfo的父类com.tencent.mm.g.c.bj,查询该行相应的值然后给字段赋值

image-20210816132511952

之后发送的emoji表情就是这个arg6

所以看int v1 = bu.jE(v0.getCount() - 1, 0);

frida得到,石头剪刀布getCount()为3;而投骰子getCount()为6

查看com.tencent.mm.sdk.platformtools.bu.jE()方法

image-20210816132833909

public int nextInt(int n)

该方法的作用是生成一个随机的int值,该值介于[0,n)的区间,也就是0到n之间的随机int值,包含0而不包含n。

arg6是恒为0的。也就是说返回的v0是介于[0,arg5]的。

经Frida调试,骰子点数1-6对应着0-5;剪刀石头布分别对应着0 1 2

若想修改相应点数,直接hook该函数返回值就好

相应堆栈信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
com.tencent.mm.sdk.platformtools.bu.jE(Native Method)
com.tencent.mm.plugin.emoji.e.f.p(SourceFile:91)
com.tencent.mm.plugin.emoji.e.f.p(Native Method)
com.tencent.mm.ca.a.p(SourceFile:240)
com.tencent.mm.ca.a.p(Native Method)
com.tencent.mm.emoji.panel.a.d.a(SourceFile:66)
com.tencent.mm.emoji.panel.a.d.a(Native Method)
com.tencent.mm.emoji.panel.a.q$1.onClick(SourceFile:41)
android.view.View.performClick(View.java:6294)
android.view.View$PerformClick.run(View.java:24770)
android.os.Handler.handleCallback(Handler.java:790)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loop(Looper.java:164)
android.app.ActivityThread.main(ActivityThread.java:6494)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

脚本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java.perform(function () {

var clazz = Java.use('com.tencent.mm.sdk.platformtools.bu');
clazz.jE.implementation = function () {
for(var i=0;i<arguments.length;i++){
console.log("bu.jE arguments"+i,arguments[i]);
}
var result = clazz.jE.apply(this, arguments);
if(arguments[0]==2){
console.log("石头剪刀布结果为:",result==0?"剪刀":result==1?"石头":"布");
}else{
console.log("骰子点数为:",result+1);
}
// 进行修改
// result = Java.use("java.lang.Integer").parseInt("1");
return result;
}
});

vx防撤回

分析

防撤回难点是监听消息啥时候发送过来,真没啥思路。刚开始从聊天页面的ListView的notifyDataSetChanged开始回溯,回溯了好久找不到。

就搜索Log日志revoke

image-20210816160615130

然后去类中搜索字符串定位到com.tencent.mm.plugin.msgquote.PluginMsgQuote.handleRevokeMsgBySvrId

打印一下堆栈信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
com.tencent.mm.plugin.msgquote.PluginMsgQuote.handleRevokeMsgBySvrId(Native Method)
com.tencent.mm.model.f.a(SourceFile:2190)
com.tencent.mm.model.ch.b(SourceFile:258)
com.tencent.mm.s.b.b(SourceFile:40)
com.tencent.mm.plugin.messenger.foundation.c.processAddMsg(SourceFile:165)
com.tencent.mm.plugin.messenger.foundation.c.a(SourceFile:1059)
com.tencent.mm.plugin.messenger.foundation.f.a(SourceFile:118)
com.tencent.mm.plugin.zero.c.a(SourceFile:57)
com.tencent.mm.modelmulti.q$a$1.onTimerExpired(SourceFile:830)
com.tencent.mm.sdk.platformtools.aw.handleMessage(SourceFile:86)
com.tencent.mm.sdk.platformtools.aq$2.handleMessage(SourceFile:362)
android.os.Handler.dispatchMessage(Handler.java:106)
com.tencent.mm.sdk.platformtools.aq$2.dispatchMessage(SourceFile:350)
android.os.Looper.loop(Looper.java:164)
android.os.HandlerThread.run(HandlerThread.java:65)

之后写出如下frida脚本

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
Java.perform(function () {
var clazz = Java.use('com.tencent.mm.plugin.msgquote.PluginMsgQuote');
clazz.handleRevokeMsgBySvrId.implementation = function () {
var log_str = "arguments:"
for (var i = 0; i < arguments.length; i++) {
log_str += " [" + i + "]" + arguments[i]
}
console.log("8 handleRevokeMsgBySvrId", log_str);

return clazz.handleRevokeMsgBySvrId.apply(this, arguments);
}
});

Java.perform(function () {
var clazz = Java.use('com.tencent.mm.model.f');
clazz.a.implementation = function () {
var log_str = "arguments:"
for (var i = 0; i < arguments.length; i++) {
log_str += " [" + i + "]" + arguments[i]
}

console.log("7 f.a()", log_str);

return clazz.a.apply(this, arguments);
}
});

Java.perform(function () {
var clazz = Java.use('com.tencent.mm.model.ch');
clazz.b.overload('com.tencent.mm.ak.e$a').implementation = function () {
var log_str = "arguments:"
for (var i = 0; i < arguments.length; i++) {
log_str += " [" + i + "]" + arguments[i]
}

console.log("6 ch.b()", log_str);

return clazz.b.overload('com.tencent.mm.ak.e$a').apply(this, arguments);
}
});


Java.perform(function () {
var clazz = Java.use('com.tencent.mm.s.b');
clazz.b.overload('com.tencent.mm.ak.e$a').implementation = function () {
var log_str = "arguments:"
for (var i = 0; i < arguments.length; i++) {
log_str += " [" + i + "]" + arguments[i]
}
console.log("5 b.b()", log_str);

return clazz.b.overload('com.tencent.mm.ak.e$a').apply(this, arguments);
}
});


Java.perform(function () {
var clazz = Java.use('com.tencent.mm.plugin.messenger.foundation.c');
clazz.processAddMsg.implementation = function () {
var log_str = "arguments:"
for (var i = 0; i < arguments.length; i++) {
log_str += " [" + i + "]" + arguments[i]
}

console.log("4 c.processAddMsg()", log_str);

return clazz.processAddMsg.apply(this, arguments);
}
});

Java.perform(function () {
var clazz = Java.use('com.tencent.mm.plugin.messenger.foundation.c');
clazz.a.implementation = function () {
var log_str = "arguments:"
for (var i = 0; i < arguments.length; i++) {
log_str += " [" + i + "]" + arguments[i]
}

console.log("3 c.a()", log_str);

return clazz.a.apply(this, arguments);
}
});

Java.perform(function () {
var clazz = Java.use('com.tencent.mm.plugin.messenger.foundation.f');
clazz.a.implementation = function () {
var log_str = "arguments:"
for (var i = 0; i < arguments.length; i++) {
log_str += " [" + i + "]" + arguments[i]
}

console.log("2 f.a()", log_str);

return clazz.a.apply(this, arguments);
}
});

Java.perform(function () {
var clazz = Java.use('com.tencent.mm.plugin.zero.c');
clazz.a.implementation = function () {
var log_str = "arguments:"
for (var i = 0; i < arguments.length; i++) {
log_str += " [" + i + "]" + arguments[i]
}

console.log("1 c.a()", log_str);

return clazz.a.apply(this, arguments);
}
});

对方发送一条消息

1
2
3
4
1 c.a() arguments: [0]com.tencent.mm.protocal.protobuf.aaf@84db3f8 [1]false
2 f.a() arguments: [0]com.tencent.mm.protocal.protobuf.aaf@84db3f8 [1][object Object] [2]false
3 c.a() arguments: [0]com.tencent.mm.protocal.protobuf.aaf@84db3f8 [1][object Object] [2]false [3][object Object]
4 c.processAddMsg() arguments: [0]AddMsgInfo(263938257), get[false], fault[false], up[false] fixTime[0] [1][object Object]

对方撤回该消息

1
2
3
4
5
6
7
8
1 c.a() arguments: [0]com.tencent.mm.protocal.protobuf.aaf@d4208e1 [1]false
2 f.a() arguments: [0]com.tencent.mm.protocal.protobuf.aaf@d4208e1 [1][object Object] [2]false
3 c.a() arguments: [0]com.tencent.mm.protocal.protobuf.aaf@d4208e1 [1][object Object] [2]false [3][object Object]
4 c.processAddMsg() arguments: [0]AddMsgInfo(47744518), get[false], fault[false], up[false] fixTime[0] [1][object Object]
5 b.b() arguments: [0]AddMsgInfo(47744518), get[false], fault[false], up[false] fixTime[0]
6 ch.b() arguments: [0]AddMsgInfo(47744518), get[false], fault[false], up[false] fixTime[0]
7 f.a() arguments: [0]revokemsg [1][object Object] [2]AddMsgInfo(47744518), get[false], fault[false], up[false] fixTime[0]
8 handleRevokeMsgBySvrId arguments: [0]796494781522958800

4 c.processAddMsg()

发现是从processAddMsg()方法开始不一样的,也就是最先在这里判断是否为撤回消息的

而判断是否进入第五层的消息是靠v2!=null来判断的,而v2来自v3.vkP,v3来自参数一arg10.gpg

image-20210816201709925

经调试,普通文本消息的v3.vkP1,而撤回消息的v3.vkP10002

image-20210816210123382

而这恰好有个10002,不过并没有详细分析这里

5 b.b()

进入调用的b(arg10)方法,也就是com.tencent.mm.s.b.b()没发现啥东西

image-20210816210228078

因为正常消息不会执行这个方法,当直接将该方法替换为空的时候,已经可以实现防撤回😂暴力yyds,但是没有撤回提示,还得继续分析

Image-20210816211759

6 ch.b()

直接进入下一层com.tencent.mm.model.ch.b方法,在一开始又对.vkP做了一次判断

image-20210816210702683

至于10001暂不知作用。对于10001只有那一点处理,之后就return null了;而default分支最后也是return null,可以得知.vkp的含义是msgType

可以知道这一个函数都是对撤回消息也就是msgType==10002进行处理的

然后该方法下面都是对v5的判断逻辑,而v5=v0.GYI.IYz

v0.GYIjson化输出,得到

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
// com.tencent.mm.protocal.protobuf.cv
{
"CreateTime": 1629120000,
"GYG": {
"IYA": true,
"IYz": "wxid_00000000000000",
"includeUnKnownField": false
},
"GYH": {
"IYA": true,
"IYz": "wxid_00000000000000",
"includeUnKnownField": false
},
"GYI": {
"IYA": true,
"IYz": "\u003csysmsg type\u003d\"revokemsg\"\u003e\n\t\u003crevokemsg\u003e\n\t\t\u003csession\u003ewxid_00000000000000\u003c/session\u003e\n\t\t\u003cmsgid\u003e1070000000\u003c/msgid\u003e\n\t\t\u003cnewmsgid\u003e600000000000000\u003c/newmsgid\u003e\n\t\t\u003creplacemsg\u003e\u003c![CDATA[\"Forgo7ten.\" 撤回了一条消息]]\u003e\u003c/replacemsg\u003e\n\t\u003c/revokemsg\u003e\n\u003c/sysmsg\u003e\n",
"includeUnKnownField": false
},
"GYJ": 1,
"GYK": {
"hasBuffer": false,
"hasILen": true,
"iLen": 0,
"includeUnKnownField": false
},
"GYN": 656800000,
"nNf": 0,
"vkP": 10002,
"ysP": 1073900000,
"ysR": 2498250000000000000,
"data": [0,0,0,0], // 这些消息的字节数据
"includeUnKnownField": false
}

而v5也就是

1
2
3
4
5
6
7
8
<sysmsg type="revokemsg">
<revokemsg>
<session>wxid_##############</session>
<msgid>10739#####</msgid>
<newmsgid>7829338############</newmsgid>
<replacemsg><![CDATA["Forgo7ten." 撤回了一条消息]]></replacemsg>
</revokemsg>
</sysmsg>

尝试一下修改信息

1
2
3
4
5
6
7
8
9
function modify_revoke_str(old_revoke_str) {
var message = "尝试撤回了一条消息";

var revoke_msg = Java.use("java.lang.String").$new(message);
var sub_str = Java.use("java.lang.String").$new("]]></replacemsg>")
var index = old_revoke_str.indexOf(sub_str);
var new_revoke_str = old_revoke_str.substring(0, index - 7) + revoke_msg + old_revoke_str.substring(index);
return Java.use("java.lang.String").$new(new_revoke_str);
}
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
Java.perform(function () {
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gson = Java.use('com.r0ysue.gson.Gson');

var clazz = Java.use('com.tencent.mm.model.ch');
clazz.b.overload('com.tencent.mm.ak.e$a').implementation = function () {
var log_str = "arguments:"
for (var i = 0; i < arguments.length; i++) {
log_str += " [" + i + "]" + arguments[i]
}
console.log("====================");
var v0 = Java.cast(arguments[0].gpg.value, Java.use("com.tencent.mm.protocal.protobuf.cv"));
console.log(gson.$new().toJson(v0));

function modify_revoke_str(old_revoke_str) {
var message = "尝试撤回了一条消息";

var revoke_msg = Java.use("java.lang.String").$new(message);
var sub_str = Java.use("java.lang.String").$new("]]></replacemsg>")
var index = old_revoke_str.indexOf(sub_str);
var new_revoke_str = old_revoke_str.substring(0, index - 7) + revoke_msg + old_revoke_str.substring(index);
return Java.use("java.lang.String").$new(new_revoke_str);
}
var revoke_xml_obj = Java.cast(v0.GYI.value, Java.use("com.tencent.mm.protocal.protobuf.deo"));

revoke_xml_obj.IYz.value = modify_revoke_str(revoke_xml_obj.IYz.value);
console.log(revoke_xml_obj.IYz.value);
console.log("====================");
console.log("6 ch.b()", log_str);

return clazz.b.overload('com.tencent.mm.ak.e$a').apply(this, arguments);
}
});

虽然消息还是被撤回了,但也说明这种修改提示信息的方式是可行的

img

通过hooknotifyDataSetChanged,json打印最后一条消息发现

撤回消息与撤回提示,仅有field_content"(文本内容)、"field_type""exe"字段不一样

其中正常消息的field_type=1,exe=true;撤回提示field_type=10000,exe=false

被撤回消息和撤回消息的提示的field_msgSvrId字段是一样的,同时对应着撤回提示xml的<newmsgid>字段

尝试修改撤回提示的<newmsgid>字段,发现没有什么用。直接就变成不撤回了。

返回该函数的分析

大概是对xml进行分析处理的一些东西

image-20210816235316259

之后就去了com.tencent.mm.model.f.a()方法

1
public final com.tencent.mm.ak.e.b a(String arg24, Map arg25, com.tencent.mm.ak.e.a arg26);
  • 参数一为字符串String,获取过程为上图中蓝框

    • 即获取的是xml中sysmsgtype属性
    • 撤回消息就为"revokemsg"
  • 参数二是一个XML内容转成的Map数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    ".sysmsg.revokemsg.newmsgid": "4308###############",
    ".sysmsg.revokemsg": "\n\t",
    ".sysmsg.revokemsg.session": "wxid_##############",
    ".sysmsg": "\n",
    ".sysmsg.$type": "revokemsg",
    ".sysmsg.revokemsg.replacemsg": "\"Forgo7ten.\" 撤回了一条消息",
    ".sysmsg.revokemsg.msgid": "##########"
    }
  • 参数三保存着原始的所有数据

    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
    // com.tencent.mm.ak.e$a
    {
    "gpg": {
    "CreateTime": 1629120000,
    "GYG": {
    "IYA": true,
    "IYz": "wxid_00000000000000",
    "includeUnKnownField": false
    },
    "GYH": {
    "IYA": true,
    "IYz": "wxid_00000000000000",
    "includeUnKnownField": false
    },
    "GYI": {
    "IYA": true,
    "IYz": "\u003csysmsg type\u003d\"revokemsg\"\u003e\n\t\u003crevokemsg\u003e\n\t\t\u003csession\u003ewxid_00000000000000\u003c/session\u003e\n\t\t\u003cmsgid\u003e1070000000\u003c/msgid\u003e\n\t\t\u003cnewmsgid\u003e600000000000000\u003c/newmsgid\u003e\n\t\t\u003creplacemsg\u003e\u003c![CDATA[\"Forgo7ten.\" 撤回了一条消息]]\u003e\u003c/replacemsg\u003e\n\t\u003c/revokemsg\u003e\n\u003c/sysmsg\u003e\n",
    "includeUnKnownField": false
    },
    "GYJ": 1,
    "GYK": {
    "hasBuffer": false,
    "hasILen": true,
    "iLen": 0,
    "includeUnKnownField": false
    },
    "GYN": 656800000,
    "nNf": 0,
    "vkP": 10002,
    "ysP": 1073900000,
    "ysR": 2498250000000000000,
    "data": [0, 0, 0, 0], // 这些消息的字节数据
    "includeUnKnownField": false
    },
    "hQb": false,
    "hQc": false,
    "hQd": false,
    "hQe": 0,
    "hQf": false,
    "what": 0
    }

7 f.a()

这个方法里面是分块对参数一也就是.sysmsg.$type的值进行判断

image-20210817135838492

之后发现将.a()方法置为空则消息不会撤回,继续跟入

image-20210817140049459

在里面发现了数据库操作方法update

image-20210817140125502

之后嵌套了三个update,最后来到了updateWithOnConflict方法

image-20210817142313024

附上堆栈信息

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
=============================updateWithOnConflict Stack strat=======================
com.tencent.wcdb.database.SQLiteDatabase.updateWithOnConflict(Native Method)
com.tencent.wcdb.database.SQLiteDatabase.update(SourceFile:1726)
com.tencent.mm.storagebase.f.update(SourceFile:895)
com.tencent.mm.storagebase.h.update(SourceFile:601)
com.tencent.mm.storage.ca.a(SourceFile:2426)
com.tencent.mm.storage.ca.a(Native Method)
com.tencent.mm.model.f.a(SourceFile:2187)
com.tencent.mm.model.f.a(Native Method)
com.tencent.mm.model.ch.b(SourceFile:258)
com.tencent.mm.model.ch.b(Native Method)
com.tencent.mm.s.b.b(SourceFile:40)
com.tencent.mm.s.b.b(Native Method)
com.tencent.mm.plugin.messenger.foundation.c.processAddMsg(SourceFile:165)
com.tencent.mm.plugin.messenger.foundation.c.processAddMsg(Native Method)
com.tencent.mm.plugin.messenger.foundation.c.a(SourceFile:1059)
com.tencent.mm.plugin.messenger.foundation.c.a(Native Method)
com.tencent.mm.plugin.messenger.foundation.f.a(SourceFile:118)
com.tencent.mm.plugin.messenger.foundation.f.a(Native Method)
com.tencent.mm.plugin.zero.c.a(SourceFile:57)
com.tencent.mm.plugin.zero.c.a(Native Method)
com.tencent.mm.modelmulti.q$a$1.onTimerExpired(SourceFile:830)
com.tencent.mm.sdk.platformtools.aw.handleMessage(SourceFile:86)
com.tencent.mm.sdk.platformtools.aq$2.handleMessage(SourceFile:362)
android.os.Handler.dispatchMessage(Handler.java:106)
com.tencent.mm.sdk.platformtools.aq$2.dispatchMessage(SourceFile:350)
android.os.Looper.loop(Looper.java:164)
android.os.HandlerThread.run(HandlerThread.java:65)
=============================updateWithOnConflict Stack end=======================

经过一系列分析之后……(其实上面肯定都有分析的,只不过是很杂乱无从记录,这块就简写了

自己撤回消息提示的msgType=10002,别人撤回消息提示的msgType=10000

1
2
3
4
5
6
7
8
9
10
11
12
Java.perform(function () {
var clazz = Java.use('com.tencent.wcdb.database.SQLiteDatabase');
clazz.updateWithOnConflict.implementation = function () {
// printStack("updateWithOnConflict");
// var log_str = "arguments:"
// for (var i = 0; i < arguments.length; i++) {
// log_str += " [" + i + "]" + arguments[i]
// }
// console.log("updateWithOnConflict", log_str);
return clazz.updateWithOnConflict.apply(this, arguments);
}
});

updateWithOnConflict()方法将参数进行sql的拼接,然后使用new SQLiteStatement()创建SQLiteStatement对象,再用executeUpdateDelete()来执行

Frida hook一下executeUpdateDelete()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java.perform(function () {
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gson = Java.use('com.r0ysue.gson.Gson');

var clazz = Java.use('com.tencent.wcdb.database.SQLiteStatement');
clazz.executeUpdateDelete.overload().implementation = function () {

// console.log("====================");
// console.log("====================");
console.log("7.1.1.1 executeUpdateDelete");
console.log("msql:", this.mSql.value, "Args:", this.mBindArgs.value);

return clazz.executeUpdateDelete.overload().apply(this, arguments);
}
});

当消息被撤回时,得到了以下输出

1
2
3
4
5
6
7
8
9
10
11
7 f.a() arguments: [0]revokemsg [1][object Object] [2]AddMsgInfo(260954771), get[false], fault[false], up[false] fixTime[0]
7.1 ca.a() arguments: [0]234 [1]com.tencent.mm.storage.bz@8b10ad0
7.1.1 updateWithOnConflict arguments:
7.1.1.1 executeUpdateDelete
msql: UPDATE message SET msgId=?,type=?,content=? WHERE msgId=? Args: 234,10000,"Forgo7ten." 撤回了一条消息,234
7.1.1 updateWithOnConflict arguments:
7.1.1.1 executeUpdateDelete
msql: UPDATE rconversation SET msgType=?,flag=?,digestUser=?,digest=?,isSend=?,hasTrunc=?,unReadCount=?,conversationTime=?,content=?,username=?,status=? WHERE username=? Args: 10000,1629182140000,,"Forgo7ten." 撤回了一条消息,0,1,0,1629182140000,"Forgo7ten." 撤回了一条消息,wxid_00000000000000,3,wxid_00000000000000
7.1.1 updateWithOnConflict arguments:
7.1.1.1 executeUpdateDelete
msql: UPDATE rconversation SET UnReadInvite=?,atCount=? WHERE username= ? Args: 0,0,wxid_00000000000000

而我的目的主要是想 有撤回消息提示,同时不撤回消息

其中的参数msgId是本地的消息id,只有第一条数据库操作指令是WHERE msgId=?

所以实际上将被撤回消息 替换成撤回提示的就是这一条。

思路

当然 已经找到数据库操作了。。剩下的就想怎么操作就怎么操作了

我想的是 将该条撤回提示直接INSERT进去,不知道msgId会不会重复,重复的话可以挨个调整?(不知道为啥无法使用select count(*)

或者让提示在指定位置的就是让他被撤回,然后聊天内容添加到撤回提示后边。原聊天内容可以通过select content from message where msgId = ?查询,缺点是只能文字内容;弥补的话只能判断非文本内容不让他撤回了

是想用frida来写的,毕竟frida比较方便。但是构造不出方法参数Object []来。

1
2
3
4
Java.array("Object",[]);

var args = Java.use("java.util.ArrayList").$new();
args.add(...);

这两种都尝试了,但都会报错

image-20210817185555176

mark一下,有知道的大佬麻烦分享一下呀。

脚本实现

这里只用xposed实现了,文本被撤回,但是有撤回提示;图片等等直接暴力不执行方法了…因为撤回提示无法解释图片啥的

效果图

image-20210817235450332

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
package com.forgo7ten.vxhook;

import android.util.Log;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class VxRevoke implements IXposedHookLoadPackage {

@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if (!"com.tencent.mm".equals(lpparam.packageName)) {
return;
}
final String className = "com.tencent.wcdb.database.SQLiteStatement";
final String methodName = "executeUpdateDelete";
Class<?> SDClazz = XposedHelpers.findClass(className, lpparam.classLoader);
XposedBridge.hookAllMethods(SDClazz, methodName, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Log.d("VxHook", "executeUpdateDelete");
Object mDatabase = XposedHelpers.getObjectField(param.thisObject, "mDatabase");
String mSql = (String) XposedHelpers.getObjectField(param.thisObject, "mSql");
Object[] mBindArgs = (Object[]) XposedHelpers.getObjectField(param.thisObject, "mBindArgs");
if ("UPDATE message SET msgId=?,type=?,content=? WHERE msgId=?".equals(mSql) && ((String) mBindArgs[2]).contains("撤回了一条消息")) {
Object sm = XposedHelpers.newInstance(lpparam.classLoader.loadClass("com.tencent.wcdb.database.SQLiteStatement"),
mDatabase,
"select content from message where msgId = ?",
new Object[]{mBindArgs[3]});
String msg = (String) XposedHelpers.callMethod(sm, "simpleQueryForString");
if (!msg.contains("<msg>")) {
mBindArgs[2] = mBindArgs[2] + ":" + msg;
Log.d("VxHook", "对方撤回了:" + msg);
} else {
param.setResult(1);
}
}
}
});

}
}

其实也想将被撤回消息下面的每条消息都msgId+1然后将撤回消息insert进去。但是select count(*)不能使用,这块知识掌握也不太熟练,就没实现……

vx自动抢红包

分析

红包消息监听

自动抢红包肯定是先要监听到消息

之前已经分析到数据库,接收到消息肯定要插入或者更新数据库,于是hook了com.tencent.wcdb.database.SQLiteDatabase.insertWithOnConflictupdateWithOnConflict方法。观察接收消息时候的参数

当接收红包的时候,insertWithOnConflict被触发了4次,分别对WalletLuckyMoney,LuckyMoneyDetailOpenRecord,message,AppMessage数据库进行了插入

其中对message数据库操作参数字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"bizClientMsgId": "",
"talker": "wxid_",
"flag": 0,
"bizChatId": -1,
"msgId": 00,
"type": 436207665,
"content": "",
"msgSvrId": 7222869153754354100,
"lvbuffer": [123, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 125],
"createTime": 1629,
"reserved": "",
"imgPath": "",
"talkerId": 33,
"isSend": 0,
"msgSeq": 723600000,
"status": 3
}

updateWithOnConflict则被触发了三次,都是对rconversation数据库进行的操作

同时发现,普通消息在插入的时候,插入到message数据库的"type":1;图片"type":3;语音"type:34";撤回消息上面说过了,为"type":10000(“你领取了xxx的红包”也是这种形式);红包"type":436207665;转账"type":419430449

同时发现了语音文件是藏在/storage/emulated/0/Android/data/com.tencent.mm/MicroMsg/####/voice2/##/##/msg_###.amr下,不知道为啥打不开

打开红包的时候,聊天界面也就是message数据库插入了一条消息"领取了xxx的红包"同时向WalletLuckyMoney数据库插入了两条消息

其中一条消息有"receiveTime": ,"hbType":0,"receiveAmount":1,"receiveStatus":2,"hbStatus":4,"mNativeUrl":等参数,而且如果点开了红包没有点击【开】按钮选择关闭的话,仍会插入上述参数,只是"receiveTime":0,"hbType":0,"receiveAmount":0,"receiveStatus":0,"hbStatus":2,"mNativeUrl":

红包流程分析

还是先DDMS寻找按钮点击事件,发现在聊天页面点击红包消息触发的方法为com.tencent.mm.ui.chatting.t$e.onClick()

最后在onDone方法里调用了startActivity

image-20210818172734198

堆栈信息为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
com.tencent.mm.br.d$9.onDone(Native Method)
com.tencent.mm.br.d.a(SourceFile:909)
com.tencent.mm.br.d.b(SourceFile:267)
com.tencent.mm.br.d.a(SourceFile:148)
com.tencent.mm.br.d.b(SourceFile:129)
com.tencent.mm.ui.chatting.viewitems.g$b.c(SourceFile:728)
com.tencent.mm.ui.chatting.viewitems.d$d.a(SourceFile:582)
com.tencent.mm.ui.chatting.t$e.onClick(SourceFile:804)
com.tencent.mm.ui.chatting.t$e.onClick(Native Method)
android.view.View.performClick(View.java:6294)
android.view.View$PerformClick.run(View.java:24770)
android.os.Handler.handleCallback(Handler.java:790)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loop(Looper.java:164)
android.app.ActivityThread.main(ActivityThread.java:6494)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

其中com.tencent.mm.ui.chatting.viewitems.g$b.c中的判断,这个url是收到红包时插入WalletLuckyMoney数据库中的那条

image-20210818221446826

com.tencent.mm.br.d.b(android.content.Context, java.lang.String, java.lang.String, android.content.Intent) : void截取一下intent的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"key_msgid": 0,
"key_material_flag": 0,
"key_has_story": false,
"key_way": 1,
"scene_id": 0,
"key_cropname": "",
"key_native_url": "wxpay://c2cbizmessagehandler/hongbao/receivehongbao?msgtype",
"key_emoji_md5": "",
"key_receive_envelope_md5": "",
"key_receive_envelope_url": "",
"key_detail_envelope_md5": "",
"key_detail_envelope_url": "",
"key_username": "wxid_00000000000000"
}

而红包界面的【开】按钮是在LuckyMoneyNotHookReceiveUI$10.onClick()这里触发的

image-20210818214917150

然后进入了auQ()方法

监听红包消息,然后让软件执行这些流程。我分析了一些,没有找到最直接的控制红包入账的方法(😂分析躁了,开学补考得开始复习了)

思路

这里参考了skyun1314/WeChat-plug-in (github.com)

监听insertWithOnConflict方法,当接收到红包消息的时候,先使用com.tencent.mm.br.d.b(android.content.Context, java.lang.String, java.lang.String, android.content.Intent) : void这个静态方法初始化LuckyMoneyNotHookReceiveUI,然后监听onCreate()方法,来执行this.wrX按钮的点击事件。

脚本实现

这样实现的缺点是会弹窗……,如果想实现不弹窗,估计要继续追着【开】按钮onClick方法进去找最主要的那个方法了😲😪

效果图

auto_luckymoney

输出

1
2
3
4
5
6
收到红包
find instance :com.tencent.mm.ui.chatting.e.a@df7c891
content = com.tencent.mm.ui.LauncherUI@f03a980
click result: true
click result: true
click result: 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
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
function main(){
Java.perform(function () {
Java.openClassFile("/data/local/tmp/r0gson.dex").load();
const gson = Java.use('com.r0ysue.gson.Gson');

var clazz = Java.use('com.tencent.wcdb.database.SQLiteDatabase');
clazz.insertWithOnConflict.implementation = function () {
// console.log("insertWithOnConflict");
// console.log("[0]"+arguments[0],"[1]"+arguments[1],"[2]"+gson.$new().toJson(arguments[2]));


if (arguments[0] == "message") {
var values = Java.cast(arguments[2], Java.use("android.content.ContentValues"));
var msg_type = values.get("type");
if (msg_type == "436207665") {
console.log("收到红包");
var content;
Java.choose("com.tencent.mm.ui.chatting.e.a", {
onMatch: function (x) {
console.log("find instance :" + x);
content = x.LDE.value.getContext();
console.log("content =", content);
},
onComplete: function () {
}
});
open_luck_money(content, values);

// var intent = Java.cast(arguments[2],Java.use("android.content.Intent"));
} else if (msg_type == "419430449") {
console.log("收到转账");
}

}
return clazz.insertWithOnConflict.apply(this, arguments);
}
});

function open_luck_money(content, values) {
function xml_get_value(content, start, end) {
var javaString = Java.use("java.lang.String");
var str_start = javaString.$new(start);
var str_end = javaString.$new(end);
var str_content = javaString.$new(content);
var start_index = str_content.indexOf(str_start);
var end_index = str_content.indexOf(str_end);
var result = str_content.substring(start_index+str_start.length(),end_index);
return result;

}
var javaString = Java.use("java.lang.String");
var intent = Java.use("android.content.Intent").$new();
var v_content = values.get("content");
intent.putExtra(javaString.$new("key_msgid"), Java.use("java.lang.Long").parseLong(""+values.get("msgId")));
intent.putExtra(javaString.$new("key_way"), Java.use("java.lang.Integer").parseInt("1"));
intent.putExtra(javaString.$new("key_has_story"), Java.use("java.lang.Boolean").FALSE.value);
intent.putExtra(javaString.$new("key_username"), javaString.$new(values.get("talker")));
intent.putExtra(javaString.$new("key_native_url"), javaString.$new(xml_get_value(v_content,"<nativeurl><![CDATA[","]]></nativeurl>")));

var luckymoney_str = javaString.$new("luckymoney");
var luckymoneyUI_str = javaString.$new(".ui.LuckyMoneyNotHookReceiveUI");
Java.use('com.tencent.mm.br.d').b(content, luckymoney_str, luckymoneyUI_str,intent);
}


Java.perform(function () {
var clazz = Java.use('com.tencent.mm.plugin.luckymoney.ui.LuckyMoneyNotHookReceiveUI');
clazz.onSceneEnd.implementation = function () {
var result = clazz.onSceneEnd.apply(this, arguments);
console.log("click result:",this.wrX.value.performClick());
return result;
}
});


}

setImmediate(main);

参考文章

微信Log日志分析——初步探索 (toutiao.com)

简单分析骰子流程 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

某聊天软件撤回流程的分析与防撤回实现 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

[原创]Xposed第二课(微信篇) 聊天界面修改文字-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com

[原创]Xposed第三课(微信篇) 防止好友消息撤回-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com

[下载]微信6.6.1 Xposed模块 包含 主动发消息 防撤回 抢红包 骰子作弊 模拟位置 步数最高-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com

深表谢意

最后

应付完考试继续研究……