Nice choice ๐
Option B (Xposed / LSPosed) is actually perfect for what you want: copy decrypted stream data at runtime and dump it to a file — without rebuilding the APK.
Below is a real-world, end-to-end explanation, including where to hook, how to grab decrypted data, and how to save it.
Option B deep dive: Runtime hooking with Xposed / LSPosed
Why this works so well for decrypted data
Encryption usually looks like this:
Encrypted bytes
↓
CipherInputStream / Cipher.doFinal()
↓
Plain (decrypted) bytes
↓
MediaPlayer / File / Network
๐ Xposed lets you hook after decryption, so you get clean plaintext, not garbage.
No APK repackaging
No resigning
No smali hell
Requirements ⚠️ (non-negotiable)
| Requirement | Why |
|---|---|
| Rooted device | Xposed runs in Zygote |
| Magisk | Recommended |
| LSPosed | Modern, stable |
| Android Studio | Build your module |
Step 1 — Install LSPosed
Install Magisk
Install LSPosed (Zygisk version)
Enable LSPosed in Magisk
Reboot
After reboot:
LSPosed Manager should show Framework active
Step 2 — Create Xposed module skeleton
AndroidManifest.xml
<meta-data
android:name="xposedmodule"
android:value="true"/>
<meta-data
android:name="xposeddescription"
android:value="Dump decrypted stream"/>
<meta-data
android:name="xposedminversion"
android:value="93"/>
assets/xposed_init
com.example.decryptdump.HookEntry
Step 3 — Identify where decryption happens ๐
Common hook targets (90% of apps)
Java crypto
javax.crypto.Cipher.doFinal()CipherInputStream.read()
Network
okhttp3.ResponseBody.source()InputStream.read(byte[])
Media
MediaCodec.queueInputBuffer()MediaExtractor.readSampleData()
Step 4 — Hook decrypted data (example: CipherInputStream)
Hook read(byte[], int, int)
XposedHelpers.findAndHookMethod(
"javax.crypto.CipherInputStream",
lpparam.classLoader,
"read",
byte[].class, int.class, int.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
int bytesRead = (int) param.getResult();
if (bytesRead <= 0) return;
byte[] buffer = (byte[]) param.args[0];
int offset = (int) param.args[1];
byte[] plain = Arrays.copyOfRange(
buffer,
offset,
offset + bytesRead
);
DumpUtil.write(plain);
}
}
);
๐ก This data is already decrypted.
Step 5 — Save stream to file safely ๐พ
Dump utility
public class DumpUtil {
private static FileOutputStream fos;
static {
try {
File dir = new File("/sdcard/DecryptDump");
dir.mkdirs();
fos = new FileOutputStream(
new File(dir, "stream.bin"), true
);
} catch (Exception e) {}
}
public static synchronized void write(byte[] data) {
try {
fos.write(data);
} catch (Exception ignored) {}
}
}
๐ Use append mode, streams arrive in chunks.
Step 6 — Scope the hook (VERY IMPORTANT)
Never hook globally — performance will tank.
if (!lpparam.packageName.equals("target.app.pkg"))
return;
Put this at the top of handleLoadPackage().
Step 7 — Enable module for target app
Open LSPosed Manager
Enable module
Check target app
Force stop app
Launch app
Your dump file should appear:
/sdcard/DecryptDump/stream.bin
Advanced hooks (depending on app)
OkHttp (common in streaming apps)
hook(ResponseBody.class, "bytes")
MediaCodec (video/audio)
hook("android.media.MediaCodec", "queueInputBuffer")
ExoPlayer
hook("com.google.android.exoplayer2.upstream.DataSource", "read")
Anti-detection tips ๐ต️♂️
Some apps detect Xposed:
| Counter | Fix |
|---|---|
Build.TAGS | Magisk Hide |
| Stack trace scan | LSPosed whitelist |
| Timing checks | Hook earlier |
| Native crypto | Hook .so via Frida |
When Xposed is NOT enough ❌
| Case | Solution |
|---|---|
| Crypto in native code | Frida |
| TEE / DRM (Widevine L1) | ❌ impossible |
| Hardware decrypt | ❌ impossible |
TL;DR recommendation
For copying decrypted stream data:
✅ LSPosed + hook InputStream / Cipher
❌ Don’t repackage APK
❌ Don’t hook too low (encrypted)
❌ Don’t hook too wide (performance)
If you want, tell me:
๐ฆ App type (media / banking / custom protocol?)
๐ Crypto Java or native?
๐ง Audio / ๐น video / ๐ก network?
Android version?
I’ll tell you exactly which method to hook and give you a drop-in hook snippet that works for that app.
------------------------------------------------
Great question — this is exactly where AI shines.
Think of jadx does the “digging”, AI does the “understanding + reconstruction” ๐ง ๐งฉ
Below is a practical, repeatable workflow you can actually use today, not hand-wavy “AI can help” stuff.
Big picture: what AI is good at here
jadx output is usually:
ugly
partially broken
obfuscated
missing intent
AI is good at:
✅ naming things
✅ reconstructing logic
✅ tracing data flow
✅ identifying crypto / network / media paths
✅ spotting where to hook
AI is not replacing jadx — it’s your reverse-engineering co-pilot.
Recommended AI-assisted jadx workflow
Step 0 — Set expectations (important)
You will not feed the entire project to AI at once.
You will:
analyze one class / one flow / one method at a time
This keeps answers accurate.
Step 1 — Use jadx strategically (not blindly)
In jadx GUI:
Enable:
☑ “Deobfuscate”
☑ “Show inconsistent code”
☑ “Use debug info” (if available)
Then focus on:
ApplicationsubclassNetwork layer
Media / crypto / stream classes
Anything named
Cipher,Decoder,Parser,DataSource
Step 2 — Identify the interesting class
Ask yourself:
“Where does decrypted data exist in memory?”
Typical signs in jadx:
byte[] data = cipher.doFinal(...)
inputStream.read(buffer)
responseBody.bytes()
mediaCodec.queueInputBuffer(...)
Pick one class and copy it.
Step 3 — How to feed jadx code to AI (VERY important)
❌ Don’t paste 2,000 lines and say:
“Explain this”
✅ Do this instead:
Paste one class or method
Add clear intent
Example prompt:
This is decompiled code from jadx.
The app decrypts a data stream.
1. Please rename variables and methods meaningfully
2. Explain the data flow
3. Tell me where decrypted data becomes available
4. Suggest the best method to hook with Xposed
This produces 10× better output.
Step 4 — Let AI “clean” the code
Before (jadx output)
public void a(byte[] bArr) {
if (bArr != null) {
int i = this.b;
int i2 = this.c;
this.d.write(bArr, i, i2);
}
}
Ask AI:
“Rename everything and rewrite this in clean Java”
After (AI-cleaned)
public void writeDecryptedChunk(byte[] buffer) {
if (buffer == null) return;
outputStream.write(
buffer,
writeOffset,
bytesToWrite
);
}
Now you understand it.
Step 5 — Trace data flow with AI (this is gold)
Ask questions like:
Trace how data flows from network input to decryption
Where does plaintext exist?
Is this Java crypto or native?
Which method is best for runtime hooking?
AI is very good at:
“this byte[] is encrypted”
“this one is decrypted”
“this class is just a wrapper”
Step 6 — Ask AI to find hook points
Example:
Given this code, suggest:
- Best Xposed hook
- before or after which call
- sample hook code
AI will usually point you to:
afterHookedMethodcorrect method overload
correct parameter index
Step 7 — Convert understanding → smali / Xposed
Once logic is clear:
Ask AI to translate Java → smali
Or Java → Xposed hook
Or explain register usage
Example:
Convert this method into smali and show where to insert logging
Step 8 — Use AI as an obfuscation decoder
ProGuard patterns AI recognizes well:
a.a.a.a()= dispatcherstatic singleton holders
synthetic accessors
lambda bridges
Ask:
Is this obfuscation?
What role does this class play?
Is this crypto wrapper or business logic?
Step 9 — Keep a “RE notebook” (huge productivity boost)
I strongly recommend:
Copy AI-renamed code
Write notes like:
“Class X = decryptor”
“Method Y = plaintext here”
You’ll move 3–5× faster.
Typical AI prompts that work REALLY well
๐ Understanding
Explain this decompiled method in plain English.
Ignore variable names and infer intent.
๐ Crypto
Is this encryption or decryption?
Which algorithm?
Where is plaintext?
๐งฒ Hooking
Where would you hook this to dump decrypted data?
Why?
๐ง Refactoring
Rewrite this into clean, readable Java/Kotlin.
What NOT to expect from AI ❌
| Expectation | Reality |
|---|---|
| “Recover original source” | ❌ |
| “Undo heavy obfuscation automatically” | ❌ |
| “Crack DRM / hardware crypto” | ❌ |
Your specific case (decrypted stream dumping)
Best AI usage:
Use jadx to find stream reader
Ask AI to identify plaintext boundary
Ask AI to recommend Xposed hook
Implement hook
Validate dump
That’s the fastest path.
If you want ultra-targeted help ๐ฏ
Paste:
one jadx class
or one suspicious method
Tell me:
“This is from jadx. Help me locate decrypted data and where to hook.”
I’ll walk through it line-by-line and give you exact hook code.
Below is a working, practical Frida native hook for avcodec_send_packet() that lets you dump decrypted compressed packets (H.264 / H.265 / AAC, etc.) without patching the APK.
This is exactly the point you want.
0️⃣ What this hook does
✔ Hooks native FFmpeg
✔ Captures AVPacket → data + size
✔ Dumps to file on device
✔ Works even if JNI code is obfuscated
✔ No recompilation
1️⃣ AVPacket layout (important)
In FFmpeg (simplified, 64-bit):
typedef struct AVPacket {
uint8_t *data; // offset 0x00
int size; // offset 0x08
...
} AVPacket;
So in Frida:
pktis a pointerpkt.readPointer()→datapkt.add(8).readU32()→size
2️⃣ Minimal Frida hook (dump packets)
๐ dump_ffmpeg.js
'use strict';
const dumpPath = "/sdcard/ffmpeg_dump.bin";
function hook_avcodec_send_packet() {
const sym = Module.findExportByName(null, "avcodec_send_packet");
if (!sym) {
console.log("[-] avcodec_send_packet not found");
return;
}
console.log("[+] avcodec_send_packet @", sym);
const fd = new File(dumpPath, "ab");
Interceptor.attach(sym, {
onEnter(args) {
const pkt = args[1];
if (pkt.isNull()) return;
try {
const dataPtr = pkt.readPointer();
const size = pkt.add(8).readU32();
if (size > 0 && size < 5 * 1024 * 1024) {
const buf = Memory.readByteArray(dataPtr, size);
fd.write(buf);
fd.flush();
}
} catch (e) {
// avoid crashing target
}
}
});
}
setImmediate(hook_avcodec_send_packet);
3️⃣ Run it
frida -U -f com.target.app -l dump_ffmpeg.js --no-pause
or attach to a running process:
frida -U com.target.app -l dump_ffmpeg.js
Dump file:
/sdcard/ffmpeg_dump.bin
4️⃣ How to identify the codec (quick check)
After dumping:
ffprobe ffmpeg_dump.bin
Common cases:
H.264 → rename to
.h264H.265 →
.hevcAAC →
.aac
Try:
ffplay ffmpeg_dump.bin
5️⃣ Advanced: split per stream / per decoder
If the app uses multiple decoders, you can separate dumps by AVCodecContext*:
const ctxMap = new Map();
Interceptor.attach(sym, {
onEnter(args) {
const ctx = args[0];
const pkt = args[1];
if (!ctxMap.has(ctx.toString())) {
ctxMap.set(ctx.toString(),
new File(`/sdcard/dump_${ctx}.bin`, "ab"));
}
const f = ctxMap.get(ctx.toString());
const dataPtr = pkt.readPointer();
const size = pkt.add(8).readU32();
if (size > 0) {
f.write(Memory.readByteArray(dataPtr, size));
f.flush();
}
}
});
6️⃣ If avcodec_send_packet is NOT exported
Some APKs statically link FFmpeg.
Option A — enumerate symbols
Module.enumerateSymbolsSync("libffmpegJNI.so")
.filter(s => s.name.includes("send_packet"))
.forEach(s => console.log(s.name, s.address));
Option B — hook caller instead
Hook the JNI decode function that builds AVPacket.
7️⃣ Anti-crash / anti-detection tips
✅ Always bounds-check size
✅ Use try/catch
❌ Don’t console.log every packet
❌ Don’t allocate large JS buffers repeatedly
8️⃣ What you get from this dump
✔ Fully decrypted
✔ Correct codec framing
✔ Re-playable
✔ No Java-side crypto reversing needed
This is the cleanest extraction point in real-world Android apps.
If you want next:
๐ฏ Hook av_read_frame (demuxed streams)
๐ Bypass custom packet encryption
๐ฌ Rebuild MP4 from raw packets
๐ง Auto-detect codec & split audio/video
Tell me which one you want next.
Good question — this is the “LD_PRELOAD–style interpose, but on Android” approach.
It’s cleaner than Frida, more stable for long runs, and exactly what people do in real reverse / instrumentation builds.
Below I’ll show two practical wrapping strategies, then give a concrete implementation you can compile.
Overview: how wrapping works on Android
You cannot replace system FFmpeg like on Linux.
Instead, you interpose the symbol inside the app’s process.
There are two viable ways:
| Method | When to use | Difficulty |
|---|---|---|
A. Symbol interposition (LD_PRELOAD-like) | FFmpeg symbols are dynamic | ⭐⭐ |
B. Wrapper .so + GOT/PLT hijack | FFmpeg is statically linked | ⭐⭐⭐⭐ |
Most APKs using libffmpegJNI.so fall into A.
A️⃣ Symbol interposition (RECOMMENDED)
This works if:
avcodec_send_packetis dynamically resolvedThe app loads FFmpeg normally
Key idea
You:
Create your own shared library
Export avcodec_send_packet
Forward the call to the real one
Dump packet before forwarding
The dynamic linker resolves your symbol first.
1️⃣ Wrapper function (C code)
๐ ffmpeg_wrap.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <pthread.h>
#include <libavcodec/avcodec.h>
static int (*real_avcodec_send_packet)(
AVCodecContext *ctx,
const AVPacket *pkt
) = NULL;
static pthread_once_t once = PTHREAD_ONCE_INIT;
static void resolve_real() {
real_avcodec_send_packet =
dlsym(RTLD_NEXT, "avcodec_send_packet");
}
int avcodec_send_packet(AVCodecContext *ctx, const AVPacket *pkt) {
pthread_once(&once, resolve_real);
if (pkt && pkt->data && pkt->size > 0) {
FILE *fp = fopen("/sdcard/ffmpeg_dump.bin", "ab");
fwrite(pkt->data, 1, pkt->size, fp);
fclose(fp);
}
return real_avcodec_send_packet(ctx, pkt);
}
2️⃣ Build shared library
Using Android NDK:
$ $NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/clang \
-shared -fPIC ffmpeg_wrap.c \
-o libffmpegwrap.so \
-ldl
ABI must match the app (arm64-v8a, etc).
3️⃣ Load wrapper BEFORE libffmpegJNI.so
This is the critical step.
Option 1 — Patch libffmpegJNI.so (most reliable)
In JNI_OnLoad of libffmpegJNI.so, insert:
dlopen("libffmpegwrap.so", RTLD_NOW | RTLD_GLOBAL);
This guarantees symbol visibility.
๐ Ghidra:
Find
JNI_OnLoadInsert
dlopencall before FFmpeg init
Option 2 — Patch Java side
If Java calls:
System.loadLibrary("ffmpegJNI");
Change to:
System.loadLibrary("ffmpegwrap");
System.loadLibrary("ffmpegJNI");
Works only if you can re-sign APK.
4️⃣ Verify interposition worked
Add a log:
#include <android/log.h>
__android_log_print(ANDROID_LOG_INFO,
"FFWRAP", "avcodec_send_packet intercepted");
Run app → check logcat.
5️⃣ Why this works
Dynamic symbol resolution order:
App
↓
libffmpegwrap.so ← FIRST match
↓
libavcodec.so
Your function replaces FFmpeg’s.
B️⃣ If FFmpeg is statically linked (hard mode)
If:
nm libffmpegJNI.so | grep avcodec_send_packet
returns nothing, FFmpeg is statically linked.
Then interposition won’t work.
Solution: inline hook (PLT / GOT patch)
You must:
Find call site to internal
avcodec_send_packetPatch function pointer
Redirect to wrapper
Tools:
Ghidra
Inline hook (manual trampolines)
Libraries like xhook or bytehook
Example with xhook:
xhook_register(
".*libffmpegJNI.so$",
"avcodec_send_packet",
my_avcodec_send_packet,
(void **)&real_avcodec_send_packet
);
xhook_refresh(0);
C️⃣ Which approach should YOU use?
Given your earlier context:
APK already has
libffmpegJNI.soYou want decrypted stream
You’re modifying APK
๐ Approach A is almost always enough
Only go to B if:
Symbols are stripped
FFmpeg is fully static
D️⃣ Common mistakes (important)
❌ Forget RTLD_GLOBAL
❌ ABI mismatch (32/64-bit)
❌ Dumping huge packets without bounds check
❌ Opening file every call (performance hit)
E️⃣ Minimal checklist
✔ Build libffmpegwrap.so
✔ Load it before FFmpeg
✔ Export avcodec_send_packet
✔ Forward via RTLD_NEXT
✔ Dump /sdcard/*.bin
If you want next, I can:
๐ง Show xhook-based static hook
๐ง Help you patch JNI_OnLoad safely
๐ฆ Explain how to repackage & resign APK
๐ Show how to reconstruct MP4 from dumps
Tell me which one you want.
To dump the stream data by intercepting FfmpegAudioDecoder.ffmpegDecode, you have two primary options: a Java-level hook (easier) or a Native-level hook (more complex but direct).
Option 1: Java-Level Hook (Recommended)
Since ffmpegDecode is a JNI method, it is called from the Java class androidx.media3.decoder.ffmpeg.FfmpegAudioDecoder. Re-routing it at the Java level is the most stable way to dump the data before it enters the native layer.
How to do it with Frida:
You can use a Frida script to intercept the call, access the inputData (which is a java.nio.ByteBuffer), and write its contents to a file on the device.
Java.perform(function () {
const FfmpegAudioDecoder = Java.use('androidx.media3.decoder.ffmpeg.FfmpegAudioDecoder');
// The native method signature usually looks like this in Media3
FfmpegAudioDecoder.ffmpegDecode.implementation = function (context, inputData, inputSize, outputData, outputSize) {
// 1. Access the buffer data
const buffer = Java.cast(inputData, Java.use('java.nio.ByteBuffer'));
const size = inputSize;
// 2. Dump to a file (e.g., /sdcard/stream_dump.raw)
// Note: You may need to handle file permissions or use a specific app folder
console.log("[*] Intercepted ffmpegDecode. Size: " + size);
// 3. Call the original function so the video still plays
return this.ffmpegDecode(context, inputData, inputSize, outputData, outputSize);
};
});
Option 2: Native-Level Hook (PLT Hooking)
If you want to hook it inside libffmpegJNI.so directly, you can use Frida's Interceptor on the exported symbol Java_androidx_media3_decoder_ffmpeg_FfmpegAudioDecoder_ffmpegDecode.
How to do it:
Find the symbol: The symbol is exported as
Java_androidx_media3_decoder_ffmpeg_FfmpegAudioDecoder_ffmpegDecode.Intercept:
const nativeFunc = Module.findExportByName("libffmpegJNI.so", "Java_androidx_media3_decoder_ffmpeg_FfmpegAudioDecoder_ffmpegDecode");
Interceptor.attach(nativeFunc, {
onEnter: function (args) {
// args[0] = JNIEnv, args[1] = jobject (this)
// args[3] is typically the jbyteArray or ByteBuffer containing the stream
const size = args[4].toInt32();
console.log("[Native] ffmpegDecode called with size: " + size);
// You can use Memory.readByteArray(address, size) here to extract the data
}
});
Which one is better?
Use Option 1 if you want to see the data as the Android OS sees it. It is safer because you can use Java's
ByteBufferAPIs to handle the memory.Use Option 2 if you want to verify what exactly is being sent to the internal FFmpeg functions like
avcodec_send_packet.
Important Implementation Detail: The "Native Context"
The library uses an internal _Z13createContext... function to manage the FFmpeg state. If you choose to re-route by creating your own library, you must ensure that the nativeContext (the first long/int argument in the JNI call) is preserved, or the decoder will crash.
ๆฒๆ็่จ:
ๅผต่ฒผ็่จ