haydream收费直播间的取证分析

篇幅有限

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

脱壳

image-20210529115117279

1
2
3
4
5
6
7
git clone https://github.com/hluwa/FRIDA-DEXDump.git
./fs1280arm64
pyenv local 3.8.0
python main.py app保持最前端,开始脱壳
python merge_dex.py ./com.hay.dreamlover/ livedex 将脱壳后的dex反编译成java,推荐!!!
git clone https://github.com/Simp1er/AndroidSec.git
python dex2apk.py -a haydream.apk -i ./com.hay.dreamlover -o output.apk 将脱壳后的dex重打包成apk

抓包

charles-postern连接socks5启动vpn 发现任何一个页面只要连接代理或者vpn都无法访问请求

某抢票app逆向续篇之干掉vpn抓包检测 api有java.net.NetworkInterface.getName(),android.net.ConnectivityManager

1
2
3
4
5
6
7
8
9
10
11
objection -g com.hay.dreamlover explore
android hooking search classes networkinterface
android hooking watch class java.net.NetworkInterface
android hooking search classes connectivitymanager
android hooking watch class android.net.ConnectivityManager
android hooking watch class android.net.IConnectivityManager 进入页面后果然触发了这些方法,说明确实做了vpn检测
android hooking watch class_method android.net.ConnectivityManager.getActiveNetworkInfo --dump-args --dump-backtrace --dump-return 打印调用栈,通过jadx查看LiveNetChecker,尝试使用frida脚本过这段代码逻辑
git clone https://github.com/r0ysue/r0capture.git
python3 r0capture.py -U com.hay.dreamlover -v 先打开app后attach,log显示127.0.0.1和127.0.0.1通信,且所有内容都已经加密
frida -U -f com.hay.dreamlover -l script.js -o out.txt
python3 r0capture.py -U com.hay.dreamlover -v -w 3 延迟3秒

使用带有kali nethunter底包的nexus 6p,开启ssh服务

1
2
3
4
5
ssh root@192.168.0.104 默认密码toor
jnettop 点开直播间,可以看到直播间地址和ip
手机自制路由器,电脑插上网卡连接到虚拟机,nm-connection-editor,新加一个Wi-Fi,设置SSID,Mode设置Hotspot,Band设置B/G(2.4 GHZ),Device选择wlan0,配置IPv4 Settings的Address,手机即可收到新wifi热点
lsusb 查看设备型号
wireshark 抓包

收费直播间分析

直播弹窗倒计时9s后强制退出

1
2
3
4
5
6
7
objection -g com.hay.dreamlover explore
android hooking search classes Dialog
android hooking search classes AlertDialog
android hooking search classes PopupWindow
android hooking watch class android.app.Dialog --dump-args --dump-backtrace --dump-return
android hooking watch class android.app.AlertDialog --dump-args --dump-backtrace --dump-return
android hooking watch class_method android.app.Dialog.show --dump-args --dump-backtrace --dump-return

(agent) [8z3ukmteu2y] Called android.app.Dialog.show()
(agent) [8z3ukmteu2y] Backtrace:
android.app.Dialog.show(Native Method)
com.fanwe.lib.dialog.impl.SDDialogBase.show(SDDialogBase.java:337)
com.fanwe.live.activity.room.LiveLayoutViewerExtendActivity.showScenePayJoinDialog(LiveLayoutViewerExtendActivity.java:618)
com.fanwe.live.activity.room.LiveLayoutViewerExtendActivity.onScenePayViewerShowWhetherJoin(LiveLayoutViewerExtendActivity.java:516)
com.fanwe.pay.LiveScenePayViewerBusiness.dealPayModelRoomInfoSuccess(LiveScenePayViewerBusiness.java:156)
com.fanwe.live.activity.room.LiveLayoutViewerExtendActivity.onBsRequestRoomInfoSuccess(LiveLayoutViewerExtendActivity.java:111)
com.fanwe.live.activity.room.LivePushViewerActivity.onBsRequestRoomInfoSuccess(LivePushViewerActivity.java:405)
com.fanwe.live.business.LiveBusiness.onRequestRoomInfoSuccess(LiveBusiness.java:306)
com.fanwe.live.business.LiveViewerBusiness.onRequestRoomInfoSuccess(LiveViewerBusiness.java:79)
com.fanwe.live.business.LiveBusiness$2.onSuccess(LiveBusiness.java:257)
com.fanwe.library.adapter.http.callback.SDRequestCallback.onSuccessInternal(SDRequestCallback.java:127)
com.fanwe.library.adapter.http.callback.SDRequestCallback.notifySuccess(SDRequestCallback.java:175)
com.fanwe.hybrid.http.AppHttpUtil$1.onSuccess(AppHttpUtil.java:105)
com.fanwe.hybrid.http.AppHttpUtil$1.onSuccess(AppHttpUtil.java:74)
org.xutils.http.HttpTask.onSuccess(HttpTask.java:447)
org.xutils.common.task.TaskProxy$InternalHandler.handleMessage(TaskProxy.java:198)
android.os.Handler.dispatchMessage(Handler.java:106)
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)

