Taking Apart a Fake "Google Photos" Android Banking Trojan
November 4, 2026 · 5 min read
Contents
I got my hands on an APK called “Gооglе Рhоtоs 04.11.26.apk” that was suspected to be a banking trojan.
Here’s what I found.
The lab: REMnux in a container
REMnux is “A Linux Toolkit for Malware Analysis”, and it ships as a VM or container image. I went with the container over a full VM because I only planned on doing static analysis. An APK is Dalvik bytecode and ARM native code, so it can’t run on my Linux host by accident. The realistic risk is a bug in a parsing tool, and a locked-down container handles that risk decently enough.
I used rootless Podman, so even a container escape would only land as my own user,
I also used the --network none flag, so there is no network at all inside the container,
the sample folder is mounted read-only, results go to a separate folder,
and the sample is renamed to .apk.bin so nothing on the host opens it by accident.
This much is fine for just some static analysis, for dynamic analysis, where you would be running the apk in an emulator, I would recommend using a proper VM and an isolated network.
My assessment
SHA256: 2d7a346182a695b6f46041002042281b23968a8983658523d637a70d84b41f55
MD5: 007cb01a254f4630b4235ddd8075aa14
The first red flag was the filename itself. Dumping the bytes shows it isn’t really “Google Photos”:
G 320 276 320 276 g l 320 265 320 240 h 320 276 t 320 276 s
The о, е and Р are Cyrillic letters that look the same as o, e, and P.
This trick threat actors use is called a homoglyph: the name looks right to a person but won’t match the real app.
apkid added a lot more:
- Packer: DPT Shell, where the real code is stored as encrypted
.datfiles in very deeply nestedassets/fb_…/folders - anti_hook: syscalls: making syscalls directly so analysis tools that hook library calls don’t see them
- compiler: dexlib 2.x: the code was reassembled with tooling instead of compiled normally, which is common for repackaged malware
- a second APK hidden inside:
AndroidManifest.xml/g8XicpP3Sfo/ZELhR6SX5emCk1j3Wao.apk
That second APK definitely deserves a closer look.
The archive has a folder named AndroidManifest.xml/ full of randomly named files.
The APK also has a zip entry with an empty name that claims to be 82 MB, and declares around 830 meaningless custom permissions.
This is done against analysis tools, both apktool and androguard failed here.
Stage 1: the dropper
Package com.pprbzc.rvnidf only asks for three Android permissions that actually do anything:
| Permission | Why it matters |
|---|---|
REQUEST_INSTALL_PACKAGES | lets it install the second stage |
QUERY_ALL_PACKAGES | lets it list every app installed on the phone |
INTERNET | network access |
It also declares a service bound with BIND_VPN_SERVICE. This may be used to monitor the affected device’s network traffic.
The outer app’s main job is to get the inner APK installed.
Stage 2: the payload
Package: com.levlbu.qzgysl
SHA256: 15cebad8c7844ea03dcda5bd12d0c1e02d0d913007054fbb973e7663c178f7c4
Permissions and components
FOREGROUND_SERVICE_MEDIA_PROJECTION: screen capture or streamingRECEIVE_BOOT_COMPLETED+QUICKBOOT_POWERON: starts again after every rebootREQUEST_IGNORE_BATTERY_OPTIMIZATIONS+WAKE_LOCK: keeps Android from stopping it- an AccessibilityService and six activities with random names, which are probably “please enable accessibility” views
The accessibility config
This is where it really shows what it is:
<accessibility-service
android:accessibilityEventTypes="typeWindowContentChanged|typeWindowStateChanged|typeViewTextChanged|typeViewLongClicked|typeViewClicked"
android:packageNames="@null"
android:canRetrieveWindowContent="true"
android:canRequestFilterKeyEvents="true"
android:canPerformGestures="true"
android:canTakeScreenshot="true"
android:isAccessibilityTool="true" />
| Setting | What it gives the threat actor |
|---|---|
packageNames="@null" | events from every app |
typeViewTextChanged | every keystroke |
canRetrieveWindowContent | everything on screen |
canPerformGestures | remote taps and swipes |
canRequestFilterKeyEvents | blocking key presses |
canTakeScreenshot | screenshots |
isAccessibilityTool="true" | falsely claims to be an accessibility tool to look legitimate |
Combined with screen capture, this matches on-device fraud, the pattern of modern Android banking trojans. Instead of stealing credentials to use somewhere else, the attacker watches your screen and makes the transfer from your phone, through your logged-in banking app.
The packer, and obfuscation
Decompiling with jadx gives about 1,000 classes per stage, but almost all of them are randomly generated filler:
AdapterSmSetUtilsdjqi2lzrObserver, CompositeFacadeSmStack9162Converter2003, and so on.
None of the classes named in the manifest exist in the decompiled code.
DPT Shell removes the real method bodies from the dex files and puts them back at runtime from a native library, which is the ELF file disguised as assets/.../arm64/fb_....dat.
The only real code left is the packer’s own Java loader, and its strings are hidden like this:
throw new NullPointerException(fgq0zq01m83tzq0uw.d("~E~2A2E449FB5D1E6D688A65328A0278A8D6601DE50E4D8F58A9FA04E20"));
The decoding routine is short, and the key is hardcoded right next to it:
private static byte[] K = {-2, -114, -48, 38, -49, -18, 112, -60, -21, 111, -86, 57, 41, 76, -14, -81};
// hex-decode everything after "~E~"; first 4 bytes are a per-string salt
out[i] = data[i+4] ^ K[i & 15] ^ data[i & 3] ^ (byte) i;
The app has to decode its own strings on the phone, with no outside help, so the key must ship inside the APK. This is obfuscation rather than encryption: the goal is to beat automated scanners and string searches, not a person with a decompiler.
Reversing it took a few lines of Python:
def dec(s, K):
b = bytes.fromhex(s[3:])
return bytes(b[i+4] ^ K[i & 15] ^ b[i & 3] ^ (i & 255)
for i in range(len(b) - 4)).decode()
The output was pretty underwhelming: android.app.ActivityThread, mBoundApplication, instantiateClassLoader, vmInstructionSet…
These are the usual loader steps of a packer that hijacks app startup and swaps in its own class loader.
The two stages use different keys, so each build was packed separately.
Where static analysis stops
The interesting parts are the C2 server, the list of targeted banks, and the fake login screens. They’re all in the code DPT removed, and that only exists in memory once the native loader has run. The next step is one of two things:
- Static unpacking: reverse the native loader, find the blob where it keeps the removed method bodies, and rebuild the dex files
- Dynamic unpacking: run it in an Android emulator inside an isolated VM, hook it with Frida, and dump the restored dex from memory. This also shows its network traffic
That may be for a follow-up post.
IOCs
| Type | Value |
|---|---|
| SHA256 (dropper) | 2d7a346182a695b6f46041002042281b23968a8983658523d637a70d84b41f55 |
| MD5 (dropper) | 007cb01a254f4630b4235ddd8075aa14 |
| SHA256 (payload) | 15cebad8c7844ea03dcda5bd12d0c1e02d0d913007054fbb973e7663c178f7c4 |
| Package (dropper) | com.pprbzc.rvnidf |
| Package (payload) | com.levlbu.qzgysl |
| Lure name | Gооglе Рhоtоs |
| Packer | DPT Shell |
At the time of analysis, neither VirusTotal, nor MalwareBazaar had entries for the dropper hash and a web search turned up nothing, so this looks like a fairly new build.
If you installed something like this
- Turn on airplane mode right away
- Go to Settings → Accessibility and turn off any service you don’t recognise
- Reboot into Safe Mode and uninstall both apps (the “Google Photos” one and the app it installed)
- Call your bank, and change passwords from a different device
- If you’re unsure, back up what you need and factory-reset the phone