How I Reverse Engineered a Video Streaming App to Extract Hidden Video URLs

Ever wondered how mobile streaming apps protect their video content? Today I’m sharing my journey into reverse engineering an Android video streaming app to uncover how it encrypts video URLs and generates API signatures. This isn’t just theory – it’s a real hands-on breakdown of the entire process.
The Challenge: Locked Behind Encryption
I recently came across this video streaming app that caught my attention. Unlike YouTube or other platforms where you can easily inspect network requests, this app had everything locked down tight. The video URLs weren’t just sitting there in plain text – they were encrypted in something called a play_info
field that looked like complete gibberish.
Here’s what a typical encrypted video URL looked like:
1 2 |
QqJBR0AGRBvtHD1k2PhxF4DSgEEFyddBq4/rER0ito6r2G9DcNXgjj0HdvHiZUuo8VSbHoNIHlaFz29brf1IzIdp3sEsAAyj+i8Gqs4VDCmOBp0R65rxfhA8byYJQQLAxmx+1Ye3AvJqZlIvDMsT4vGXjnhd6IvFtNwTefQQxAst7QOTYKJHcRwnul8xn1ex6CdudoVfBuzt8Tezr0GFph7HtsgSWEUBv1AXRmJQZ2ZY18Y/4gDxiaTu6aaAPAOu0vUw3kmr3MF4/NnNqweHKcQIBsQfjBWx30YG3qUW1b6WycxuoGXIucv4Zr2xyYWw1ZuFsvc2qwiIgRk9DGgaxMZilM+S1J1PXshbm2Qj3jivWG/pGTygwnRVLQyovmN/y2HaNvRUpTzlKgim2VA8pcdQ3DdHG3I84QP1/yQfoa8JLw1TtZ0Dvi+6mOaW9nWn50a+r9itvJ+NEgOF3CrdtCez4pFO3FLowX6akoH58qDWgxX52MdESxG4eZZArVamBEFP4bwik5yNOwcrc5lEh3WtaLwG1UCAh2Epqu0qi1vyoCJBCKJg/dnubHpkI6G1 |
Not exactly readable, right? But hidden inside this encrypted mess were multiple high-quality video URLs for different resolutions and codecs. The question was: how do I crack it?
Tools of the Trade
Before diving into the technical details, let me share the tools that made this possible:
Frida – This is hands-down the best dynamic analysis tool for mobile apps. Think of it as a way to inject your own code into a running app and see what’s happening under the hood.
Ghidra – The NSA’s free reverse engineering tool. Yeah, you read that right. The same agency that deals with nation-state cyber threats released this powerful disassembler for public use.
Android Debug Bridge (ADB) – Essential for working with Android devices and extracting app files.
Node.js – For building the final decryption script once I figured out the algorithm.
Step 1: Finding the Decryption Logic
The first challenge was figuring out where in the app’s code the decryption was happening. Mobile apps aren’t like websites where you can just “view source” – they’re compiled binaries with thousands of functions.
I started by decompiling the APK file and searching for anything related to video playback. After digging through hundreds of Java classes, I found this interesting piece in the PlayerViewModel
class:
1 2 3 4 5 |
list2 = (List) com.newleaf.app.android.victor.util.q.a.fromJson( SBUtil.decryptChapterContent(play_info, SBUtil.PRIVATE_KEY_VERSION), new rh.r().getType() ); |
Bingo! The play_info
was being decrypted using SBUtil.decryptChapterContent()
with something called PRIVATE_KEY_VERSION
. This was my smoking gun.
Step 2: Hooking Into the Native Code
The SBUtil
class was interesting because it was calling native functions (written in C/C++, not Java). This meant the actual encryption logic was hidden in a compiled library file called libstupid.so
– and yes, that’s actually what they named it.
Using Frida, I was able to hook into the app while it was running and capture exactly what was happening during decryption:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Java.perform(function() { var SBUtil = Java.use("com.newleaf.app.android.victor.util.SBUtil"); SBUtil.decryptChapterContent.implementation = function(str, keyVersion) { console.log("Input: " + str.substring(0, 50) + "..."); console.log("Key version: " + keyVersion); var result = this.decryptChapterContent(str, keyVersion); console.log("Decrypted result: " + result); return result; }; }); |
This revealed something fascinating. The decryption process had multiple steps:
- Base64 decode the input
- AES-128-CBC decryption
- Another Base64 decode
- zlib decompression
- JSON parsing
Step 3: Extracting the AES Key
The real breakthrough came when I used Ghidra to analyze the native library. Deep in the assembly code, I found two functions that generated the encryption keys:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Function that returns the AES key void * FUN_00147de4(void) { void *pvVar2 = malloc(0x11); // Key bytes: 0x56, 0x76, 0x52, 0x53, 0x4e, 0x47, 0x46, 0x79, // 0x6e, 0x4c, 0x42, 0x57, 0x37, 0x61, 0x43, 0x50 // Which translates to: VvRSNGFynLBW7aCP } // Function that returns the AES IV void * FUN_00147ff4(void) { void *pvVar2 = malloc(0x11); // IV bytes: 0x67, 0x4c, 0x6e, 0x38, 0x73, 0x78, 0x71, 0x70, // 0x7a, 0x79, 0x4e, 0x6a, 0x65, 0x68, 0x44, 0x50 // Which translates to: gLn8sxqpzyNjehDP } |
With these keys in hand, I could now decrypt the video URLs outside of the app.
Step 4: Building the Decryption Script
Armed with the AES key, IV, and understanding of the process, I built a Node.js script that could decrypt any play_info
string:
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 |
const crypto = require('crypto'); const zlib = require('zlib'); function decryptPlayInfo(encryptedPlayInfo) { try { // Step 1: Base64 decode const encryptedData = Buffer.from(encryptedPlayInfo, 'base64'); // Step 2: AES-128-CBC decryption const keyBytes = [0x56, 0x76, 0x52, 0x53, 0x4e, 0x47, 0x46, 0x79, 0x6e, 0x4c, 0x42, 0x57, 0x37, 0x61, 0x43, 0x50]; const ivBytes = [0x67, 0x4c, 0x6e, 0x38, 0x73, 0x78, 0x71, 0x70, 0x7a, 0x79, 0x4e, 0x6a, 0x65, 0x68, 0x44, 0x50]; const key = Buffer.from(keyBytes); const iv = Buffer.from(ivBytes); const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv); let decrypted = decipher.update(encryptedData); decrypted = Buffer.concat([decrypted, decipher.final()]); // Step 3: Base64 decode the result const compressedData = Buffer.from(decrypted.toString(), 'base64'); // Step 4: Decompress with zlib const decompressed = zlib.inflateSync(compressedData); // Step 5: Parse JSON return JSON.parse(decompressed.toString('utf8')); } catch (error) { console.error('Decryption failed:', error); return null; } } |
The Payoff: Clean Video URLs
When I ran this script on the encrypted play_info
, here’s what came out:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[ { "PlayURL": "https://v-mps.crazymaplestudios.com/c055a411470071f0bf9f87c7361c0102/video/f76592e7c0584f878e5b0d1ec1dec2d8-38632476b0de7c8d75bff1df3712145a-video-sd.m3u8", "Definition": "SD", "Specification": "H265.SD", "Bitrate": "670.19", "Width": 720, "Height": 1280 }, { "PlayURL": "https://v-mps.crazymaplestudios.com/vod-112094/c055a411470071f0bf9f87c7361c0102/f76592e7c0584f878e5b0d1ec1dec2d8-a8054907667e82a81e394edd335072c8-sd.m3u8", "Definition": "SD", "Specification": "H264" } ] |
Perfect! Multiple video URLs with different codecs (H264 and H265) and quality options, all neatly organized in JSON format.
The API Signature Challenge
But wait, there was another layer of protection. To actually request video information from their API, I needed to generate proper authentication signatures. Every API request included a sign
parameter that looked like this:
1 2 |
sign: abefb3ff20f20a92e028e3abe25db1e3c4e27ce489724363a5ef98a3f42d8c50 |
Using Frida again, I discovered this signature was generated by taking all the request parameters, sorting them alphabetically, and running them through HMAC-SHA256 with a secret key.
The pattern looked like this:
1 2 3 |
Data: apiVersion=1.3.5&book_id=6825b313f981e578730e6174&channelId=AVG10003&clientTraceId=17529147863572461&clientVer=2.9.01&devId=6435b9271aa707cb&from=0&lang=en&play_details=0&session=09bb81606aecb46012e8d578a6128f05&ts=1752914786&uid=458298628 Result: abefb3ff20f20a92e028e3abe25db1e3c4e27ce489724363a5ef98a3f42d8c50 |
Lessons Learned
This reverse engineering journey taught me several important lessons:
Nothing is truly secure on the client side. No matter how much you obfuscate or encrypt, if the decryption happens on the user’s device, it can be reverse engineered with enough patience and the right tools.
Multiple layers of protection are common. Modern apps don’t rely on just one security mechanism. This app had encrypted URLs, API signatures, and obfuscated native code.
The right tools make all the difference. Without Frida’s dynamic analysis capabilities and Ghidra’s static analysis, this would have been nearly impossible.
Documentation is your friend. Understanding crypto algorithms, Android internals, and assembly language was crucial for success.
The Bigger Picture
Why does this matter beyond just satisfying technical curiosity? Understanding how content protection works helps developers build better security systems. It also highlights the ongoing cat-and-mouse game between content creators trying to protect their assets and researchers trying to understand how systems work.
For content creators, this demonstrates why server-side validation and DRM systems are so important. Client-side security can slow down attackers, but it can’t stop determined reverse engineers.
For developers, this shows the importance of defense in depth – using multiple security layers and not relying solely on obscurity.
Final Thoughts
Reverse engineering mobile apps is like solving a complex puzzle. Each piece you uncover leads to the next challenge, and the satisfaction of finally cracking the system is incredible.
The techniques I used here – dynamic analysis with Frida, static analysis with Ghidra, and pattern recognition through careful observation – apply to many different reverse engineering challenges.
Whether you’re a security researcher, developer, or just someone curious about how apps work under the hood, these tools and techniques can open up a whole new world of understanding about the software we use every day.
Remember though: always use these skills responsibly and in accordance with applicable laws and terms of service. The goal should be learning and improving security, not causing harm.
Have you tried reverse engineering mobile apps? What tools and techniques have worked best for you? Drop a comment below and share your experiences!
Tags: