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 .dat files in very deeply nested assets/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:

PermissionWhy it matters
REQUEST_INSTALL_PACKAGESlets it install the second stage
QUERY_ALL_PACKAGESlets it list every app installed on the phone
INTERNETnetwork 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 streaming
  • RECEIVE_BOOT_COMPLETED + QUICKBOOT_POWERON: starts again after every reboot
  • REQUEST_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" />
SettingWhat it gives the threat actor
packageNames="@null"events from every app
typeViewTextChangedevery keystroke
canRetrieveWindowContenteverything on screen
canPerformGesturesremote taps and swipes
canRequestFilterKeyEventsblocking key presses
canTakeScreenshotscreenshots
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:

  1. Static unpacking: reverse the native loader, find the blob where it keeps the removed method bodies, and rebuild the dex files
  2. 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

TypeValue
SHA256 (dropper)2d7a346182a695b6f46041002042281b23968a8983658523d637a70d84b41f55
MD5 (dropper)007cb01a254f4630b4235ddd8075aa14
SHA256 (payload)15cebad8c7844ea03dcda5bd12d0c1e02d0d913007054fbb973e7663c178f7c4
Package (dropper)com.pprbzc.rvnidf
Package (payload)com.levlbu.qzgysl
Lure nameGооglе Рhоtоs
PackerDPT 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