Android Malware Forensic Script

by
4 views e440625c...

Description

Android malware hides actions like data theft, SMS interception, shell commands, and camera misuse. Our Frida-based script hooks sensitive APIs and decodes parameters at runtime, revealing clear high-level behaviors. This helps forensic analysts accurately reconstruct the malware’s true impact.

How to Use

Download the script and run it with Frida CLI:

Download Script

Then run with Frida:

frida -U -f YOUR_PACKAGE_NAME -l android-malware-forensic-script.js

Replace YOUR_PACKAGE_NAME with the target app's package name.

Source Code

JavaScript
'use strict';

/*
 * Frida: Framework-level exfil + shell + SMS + network hooks (Android 7–14)
 * KEPT:  Shell invoke, SmsManager.sendTextMessage
 * ADDED: File staging, compression, Base64, ContentResolver queries/streams, Socket connects
 * Output: Human-readable text lines (no JSON)
 */

function nowIso() { return new Date().toISOString(); }

function jStack() {
  try {
    const Log = Java.use('android.util.Log');
    const Exception = Java.use('java.lang.Exception');
    return Log.getStackTraceString(Exception.$new());
  } catch (e) { return '(stack unavailable: ' + e + ')'; }
}

function jStr(x) {
  try { return (x === null || x === undefined) ? null : x.toString(); }
  catch (_) { return '<toString() failed>'; }
}

function jHost(addrObj) {
  try {
    if (!addrObj) return null;
    const s = addrObj.toString();
    // Typical forms: /192.168.1.5 or hostname/1.2.3.4
    if (s.indexOf('/') >= 0) return s.split('/').pop();
    return s;
  } catch (_) { return null; }
}

// Track the most recent outbound socket context to annotate later events
const lastNet = {
  localIp: null,
  localPort: null,
  remoteIp: null,
  remotePort: null,
  ts: null
};

function setLastNetFromSocket(sock) {
  try {
    const laddr = sock.getLocalAddress();
    const raddr = sock.getInetAddress();
    lastNet.localIp = jHost(laddr);
    lastNet.localPort = sock.getLocalPort();
    lastNet.remoteIp = jHost(raddr);
    lastNet.remotePort = sock.getPort();
    lastNet.ts = nowIso();
  } catch (_) {}
}

function sessionSuffix() {
  if (lastNet.remoteIp && lastNet.localIp) {
    return ` [session local ${lastNet.localIp}:${lastNet.localPort} → remote ${lastNet.remoteIp}:${lastNet.remotePort}]`;
  }
  return '';
}

function logLine(s) { try { console.log(s); } catch (_) {} }