(agent) [8z3ukmteu2y] Return Value: (none)

frida -UF -l trace.js -o hay.txt 打印了多个android.app.Dialog.show

1
traceClass("android.app.Dialog")

结合jadx分析弹窗堆栈,实现逻辑可以干掉弹窗或者干掉倒计时

1
2
3
4
android hooking search methods startCountDown
android hooking watch class com.fanwe.pay.appview.PayLiveBlackBgView 进入直播间确实触发了该方法
android hooking watch class_method com.fanwe.pay.appview.PayLiveBlackBgView.startCountDown --dump-args --dump-backtrace --dump-return
android hooking list class_methods com.fanwe.lib.dialog.impl.SDDialogBase

frida -UF -l hookVIP.js 破解收费直播间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setImmediate(function(){
Java.perform(function(){
console.log("Entering hook")
// 干掉弹窗
Java.use("com.fanwe.lib.dialog.impl.SDDialogBase").show.implementation = function(){
console.log("hook show ")
}
// 设置倒计时
Java.use("com.fanwe.pay.appview.PayLiveBlackBgView").startCountDown.implementation = function(x){
console.log("Calling countdown ")
return this.startCountDown(1000*3600)
}
})
})

image-20210529165859370

com.fanwe.live.business.LiveBusiness.onRequestRoomInfoSuccess类中大部分request都来自于CommonInterface类

1
2
3
4
5
6
android hooking watch class com.fanwe.live.common.CommonInterface  每进一个房间或下拉自动触发该类的方法
android hooking watch class_method com.fanwe.live.common.CommonInterface.requestIndex --dump-args --dump-backtrace --dump-return
android hooking watch class_method com.fanwe.live.common.CommonInterface.requestRoomInfo --dump-args --dump-backtrace --dump-return 进入直播间,log打印room_id
plugin wallbreaker classdump --fullname com.fanwe.live.business.LiveBusiness$2
android hooking search classes com.fanwe.live.business.LiveBusiness
android hooking watch class com.fanwe.live.business.LiveBusiness

(agent) [2xhzmmkxx1r] Called com.fanwe.live.common.CommonInterface.requestIndex(int, int, int, java.lang.String, com.fanwe.hybrid.http.AppRequestCallback)

(agent) [2xhzmmkxx1r] Backtrace:
com.fanwe.live.common.CommonInterface.requestIndex(Native Method)
com.fanwe.live.appview.main.LiveTabHotView.requestData(LiveTabHotView.java:390)
com.fanwe.live.appview.main.LiveTabHotView.onLoopRun(LiveTabHotView.java:382)
com.fanwe.live.appview.main.LiveTabBaseView$1.run(LiveTabBaseView.java:116)
com.fanwe.lib.looper.impl.SDSimpleLooper$1.handleMessage(SDSimpleLooper.java:54)
android.os.Handler.dispatchMessage(Handler.java:106)
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)

(agent) [2xhzmmkxx1r] Arguments com.fanwe.live.common.CommonInterface.requestIndex(1, (none), (none), 热门, com.fanwe.live.appview.main.LiveTabHotView$4@89cbef4)
(agent) [2xhzmmkxx1r] Return Value: (none)

1
2
3
android hooking search classes SDResponse
plugin wallbreaker classdump --fullname com.fanwe.library.adapter.http.model.SDResponse
android hooking watch class_method com.fanwe.library.adapter.http.model.SDResponse.getDecryptedResult --dump-args --dump-backtrace --dump-return

