/**
 * TNG — catch Promon exit after runtime code decrypt (mmap/mprotect RX).
 * Frida 17 compatible.
 */
"use strict";

function log(msg) {
  send("[TNG-native] " + msg);
}

function findExport(moduleName, name) {
  try {
    if (moduleName) {
      var m = Process.findModuleByName(moduleName);
      if (m) {
        var a = m.findExportByName(name);
        if (a) return a;
      }
    }
  } catch (e) {}
  try {
    return Module.getGlobalExportByName(name);
  } catch (e2) {
    return null;
  }
}

var patched = {};

function looksLikeExitSetup(addr) {
  for (var i = 1; i <= 12; i++) {
    try {
      var w = addr.sub(i * 4).readU32();
      var opc = w & 0xff800000;
      if (opc === 0x52800000 || opc === 0xd2800000) {
        var rd = w & 0x1f;
        var imm = (w >> 5) & 0xffff;
        if (rd === 8 && (imm === 93 || imm === 94)) return imm;
      }
      // mov x8, xN then earlier load — also catch svc after mov x0, #imm (exit code)
      if ((w & 0xffe0ffff) === 0xaa0003e8) return 8; // mov x8, x0.. pattern loose
    } catch (e) {}
  }
  return 0;
}

function patchRegion(base, size, tag) {
  if (size <= 0 || size > 64 * 1024 * 1024) return;
  var key = base + ":" + size;
  if (patched[key]) return;
  patched[key] = true;
  var nop = [0x1f, 0x20, 0x03, 0xd5];
  var n = 0;
  var totalSvc = 0;
  try {
    // only scan 4-byte aligned by walking manually for reliability
    var end = base.add(size - 4);
    for (var p = base; p.compare(end) <= 0; p = p.add(4)) {
      var w;
      try {
        w = p.readU32();
      } catch (e) {
        break;
      }
      if (w !== 0xd4000001) continue; // svc #0
      totalSvc++;
      var kind = looksLikeExitSetup(p);
      if (!kind) continue;
      try {
        Memory.protect(p, 4, "rwx");
        p.writeByteArray(nop);
        n++;
        log("patched exit SVC#" + kind + " @ " + p + " [" + tag + "]");
      } catch (e2) {
        log("patch err " + p + ": " + e2);
      }
    }
    if (totalSvc > 0) {
      log("region " + tag + " svc#0=" + totalSvc + " patched=" + n + " size=" + size);
    }
  } catch (e) {
    log("scan err " + tag + ": " + e);
  }
}

function scanAllExecutable(tag) {
  Process.enumerateRanges("r-x").forEach(function (r) {
    var file = r.file ? r.file.path : "anon";
    // skip system libs except if anonymous / app
    if (file.indexOf("/system/") === 0 || file.indexOf("/apex/") === 0) return;
    if (file.indexOf("frida") >= 0) return;
    patchRegion(r.base, r.size, tag + ":" + file);
  });
}

function installLibcExitHooks() {
  function blockExit(name, address) {
    try {
      Interceptor.replace(
        address,
        new NativeCallback(
          function (code) {
            log("BLOCKED " + name + "(" + (code | 0) + ")");
          },
          "void",
          ["int"]
        )
      );
      log("replaced " + name);
    } catch (e) {
      Interceptor.attach(address, {
        onEnter: function (args) {
          log("BLOCKED(attach) " + name + "(" + args[0].toInt32() + ")");
          while (true) Thread.sleep(60);
        },
      });
      log("attached " + name);
    }
  }

  ["_exit", "exit", "abort", "quick_exit"].forEach(function (n) {
    var a = findExport("libc.so", n);
    if (a) blockExit(n, a);
  });

  ["kill", "tgkill", "pthread_kill", "raise"].forEach(function (n) {
    var a = findExport("libc.so", n);
    if (!a) return;
    Interceptor.attach(a, {
      onEnter: function (args) {
        var pid = args[0].toInt32();
        var sig = args[1].toInt32();
        if ((pid === Process.id || pid === 0 || pid === -1) &&
            (sig === 9 || sig === 15 || sig === 6 || sig === 5)) {
          log("BLOCKED " + n + " sig=" + sig);
          args[1] = ptr(0);
        }
      },
    });
  });

  var sys = findExport("libc.so", "syscall");
  if (sys) {
    Interceptor.attach(sys, {
      onEnter: function (args) {
        var nr = args[0].toInt32();
        if (nr === 93 || nr === 94) {
          log("BLOCKED syscall exit " + nr);
          args[0] = ptr(-1);
        }
      },
    });
  }
  log("libc hooks OK");
}

