Initial commit: Null DRM Official

Capture, decrypt, and restream toolkit with compiled-in app modules
(RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www.
BBC module.yaml is published (clear streams); other module values stay local.
This commit is contained in:
404errordeveloper 2026-10-06 00:25:35 +02:00
commit 2fa8f2435f
121 changed files with 17802 additions and 0 deletions

577
apps/pkg/proxy/proxy.go Normal file
View file

@ -0,0 +1,577 @@
package proxy
import (
"crypto/md5"
"crypto/x509"
"encoding/binary"
"encoding/pem"
"fmt"
"net"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"drmdecryption/adb"
"drmdecryption/repo"
)
const (
// Universal on-device paths (still kill legacy rteproxy process names).
RemoteBin = "/data/local/tmp/appproxy"
RemoteCap = "/data/local/tmp/appproxy_cap.json"
RemoteLog = "/data/local/tmp/appproxy.log"
RemoteTraffic = "/data/local/tmp/appproxy_traffic.jsonl"
RemoteCACrt = "/data/local/tmp/rteproxy-ca.crt"
DefaultCAHash = "6c3578b4"
)
var reWLANIPv4 = regexp.MustCompile(`(?m)^\s*inet\s+(\d{1,3}(?:\.\d{1,3}){3})/`)
// DeviceWLANIPv4 returns the phone's current wlan0 IPv4 address.
func DeviceWLANIPv4(c *adb.Client) (string, error) {
out := c.Out("shell", "ip", "-f", "inet", "addr", "show", "wlan0")
if m := reWLANIPv4.FindStringSubmatch(out); len(m) == 2 {
return m[1], nil
}
// Fallbacks used on some OEM builds.
for _, prop := range []string{"dhcp.wlan0.ipaddress", "dhcp.eth0.ipaddress"} {
if v := c.Out("shell", "getprop", prop); net.ParseIP(v) != nil && v != "0.0.0.0" {
return v, nil
}
}
return "", fmt.Errorf("no wlan0 IPv4 (is Wi‑Fi connected?)")
}
// DeviceHTTPProxyAddr returns "<wlan-ip>:port" for the on-device MITM.
// Loopback (127.0.0.1) must not be used: after MITM, some clients rewrite the
// upstream Host to the proxy address, and appproxy then dials 127.0.0.1:443.
func DeviceHTTPProxyAddr(c *adb.Client, port string) (string, error) {
if strings.TrimSpace(port) == "" {
port = "8080"
}
ip, err := DeviceWLANIPv4(c)
if err != nil {
return "", err
}
return net.JoinHostPort(ip, port), nil
}
func looksLikeLoopbackProxy(want string) bool {
host, _, err := net.SplitHostPort(strings.TrimSpace(want))
if err != nil {
host = strings.TrimSpace(want)
}
host = strings.Trim(host, "[]")
return host == "127.0.0.1" || host == "localhost" || host == "::1" || host == "0.0.0.0" || host == ""
}
// EnsureHTTPProxy points the device at the on-device mitm.
// Empty or loopback want is rewritten to the phone's WLAN IP:8080 so upstream
// MITM dials keep the real destination host (BBC/RTE/etc.).
func EnsureHTTPProxy(c *adb.Client, want string) error {
if looksLikeLoopbackProxy(want) {
port := "8080"
if hostport := strings.TrimSpace(want); hostport != "" {
if _, p, err := net.SplitHostPort(hostport); err == nil && p != "" {
port = p
}
}
addr, err := DeviceHTTPProxyAddr(c, port)
if err != nil {
return err
}
want = addr
}
cur := c.Out("shell", "settings", "get", "global", "http_proxy")
if cur == want {
fmt.Println("[+] HTTP proxy already", want)
return nil
}
fmt.Printf("[*] Setting HTTP proxy -> %s (was %q)\n", want, cur)
_, err := c.Shell("settings", "put", "global", "http_proxy", want)
got := c.Out("shell", "settings", "get", "global", "http_proxy")
if got != want {
return fmt.Errorf("failed to set http_proxy (got %q)", got)
}
return err
}
// ClearHTTPProxy disables the global HTTP proxy and stops any leftover mitm.
// Always call this after a capture/agent job so the phone can play apps normally.
func ClearHTTPProxy(c *adb.Client) {
_, _ = c.Shell("settings", "put", "global", "http_proxy", ":0")
_, _ = c.Shell("settings", "delete", "global", "http_proxy")
_, _ = c.Shell("settings", "delete", "global", "global_http_proxy_host")
_, _ = c.Shell("settings", "delete", "global", "global_http_proxy_port")
_, _ = c.Shell("settings", "delete", "global", "global_http_proxy_exclusion_list")
stopProxy(c)
fmt.Println("[*] HTTP proxy cleared")
}
func stopProxy(c *adb.Client) {
script := `
for name in appproxy rteproxy; do
pid=$(pidof $name 2>/dev/null || true)
if [ -n "$pid" ]; then kill $pid >/dev/null 2>&1 || true; fi
done
sleep 0.5
for name in appproxy rteproxy; do
pid=$(pidof $name 2>/dev/null || true)
if [ -n "$pid" ]; then kill -9 $pid >/dev/null 2>&1 || true; fi
done
# Root fallback — some builds ignore non-root kill.
if command -v su >/dev/null 2>&1; then
su -c 'killall appproxy rteproxy 2>/dev/null; killall -9 appproxy rteproxy 2>/dev/null; true' 2>/dev/null || true
fi
sleep 0.3
(pidof appproxy || pidof rteproxy) >/dev/null 2>&1 && echo STILL || echo STOPPED`
out, _ := c.Shell("sh", "-c", script)
if strings.Contains(out, "STILL") {
fmt.Println("[!] old appproxy still running after stop — port 8080 may stay busy")
}
}
// extraFlags renders extra device flags, quoting each value.
func extraFlags(args []string) string {
if len(args) == 0 {
return ""
}
var b strings.Builder
for _, a := range args {
b.WriteString(" ")
if strings.HasPrefix(a, "-") {
b.WriteString(a)
continue
}
b.WriteString("'" + strings.ReplaceAll(a, "'", "") + "'")
}
return b.String()
}
// PushAndStart installs and launches the on-device mitm binary. extraArgs are
// appended to its command line — app modules pass their manifest-scoring hosts
// that way, since the device binary cannot read a module's values file.
func PushAndStart(c *adb.Client, localBin string, extraArgs ...string) error {
if st, err := os.Stat(localBin); err != nil || st.IsDir() {
return fmt.Errorf("missing proxy binary %s — build apps/proxy first (proxyctl build)", localBin)
}
fmt.Println("[*] Stopping any old appproxy/rteproxy...")
stopProxy(c)
fmt.Println("[*] Pushing appproxy...")
if err := c.Push(localBin, RemoteBin); err != nil {
return err
}
_, _ = c.Shell("chmod", "755", RemoteBin)
_, _ = c.Shell("rm", "-f", RemoteCap, "/data/local/tmp/rte_cap.json", "/sdcard/Download/rte_cap.json", "/storage/emulated/0/Download/rte_cap.json", RemoteLog, "/data/local/tmp/rteproxy.log")
starter := "#!/system/bin/sh\n" +
"exec " + RemoteBin +
" -listen :8080" +
" -out " + RemoteCap +
" -ca-dir /data/local/tmp" +
" -dns 1.1.1.1,1.0.0.1,8.8.8.8,192.168.1.1" +
" >>" + RemoteLog + " 2>&1\n"
tmp := filepath.Join(os.TempDir(), "start_appproxy.sh")
if err := os.WriteFile(tmp, []byte(starter), 0o755); err != nil {
return err
}
defer os.Remove(tmp)
if err := c.Push(tmp, "/data/local/tmp/start_appproxy.sh"); err != nil {
return err
}
_, _ = c.Shell("chmod", "755", "/data/local/tmp/start_appproxy.sh")
// Keep backgrounded after adb exits.
_, _, _ = c.Run("shell", "setsid /data/local/tmp/start_appproxy.sh </dev/null >/dev/null 2>&1 &")
var pid, logHead string
for i := 0; i < 10; i++ {
time.Sleep(400 * time.Millisecond)
pid = c.Out("shell", "pidof", "appproxy")
if pid == "" {
pid = c.Out("shell", "pidof", "rteproxy")
}
logHead = c.Out("shell", "head", "-12", RemoteLog)
if pid != "" && strings.Contains(logHead, "listening") {
break
}
}
if logHead != "" {
fmt.Println(logHead)
}
errLog := c.Out("shell", "cat", RemoteLog)
if strings.Contains(errLog, "address already in use") ||
(strings.Contains(errLog, "listen ") && strings.Contains(errLog, "bind:")) {
fmt.Fprintln(os.Stderr, errLog)
return fmt.Errorf("appproxy failed to bind :8080 (old process still holding the port?)")
}
if pid == "" || !strings.Contains(logHead, "listening") {
if errLog != "" {
fmt.Fprintln(os.Stderr, errLog)
}
return fmt.Errorf("appproxy failed to start")
}
fmt.Printf("[+] appproxy pid=%s; capture -> %s\n", pid, RemoteCap)
return nil
}
// FindLocalBin returns the first existing proxy binary under the repo.
func FindLocalBin(root string) string {
if root == "" {
root = repo.Root()
}
for _, p := range []string{
filepath.Join(root, "bin", "proxy-android-arm64"),
filepath.Join(root, "apps", "proxy", "proxy-android-arm64"),
filepath.Join(root, "apps", "proxy", "rteproxy-android-arm64"),
filepath.Join(root, "bin", "rteproxy-android-arm64"),
} {
if st, err := os.Stat(p); err == nil && !st.IsDir() {
return p
}
}
return ""
}
// FindLocalCA returns the first existing MITM CA cert under the repo.
func FindLocalCA(root string) string {
if root == "" {
root = repo.Root()
}
for _, p := range []string{
filepath.Join(root, "apps", "proxy", "rteproxy-ca.crt"),
filepath.Join(root, "data", "proxy-ca.crt"),
} {
if st, err := os.Stat(p); err == nil && !st.IsDir() {
return p
}
}
return ""
}
// AndroidCAHash returns the OpenSSL subject_hash_old used for system CA files.
func AndroidCAHash(certPEM []byte) (string, error) {
block, _ := pem.Decode(certPEM)
if block == nil {
return "", fmt.Errorf("no PEM certificate found")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", err
}
sum := md5.Sum(cert.RawSubject)
n := binary.LittleEndian.Uint32(sum[0:4])
return fmt.Sprintf("%08x", n), nil
}
// InstallCA pushes the MITM CA onto the device as HASH.0 and optionally Magisk-reinjects it.
// When reinject is true, runs the full Magisk conscrypt bind (needs root / Magisk busybox).
func InstallCA(c *adb.Client, localCA string, reinject bool) (hash string, err error) {
if localCA == "" {
localCA = FindLocalCA("")
}
if localCA == "" {
return "", fmt.Errorf("no local CA cert found (expected apps/proxy/rteproxy-ca.crt)")
}
pemBytes, err := os.ReadFile(localCA)
if err != nil {
return "", err
}
hash, err = AndroidCAHash(pemBytes)
if err != nil {
return "", err
}
fmt.Printf("[*] Installing CA %s (hash %s)\n", localCA, hash)
if err := c.Push(localCA, RemoteCACrt); err != nil {
return hash, err
}
// Also drop a copy named after the process for clarity.
_ = c.Push(localCA, "/data/local/tmp/appproxy-ca.crt")
tmpHash := filepath.Join(os.TempDir(), hash+".0")
if err := os.WriteFile(tmpHash, pemBytes, 0o644); err != nil {
return hash, err
}
defer os.Remove(tmpHash)
remoteHash := "/data/local/tmp/" + hash + ".0"
if err := c.Push(tmpHash, remoteHash); err != nil {
return hash, err
}
_, _ = c.Shell("chmod", "644", remoteHash, RemoteCACrt)
fmt.Printf("[+] Pushed %s and %s\n", RemoteCACrt, remoteHash)
if reinject {
ReinjectCA(c, hash)
} else {
fmt.Println("[*] Skipped Magisk reinject (pass -reinject / install-ca --reinject)")
fmt.Println(" User-CA path: Settings → Security → Install a certificate → CA certificate")
}
return hash, nil
}
// Stop stops the on-device mitm process.
func Stop(c *adb.Client) {
stopProxy(c)
}
const (
RemoteTProxyScript = "/data/local/tmp/tproxy_iptables.sh"
RemoteCaptureRoot = "/data/local/tmp/capture"
)
// StartTransparent pushes appproxy in -transparent mode (no Wi‑Fi http_proxy),
// installs iptables UID REDIRECT for pkg, and writes captures under remoteDir.
func StartTransparent(c *adb.Client, localBin, pkg, port, remoteDir string, reinject bool) error {
if port == "" {
port = "8080"
}
if remoteDir == "" {
remoteDir = RemoteCaptureRoot + "/" + pkg
}
stopProxy(c)
_ = stopTransparentRules(c)
// Ensure no global HTTP proxy — transparent mode must not use one.
ClearHTTPProxy(c)
if reinject {
ReinjectCA(c, DefaultCAHash)
}
if st, err := os.Stat(localBin); err != nil || st.IsDir() {
return fmt.Errorf("missing proxy binary %s", localBin)
}
fmt.Println("[*] Pushing appproxy (transparent)...")
if err := c.Push(localBin, RemoteBin); err != nil {
return err
}
_, _ = c.Shell("chmod", "755", RemoteBin)
scriptLocal := filepath.Join(repo.Root(), "apps", "proxy", "device", "tproxy_iptables.sh")
if _, err := os.Stat(scriptLocal); err != nil {
return fmt.Errorf("missing %s", scriptLocal)
}
if err := c.Push(scriptLocal, RemoteTProxyScript); err != nil {
return err
}
_, _ = c.Shell("chmod", "755", RemoteTProxyScript)
_, _ = c.Shell("mkdir", "-p", remoteDir)
_, _ = c.Shell("rm", "-f", remoteDir+"/cap.json", remoteDir+"/traffic.jsonl", remoteDir+"/appproxy.log")
starter := "#!/system/bin/sh\n" +
"mkdir -p '" + remoteDir + "'\n" +
"exec " + RemoteBin +
" -transparent" +
" -listen :" + port +
" -mitm-internal 127.0.0.1:18080" +
" -out-dir '" + remoteDir + "'" +
" -ca-dir /data/local/tmp" +
" -dns 1.1.1.1,1.0.0.1,8.8.8.8" +
" -log-all" +
" -v" +
"\n"
tmp := filepath.Join(os.TempDir(), "start_appproxy_tproxy.sh")
if err := os.WriteFile(tmp, []byte(starter), 0o755); err != nil {
return err
}
defer os.Remove(tmp)
if err := c.Push(tmp, "/data/local/tmp/start_appproxy_tproxy.sh"); err != nil {
return err
}
_, _ = c.Shell("chmod", "755", "/data/local/tmp/start_appproxy_tproxy.sh")
_, _, _ = c.Run("shell", "setsid /data/local/tmp/start_appproxy_tproxy.sh </dev/null >/dev/null 2>&1 &")
var pid string
for i := 0; i < 15; i++ {
time.Sleep(400 * time.Millisecond)
pid = c.Out("shell", "pidof", "appproxy")
if pid != "" {
break
}
}
if pid == "" {
return fmt.Errorf("appproxy failed to start (transparent)")
}
fmt.Printf("[+] appproxy pid=%s; capture → %s\n", pid, remoteDir)
out, errOut, err := c.Run("shell", "su", "-c", "sh "+RemoteTProxyScript+" start "+pkg+" "+port)
fmt.Print(out)
if err != nil {
return fmt.Errorf("iptables: %v (%s)", err, strings.TrimSpace(errOut+out))
}
fmt.Println("[+] iptables REDIRECT installed (UK VPN can stay ON; do not set Wi‑Fi proxy)")
return nil
}
// StopTransparent removes iptables rules and stops appproxy (does not require Wi‑Fi proxy clear beyond safety).
func StopTransparent(c *adb.Client) {
_ = stopTransparentRules(c)
stopProxy(c)
ClearHTTPProxy(c)
fmt.Println("[*] transparent capture stopped")
}
func stopTransparentRules(c *adb.Client) error {
out, errOut, err := c.Run("shell", "su", "-c", "sh "+RemoteTProxyScript+" stop")
if strings.TrimSpace(out) != "" {
fmt.Print(out)
}
if err != nil && !strings.Contains(errOut+out, "STOPPED") {
return fmt.Errorf("iptables stop: %v %s", err, errOut)
}
return nil
}
// PullDir pulls regular files under remoteDir into destDir.
// Avoids `adb shell sh -c "ls …"` — on Windows that often lists `/` instead of the path.
func PullDir(c *adb.Client, remoteDir, destDir string) error {
if err := os.MkdirAll(destDir, 0o755); err != nil {
return err
}
remoteDir = strings.TrimRight(strings.TrimSpace(remoteDir), "/")
// Prefer known capture artifacts; fall back to find -type f.
// Do not use `adb shell sh -c "ls …"` — on Windows that often lists `/`.
var names []string
for _, n := range []string{"cap.json", "traffic.jsonl", "appproxy.log"} {
if fileExistsOnDevice(c, remoteDir+"/"+n) {
names = append(names, n)
}
}
if len(names) == 0 {
list := c.Out("shell", "find", remoteDir, "-maxdepth", "1", "-type", "f")
for _, line := range strings.Split(list, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
names = append(names, filepath.Base(filepath.Clean(line)))
}
}
if len(names) == 0 {
return fmt.Errorf("no files in %s", remoteDir)
}
seen := map[string]bool{}
pulled := 0
for _, name := range names {
name = strings.TrimSpace(name)
if name == "" || name == "." || name == ".." || strings.ContainsAny(name, `/\`) || seen[name] {
continue
}
seen[name] = true
remote := remoteDir + "/" + name
local := filepath.Join(destDir, name)
if err := c.Pull(remote, local); err != nil {
fmt.Printf("[!] pull %s: %v\n", remote, err)
continue
}
fmt.Printf("[+] %s\n", local)
pulled++
}
if pulled == 0 {
return fmt.Errorf("failed to pull any files from %s", remoteDir)
}
return nil
}
func fileExistsOnDevice(c *adb.Client, remote string) bool {
_, _, err := c.Run("shell", "ls", remote)
return err == nil
}
// PullCaptures pulls traffic/cap/log into destDir (created if missing).
func PullCaptures(c *adb.Client, destDir string) error {
if err := os.MkdirAll(destDir, 0o755); err != nil {
return err
}
for _, remote := range []string{RemoteTraffic, RemoteCap, RemoteLog} {
base := filepath.Base(remote)
local := filepath.Join(destDir, base)
if err := c.Pull(remote, local); err != nil {
fmt.Printf("[!] pull %s: %v\n", remote, err)
continue
}
fmt.Printf("[+] %s\n", local)
}
return nil
}
// DiscoverOutDir returns outputs/discover/<stamp> under the repo root.
func DiscoverOutDir(root, stamp string) string {
if root == "" {
root = repo.Root()
}
if stamp == "" {
stamp = time.Now().Format("20060102-150405")
}
return filepath.Join(root, "outputs", "discover", stamp)
}
// ReinjectCA best-effort Magisk bind of the mitm CA into conscrypt.
func ReinjectCA(c *adb.Client, caHash string) {
if caHash == "" {
caHash = DefaultCAHash
}
// Bound the per-app nsenter loop — wait on hundreds of PIDs can hang forever.
script := fmt.Sprintf(`
BB=/data/adb/magisk/busybox
HASH=%s
[ -f /data/local/tmp/$HASH.0 ] || { echo CA_SKIP; exit 0; }
[ -x "$BB" ] || { echo CA_SKIP; exit 0; }
rm -rf /data/local/tmp/cacerts-overlay
mkdir -p /data/local/tmp/cacerts-overlay
cp /apex/com.android.conscrypt/cacerts/* /data/local/tmp/cacerts-overlay/ 2>/dev/null || true
cp /data/local/tmp/$HASH.0 /data/local/tmp/cacerts-overlay/$HASH.0
chmod 644 /data/local/tmp/cacerts-overlay/*
$BB mount -t tmpfs tmpfs /system/etc/security/cacerts 2>/dev/null || true
cp /data/local/tmp/cacerts-overlay/* /system/etc/security/cacerts/ 2>/dev/null || true
chmod 644 /system/etc/security/cacerts/* 2>/dev/null || true
chcon u:object_r:system_file:s0 /system/etc/security/cacerts/* 2>/dev/null || true
$BB mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts 2>/dev/null || true
for Z in $(pidof zygote64 2>/dev/null); do
nsenter --mount=/proc/$Z/ns/mnt -- $BB mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts 2>/dev/null || true
done
# Only rebind the target app if set; otherwise skip the full zygote-child sweep (hangs).
PKG_UID_FILE=/data/local/tmp/reinject_target_uid
if [ -f "$PKG_UID_FILE" ]; then
TUID=$(cat "$PKG_UID_FILE")
for PID in $(ps -A -o PID=,UID= 2>/dev/null | awk -v u="$TUID" '$2==u {print $1}'); do
nsenter --mount=/proc/$PID/ns/mnt -- $BB mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts 2>/dev/null || true
done
fi
ls /apex/com.android.conscrypt/cacerts/$HASH.0 2>/dev/null && echo CA_OK || echo CA_SKIP
`, caHash)
fmt.Println("[*] Re-injecting system CA (Magisk)...")
tmp := filepath.Join(os.TempDir(), "reinject_ca.sh")
_ = os.WriteFile(tmp, []byte("#!/system/bin/sh\n"+script), 0o755)
defer os.Remove(tmp)
_ = c.Push(tmp, "/data/local/tmp/reinject_ca.sh")
_, _ = c.Shell("chmod", "755", "/data/local/tmp/reinject_ca.sh")
// Host-side timeout: su -mm + nsenter has hung on some Magisk builds.
type runResult struct {
out string
}
ch := make(chan runResult, 1)
go func() {
out, _, _ := c.Run("shell", "su", "-mm", "-c", "sh /data/local/tmp/reinject_ca.sh")
ch <- runResult{out: out}
}()
var out string
select {
case r := <-ch:
out = r.out
case <-time.After(20 * time.Second):
fmt.Println("[!] CA reinject timed out after 20s — continuing (CA likely already mounted)")
_, _ = c.Shell("su", "-c", "killall reinject_ca.sh 2>/dev/null; true")
return
}
if strings.Contains(out, "CA_OK") {
fmt.Println("[+] System CA present in conscrypt")
} else {
fmt.Println("[!] CA inject skipped/failed — if TLS errors, re-run Magisk CA mount")
}
}