function logEvent(type, payload) {
  const ts = nowIso();
  const p = payload || {};
  let msg = '';

  switch (type) {
    // === Shell ===
    case 'shell.exec':
      msg = `[${ts}] Executed shell command: ${p.cmd}` + sessionSuffix();
      break;
    case 'shell.processbuilder.start':
      msg = `[${ts}] ProcessBuilder start with command: ${Array.isArray(p.cmd) ? p.cmd.join(' ') : p.cmd}` + sessionSuffix();
      break;

    // === SMS ===
    case 'sms.sendTextMessage':
      msg = `[${ts}] Sent SMS \"${p.text}\" to ${p.dest}`;
      break;

    // === File I/O ===
    case 'file.fos.open':
      msg = `[${ts}] Opened file for write: ${p.path} (append=${p.append})` + sessionSuffix();
      break;
    case 'file.fos.write':
      msg = `[${ts}] Wrote ${p.len} bytes to ${p.path}` + sessionSuffix();
      break;
    case 'file.fis.open':
      msg = `[${ts}] Opened file for read: ${p.path}` + sessionSuffix();
      break;
    case 'file.fis.read':
      msg = `[${ts}] Read ${p.len} bytes from ${p.path}` + sessionSuffix();
      break;

    // === Compression / Archiving ===
    case 'zip.putNextEntry':
      msg = `[${ts}] Adding file to ZIP: ${p.entry}` + sessionSuffix();
      break;
    case 'zip.closeEntry':
      msg = `[${ts}] Closed ZIP entry: ${p.entry}` + sessionSuffix();
      break;
    case 'zip.write':
      msg = `[${ts}] Wrote ${p.len} bytes into ZIP entry ${p.entry}` + sessionSuffix();
      break;
    case 'gzip.init':
      msg = `[${ts}] Started GZIP compression` + sessionSuffix();
      break;

    // === Base64 ===
    case 'b64.encodeToString':
      msg = `[${ts}] Encoded data to Base64 (input ${p.inLen} bytes → output ${p.outLen})` + sessionSuffix();
      break;
    case 'b64.encode':
      msg = `[${ts}] Encoded data to Base64 (input ${p.inLen} bytes → output ${p.outLen})` + sessionSuffix();
      break;

    // === ContentResolver ===
    case 'content.query.sensitive':
      msg = `[${ts}] Queried sensitive content: ${p.uri}` + sessionSuffix();
      break;
    case 'content.openInputStream':
      msg = `[${ts}] Opened input stream on ${p.uri}` + sessionSuffix();
      break;
    case 'content.openOutputStream':
      msg = `[${ts}] Opened output stream on ${p.uri} (mode=${p.mode})` + sessionSuffix();
      break;

    // === Network ===
    case 'net.connect': {
      const li = p.localIp, lp = p.localPort, ri = p.remoteIp, rp = p.remotePort;
      msg = `[${ts}] Socket connect: local ${li}:${lp} → remote ${ri}:${rp}`;
      // Heuristic label for common Meterpreter ports
      const mports = { 4444: true, 5555: true, 6666: true, 8080: true };
      if (ri && rp && mports[rp]) msg += ' (possible Meterpreter)';
      break;
    }

    // === Misc ===
    case 'hook.error':
      msg = `[${ts}] Hook error in ${p.where}: ${p.error}`;
      break;
    case 'hook.warn':
      msg = `[${ts}] Hook warning in ${p.where}: ${p.warn}`;
      break;

    case 'frida.ready':
      msg = `[${ts}] Hooks armed: ${p.message}`;
      break;

    default:
      msg = `[${ts}] ${type}` + (p && Object.keys(p).length ? ' ' + jStr(p) : '');
  }

  logLine(msg);
}