function installMprotectWatcher() {
  var mprotect = findExport("libc.so", "mprotect");
  var mmap = findExport("libc.so", "mmap");
  if (mprotect) {
    Interceptor.attach(mprotect, {
      onEnter: function (args) {
        this.addr = args[0];
        this.len = args[1].toInt32();
        this.prot = args[2].toInt32();
      },
      onLeave: function () {
        // PROT_EXEC = 4
        if (this.prot & 4) {
          log("mprotect+EXEC " + this.addr + " len=" + this.len);
          patchRegion(this.addr, this.len, "mprotect");
        }
      },
    });
  }
  if (mmap) {
    Interceptor.attach(mmap, {
      onEnter: function (args) {
        this.len = args[1].toInt32();
        this.prot = args[2].toInt32();
      },
      onLeave: function (retval) {
        if ((this.prot & 4) && !retval.isNull()) {
          log("mmap+EXEC " + retval + " len=" + this.len);
          patchRegion(retval, this.len, "mmap");
        }
      },
    });
  }
  log("mprotect/mmap watchers OK");
}

function installMapsHide() {
  var markers = ["frida", "gadget", "xposed", "lsposed", "vector", "zygisk", "magisk", "liblspd"];
  var tracked = {};
  function hide(line) {
    var l = (line || "").toLowerCase();
    for (var i = 0; i < markers.length; i++) if (l.indexOf(markers[i]) >= 0) return true;
    return false;
  }
  function filter(buf, len) {
    try {
      var text = buf.readUtf8String(len);
      if (!text) return len;
      var out = text.split("\n").filter(function (x) { return !hide(x); }).join("\n");
      var bytes = Memory.allocUtf8String(out);
      var n = Math.min(len, out.length);
      Memory.copy(buf, bytes, n);
      return n;
    } catch (e) {
      return len;
    }
  }
  var openat = findExport("libc.so", "openat");
  var readFn = findExport("libc.so", "read");
  if (openat) {
    Interceptor.attach(openat, {
      onEnter: function (args) {
        this.path = args[1].isNull() ? null : args[1].readCString();
      },
      onLeave: function (retval) {
        var fd = retval.toInt32();
        if (fd >= 0 && this.path && this.path.indexOf("maps") >= 0) tracked[fd] = 1;
      },
    });
  }
  if (readFn) {
    Interceptor.attach(readFn, {
      onEnter: function (args) {
        this.fd = args[0].toInt32();
        this.buf = args[1];
      },
      onLeave: function (retval) {
        var n = retval.toInt32();
        if (n > 0 && tracked[this.fd]) retval.replace(ptr(filter(this.buf, n)));
      },
    });
  }
  log("maps hide OK");
}

function installJavaGuards() {
  if (typeof Java === "undefined") {
    setTimeout(installJavaGuards, 500);
    return;
  }
  Java.perform(function () {
    try {
      Java.use("java.lang.System").exit.implementation = function (c) {
        log("Java System.exit(" + c + ") blocked");
      };
    } catch (e) {}
    try {
      var R = Java.use("java.lang.Runtime");
      R.exit.overload("int").implementation = function (c) {
        log("Java Runtime.exit(" + c + ") blocked");
      };
    } catch (e) {}
    try {
      var P = Java.use("android.os.Process");
      P.killProcess.implementation = function (pid) {
        if (pid === P.myPid()) {
          log("Java killProcess(self) blocked");
          return;
        }
        return this.killProcess(pid);
      };
    } catch (e) {}
    log("Java guards OK");
  });
}

log("load pid=" + Process.id);
installLibcExitHooks();
installMapsHide();
installMprotectWatcher();
scanAllExecutable("boot");
setInterval(function () {
  scanAllExecutable("tick");
}, 1000);
installJavaGuards();
log("ready");