frida -UF -l requestIndex.js 主动调用获取房间列表和详情

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
function hook() {
Java.perform(function () {
var JSON = Java.use("com.alibaba.fastjson.JSON")
var Index_indexActModel = Java.use("com.fanwe.live.model.Index_indexActModel");
var gson = Java.use("com.google.gson.Gson").$new();
var LiveRoomModel = Java.use("com.fanwe.live.model.LiveRoomModel");
Java.use("com.fanwe.live.appview.main.LiveTabHotView$4").onSuccess.implementation = function (resp) {
console.log("Entering Room List Parser => ", resp)
var result = resp.getDecryptedResult();
// 转成json对象
var resultModel = JSON.parseObject(result, Index_indexActModel.class);
// json转成java对象,并调用getList方法
var roomList = Java.cast(resultModel, Index_indexActModel).getList();
console.log("size : ", roomList.size(), roomList.get(0))
for (var i = 0; i < roomList.size(); i++) {
var LiveRoomModelInfo = Java.cast(roomList.get(i), LiveRoomModel);
console.log("roominfo: ", i, " ", gson.toJson(LiveRoomModelInfo));
}

return this.onSuccess(resp)
}
})
}

// 主动调用
function invoke(){
Java.perform(function(){
Java.choose("com.fanwe.live.appview.main.LiveTabHotView",{
onMatch:function(ins){
console.log("found ins => ",ins)
ins.requestData();
},onComplete:function(){
console.log("Search completed!")
}
})
})

}

function main() {
hook()
// invoke()
}

setImmediate(main)

image-20210529170029292

frida -UF -l requestRoomInfo.js

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 inspectObject(obj) {
Java.perform(function () {
const Class = Java.use("java.lang.Class");
const obj_class = Java.cast(obj.getClass(), Class);
const fields = obj_class.getDeclaredFields();
const methods = obj_class.getMethods();
console.log("Inspecting " + obj.getClass().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());
})
}

// 打印调用结果的域及信息,类似wallbreaker
function hookROOMinfo() {
Java.perform(function () {
var JSON = Java.use("com.alibaba.fastjson.JSON")
var gson = Java.use("com.google.gson.Gson").$new();
var App_get_videoActModel = Java.use("com.fanwe.live.model.App_get_videoActModel");

Java.use("com.fanwe.live.business.LiveBusiness$2").onSuccess.implementation = function (resp) {
console.log("Enter LiveBusiness$2 ... ", resp)
var result = resp.getDecryptedResult();
var resultVideoModel = JSON.parseObject(result, App_get_videoActModel.class);
var roomDetail = Java.cast(resultVideoModel, App_get_videoActModel);
console.log("room id is => ", roomDetail.getRoom_id());
inspectObject(roomDetail);
return this.onSuccess(resp);
}
})

}

image-20210529170418419

当jadx找不到类的时候,说明脱壳没脱全,通过objection去内存即可搜刮wallbreak内存漫游。

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
// 内存中捞不到类
function invoke(){

Java.perform(function(){
Java.choose("com.fanwe.live.business.LiveBusiness",{
onMatch:function(ins){
console.log("found ins => ",ins)
// ins.requestData();
},onComplete:function(){
console.log("Search completed!")
}
})
})
}

// 自行调用构造函数创建类
function invoke2(){
Java.perform(function(){

// com.fanwe.live.business.LiveBusiness(ILiveActivity);
var ILiveActivity = Java.use("com.fanwe.live.activity.room.ILiveActivity");
// 实现接口
const ILiveActivityImpl = Java.registerClass({
name: 'com.fanwe.live.activity.room.ILiveActivityImpl',
implements: [ILiveActivity],
methods: {
openSendMsg(){},
getCreaterId(){},
getGroupId(){},
getRoomId(){},
getRoomInfo(){},
getSdkType(){},
isAuctioning(){},
isCreater(){},
isPlayback(){},
isPrivate(){}
}
});

var result = Java.use("com.fanwe.live.business.LiveBusiness").$new(ILiveActivityImpl.$new());
console.log("result is => ",result.requestRoomInfo("123454"))
})
}

var LiveBusiness = null ;
console.log("LiveBusiness is => ", LiveBusiness)
function hook3(){
Java.perform(function(){
Java.use("com.fanwe.live.business.LiveBusiness").getLiveQualityData.implementation = function(){
LiveBusiness = this;
console.log("now LiveBusiness is => ", LiveBusiness)
LiveBusiness.requestRoomInfo("12343");
var result = this.getLiveQualityData()
return result;
}
})
}

