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

View file

@ -0,0 +1,468 @@
// proxyctl — host-side CLI for the on-device HTTPS MITM (appproxy).
//
// Build the android binary, push it, install/reinject the CA, start/stop the
// proxy, run discover mode, and pull captures into outputs/discover/<stamp>.
// Package proxyctlcmd is the on-device MITM host CLI, exposed as a library so the
// single drm binary can host it as a subcommand.
package proxyctlcmd
import (
"flag"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"drmdecryption/adb"
"drmdecryption/proxy"
"drmdecryption/repo"
)
// Run dispatches a proxy subcommand. args[0] is the subcommand name.
func Run(args []string) error {
if len(args) < 1 {
Usage()
return fmt.Errorf("proxy: subcommand required")
}
sub, rest := args[0], args[1:]
switch sub {
case "build":
buildCmd(rest)
case "push":
pushCmd(rest)
case "install-ca":
installCACmd(rest)
case "reinject-ca":
reinjectCACmd(rest)
case "start":
startCmd(rest)
case "stop":
stopCmd(rest)
case "discover":
discoverCmd(rest)
case "pull":
pullCmd(rest)
case "clear-proxy":
clearProxyCmd(rest)
case "transparent", "tproxy":
transparentCmd(rest)
case "help", "-h", "--help":
Usage()
default:
Usage()
return fmt.Errorf("proxy: unknown subcommand %q", sub)
}
return nil
}
// Usage prints the proxy subcommand help.
func Usage() {
fmt.Fprintf(os.Stderr, `proxyctl — on-device MITM (appproxy) host control
Usage:
proxyctl build cross-compile linux/arm64 → bin/ + apps/proxy/
proxyctl push [--serial S] push binary to /data/local/tmp/appproxy
proxyctl install-ca [--serial S] [--ca PATH] [--reinject]
push CA + HASH.0; optional Magisk reinject
proxyctl reinject-ca [--serial S] [--hash HASH]
Magisk conscrypt bind only (CA already on device)
proxyctl start [--serial S] [--bin PATH] [--install-ca] [--reinject]
stop old → push → start → set http_proxy
proxyctl stop [--serial S] kill the on-device proxy + clear http_proxy
proxyctl discover [--serial S] [--out DIR] [--skip-build] [--install-ca] [--reinject]
-log-all session; Ctrl+C → outputs/discover/<stamp>
proxyctl pull [--serial S] [--out DIR]
pull traffic/cap/log into outputs/discover/<stamp>
proxyctl clear-proxy [--serial S] clear global http_proxy (+ stop mitm)
proxyctl transparent --package PKG [--serial S] [--out DIR] [--reinject]
iptables REDIRECT capture (UK VPN OK; no Wi‑Fi proxy)
writes /data/local/tmp/capture/<pkg>/ ; adb-pulled to --out
Artifacts land under outputs/discover/<timestamp>/ by default.
Transparent captures pull into outputs/transparent/<stamp>/ by default.
`)
}
func clientFrom(fs *flag.FlagSet, args []string) *adb.Client {
serial := fs.String("serial", "", "adb device serial")
_ = fs.Parse(args)
c := adb.New()
if *serial != "" {
c = c.WithSerial(*serial)
}
return c
}
func buildCmd(args []string) {
fs := flag.NewFlagSet("build", flag.ExitOnError)
_ = fs.Parse(args)
root := repo.Root()
deviceDir := filepath.Join(root, "apps", "proxy", "device")
outLocal := filepath.Join(root, "apps", "proxy", "proxy-android-arm64")
outBin := filepath.Join(root, "bin", "proxy-android-arm64")
fmt.Println("[*] Building linux/arm64 appproxy...")
cmd := exec.Command("go", "build", "-ldflags=-s -w", "-o", outLocal, ".")
cmd.Dir = deviceDir
cmd.Env = append(os.Environ(),
"GOOS=linux",
"GOARCH=arm64",
"CGO_ENABLED=0",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "build failed: %v\n", err)
os.Exit(1)
}
_ = os.MkdirAll(filepath.Join(root, "bin"), 0o755)
data, err := os.ReadFile(outLocal)
if err != nil {
fmt.Fprintf(os.Stderr, "read binary: %v\n", err)
os.Exit(1)
}
if err := os.WriteFile(outBin, data, 0o755); err != nil {
fmt.Fprintf(os.Stderr, "copy to bin/: %v\n", err)
os.Exit(1)
}
// Compat name next to the canonical one.
// Legacy copy so hosts that still look for the old name keep working.
_ = os.WriteFile(filepath.Join(root, "apps", "proxy", "rteproxy-android-arm64"), data, 0o755)
fmt.Println("[+] apps/proxy/proxy-android-arm64")
fmt.Println("[+] bin/proxy-android-arm64")
}
func pushCmd(args []string) {
fs := flag.NewFlagSet("push", flag.ExitOnError)
bin := fs.String("bin", "", "local proxy binary (default: auto)")
c := clientFrom(fs, args)
if err := c.EnsureDevice(); err != nil {
fatal(err)
}
local := *bin
if local == "" {
local = proxy.FindLocalBin("")
}
if local == "" {
fatal(fmt.Errorf("no proxy binary — run: proxyctl build"))
}
fmt.Printf("[*] Pushing %s → %s\n", local, proxy.RemoteBin)
if err := c.Push(local, proxy.RemoteBin); err != nil {
fatal(err)
}
_, _ = c.Shell("chmod", "755", proxy.RemoteBin)
fmt.Println("[+] pushed")
}
func installCACmd(args []string) {
fs := flag.NewFlagSet("install-ca", flag.ExitOnError)
ca := fs.String("ca", "", "local CA PEM (default: the CA pulled from the device)")
reinject := fs.Bool("reinject", false, "Magisk-reinject into conscrypt after push")
c := clientFrom(fs, args)
if err := c.EnsureDevice(); err != nil {
fatal(err)
}
hash, err := proxy.InstallCA(c, *ca, *reinject)
if err != nil {
fatal(err)
}
fmt.Printf("[+] CA hash %s installed on device\n", hash)
}
func reinjectCACmd(args []string) {
fs := flag.NewFlagSet("reinject-ca", flag.ExitOnError)
hash := fs.String("hash", proxy.DefaultCAHash, "Android CA subject_hash_old")
c := clientFrom(fs, args)
if err := c.EnsureDevice(); err != nil {
fatal(err)
}
proxy.ReinjectCA(c, *hash)
}
func startCmd(args []string) {
fs := flag.NewFlagSet("start", flag.ExitOnError)
bin := fs.String("bin", "", "local proxy binary (default: auto)")
installCA := fs.Bool("install-ca", false, "push CA + HASH.0 before start")
reinject := fs.Bool("reinject", false, "Magisk-reinject CA (implies -install-ca)")
c := clientFrom(fs, args)
if err := c.EnsureDevice(); err != nil {
fatal(err)
}
if *reinject {
*installCA = true
}
if *installCA {
if _, err := proxy.InstallCA(c, "", *reinject); err != nil {
fatal(err)
}
}
local := *bin
if local == "" {
local = proxy.FindLocalBin("")
}
if local == "" {
fatal(fmt.Errorf("no proxy binary — run: proxyctl build"))
}
if err := proxy.PushAndStart(c, local); err != nil {
fatal(err)
}
if err := proxy.EnsureHTTPProxy(c, ""); err != nil {
fatal(err)
}
}
func stopCmd(args []string) {
fs := flag.NewFlagSet("stop", flag.ExitOnError)
c := clientFrom(fs, args)
proxy.Stop(c)
proxy.ClearHTTPProxy(c)
}
func clearProxyCmd(args []string) {
fs := flag.NewFlagSet("clear-proxy", flag.ExitOnError)
c := clientFrom(fs, args)
proxy.ClearHTTPProxy(c)
}
func transparentCmd(args []string) {
fs := flag.NewFlagSet("transparent", flag.ExitOnError)
pkg := fs.String("package", "", "app package to redirect (e.g. bbc.iplayer.android)")
out := fs.String("out", "", "host pull dir (default: outputs/transparent/<stamp>)")
bin := fs.String("bin", "", "local proxy binary (default: auto)")
reinject := fs.Bool("reinject", false, "Magisk-reinject CA before start (slow; CA usually already mounted)")
port := fs.String("port", "8080", "transparent listen port on device")
noTail := fs.Bool("no-tail", false, "start only; do not wait / pull on Ctrl+C")
c := clientFrom(fs, args)
if err := c.EnsureDevice(); err != nil {
fatal(err)
}
if strings.TrimSpace(*pkg) == "" {
fatal(fmt.Errorf("--package is required (e.g. --package bbc.iplayer.android)"))
}
local := *bin
if local == "" {
local = proxy.FindLocalBin("")
}
if local == "" {
fatal(fmt.Errorf("no proxy binary — run: proxyctl build"))
}
// Never leave a stale Wi‑Fi proxy when using transparent mode.
proxy.ClearHTTPProxy(c)
remoteDir := "/data/local/tmp/capture/" + *pkg
fmt.Printf("[*] Transparent capture for %s → %s\n", *pkg, remoteDir)
if err := proxy.StartTransparent(c, local, *pkg, *port, remoteDir, *reinject); err != nil {
fatal(err)
}
fmt.Println("[+] running. Leave UK VPN ON. Open the app and play.")
fmt.Println(" Ctrl+C → stop iptables + pull captures")
if *noTail {
return
}
dest := *out
if dest == "" {
dest = filepath.Join(repo.Root(), "outputs", "transparent", time.Now().Format("20060102-150405"))
}
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
<-sig
fmt.Println("\n[*] Stopping transparent capture...")
proxy.StopTransparent(c)
_ = os.MkdirAll(dest, 0o755)
if err := proxy.PullDir(c, remoteDir, dest); err != nil {
fmt.Fprintf(os.Stderr, "pull: %v\n", err)
} else {
fmt.Println("[+] pulled into", dest)
}
}
func pullCmd(args []string) {
fs := flag.NewFlagSet("pull", flag.ExitOnError)
out := fs.String("out", "", "destination dir (default: outputs/discover/<stamp>)")
c := clientFrom(fs, args)
if err := c.EnsureDevice(); err != nil {
fatal(err)
}
dest := *out
if dest == "" {
dest = proxy.DiscoverOutDir("", "")
}
if err := proxy.PullCaptures(c, dest); err != nil {
fatal(err)
}
fmt.Println("[+] pulled into", dest)
}
func discoverCmd(args []string) {
fs := flag.NewFlagSet("discover", flag.ExitOnError)
out := fs.String("out", "", "destination dir (default: outputs/discover/<stamp>)")
skipBuild := fs.Bool("skip-build", false, "do not rebuild the android binary")
installCA := fs.Bool("install-ca", true, "push CA + HASH.0 before discover")
reinject := fs.Bool("reinject", true, "Magisk-reinject CA (default on for discover)")
noTail := fs.Bool("no-tail", false, "start only; do not tail / wait for Ctrl+C")
listen := fs.String("listen", ":8080", "proxy listen address on device")
pkgForce := fs.String("force-stop", "", "package to force-stop after CA reinject (so it inherits the new mount)")
c := clientFrom(fs, args)
root := repo.Root()
dest := *out
if dest == "" {
dest = proxy.DiscoverOutDir(root, "")
}
_ = os.MkdirAll(dest, 0o755)
if !*skipBuild {
buildCmd(nil)
}
if err := c.EnsureDevice(); err != nil {
fatal(err)
}
fmt.Println("[*] Device:", c.Out("get-serialno"))
local := proxy.FindLocalBin(root)
if local == "" {
fatal(fmt.Errorf("no proxy binary — run: proxyctl build"))
}
if *installCA || *reinject {
if _, err := proxy.InstallCA(c, "", *reinject); err != nil {
fmt.Fprintf(os.Stderr, "[!] install-ca: %v\n", err)
}
}
if *pkgForce != "" {
c.ForceStop(*pkgForce)
fmt.Printf("[*] Force-stopped %s\n", *pkgForce)
}
proxy.Stop(c)
fmt.Printf("[*] Pushing %s → %s (discover / -log-all)\n", local, proxy.RemoteBin)
if err := c.Push(local, proxy.RemoteBin); err != nil {
fatal(err)
}
_, _ = c.Shell("chmod", "755", proxy.RemoteBin)
_, _ = c.Shell("rm", "-f", proxy.RemoteLog, proxy.RemoteCap, proxy.RemoteTraffic)
starter := "#!/system/bin/sh\n" +
"exec " + proxy.RemoteBin +
" -listen " + *listen +
" -out " + proxy.RemoteCap +
" -ca-dir /data/local/tmp" +
" -dns 1.1.1.1,1.0.0.1,8.8.8.8,192.168.1.1" +
" -log-all -traffic " + proxy.RemoteTraffic +
" -v >>" + proxy.RemoteLog + " 2>&1\n"
tmp := filepath.Join(os.TempDir(), "start_appproxy_discover.sh")
if err := os.WriteFile(tmp, []byte(starter), 0o755); err != nil {
fatal(err)
}
defer os.Remove(tmp)
if err := c.Push(tmp, "/data/local/tmp/start_appproxy.sh"); err != nil {
fatal(err)
}
_, _ = c.Shell("chmod", "755", "/data/local/tmp/start_appproxy.sh")
_, _, _ = c.Run("shell", "setsid /data/local/tmp/start_appproxy.sh </dev/null >/dev/null 2>&1 &")
ok := false
var pid, head string
for i := 0; i < 12; i++ {
time.Sleep(400 * time.Millisecond)
pid = c.Out("shell", "pidof", "appproxy")
if pid == "" {
// Legacy process name.
pid = c.Out("shell", "pidof", "rteproxy")
}
head = c.Out("shell", "head", "-20", proxy.RemoteLog)
if pid != "" && containsListening(head) {
ok = true
break
}
}
if head != "" {
fmt.Println(head)
}
if !ok {
fmt.Fprintln(os.Stderr, c.Out("shell", "cat", proxy.RemoteLog))
fatal(fmt.Errorf("appproxy failed to start"))
}
fmt.Printf("[+] appproxy pid=%s\n", pid)
if err := proxy.EnsureHTTPProxy(c, ""); err != nil {
fatal(err)
}
fmt.Println()
fmt.Println("=== Discover mode ready ===")
fmt.Println("1. Unlock the phone and open the target app.")
fmt.Println("2. Start playback so DRM + manifest traffic flows.")
fmt.Println("3. Ctrl+C stops the tail and pulls captures.")
fmt.Println()
fmt.Println("Local folder:", dest)
fmt.Println()
if *noTail {
fmt.Println("Started without tail (-no-tail). Pull later with: proxyctl pull")
return
}
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
tailDone := make(chan struct{})
go func() {
defer close(tailDone)
cmd := exec.Command(c.Bin, append(serialArgs(c), "shell", "tail", "-f", proxy.RemoteLog)...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
}()
select {
case <-sig:
fmt.Println()
fmt.Println("[*] Stopping tail...")
case <-tailDone:
}
fmt.Println("[*] Pulling captures into", dest)
_ = proxy.PullCaptures(c, dest)
proxy.ClearHTTPProxy(c)
fmt.Println("[+] Done. Inspect:")
entries, _ := os.ReadDir(dest)
for _, e := range entries {
info, _ := e.Info()
size := int64(0)
if info != nil {
size = info.Size()
}
fmt.Printf(" %s (%d bytes)\n", e.Name(), size)
}
tip := filepath.Join(dest, "appproxy_traffic.jsonl")
if runtime.GOOS == "windows" {
fmt.Printf("Tip: Select-String -Path '%s' -Pattern 'mpd|license|widevine|manifest'\n", tip)
} else {
fmt.Printf("Tip: grep -E 'mpd|license|widevine|manifest' %s\n", tip)
}
}
func serialArgs(c *adb.Client) []string {
if c.Serial == "" {
return nil
}
return []string{"-s", c.Serial}
}
func containsListening(s string) bool {
return strings.Contains(strings.ToLower(s), "listening")
}
func fatal(err error) {
fmt.Fprintf(os.Stderr, "proxyctl: %v\n", err)
os.Exit(1)
}