Java.perform(function () {
  // ===================== Shell invoking (kept) =====================
  try {
    const Runtime = Java.use('java.lang.Runtime');

    Runtime.exec.overload('java.lang.String').implementation = function (cmd) {
      logEvent('shell.exec', { cmd: jStr(cmd) });
      return this.exec(cmd);
    };
    Runtime.exec.overload('java.lang.String', '[Ljava.lang.String;').implementation = function (cmd, envp) {
      logEvent('shell.exec', { cmd: jStr(cmd) });
      return this.exec(cmd, envp);
    };
    Runtime.exec.overload('[Ljava.lang.String;').implementation = function (cmds) {
      logEvent('shell.exec', { cmd: jStr(cmds) });
      return this.exec(cmds);
    };
    Runtime.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;').implementation = function (cmds, envp) {
      logEvent('shell.exec', { cmd: jStr(cmds) });
      return this.exec(cmds, envp);
    };
    Runtime.exec.overload('java.lang.String', '[Ljava.lang.String;', 'java.io.File').implementation = function (cmd, envp, dir) {
      logEvent('shell.exec', { cmd: jStr(cmd) });
      return this.exec(cmd, envp, dir);
    };
    Runtime.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.io.File').implementation = function (cmds, envp, dir) {
      logEvent('shell.exec', { cmd: jStr(cmds) });
      return this.exec(cmds, envp, dir);
    };

    const ProcessBuilder = Java.use('java.lang.ProcessBuilder');
    ProcessBuilder.start.implementation = function () {
      try {
        const list = this.command();
        const arr = [];
        for (let i = 0; i < list.size(); i++) arr.push(jStr(list.get(i)));
        logEvent('shell.processbuilder.start', { cmd: arr });
      } catch (_) { logEvent('shell.processbuilder.start', {}); }
      return this.start();
    };
  } catch (e) { logEvent('hook.error', { where: 'shell', error: String(e) }); }

  // ===================== SMS send (kept) =====================
  try {
    const SmsManager = Java.use('android.telephony.SmsManager');
    SmsManager.sendTextMessage.overload(
      'java.lang.String', 'java.lang.String', 'java.lang.String',
      'android.app.PendingIntent', 'android.app.PendingIntent'
    ).implementation = function (dest, scAddr, text, sentPI, deliveryPI) {
      logEvent('sms.sendTextMessage', {
        dest: jStr(dest),
        scAddr: jStr(scAddr),
        text: jStr(text)
      });
      return this.sendTextMessage(dest, scAddr, text, sentPI, deliveryPI);
    };
  } catch (e) { logEvent('hook.warn', { where: 'smsmanager', warn: String(e) }); }

  // ===================== Exfil signals (added) =====================

  // ---- File staging: track per-stream path + read/write sizes ----
  const fosPath = new Map(); // key: this.$h => path
  const fisPath = new Map();

  try {
    const FileOutputStream = Java.use('java.io.FileOutputStream');

    // Constructors
    FileOutputStream.$init.overload('java.lang.String').implementation = function (path) {
      const ret = this.$init(path);
      fosPath.set(this.$h, jStr(path));
      logEvent('file.fos.open', { path: jStr(path), append: false });
      return ret;
    };
    FileOutputStream.$init.overload('java.lang.String', 'boolean').implementation = function (path, append) {
      const ret = this.$init(path, append);
      fosPath.set(this.$h, jStr(path));
      logEvent('file.fos.open', { path: jStr(path), append: !!append });
      return ret;
    };
    FileOutputStream.$init.overload('java.io.File').implementation = function (file) {
      const ret = this.$init(file);
      const p = jStr(file);
      fosPath.set(this.$h, p);
      logEvent('file.fos.open', { path: p, append: false });
      return ret;
    };
    FileOutputStream.$init.overload('java.io.File', 'boolean').implementation = function (file, append) {
      const ret = this.$init(file, append);
      const p = jStr(file);
      fosPath.set(this.$h, p);
      logEvent('file.fos.open', { path: p, append: !!append });
      return ret;
    };

    // Writes
    FileOutputStream.write.overload('[B').implementation = function (b) {
      const path = fosPath.get(this.$h) || null;
      const len = b ? b.length : -1;
      logEvent('file.fos.write', { path, len });
      return this.write(b);
    };
    FileOutputStream.write.overload('[B', 'int', 'int').implementation = function (b, off, len) {
      const path = fosPath.get(this.$h) || null;
      logEvent('file.fos.write', { path, len: len, off: off });
      return this.write(b, off, len);
    };
    FileOutputStream.write.overload('int').implementation = function (one) {
      const path = fosPath.get(this.$h) || null;
      logEvent('file.fos.write', { path, len: 1 });
      return this.write(one);
    };
  } catch (e) { logEvent('hook.warn', { where: 'fos', warn: String(e) }); }

  try {
    const FileInputStream = Java.use('java.io.FileInputStream');

    // Constructors
    FileInputStream.$init.overload('java.lang.String').implementation = function (path) {
      const ret = this.$init(path);
      fisPath.set(this.$h, jStr(path));
      logEvent('file.fis.open', { path: jStr(path) });
      return ret;
    };
    FileInputStream.$init.overload('java.io.File').implementation = function (file) {
      const ret = this.$init(file);
      const p = jStr(file);
      fisPath.set(this.$h, p);
      logEvent('file.fis.open', { path: p });
      return ret;
    };

    // Reads
    FileInputStream.read.overload('[B').implementation = function (b) {
      const bytes = this.read(b);
      const path = fisPath.get(this.$h) || null;
      logEvent('file.fis.read', { path, len: bytes });
      return bytes;
    };
    FileInputStream.read.overload('[B', 'int', 'int').implementation = function (b, off, len) {
      const n = this.read(b, off, len);
      const path = fisPath.get(this.$h) || null;
      logEvent('file.fis.read', { path, len: n, off: off, req: len });
      return n;
    };
    FileInputStream.read.overload().implementation = function () {
      const n = this.read();
      const path = fisPath.get(this.$h) || null;
      logEvent('file.fis.read', { path, len: (n >= 0 ? 1 : n) });
      return n;
    };
  } catch (e) { logEvent('hook.warn', { where: 'fis', warn: String(e) }); }

  // ---- Compression / Archiving (common before exfil) ----
  try {
    const ZipOutputStream = Java.use('java.util.zip.ZipOutputStream');
    const zipNames = new Map(); // ZipOutputStream -> current entry

    ZipOutputStream.putNextEntry.implementation = function (entry) {
      const name = entry ? jStr(entry.getName()) : null;
      zipNames.set(this.$h, name);
      logEvent('zip.putNextEntry', { entry: name });
      return this.putNextEntry(entry);
    };
    ZipOutputStream.closeEntry.implementation = function () {
      const name = zipNames.get(this.$h) || null;
      logEvent('zip.closeEntry', { entry: name });
      return this.closeEntry();
    };
    ZipOutputStream.write.overload('[B', 'int', 'int').implementation = function (b, off, len) {
      const name = zipNames.get(this.$h) || null;
      logEvent('zip.write', { entry: name, len: len });
      return this.write(b, off, len);
    };
  } catch (e) { logEvent('hook.warn', { where: 'zip', warn: String(e) }); }

  try {
    const GZIPOutputStream = Java.use('java.util.zip.GZIPOutputStream');
    GZIPOutputStream.$init.overload('java.io.OutputStream').implementation = function (os) {
      const ret = this.$init(os);
      logEvent('gzip.init', {});
      return ret;
    };
  } catch (e) { /* optional */ }

  // ---- Base64 (often used before network/SMS exfil) ----
  try {
    const Base64 = Java.use('android.util.Base64');
    Base64.encodeToString.overload('[B', 'int').implementation = function (b, flags) {
      const s = this.encodeToString(b, flags);
      logEvent('b64.encodeToString', { inLen: (b ? b.length : -1), outLen: (s ? s.length : -1), flags });
      return s;
    };
    Base64.encode.overload('[B', 'int').implementation = function (b, flags) {
      const out = this.encode(b, flags);
      logEvent('b64.encode', { inLen: (b ? b.length : -1), outLen: (out ? out.length : -1), flags });
      return out;
    };
  } catch (e) { /* optional */ }

  // ---- ContentResolver: queries to sensitive stores + streams ----
  try {
    const ContentResolver = Java.use('android.content.ContentResolver');

    function trackIfSensitive(uriStr) {
      if (!uriStr) return false;
      return uriStr.startsWith('content://sms') ||
             uriStr.startsWith('content://call_log') ||
             uriStr.startsWith('content://com.android.contacts') ||
             uriStr.startsWith('content://contacts');
    }

    // query(Uri, String[], String, String[], String) and query(Uri, String[], Bundle, CancellationSignal)
    [
      ['android.net.Uri', '[Ljava.lang.String;', 'java.lang.String', '[Ljava.lang.String;', 'java.lang.String'],
      ['android.net.Uri', '[Ljava.lang.String;', 'android.os.Bundle', 'android.os.CancellationSignal']
    ].forEach(function (sig) {
      try {
        const ov = ContentResolver.query.overload.apply(ContentResolver.query, sig);
        ov.implementation = function () {
          const uri = arguments[0];
          const uriStr = jStr(uri);
          if (trackIfSensitive(uriStr)) {
            const payload = { uri: uriStr };
            if (sig.length === 5) {
              payload.projection = jStr(arguments[1]);
              payload.selection = jStr(arguments[2]);
              payload.selectionArgs = jStr(arguments[3]);
              payload.sortOrder = jStr(arguments[4]);
            } else {
              payload.queryArgsBundle = jStr(arguments[2]);
            }
            logEvent('content.query.sensitive', payload);
          }
          return ov.apply(this, arguments);
        };
      } catch (_) {}
    });

    // Streams via content URIs
    try {
      const openIn = ContentResolver.openInputStream.overload('android.net.Uri');
      openIn.implementation = function (uri) {
        const uriStr = jStr(uri);
        if (trackIfSensitive(uriStr)) {
          logEvent('content.openInputStream', { uri: uriStr });
        }
        return openIn.call(this, uri);
      };
    } catch (_) {}

    try {
      const openOut1 = ContentResolver.openOutputStream.overload('android.net.Uri');
      openOut1.implementation = function (uri) {
        const uriStr = jStr(uri);
        if (trackIfSensitive(uriStr)) {
          logEvent('content.openOutputStream', { uri: uriStr, mode: null });
        }
        return openOut1.call(this, uri);
      };
    } catch (_) {}

    try {
      const openOut2 = ContentResolver.openOutputStream.overload('android.net.Uri', 'java.lang.String');
      openOut2.implementation = function (uri, mode) {
        const uriStr = jStr(uri);
        if (trackIfSensitive(uriStr)) {
          logEvent('content.openOutputStream', { uri: uriStr, mode: jStr(mode) });
        }
        return openOut2.call(this, uri, mode);
      };
    } catch (_) {}

  } catch (e) { logEvent('hook.warn', { where: 'contentresolver', warn: String(e) }); }

  // ===================== Network sockets (added) =====================
  try {
    const Socket = Java.use('java.net.Socket');
    const InetSocketAddress = Java.use('java.net.InetSocketAddress');
    const SocketAddress = Java.use('java.net.SocketAddress');

    // Constructors with host/port
    Socket.$init.overload('java.lang.String', 'int').implementation = function (host, port) {
      const ret = this.$init(host, port);
      setLastNetFromSocket(this);
      logEvent('net.connect', {
        localIp: lastNet.localIp, localPort: lastNet.localPort,
        remoteIp: lastNet.remoteIp, remotePort: lastNet.remotePort
      });
      return ret;
    };

    Socket.$init.overload('java.net.InetAddress', 'int').implementation = function (addr, port) {
      const ret = this.$init(addr, port);
      setLastNetFromSocket(this);
      logEvent('net.connect', {
        localIp: lastNet.localIp, localPort: lastNet.localPort,
        remoteIp: lastNet.remoteIp, remotePort: lastNet.remotePort
      });
      return ret;
    };

    // Post-construction connect(SocketAddress) variants
    Socket.connect.overload('java.net.SocketAddress').implementation = function (endpoint) {
      const r = this.connect(endpoint);
      try {
        setLastNetFromSocket(this);
        logEvent('net.connect', {
          localIp: lastNet.localIp, localPort: lastNet.localPort,
          remoteIp: lastNet.remoteIp, remotePort: lastNet.remotePort
        });
      } catch (_) {}
      return r;
    };

    Socket.connect.overload('java.net.SocketAddress', 'int').implementation = function (endpoint, timeout) {
      const r = this.connect(endpoint, timeout);
      try {
        setLastNetFromSocket(this);
        logEvent('net.connect', {
          localIp: lastNet.localIp, localPort: lastNet.localPort,
          remoteIp: lastNet.remoteIp, remotePort: lastNet.remotePort
        });
      } catch (_) {}
      return r;
    };

  } catch (e) { logEvent('hook.warn', { where: 'socket', warn: String(e) }); }

  logEvent('frida.ready', { message: 'Shell+SMS+Exfil+Network hooks armed (narrative output).' });
});
Share this script:
Twitter LinkedIn

Comments

Login or Sign up to leave a comment.
Loading comments...