Automated Transfer Systems (ATS)¶
On-device fraud that automates money transfers without user interaction. Rather than stealing credentials and replaying them from an attacker-controlled device, ATS operates the victim's real banking app directly using accessibility services. The transaction originates from the victim's trusted device, IP address, and session -- making it nearly invisible to bank-side fraud detection.
See also: Accessibility Abuse, Overlay Attacks, Notification Suppression, Device Wipe & Ransomware
Requirements
| Requirement | Details |
|---|---|
| Permission | BIND_ACCESSIBILITY_SERVICE |
| Banking App | Target banking app installed and authenticated |
| Configuration | Recipient account, transfer amount, app-specific UI navigation script |
ATS vs Overlay vs VNC¶
Three distinct approaches to banking fraud, often combined within a single family.
| Aspect | Overlay | ATS | VNC / RAT |
|---|---|---|---|
| Where fraud happens | Attacker's device (credentials exfiltrated) | Victim's device (automated) | Victim's device (remote-controlled) |
| Credential exfiltration | Yes -- sent to C2 | Not required (uses existing session) | Optional |
| Detection by bank | Different device/IP/fingerprint | Same device, same session | Same device, same session |
| Operator involvement | Manual (attacker logs in remotely) | None (fully scripted) | Manual (attacker controls device live) |
| Scalability | Moderate (requires human operators) | High (runs unattended) | Low (1:1 operator-to-victim ratio) |
| Behavioral biometrics | Fails (different typing/interaction patterns) | Can mimic human behavior | Partially detectable |
How ATS Works¶
The Transfer Workflow¶
- Wait for authentication -- the malware waits for the user to log into the banking app, or triggers the app open and uses previously stolen credentials via overlay
- Navigate to transfer screen -- accessibility gestures scroll, tap menu items, and navigate to the money transfer section
- Fill transfer fields -- the recipient IBAN/account and amount are injected into text fields using
AccessibilityNodeInfo.ACTION_SET_TEXT - Confirm transaction -- the malware clicks through confirmation dialogs, including "Are you sure?" screens
- Handle 2FA -- intercepts SMS OTP from notifications or reads it via
NotificationListenerService, then enters it into the confirmation field - Clean up -- deletes confirmation SMS, dismisses push notifications from the bank, clears recent transactions view if possible
Anti-Detection During Transfer¶
ATS families employ stealth measures to prevent the user from noticing the fraud in progress:
| Technique | Purpose | Implementation |
|---|---|---|
| Screen brightness to 0 | User cannot see device activity | Settings.System.SCREEN_BRIGHTNESS set to 0 |
| Mute all audio | Suppress notification sounds | AudioManager.setStreamMute() on all streams |
| Enable Do Not Disturb | Block incoming calls/notifications during transfer | NotificationManager.setInterruptionFilter() |
| Black overlay | Cover screen with opaque window | TYPE_APPLICATION_OVERLAY with Color.BLACK |
| Fake "updating" screen | Explain device unresponsiveness | Overlay displaying "System update in progress..." |
| Disable notifications | Prevent bank alerts during transfer | Accessibility dismisses notifications as they arrive |
| Lock user out | Prevent interference mid-transfer | Custom lock screen overlay or GLOBAL_ACTION_LOCK_SCREEN |
Scripting Engines¶
Early ATS implementations were hardcoded per banking app. Modern families use configurable scripting systems.
Xenomorph v3 ATS Engine: introduced a JSON-based scripting language that defines UI navigation as a sequence of operations. Scripts are downloaded from C2 per target app, making it possible to add new bank targets without updating the malware binary.
Xenomorph v3 ATS Script Example
{
"module": "ats",
"target": "com.target.bankapp",
"steps": [
{"action": "open_app", "package": "com.target.bankapp"},
{"action": "wait", "condition": "text_visible", "value": "Transfer"},
{"action": "click", "selector": {"text": "Transfer"}},
{"action": "set_text", "selector": {"id": "recipient_field"}, "value": "<iban>"},
{"action": "set_text", "selector": {"id": "amount_field"}, "value": "<amount>"},
{"action": "click", "selector": {"text": "Confirm"}},
{"action": "wait", "condition": "text_visible", "value": "SMS code"},
{"action": "set_text", "selector": {"id": "otp_field"}, "value": "<intercepted_sms>"}
]
}
SharkBot: uses a similar declarative approach where each target bank has a configuration defining the UI element IDs, button text, and navigation paths. The C2 pushes updated configs when banking apps change their UI.
Hook / Octo: instead of scripted ATS, these families provide full VNC-based remote access, letting the operator navigate the banking app manually. This is more flexible but less scalable.
Bypassing Transaction Limits¶
Banks enforce per-transaction and daily transfer limits. ATS handles this by:
- Splitting transfers -- breaking a large amount into multiple smaller transfers below the per-transaction threshold
- Modifying beneficiary lists -- adding the mule account to the trusted recipient list first (some banks skip 2FA for trusted recipients)
- Timing across days -- spreading transfers across multiple days to stay under daily limits
- Draining all accounts -- iterating through savings, checking, and linked accounts
Account Takeover Workflow¶
A full ATS-driven account takeover chains multiple techniques:
- Victim installs dropper from phishing SMS or Play Store
- Dropper requests accessibility permission via social engineering overlay
- Malware uses accessibility to auto-grant all additional permissions
- Overlay phishes banking credentials when user opens banking app
- Malware waits for idle period (typically 2-4 AM local time)
- ATS opens banking app, authenticates with stolen credentials
- ATS changes registered phone number and email to attacker-controlled values
- ATS initiates transfer, intercepts OTP, confirms transaction
- ATS repeats until accounts are drained or limits are hit
- Some families wipe the device post-fraud to destroy evidence (BRATA, BingoMod)
Families With ATS Capability¶
| Family | ATS Type | Scripting | First ATS Version | Targets |
|---|---|---|---|---|
| Gustuff | Scripted | Hardcoded per app | 2019 | Australian banks |
| Cerberus | Scripted | Hardcoded | v2 (2020) | European banks |
| SharkBot | Scripted | JSON config | v1 (2021) | EU/UK banks |
| Anatsa | Scripted | Configurable | 2021 | EU/US banks |
| Ermac | Scripted | Hardcoded | v2 (2022) | 400+ targets |
| Xenomorph | Scripted | JSON ATS engine | v3 (2023) | 400+ targets |
| Hook | VNC + ATS | Operator-assisted | 2023 | 400+ targets |
| Octo | VNC + ATS | Operator-assisted | v2 (2023) | 200+ targets |
| GodFather | Scripted | Configurable | v2 (2023) | 400+ targets |
| Medusa | VNC-based | Operator-controlled | v2 (2024) | European banks |
| BingoMod | VNC + scripted | Hybrid | 2024 | Italian banks |
| TsarBot | Scripted | Configurable | 2024 | 750+ targets |
| Copybara | VNC + scripted | Hybrid | 2024 | Italian banks |
| Herodotus | Scripted | Human behavior mimicry | 2025 | EU banks |
Accessibility Code for ATS¶
Core accessibility operations used in ATS
public void fillField(AccessibilityNodeInfo root, String viewId, String value) {
List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByViewId(viewId);
if (!nodes.isEmpty()) {
Bundle args = new Bundle();
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, value);
nodes.get(0).performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
}
}
public void clickButton(AccessibilityNodeInfo root, String buttonText) {
List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText(buttonText);
for (AccessibilityNodeInfo node : nodes) {
if (node.isClickable()) {
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
return;
}
AccessibilityNodeInfo parent = node.getParent();
if (parent != null && parent.isClickable()) {
parent.performAction(AccessibilityNodeInfo.ACTION_CLICK);
return;
}
}
}
public void navigateToTransfer(AccessibilityService service) {
Intent intent = service.getPackageManager()
.getLaunchIntentForPackage("com.target.bankapp");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
service.startActivity(intent);
}
Android Mitigations¶
| Version | Restriction | ATS Impact |
|---|---|---|
| Android 7 | Background execution limits | Minimal -- accessibility runs as foreground service |
| Android 10 | Background activity launch restrictions | ATS must wait for user interaction or use USE_FULL_SCREEN_INTENT |
| Android 13 | Restricted settings for sideloaded apps | Accessibility harder to enable -- bypassed via session-based install |
| Android 13 | Non-dismissible notification for active accessibility | User may notice -- malware hides behind legitimate-looking service name |
| Android 14 | Accessibility declaration restrictions | Apps must declare specific accessibility capabilities |
| Android 15 | Expanded restricted settings enforcement | Closes session-installer bypass for some OEMs |
Fundamental Limitation
Once accessibility is granted, there is no OS-level mechanism to distinguish ATS operations from legitimate accessibility tool usage. The permission model is all-or-nothing.
Detection During Analysis¶
Static Indicators
- Accessibility service config requesting
flagDefaultorflagRetrieveInteractiveWindows - References to banking app package names in strings or assets
ACTION_SET_TEXTandACTION_CLICKusage patterns onAccessibilityNodeInfo- JSON/XML configuration files mapping package names to UI navigation sequences
AudioManager,Settings.System.SCREEN_BRIGHTNESSmanipulation
Dynamic Indicators
- Accessibility service performing actions while screen is off or brightness is 0
- Automated navigation through banking app UI at non-human speeds
- SMS read/delete operations immediately following a transfer confirmation
- Network traffic to C2 containing transaction confirmation details
Frida Hook to Intercept ATS Actions
Java.perform(function() {
var ANI = Java.use("android.view.accessibility.AccessibilityNodeInfo");
ANI.performAction.overload("int", "android.os.Bundle").implementation = function(action, args) {
if (action === 0x200000) {
console.log("[ATS] SET_TEXT: " + args.getCharSequence("ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE"));
}
if (action === 0x10) {
console.log("[ATS] CLICK on: " + this.getViewIdResourceName());
}
return this.performAction(action, args);
};
});