function invoke3(){
Java.perform(function(){
var result = LiveBusiness.requestRoomInfo("12343");
console.log("result is => ",result)
})
}

function invoke4(){
Java.perform(function(){

// com.fanwe.live.business.LiveBusiness(ILiveActivity);
var ILiveActivity = Java.use("com.fanwe.live.activity.room.ILiveActivity");

const ILiveActivityImpl = Java.registerClass({
name: 'com.fanwe.live.activity.room.ILiveActivityImpl',
implements: [ILiveActivity],
methods: {
openSendMsg(){},
getCreaterId(){},
getGroupId(){},
getRoomId(){},
getRoomInfo(){},
getSdkType(){},
isAuctioning(){},
isCreater(){},
isPlayback(){},
isPrivate(){}
}
});

var LB = Java.use("com.fanwe.live.business.LiveBusiness").$new(ILiveActivityImpl.$new());

var LB2 = Java.use("com.fanwe.live.business.LiveBusiness$2");
var AppRequestCallback = Java.use('com.fanwe.hybrid.http.AppRequestCallback');
Java.use("com.fanwe.live.common.CommonInterface").requestRoomInfo(1377894,123,"1234",Java.cast(LB2.$new(LB),AppRequestCallback));
})
}


function main() {
hookROOMinfo();
hook3();
}

setImmediate(main)

image-20210529172252177

以上hook2时无法返回数据,是因为roomId为空,获取room_id时发现getRoomId来自于一个接口ILiveInfo,可以通过该接口找到实现类,或者通过jadx的smali查看是不是确实为getRoomId。

1
2
android hooking search methods getRoomId  全局搜索方法
android hooking watch class_method com.fanwe.live.activity.room.LiveActivity.getRoomId --dump-args --dump-backtrace --dump-return

image-20210530232624680

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
function hookROOMinfo() {
Java.perform(function () {
var JSON = Java.use("com.alibaba.fastjson.JSON")
var gson = Java.use("com.google.gson.Gson").$new();
var App_get_videoActModel = Java.use("com.fanwe.live.model.App_get_videoActModel");

Java.use("com.fanwe.live.business.LiveBusiness$2").onSuccess.implementation = function (resp) {
console.log("Enter LiveBusiness$2 ... ", resp)
var result = resp.getDecryptedResult();
var resultVideoModel = JSON.parseObject(result, App_get_videoActModel.class);
var roomDetail = Java.cast(resultVideoModel, App_get_videoActModel);
console.log("room id is => ", roomDetail.getRoom_id());
inspectObject(roomDetail);
return this.onSuccess(resp);
}
// 直接主动调用,设置房间号
// Java.use("com.fanwe.live.common.CommonInterface").requestRoomInfo.implementation = function (roomid, vod, key, ins) {
// console.log("Calling common.CommonInterface.requestRoomInfo...")
// return this.requestRoomInfo(1379212, vod, key, ins);
// }

Java.use("com.fanwe.live.LiveInformation").getRoomId.implementation = function(){
console.log("calling com.fanwe.live.activity.room.LiveActivity.getRoomId ...")
return 1379212 ;
}

// com.fanwe.live.ILiveInfo.getRoomId
// com.fanwe.live.LiveInformation.getRoomId
// com.fanwe.live.activity.room.LiveActivity.getRoomId
// com.fanwe.live.appview.room.RoomSelectFriendsView.getRoomId
// com.fanwe.live.model.CreateLiveData.getRoomId
// com.fanwe.live.model.JoinLiveData.getRoomId
// com.fanwe.live.model.JoinPlayBackData.getRoomId
})

}

frida -UF -l rquestRoomInfo.js 再次调用invoke2()实现房间详情抓取

主动调用的原则:离数据越远,中间需要自己实现的细节就越多;哪个细节实现不对,APP就崩掉了。

针对单个类AppHttpUtil找不到,使用fart可以脱单个类

1
2
3
4
5
6
7
android hooking search classes AppHttpUtil
git clone https://github.com/hanbinglengyue/FART.git
adb push lib/fart* /data/local/tmp
adb shell ->cp fart* /data/app && chmod 777
frida -UF -l frida_fart_reflection.js
dump("com.fanwe.hybrid.http.AppHttpUtil")
adb pull /sdcard/6850924_22686.dex

