fix: v8 bridge - attach after voice chat open, remove open_voice from relay
Root cause: Frida crashed because open_voice triggered UI transition while hook was active. Now: open voice chat first (one-shot script), then attach bridge when libantaudio.so is already loaded. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bbdb59cc05
commit
4eac803699
|
|
@ -0,0 +1,18 @@
|
|||
// open_voice.js — One-shot: open voice chat fragment then exit
|
||||
// Usage: frida -U -p PID -l open_voice.js --no-pause
|
||||
Java.perform(function() {
|
||||
Java.scheduleOnMainThread(function() {
|
||||
try {
|
||||
Java.choose("com.antgroup.aijk.android.ijklauncher.biz.activity.IJKActivity", {
|
||||
onMatch: function(a) {
|
||||
var fm = a.getSupportFragmentManager();
|
||||
var f = Java.use("com.antgroup.aijk.android.ijkchat.biz.voicechat.IjkVoiceChatFragment").$new();
|
||||
f.show(fm, "v");
|
||||
console.log("[OK] Voice chat opened");
|
||||
}, onComplete: function() {}
|
||||
});
|
||||
} catch(e) {
|
||||
console.log("[ERR] " + e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
// voice_bridge_v8.js — Voice Bridge with Audio Injection (attach after voice chat opened)
|
||||
//
|
||||
// STARTUP ORDER:
|
||||
// 1. Launch app: adb shell monkey -p com.antgroup.aijk.android ...
|
||||
// 2. Open voice chat manually or via adb tap
|
||||
// 3. Wait for libantaudio.so to load
|
||||
// 4. Attach frida with this script
|
||||
//
|
||||
// Hook point: libantaudio.so MFAntAudio3AV2Filter::process(micIn, spkRef, out, size, &result)
|
||||
// TCP :18901
|
||||
// Frame: 4-byte len + 1-byte type + payload
|
||||
// type 0: speaker/AI audio (spkRef, downstream to client)
|
||||
// type 1: text/JSON command
|
||||
// type 2: mic audio (micIn, downstream to client)
|
||||
// type 3: inject audio (upstream from client, replaces micIn)
|
||||
|
||||
var voiceActive = false;
|
||||
var clientOS = null;
|
||||
var capturedSpk = 0, capturedMic = 0, spkBytes = 0, micBytes = 0;
|
||||
var injectMode = false;
|
||||
var injectQueue = [];
|
||||
|
||||
function wf(os, type, jArr) {
|
||||
try {
|
||||
var len = jArr.length;
|
||||
var h = Java.array("byte", [(len>>24)&0xFF,(len>>16)&0xFF,(len>>8)&0xFF,len&0xFF, type]);
|
||||
os.write(h); os.write(jArr); os.flush();
|
||||
} catch(e) {}
|
||||
}
|
||||
function wt(os, text) {
|
||||
wf(os, 1, Java.use("java.lang.String").$new(text).getBytes("UTF-8"));
|
||||
}
|
||||
|
||||
// === Hook libantaudio.so (should already be loaded) ===
|
||||
var hooked = false;
|
||||
function tryHook() {
|
||||
if (hooked) return;
|
||||
var m = Process.findModuleByName("libantaudio.so");
|
||||
if (!m) return;
|
||||
var addr = m.findExportByName("_ZN8antaudio20MFAntAudio3AV2Filter7processEPhS1_S1_iRi");
|
||||
if (!addr) return;
|
||||
hooked = true;
|
||||
|
||||
Interceptor.attach(addr, {
|
||||
onEnter: function(args) {
|
||||
if (!voiceActive || !clientOS) return;
|
||||
var size = args[4].toInt32();
|
||||
if (size <= 0) return;
|
||||
|
||||
try {
|
||||
if (injectMode) {
|
||||
if (injectQueue.length > 0) {
|
||||
var frame = injectQueue.shift();
|
||||
if (frame.byteLength === size) {
|
||||
args[1].writeByteArray(frame);
|
||||
} else {
|
||||
var buf = new ArrayBuffer(size);
|
||||
var dst = new Uint8Array(buf);
|
||||
var src = new Uint8Array(frame);
|
||||
var copyLen = Math.min(size, frame.byteLength);
|
||||
for (var k = 0; k < copyLen; k++) dst[k] = src[k];
|
||||
args[1].writeByteArray(buf);
|
||||
}
|
||||
} else {
|
||||
var silence = new ArrayBuffer(size);
|
||||
args[1].writeByteArray(silence);
|
||||
}
|
||||
}
|
||||
|
||||
// Always capture speaker/AI output (type 0)
|
||||
var spkPcm = args[2].readByteArray(size);
|
||||
var spkArr = Java.array("byte", Array.from(new Uint8Array(spkPcm)));
|
||||
wf(clientOS, 0, spkArr);
|
||||
capturedSpk++; spkBytes += size;
|
||||
|
||||
if (!injectMode) {
|
||||
var micPcm = args[1].readByteArray(size);
|
||||
var micArr = Java.array("byte", Array.from(new Uint8Array(micPcm)));
|
||||
wf(clientOS, 2, micArr);
|
||||
}
|
||||
capturedMic++; micBytes += size;
|
||||
|
||||
if (capturedMic <= 3 || capturedMic % 500 === 0)
|
||||
console.log("[VOICE] mic=" + capturedMic + " spk=" + capturedSpk + " inject=" + injectQueue.length);
|
||||
} catch(e) {}
|
||||
}
|
||||
});
|
||||
console.log("[VOICE] process hooked @ " + addr);
|
||||
}
|
||||
|
||||
// Hook immediately — lib should already be loaded since voice chat is open
|
||||
tryHook();
|
||||
if (!hooked) {
|
||||
// Retry a few times in case of timing
|
||||
[500, 1000, 2000, 5000].forEach(function(ms) { setTimeout(tryHook, ms); });
|
||||
}
|
||||
|
||||
// === TCP Server ===
|
||||
Java.perform(function() {
|
||||
var SS = Java.use("java.net.ServerSocket");
|
||||
var JS = Java.use("java.lang.String");
|
||||
var server = SS.$new(18901);
|
||||
console.log("[VOICE] Listening :18901");
|
||||
|
||||
var Srv = Java.registerClass({
|
||||
name: "com.antaf.voice.S8",
|
||||
implements: [Java.use("java.lang.Runnable")],
|
||||
methods: {
|
||||
run: function() {
|
||||
while (true) {
|
||||
try {
|
||||
console.log("[VOICE] Waiting for client...");
|
||||
var c = server.accept();
|
||||
var is = c.getInputStream();
|
||||
var os = c.getOutputStream();
|
||||
clientOS = os;
|
||||
console.log("[VOICE] Client connected");
|
||||
wt(os, JSON.stringify({
|
||||
event:"connected", protocol:"antaf-voice-v8",
|
||||
hooked: hooked,
|
||||
commands:["start","stop","status","inject_on","inject_off"],
|
||||
audio:"pcm-16bit-960b-frames",
|
||||
frameTypes:{0:"spk_ai",1:"text",2:"mic",3:"inject"}
|
||||
}));
|
||||
|
||||
while (true) {
|
||||
var hb = [];
|
||||
for (var i=0;i<5;i++) { var b=is.read(); if(b<0) throw "EOF"; hb.push(b); }
|
||||
var fl=(hb[0]<<24)|(hb[1]<<16)|(hb[2]<<8)|hb[3], ft=hb[4];
|
||||
if (fl>1048576) break;
|
||||
var pb = [];
|
||||
for (var i=0;i<fl;i++) { var b=is.read(); if(b<0) throw "EOF"; pb.push(b&0xFF); }
|
||||
|
||||
if (ft === 3) {
|
||||
var arr = new ArrayBuffer(pb.length);
|
||||
var view = new Uint8Array(arr);
|
||||
for (var j=0;j<pb.length;j++) view[j] = pb[j];
|
||||
injectQueue.push(arr);
|
||||
}
|
||||
else if (ft === 1) {
|
||||
var pl = Java.array("byte", pb);
|
||||
var cmd = JSON.parse(JS.$new(pl,"UTF-8").toString());
|
||||
console.log("[VOICE] Cmd: " + JSON.stringify(cmd));
|
||||
if (cmd.cmd === "start") {
|
||||
voiceActive = true;
|
||||
capturedSpk=0;capturedMic=0;spkBytes=0;micBytes=0;
|
||||
injectQueue = [];
|
||||
wt(os, JSON.stringify({event:"started",hooked:hooked}));
|
||||
}
|
||||
else if (cmd.cmd === "stop") {
|
||||
voiceActive = false;
|
||||
injectMode = false;
|
||||
injectQueue = [];
|
||||
wt(os, JSON.stringify({event:"stopped",spk:{frames:capturedSpk,bytes:spkBytes},mic:{frames:capturedMic,bytes:micBytes}}));
|
||||
}
|
||||
else if (cmd.cmd === "inject_on") {
|
||||
injectMode = true;
|
||||
injectQueue = [];
|
||||
wt(os, JSON.stringify({event:"inject_on"}));
|
||||
console.log("[VOICE] Inject ON");
|
||||
}
|
||||
else if (cmd.cmd === "inject_off") {
|
||||
injectMode = false;
|
||||
injectQueue = [];
|
||||
wt(os, JSON.stringify({event:"inject_off"}));
|
||||
console.log("[VOICE] Inject OFF");
|
||||
}
|
||||
else if (cmd.cmd === "status") {
|
||||
wt(os, JSON.stringify({event:"status",active:voiceActive,hooked:hooked,inject:injectMode,queue:injectQueue.length,spk:{frames:capturedSpk,bytes:spkBytes},mic:{frames:capturedMic,bytes:micBytes}}));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) { console.log("[VOICE] Client disconnected: "+e); }
|
||||
finally { voiceActive=false; clientOS=null; injectMode=false; injectQueue=[]; }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Java.use("java.lang.Thread").$new(Srv.$new()).start();
|
||||
console.log("[VOICE] Ready (v8, no open_voice)");
|
||||
});
|
||||
30
relay.py
30
relay.py
|
|
@ -100,32 +100,26 @@ class BridgeClient:
|
|||
except Exception as e:
|
||||
log.error(f"Bridge recv error: {e}")
|
||||
|
||||
async def _send_and_wait(self, cmd, wait_secs=1):
|
||||
"""Send command and consume the response."""
|
||||
self.send_cmd(cmd)
|
||||
await asyncio.sleep(wait_secs)
|
||||
# Drain any pending responses
|
||||
while True:
|
||||
try:
|
||||
ftype, data = await asyncio.wait_for(self._recv_frame(), timeout=0.5)
|
||||
async def setup_voice(self):
|
||||
"""Start capture and enable inject. Voice chat must already be open."""
|
||||
self.send_cmd({"cmd": "start"})
|
||||
ftype, data = await self._recv_frame()
|
||||
if ftype == 1:
|
||||
msg = json.loads(data.decode())
|
||||
log.info(f"Bridge: {msg}")
|
||||
self.send_cmd({"cmd": "inject_on"})
|
||||
ftype, data = await self._recv_frame()
|
||||
if ftype == 1:
|
||||
msg = json.loads(data.decode())
|
||||
log.info(f"Bridge: {msg}")
|
||||
except asyncio.TimeoutError:
|
||||
break
|
||||
|
||||
async def setup_voice(self):
|
||||
"""Open voice chat, start capture, enable inject."""
|
||||
await self._send_and_wait({"cmd": "open_voice"}, wait_secs=3)
|
||||
await self._send_and_wait({"cmd": "start"}, wait_secs=1)
|
||||
await self._send_and_wait({"cmd": "inject_on"}, wait_secs=0.5)
|
||||
log.info("Voice bridge ready (inject mode)")
|
||||
|
||||
async def close(self):
|
||||
try:
|
||||
self.send_cmd({"cmd": "inject_off"})
|
||||
self.send_cmd({"cmd": "stop"})
|
||||
self.send_cmd({"cmd": "close_voice"})
|
||||
await asyncio.sleep(1)
|
||||
except Exception:
|
||||
pass
|
||||
if self.writer:
|
||||
self.writer.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
#!/bin/bash
|
||||
# Start voice bridge on Debian (where phone is connected via USB)
|
||||
# Run this ONCE before starting relay.py on PlugAI
|
||||
#
|
||||
# Correct order:
|
||||
# 1. Launch app
|
||||
# 2. Open voice chat (one-shot frida attach + detach)
|
||||
# 3. Wait for libantaudio.so
|
||||
# 4. Attach frida with voice_bridge_v8.js (lib already loaded, stable)
|
||||
# 5. Setup adb forward
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="/home/ceshi/reverse-lab/antaf/scripts"
|
||||
FRIDA="/home/ceshi/.local/bin/frida"
|
||||
|
||||
echo "[1] Launching Antaf app..."
|
||||
adb shell "monkey -p com.antgroup.aijk.android -c android.intent.category.LAUNCHER 1" 2>/dev/null
|
||||
sleep 3
|
||||
|
||||
PID=$(adb shell ps -A | grep 'com.antgroup.aijk.android$' | awk '{print $2}')
|
||||
if [ -z "$PID" ]; then echo "ERROR: App not running"; exit 1; fi
|
||||
echo " PID=$PID"
|
||||
|
||||
echo "[2] Opening voice chat..."
|
||||
timeout 8 $FRIDA -U -p $PID -l $SCRIPT_DIR/open_voice.js --no-pause -q 2>/dev/null || true
|
||||
sleep 3
|
||||
|
||||
echo "[3] Waiting for libantaudio.so..."
|
||||
sleep 2
|
||||
|
||||
echo "[4] Attaching voice_bridge_v8..."
|
||||
pkill -f frida 2>/dev/null || true
|
||||
sleep 1
|
||||
adb forward tcp:18901 tcp:18901
|
||||
nohup $FRIDA -U -p $PID -l $SCRIPT_DIR/voice_bridge_v8.js > /tmp/frida_voice.log 2>&1 &
|
||||
sleep 5
|
||||
tail -5 /tmp/frida_voice.log
|
||||
|
||||
echo ""
|
||||
echo "=== Voice bridge ready on :18901 ==="
|
||||
Loading…
Reference in New Issue