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:
commit
2fa8f2435f
121 changed files with 17802 additions and 0 deletions
468
apps/proxy/proxyctlcmd/proxyctlcmd.go
Normal file
468
apps/proxy/proxyctlcmd/proxyctlcmd.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue