Reverse-Engineering a Gray-Market Livestream App: Unpacking, Payment Bypass, and Cloud Forensics
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
Reverse-Engineering a Gray-Market Livestream App: Unpacking, Payment Bypass, and Cloud Forensics
“If you gaze long into an abyss, the abyss also gazes into you.” — Nietzsche.
Livestreaming platforms exploded around 2013. What started as game streams gradually drifted, then got cracked down on. But where the sun doesn’t shine, the current still flows.
The discovery
I noticed an influx of videos shot by Chinese creators on a Luxembourg-based affiliate site — unusual for an international site. They all shared a common watermark from a certain livestream service. I didn’t think much of it at the time, but eventually circled back to investigate.
Searching the watermark logo led to a website:

The homepage looks deceptively artsy, but the titles give it away. Checking whether Baidu indexes it:

Not indexed at all — hidden well. What is the “dark web”? It’s not only Tor; this is a kind of dark web too. The site is just a download hub with no web-facing UI, so to go deeper you need the mobile app. It offers Android and iOS versions; I started with Android.
First step is using it — in a sandbox, just in case it hides a stubborn trojan. Unsurprisingly it doesn’t support x86 emulators; it runs fine in an ARM environment. A screenshot of the scene:

It’s split into live streams and replays. Nothing’s free: each video or stream plays 8 seconds before a purchase dialog pops up, priced 288–988 coins. Coin prices support WeChat Pay:

Exploration
After poking through the app, I had the basic picture. Time to look under the hood — no specific goal this time, just brushing up reverse-engineering skills.
Unpacking
Download the APK from the homepage and look at its structure in JADX:

Looks like 360’s hardening. I had an unpacking script from before, so one command strips it:

Each dex can then be opened in jadx-gui. A workflow I prefer: decompile each dex with the jadx CLI, then merge with rsync.
mkdir t380.jadx
for i in {1..4};do
JAVA_OPTS="-Xmx8G" jadx -j 1 -r -d t380_${i}.jadx unpacked_classes${i}.dex
rsync -a t380_${i}.jadx t380.jadx
done
Why? Because opening each dex separately in jadx-gui makes both symbol search and reference search awkward. Merging them into one tree lets you use plain find/grep. For 100 MB+ APKs this is genuinely handy.
Reverse engineering
Even at 50 MB the codebase is large, so start from the money — the core of the whole thing. Open a stream, wait 8 seconds for the paywall, and locate it:

