直播平台的黑暗面:从游戏到擦边球,再到国际化的暗流涌动
[id_[id_11[id_1285114657]03[id_1128725439]4600]988015968]
前言
大约从 13 年起,众多直播平台纷纷涌现。还记得当初常在午休时观看 DOTA 的直播来下饭。之后不知何时起,不再仅仅是游戏直播,而是一度刮起了女主播打擦边球的不良风气。接着经过严厉整顿,各个平台才逐渐控制住了野蛮的发展态势,开始步入正轨。然而,在那些阳光难以照到的地方,依然存在着暗流涌动的情况。
在总部位于卢森堡的某联盟网站上发现开始有大量中国人拍摄的视频,这是一个契机,因为在国际网站中这种情况比较少见。这些视频有一个共同特征,即都带有某某直播的水印。当时没有在意,并且年底比较忙也没空去深入探究。直到今天闲下来,才想起要去探究一下其中的缘由。
[id_454124706]
主页的样子中规中矩,呈现出一副文艺社交的模样。然而,一看标题就明白它不一般。SM是什么东西呢?熟悉二次元的朋友对此应该不会感到陌生。虽然我也有一定的了解,但在深入探究之后,还是被震惊到了。因此,建议未成年的小兄弟在面对这个问题时,要三思而后行,最好通过百度去了解。
说到百度, 来看看百度对这个站的收录是如何的:
没有被收录!它确实藏得很深。什么是暗网呢?并非只有洋葱网络才叫暗网,这也是一种暗网。回过头来看,这个网站明显只是一个下载站,并且没有 web 访问界面,所以我们要深入研究的话就需要下载它的手机 APP。该网站提供了安卓和 iOS 版本,我们先选择安卓版本来进行分析。
分析 APP 的第一步是进行了使用。为了以防万一,这种 APP 通常会先放在沙盒中运行,因为保不准会有顽固的木马存在。意料之中的是它不支持 x86 的模拟器,而换到 arm 环境中就能正常启动。大家感受下贵圈的画风吧,我截了一张图:
里面分为直播和回放。天下没有免费的午餐。每个视频或直播只能免费播放 8 秒,8 秒后会自动弹出购买窗口。平均购买价格在 288 到 988 金币之间,金币价格情况如下,且支持微信支付。
探索
玩了一遍这个 APP 后,基本了解了它的功能。接下来从技术角度去探索其实现。通常逆向需要事先定下目标,但这次我们没有很明确的目标,只是随意逛逛,顺便复习逆向技能。
脱壳
首先在刚才的那个首页下载安卓的 apk 。然后用 JADX 来查看大致的结构。
看样子使用了 360 的安全加固,之前恰好写了一个脱壳脚本,能够一键脱掉。
之后每个 dex 都能够正常使用 jadx-gui 打开查看。不过,在此我要分享一个我较为喜爱的操作,那就是利用 jadx 命令行分别对各个 dex 进行反编译,接着使用 rsync 将它们合并到一个地方。
mkdir t380.jadx
[id_933600148]
JAVA_OPTS 设定为“-Xmx8G”,然后使用 jadx 进行操作,其中 -j 为 1,-r 也被使用,最后将结果输出到 t380_${i}.jadx 目录下,同时对 unpacked_classes${i}.dex 进行解压
使用 rsync 命令进行同步操作,将 t380_${i}.jadx 同步到 t380.jadx ,同步方式为 -a 。
done
为什么要这么做呢?因为当使用 jadx-gui 分别打开每个 dex 时,无论是查找函数还是查找引用,都不是很方便。而将它们合并到一处,就可以直接用 find/grep 来进行查找。这对于那些有上百兆大小的巨型 APK 进行逆向操作来说,是非常实用的。
逆向
这个 APK 仅有 50 多 M ,然而其代码量并不小,直接去看的话会有些不知从何处入手,那就从收费的地方开始吧!因为这是整个环境最为核心的部分。我们打开一个直播间,等 8 秒之后会弹出付费窗口,然后对这个地方进行定位。
当然有个较为直接的办法,那就是直接运用 adb 去查看当前处于置顶状态的 activity。
然后通过 grep Focus 进行筛选
mCurrentFocus 等于 Window,其标识为 be2e8cd u0 com.zhuoyigou.dese/com.zhuoyigou.dese.ui.live.activity.PlayerLiveActivtiy
mFocusedApp 等于 AppWindowToken,其 token 为 Token,Token 对应的 ActivityRecord 为 {2d0fb2a u0 com.zhuoyigou.dese/.ui.live.activity.PlayerLiveActivtiy t50}
这个直播软件的独特之处在于,其中除了直播之外,大部分是视频回放,观看视频回放也需要收费。其定位方法也是如此,回放的活动为 com.zhuoyigou.dese.ui.live.activity.PlayBackActivity。
这里分享一个小技巧,在逆向过程中常常需要用编辑器打开某个文件,定义一个 shell 函数能够加快这个流程。
function jopen() {
p=`echo -n $1 | tr . /`
file=$p.java
gvim -R $file
}
以 PlayBackActivity 为例,在 jadx 代码目录下,能够直接打开文件的方式如下:
jopen 公司的.zhuoyigou.dese.ui.live.activity.PlayBackActivity
哈,先不说那些了,让我们来看看弹窗付费的地方是怎样实现的吧。这类弹窗付费的部分代码风格呈现如下:
进行了一些对抗,然而并未完全混淆,由此看来是对木桶原理了解不足啊。从其中的方法名称来看,发现了一个较为有趣的函数:
private void showTimeOutDialog[id_425900094] {
if如果当前活动是 GiftActivity 类的主活动,那么 {
reflashGiftActivity("1", null);
}
if (!isFinishing()) {
this.mLiveTimeOutDialog = new LiveTimeOutDialog(this, 2131493319);
this.mLiveTimeOutDialog.show();
this.mLiveTimeOutDialog.setGold(this获取 mPlayBackBean 中的直播房间对象,然后获取该直播房间的付费金额。
this.mLiveTimeOutDialog.setTitle("replay");
this.mLiveTimeOutDialog 可以设置为可取消的。false);
this.mLiveTimeOutDialog 进行设置,当点击时执行购买操作。new 12(this));
this.mLiveTimeOutDialog 设定了点击时不购买的功能。new 13(this));
}
}
试试直接把这个 dialog 禁掉。可以用一些 hook 框架来进行测试,比如 xposed 或者 frida,这里选择 frida,因为可能需要做 native 的分析(是否能发现这个 Activity 的 onCreate 函数是 native 的?)。frida 的更新速度很快,并且很多用法和特性都未写入文档中,多看看别人写的脚本或许会获得很多灵感。
劫持函数是常规操作了:
const PlayBackActivity = Java.use(com.zhuoyigou.dese.ui.live.activity 这个部分是 PlayBackActivity 所在的位置);
PlayBackActivity 展示超时对话框的实现方式是function() {
log("skip showTimeOutDialog()");
}
重新附加进程,接着打开直播间,发现确实不再弹窗了,然而过了十几秒后,视频窗口依然会自己返回。了解安卓开发的人应该都清楚,Activity 的返回通常是调用 finish 函数的,不过找了一圈后发现调用的地方有很多,由于懒癌发作不想一个个去检查,于是使用了 frida 大法,在 finish 函数中查看堆栈。
[+] skip showTimeOutDialog()
[+] skip finish()
java.lang.Exception 在此处打印堆栈跟踪:
在 android.app.Activity 类的 finish 方法中(Native Method)
在 com.zhuoyigou.dese.ui.live.activity.PlayBackActivity 的 11 号内部类的 handleMessage 方法中,该方法位于 PlayBackActivity.java 文件的第 561 行。
在 android.os.Handler 这个类的 dispatchMessage 方法中,具体位置是在 Handler.java 文件的第 102 行。
在 android.os.Looper 的 loop 方法中(位于 Looper.java:148 处)
在 android.app.ActivityThread 的 main 方法中,该方法位于 ActivityThread.java 文件的 5539 行。
在 java.lang.reflect.Method 这个类中,通过 invoke 方法进行调用,该方法为 Native Method(本地方法)。
在 com.android.internal.os.ZygoteInit 的 MethodAndArgsCaller 中运行(ZygoteInit.java:726)
在 com.android.internal.os.ZygoteInit 类的 main 方法中(位于 ZygoteInit.java 文件的 616 行)
在 de.robv.android.xposed.XposedBridge 的 main 方法中(位于 XposedBridge.java:107 处)
通过查看 finish 的堆栈来对 com.zhuoyigou.dese.ui.live.activity.PlayBackActivity$11 类中的 handleMessage 函数进行验证,能够确定存在这样的逻辑。
void handleMessage(Message msg) {
switch(msg.what) {
// ...
case 1002:
this.this$0设置.mPointProgressBar 的可见性为(可见)。8);
PlayBackActivity 能够进行某种访问操作,这种访问操作被标记为特定的符号,具体来说就是 access$ 所代表的操作。PlayBackActivity 具备执行该访问操作的能力,它可以通过特定的方式触发或调用这个 access$ 所对应的功能,从而实现与该访问相关的一系列行为和处理。1400(this.this$0).onPause();
if (PlayBackActivity.access$400(this.this$0)) {
PlayBackActivity 可以实现访问方面的行为。1500(this.this$0);
} else {
PlayBackActivity.access$1600(this.this$0);
}
PlayBackActivity.access$600(this.this$0.sendEmptyMessageDelayed ,延迟时间为 AidConstants.EVENT_NETWORK_ERROR ,发送空消息11000);
return;
caseAidConstants 中的 EVENT_NETWORK_ERROR/*1003*/:
if (PlayBackActivity.access$400(this.this$0) && PlayBackActivity.access$900(this.this$0).isShowing()) {
PlayBackActivity.access$900(this.this$0).dismiss();
}
this.this$0.finish();
return;
// ...
}
}
触发事件在 11 秒后,然后直接退出窗口。8 秒试看之后,还会给你 3 秒时间来选择是否购买哦。作为验证,我们直接对 finish 函数进行操作,发现禁用 dialog 的同时禁用 finish 函数,就能够绕过金币购买的限制,这样就可以想看多久就看多久了。
云存储
现在可以无限制地看片,然而正事是必须要做的。接下来要看看是哪家对象存储提供了服务,或许还能找到硬编码的 key。在代码中四处闲逛时,又发现了一个有趣的函数:
private void initPlay获取字符串类型的视频链接 videoUrl 和播放回退信息类 PlayBackBean {
MyLogger.jLog().e("reVideoUrl:" + videoUrl);
this.mVideoUrl = videoUrl;
if如果 videoUrl 为 null:
展示错误(获取字符串资源 R.string.video_invalid);
return;
}
if (this.mPLVideoTextureView == null) {
this.mPLVideoTextureView 等于通过 findViewById 方法找到的一个 PLVideoTextureView 对象。2131362384);
}
AVOptions options = new AVOptions();
options.setInteger("timeout", StatusCodes.AUTH_DISABLED);
options.setInteger("fast-open", 1);
options.setInteger("mediacodec", 0);
PLVideoTextureView 是一个特定的视图对象,它被用于处理视频纹理相关的操作。在代码中,通过创建一个名为 pLVideoTextureView 的实例来进行相关的视频纹理处理工作。这个视图对象在视频播放、渲染等场景中起到了重要的作用,它能够与视频源进行交互,并将视频内容以纹理的形式展示出来。this.mPLVideoTextureView;
PLVideoTextureView 被命名为 pLVideoTextureView2 ,它是…… ;或者 有一个 PLVideoTextureView 叫做 pLVideoTextureView2 ,该…… ;又或者 名为 pLVideoTextureView2 的是一个 PLVideoTextureView ,它……this.mPLVideoTextureView;
pLVideoTextureView 对其显示的宽高比进行了设置操作。2);
thismPLVideoTextureView 对 AVOptions 进行了设置,设置的内容为 options 。
this.mCustomController = new CustomController(this);
this.mPLVideoTextureView 为其设置了媒体控制器。this.mCustomController);
this.mPLVideoTextureView 将视频路径设置为 videoUrl 。
this.mPLVideoTextureView.start();
thismPLVideoTextureView 设置了信息监听器,这个监听器会接收一些信息事件。当特定的信息事件发生时,监听器会被触发并执行相应的处理逻辑。new 4(this, playBackBean));
this.mPLVideoTextureView 设置了完成监听器,该监听器会在视频播放完成时被触发。new 5(this));
thismPLVideoTextureView 设置了错误监听器,该监听器用于处理错误事件。new 6(this));
}
劫持这个 videoUrl 函数并将其打印出来,其对应的 url 地址如下(已进行打码处理):
该链接指向的是七牛云存储的视频文件,其路径为 http://qiniuvod.xxxxx.com/recordings/z1.xxxxx.qn1546622961492A/0_1546635114.mp4,访问该文件时需要携带签名 sign=b6c6587f1794a14c250e143f28e07620 以及时间戳 t=5c303623。
这个链接通过 wget 能够直接进行下载,其下载结果为 mp4 视频文件,也就是用于播放的回放视频。除了 mp4 格式外,还有 m3u8 格式的链接,m3u8 链接可以直接使用 ffmpeg 来下载。
使用 ffmpeg 命令,将输入地址为 http://xxx.com/vid.m3u8 的视频文件进行处理,采用 -c copy 选项,输出为 vid.mp4 文件。
言归正传,从这里的 url 地址中看到了熟悉的 qiniu。难道是七牛云吗?查看代码后发现确实使用了七牛的安卓 SDK。一年前正好写了个七牛云的对象存储管理命令行工具 qncli[1],因此对七牛云是有了解的。七牛云自身带有 key 和 secret,不过是通过 token 来对 bucket 进行访问的。对于客户端来说,建议的安全做法是采用服务器下发 token 的方式来进行操作。
查看代码后,发现这个 url 确实是服务端下发的。通常情况下,七牛云的私有 bucket 会通过将超时与资源一起签名的方式来控制对内容的访问,其格式为:
该链接为 http://qiniu.example.com/resource.txt ,其中包含参数 e=1546686297 以及 token=xxx 。
不知为何上述链接的格式并非传统私有链接,或许是其他产品,有了解情况的朋友可以留言告知同步一下。
MISC
该直播软件除了采用云存储外,和很多小众直播一样,通过使用三方直播 SDK 来减少开发量,例如某拍 SDK。并且集成了多家厂商的地址定位,推测其目的是做 LBS 社交,而主页中出现的“同城” banner 也在一定程度上证实了我们的这种想法。至于同城见面的用途是什么,那就靠大家自己去想象了。
该 APP 具备分享赚金币的功能,它借助 ShareSDK,能够支持 QQ、微信、微博、豆瓣等知名社交网站。这也就说明了,尽管这个 APP 十分低调,甚至都未进入百度收录,但却拥有相当数量的用户量。如前文所述,无论是直播还是回放,都需要用金币付费才能访问。并且,在其中还可以发布非直播的视频,以有偿的方式提供下载,已然逐渐发展成为了一个小视频交易社区。
根据平台提供的统计数据, 充值用户的“土豪榜”如下:
另外在擦键盘时不小心进入后台,发现此 APP 的注册用户数约为 25 万。用户数量虽不算多,但都是实实在在的付费用户。依据金币的平均价格以及用户的付费状况进行估算,排除部分托儿刷榜的情况,保守估计该平台在 18 年的收入达到千万级。这仅仅是单个 APP 表面上的数据,实际上一个公司背后通常关联着数十个平台,会连带出其他相关情况。因为该平台本身并未做什么恶劣的事情,所以就不再深入探讨了。
小结
本文因无聊且抱有猎奇心理,又手痒难耐,便与这个灰色直播软件玩了许久。期间顺便记录了一些 Android 逆向的常用操作,有些技术写出来确实能让人印象更深刻。俗话说,猎人需时刻保持自己的技能锋利,对于安全研究人员也是如此。安全技术在不断变化,闲暇时也要把握好各种实战机会。
后记
在使用该直播软件时,惊讶地发现其中有不少用户是在校学生,包括用户和主播。通过某种方式添加其中一个主播聊天后得知,SM 行业“水很深”,主播除了在直播间收礼物外,一般还会把高质量用户引流到其他社交平台(刷“飞机”加微信),然后私下会根据情况进行现实调教。用该主播的话来说,“一天 2 个或者 3 个,元旦那天最多,一天有 8 个”,这每一个的收费自然不低,内容大概有某某、某某和某某,重口味一点的有某某、某某等。
该“圈子”的是非暂且不论。此公司处于法律边缘,对未成年人影响较大,有向日本文化发展的趋势。成年观众具备辨别是非的能力,对此就不多加阐述了。至于主播,很多人只是将其作为工作或兼职,赚取的是辛苦钱,也不做过多评判。
笔者刚学安卓逆向不久时写了本文,此文旨在分享思路。其余的故事,就纯粹当作吐槽吧。
References
[1]qncli:
Love & Share
[ 完 ]
朕已阅
文章评论