通过jadx-gui打开看不到源码,file 6850924_22686.dex是data格式,而非Dalvik dex格式。通过010 Editor打开该dex发现文件魔术字全是00 00 00 00 00 00 00 00, 查看正常dex文件头为64 65 78 0a 30 33 35 00,再次打开即可找到AppHttpUtil,可以看到拼接参数时用的标准的加密库。

image-20210529172701652

使用沙箱安装该apk后查看/data/data/com.hay.dreamlover的加密文件数据,找到Cipher文件中base64定位加解密的方法。

1
android hooking watch class_method com.fanwe.library.utils.MD5Util.MD5 --dump-args --dump-backtrace --dump-return

通过vnc连接kali nethunter后启动wireshark抓包,保存为pcapng格式文件,使用电脑分析,找到最终发出去的包,发现通过本地端口转发出去,获取请求头参数后实现解密

image-20210530235438489

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
## Time: 2020-7-31 19:41:11
## com.hay.dreamlover

import requests, os, time, sys
from lxml import etree
import re
import json
import threading
import hashlib
import base64
from urllib import parse

# import click
# import frida
# import logging
# import traceback

import base64
import re

from Crypto.Cipher import AES

# https://blog.csdn.net/wangziyang777/article/details/104982823

## aes 加密/解密
class AESECB:
def __init__(self, key):
self.key = key
self.mode = AES.MODE_ECB
self.bs = 16 # block size
self.PADDING = lambda s: s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)
def encrypt(self, text):
generator = AES.new(self.key, self.mode) # ECB模式无需向量iv
crypt = generator.encrypt(self.PADDING(text))
crypted_str = base64.b64encode(crypt)
result = crypted_str.decode()
return result

def decrypt(self, text):
generator = AES.new(self.key, self.mode) # ECB模式无需向量iv
text += (len(text) % 4) * '='
decrpyt_bytes = base64.b64decode(text)
meg = generator.decrypt(decrpyt_bytes)
# 去除解码后的非法字符
try:
result = re.compile('[\\x00-\\x08\\x0b-\\x0c\\x0e-\\x1f\n\r\t]').sub('', meg.decode())
except Exception:
result = '解码失败,请重试!'
return result


#计算密码的md5值
def get_md5(s):
md = hashlib.md5()
md.update(s.encode('utf-8'))
return md.hexdigest()

if __name__ == '__main__':
aes = AESECB('8648754518945235')
ctl = "index"
act = "index"
signqt = get_md5("528094&*3564695()" + ctl + "+_" + act + "@!@###@");
timeqt = str(round(time.time() * 1000));

headers = {"X-JSL-API-AUTH":"sha1|1596358731|VOI1X6448Y4f4E|fd941812d5b875b021f92cf2b0044552462d8cd9"};
body = {"screen_width":1080,
"screen_height":1794,
"sdk_type":"android",
"sdk_version_name":"1.3.0",
"sdk_version":2020031801,
"xpoint":120.107042,
"ypoint":30.302162,
"ctl":ctl,
"act":"new_video",
"p":1,
"signqt":signqt,
"timeqt":timeqt}
requestData = aes.encrypt(str(body));
url = "http://hhy2.hhyssing.com:37462/mapi/index.php?requestData=" + requestData + "i_type=1&ctl=" + ctl + "&act=" + act;
rsp = requests.post(url, headers = headers);
result = json.loads(rsp.text).get("output");
decodeAes = AESECB("7489148794156147");
print(decodeAes.decrypt(result));

python r0capture.py -U com.hay.dreamlover -v -w 3 >> hay.txt 抓包进出直播间,查看请求

netstat -tuulp|grep hay 查看端口

adb forward tcp:37462 tcp:37462 将给本地发请求包转发到手机端的37462端口

nethunter 中wireshark抓lo包,本地对本地的包,因为app都是对本地请求,再转发到服务端(使用vnc viewer连接)

阿里游戏盾SDK的作用:

  • 防止抓包
  • 隐藏真实的服务器地址,流量包二次
  • 抗D
文章作者: J
文章链接: http://onejane.github.io/2021/05/29/haydream收费直播间的取证分析/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