A more direct approach is asking adb for the top activity:
$ adb shell dumpsys window windows | grep Focus
mCurrentFocus=Window{be2e8cd u0 com.zhuoyigou.dese/com.zhuoyigou.dese.ui.live.activity.PlayerLiveActivtiy}
mFocusedApp=AppWindowToken{73e5eb8 token=Token{bfded1b ActivityRecord{2d0fb2a u0 com.zhuoyigou.dese/.ui.live.activity.PlayerLiveActivtiy t50}}}
This app is unusual in that most content is video replays, also paid. The replay activity is com.zhuoyigou.dese.ui.live.activity.PlayBackActivity.
A small tip: when reverse-engineering you often open files by class name; a shell function speeds that up:
function jopen() {
p=`echo -n $1 | tr . /`
file=$p.java
gvim -R $file
}
Then from the jadx output directory:
jopen com.zhuoyigou.dese.ui.live.activity.PlayBackActivity
Anyway — the paywall implementation. PlayBackActivity’s code is partly obfuscated, but not fully — the weakest-link principle, apparently. By method name, one function stands out:
private void showTimeOutDialog() {
if (isMainActivityTop(GiftActivity.class)) {
reflashGiftActivity("1", null);
}
if (!isFinishing()) {
this.mLiveTimeOutDialog = new LiveTimeOutDialog(this, 2131493319);
this.mLiveTimeOutDialog.show();
this.mLiveTimeOutDialog.setGold(this.mPlayBackBean.getLiveRoom().getPayMoney());
this.mLiveTimeOutDialog.setTitle("replay");
this.mLiveTimeOutDialog.setCancelable(false);
this.mLiveTimeOutDialog.setBuyOnClick(new 12(this));
this.mLiveTimeOutDialog.setNoBuyOnClick(new 13(this));
}
}
Just disable this dialog and see. For hooking I picked frida over Xposed, since I might need native analysis (did you notice this Activity’s onCreate is native?). Frida moves fast and a lot of its features aren’t documented; reading others’ scripts sparks ideas.
Hijacking the method is routine:
const PlayBackActivity = Java.use("com.zhuoyigou.dese.ui.live.activity.PlayBackActivity");
PlayBackActivity.showTimeOutDialog.implementation = function() {
log("skip showTimeOutDialog()");
}
Re-attach, open a stream — no dialog. But after a dozen seconds the video window still exits on its own. In Android, an Activity exits by calling finish — but there were too many call sites to check by hand. Frida to the rescue: dump the stack inside finish:
[+] skip showTimeOutDialog()
[+] skip finish()
[+] java.lang.Exception: printStackTrace here:
at android.app.Activity.finish(Native Method)
at com.zhuoyigou.dese.ui.live.activity.PlayBackActivity$11.handleMessage(PlayBackActivity.java:561)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5539)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
at de.robv.android.xposed.XposedBridge.main(XposedBridge.java:107)
Check PlayBackActivity$11.handleMessage from the stack — sure enough:
void handleMessage(Message msg) {
switch(msg.what) {
// ...
case 1002:
this.this$0.mPointProgressBar.setVisibility(8);
PlayBackActivity.access$1400(this.this$0).onPause();
if (PlayBackActivity.access$400(this.this$0)) {
PlayBackActivity.access$1500(this.this$0);
} else {
PlayBackActivity.access$1600(this.this$0);
}
PlayBackActivity.access$600(this.this$0).sendEmptyMessageDelayed(AidConstants.EVENT_NETWORK_ERROR, 11000);
return;
case AidConstants.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;
// ...
}
}
It fires 11 seconds later and exits the window — 8 seconds of free preview, then 3 seconds to decide. As verification: disabling both the dialog and finish bypasses the coin purchase entirely. Watch as long as you like.
Cloud storage
Unlimited viewing is nice, but the real goal is elsewhere. Which object-storage provider backs this? Maybe there’s a hardcoded key. Wandering the code, another interesting function:
private void initPlay(String videoUrl, PlayBackBean playBackBean) {
MyLogger.jLog().e("reVideoUrl:" + videoUrl);
this.mVideoUrl = videoUrl;
if (TextUtils.isEmpty(videoUrl)) {
showError(getString(R.string.video_invalid));
return;
}
if (this.mPLVideoTextureView == null) {
this.mPLVideoTextureView = (PLVideoTextureView) findViewById(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 = this.mPLVideoTextureView;
pLVideoTextureView.setDisplayAspectRatio(2);
this.mPLVideoTextureView.setAVOptions(options);
this.mCustomController = new CustomController(this);
this.mPLVideoTextureView.setMediaController(this.mCustomController);
this.mPLVideoTextureView.setVideoPath(videoUrl);
this.mPLVideoTextureView.start();
this.mPLVideoTextureView.setOnInfoListener(new 4(this, playBackBean));
this.mPLVideoTextureView.setOnCompletionListener(new 5(this));
this.mPLVideoTextureView.setOnErrorListener(new 6(this));
}
videoUrl? Hook it and print — the URL (redacted):
http://qiniuvod.xxxxx.com/recordings/z1.xxxxx.qn1546622961492A/0_1546635114.mp4?sign=b6c6587f1794a14c250e143f28e07620&t=5c303623
wget downloads it directly — the result is the mp4 replay. There are also m3u8 links, downloadable with ffmpeg:
ffmpeg -i http://xxx.com/vid.m3u8 -c copy vid.mp4
The qiniu in the URL is a tell — Qiniu Cloud? The code confirms it uses Qiniu’s Android SDK. I’d written a Qiniu object-storage CLI tool (qncli) a year earlier, so I know the model: Qiniu uses key/secret, but access to a bucket goes through a token. For clients, best practice is server-issued tokens.
Reading the code, this URL is indeed server-issued. Normally a Qiniu private bucket signs the timeout together with the resource:
http://qiniu.example.com/resource.txt?e=1546686297&token=xxx
This link isn’t the traditional private format, though — maybe a different product. If you know, drop a comment.
MISC
Beyond cloud storage, this app — like many niche streamers — uses a third-party livestream SDK to cut dev work, plus several vendors’ location services (likely for LBS “same-city” social features, as the homepage’s “nearby” banner hints). It also has a share-for-coins feature via ShareSDK, supporting QQ, WeChat, Weibo, and Douban — which explains how an app invisible to Baidu still has a decent user base. Replays and streams are coin-gated, and users can publish non-live videos for paid download — a small video marketplace.
The platform’s top-spender leaderboard:

By technical means I found the app has around 250,000 registered users — not huge, but all paying. Back-of-envelope against coin pricing and spending patterns (discounting shill rankings) suggests a single app clearing tens of millions of RMB in a year. And that’s one app’s visible data; a single company often runs dozens of platforms. Since the platform itself didn’t do anything egregious, I’ll stop here.
Wrapping up
This was part boredom, part curiosity, part keeping my hands sharp — the classic “hunters keep their skills honed.” Reversing this gray-market app for a day also refreshed a handful of common Android RE operations, which stick better once written down.
Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch