Triage Insights

Triage Insights: Octo2 Android Banker Evolves with New Malformed Evasion Techniques

Blog.

Octo2 Is Evolving with New Malformed APK Techniques

Recently, we observed a significant increase in Octo2 samples appearing in public Triage submissions. These samples feature newly generated Domain Generation Algorithm (DGA) C2 domains and target an expanded set of applications, including banking, payment gateways, and cryptocurrency wallets. Octo2 was first reported by ThreatFabric, and we previously analyzed the earlier Octo variant in our first Triage Insights post.

While analyzing this latest campaign, we identified multiple APK malformation techniques not previously documented for this malware family. These techniques leave the APK fully installable on victim devices while hindering static analysis. In this post, we walk through how these mechanisms work and how they interact with common analysis workflows.

Technical Analysis

These techniques build on what we covered in Detection Evasion In Android APK Headers. In this episode of Triage Insights, we focus on three primary malformations and their impact on standard analysis tools, highlighting the malware’s use of sophisticated evasion techniques.

Targeted Sample Information

1. Decoy DEX Entries with Zero-Prefixed Names

Decompiling this sample with Jadx succeeds and produces analyzable code, but it triggers 12 distinct parsing errors. All 12 errors originate from entries named classes000.dex through classes0011.dex, a naming convention that does not follow Android’s multi-DEX standard.

Binary analysis confirms that each decoy consists entirely of zero-filled (0x00) bytes and contains no valid DEX header. Despite their file extensions, they contain no executable Dalvik bytecode. In contrast, the four legitimate DEX files retain valid dex\n039 magic headers:

File Magic
classes.dex dex\n039\0
classes2.dex dex\n039\0
classes3.dex dex\n039\0
classes4.dex dex\n039\0
classes0*.dex (x12) 00 00 00 00 00 00 00 00

The decoys range from classes000.dex to classes0011.dex. The jump from three to four digits at index 10 occurs because the threat actor concatenated a fixed "classes00" prefix with a loop counter rather than using fixed-width padding.

Under normal execution, the Android runtime loads classes.dex, followed sequentially by classes2.dex, classes3.dex, etc., stopping at the first missing index. Because ART searches for secondary DEX files using an unpadded integer format classes%zu.dex starting at index 2, it never looks for zero-prefixed names like classes000.dex, allowing the app to execute without error.

Static analysis tools, however, often attempt to pass any file ending in .dex directly to a DEX parser. Jadx throws an exception for each decoy entry it encounters.

2. Malformed ZIP Extra Field Records

APKs are structured as standard ZIP archives. The ZIP specification defines the Extra Field as a chain of Type-Length-Value (TLV) records, each consisting of a 2-byte Header ID, a 2-byte data size, and the designated payload bytes. A parser reads these records sequentially until it consumes the declared length of the extra field.

In this Octo2 sample, while the total length of the extra field is correctly declared, its internal content is malformed.

A clear example appears in the assets/bg_25.webp entry.

assets/bg_25.webp
 extra field length: 557 bytes (declared identically in both headers)
 local file header       at 0x0001CAA7  ->  extra field at 0x0001CAD6
 central directory entry at 0x0075CC54  ->  extra field at 0x0075CC93

 first four bytes of the extra field: 2e 2e 6d 2e ("..m." in ASCII)
 -> read as Header ID 0x2E2E, data size 0x2E6D (11,885 bytes)
 -> total required record size: 4 + 11,885 = 11,889 bytes
 -> declared field length: 557 bytes (553 remaining after header)

This pattern recurs across multiple entries with varying junk bytes. Strict archive parsers, such as Python’s zipfile module and Java’s java.util.zip.ZipFile, index the entire central directory before returning an archive object, so both throw exceptions when they encounter the first malformed record:

$ python3 -c "import zipfile; zipfile.ZipFile('9188452f....apk')"
zipfile.BadZipFile: Corrupt extra field 782e (size=11864)
java.util.zip.ZipException: Invalid CEN header (invalid extra data field size for tag: 0x782e at 7002)
	at java.base/java.util.zip.ZipFile$Source.checkExtraFields(ZipFile.java:1301)
	at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1779)

Both libraries abort at the same record-assets/cx.png, the first entry in central-directory order with a malformed extra field, rather than the bg_25.webp entry shown above. Java’s at 7002 is the byte offset of that entry in the central directory.

Reported Header ID Reported Size Raw Bytes ASCII
0x782E 11864 2e 78 58 2e .xX.

All four bytes are printable ASCII characters. This header ID was not engineered to exploit specific parser logic; rather, it is arbitrary ASCII junk that strict parsers interpret as metadata.

3. Payloads Hidden Behind Unresolvable Paths & Resource Table Indirection

Both payload stages are placed in non-standard locations designed to bypass routine detection methods. While straightforward in execution, each approach undermines a distinct analytical assumption.

The Stage-2 payload: an oversized Unicode path. PbxRHA.dat is not stored under a clean path like assets/PbxRHA.dat. Its full archive path spans roughly 1,700 characters, consisting of deeply nested pseudo-random directory names padded with Cyrillic and multi-byte Unicode characters, with PbxRHA.dat appended at the very end. The result is long enough to exceed the path-length assumptions of common extraction tools.

The Stage-5 payload: a resource-table indirection. The final DEX is stored as RC4-encrypted, with its DEX magic stripped, at uknown/pnrg5cu=1i1h. The native loader never references that path. It requests com.fnotes20_safety:raw/zk5mfka and lets resources.arsc supply the mapping at runtime.

The real resolution takes two steps, resources.arsc keeps names and values in separate pools: getIdentifier("zk5mfka", "raw", "com.fnotes20_safety") matches the key pool and returns only an integer, 0x7F020001. openRawResource(0x7F020001) then reads that entry’s value - a TYPE_STRING indexing the global pool - and retrieves uknown/pnrg5cu=1i1h.

Detailed APK Payload Chain

Unpacking the container reveals a six-stage execution chain:

  1. Initial Analysis: Analysis of the primary Stage-0/Stage-1 APK shows that the malware extracts and decrypts an RC4-encrypted asset, PbxRHA.dat, to obtain the Stage-2 DEX loader.
  1. DEX Loader: Once decrypted, the DEX loader serves a single purpose: dynamically loading the native library libigv2wWx29.so.
  1. Native Extraction: The native library reads encrypted payload data embedded in its .rodata section, decrypts it with RC4, and loads it as a secondary native library.
  1. Resource Mapping: The final core DEX payload is stored as an Android resource. The native loader references it by name (com.fnotes20_safety:raw/zk5mfka), which does not map to a physical ZIP path (e.g., res/raw/zk5mfka). At runtime, getIdentifier() resolves the string to Resource ID 0x7F020001, which openRawResource then resolves via resources.arsc to uknown/pnrg5cu=1i1h.
  1. In-Memory Payload Execution: The final native library decrypts the payload DEX, repairs its stripped DEX magic header, and loads it directly into memory without writing it to disk.

Execution Stage Overview

Domain Generation Algorithm

In this Octo2 sample, C2 is computed at runtime by make_DGA, which is exported from the dropped ELF (Stage 4) and called from Java via the native method.

The work is deliberately split between the two layers. Java supplies only the TLD, while the native library holds the salt, the seed, and the hashing.

The TLD list comes from the s_DGA_ZONES configuration value. The DEX compiles with a default three-zone list of com, org, info, but this sample ships a twelve-entry list.

Input Value
Salt kurbanguly
Week number tm_yday / 7 + 1
Zone e.g. com
Seed zw5L1P3YDKFF64s5jt5d
Tag chrome

The five values are concatenated in that order, hashed with MD5, and then assembled into a URL. Note that the tag contributes to the hash but never appears in the result:

host = MD5( "kurbanguly" + week + zone + seed + tag )
url  = "https://" + host + "." + zone + "/" + seed + "/"

Validation against the sandbox DNS logs confirmed our reconstruction. During analysis on August 25, 2026 (week 34), all twelve queried domains matched the three static constants:

week 34

 com   06f5cab703c12b8d1af1270da681761f      net   847a6f1d223d8464985e8a441564ba42
 org   14c45f4e8929d369d6c18b460158f359      us    01144444ab4135bd1e2468f0f7130967
 info  1aeaa60481d1c988a34cfd150fc9b9b1      uk    c50525b85b60f08d6d3ccc80e7071867
 au    fbcd6e49f48551fb33b69caff1bb4191      ir    5a490f954053abbaf743b5ecfcb0e80e
 ca    a0bac1110693eb4221f687b9aafb6c45      in    43fe60c1f124f3408213621f6af30d28
 ua    179573063715c067b2af4eefb2a38c52      de    b76f775d662ca32c00d3273376c1ed41

The observed zone list in this sample is com, net, org, info, in, ir, uk, au, de, ua, ca, us.

Because the week number is the only time-varying input, the domain set remains stable, and the complete candidate set for any past or future week can therefore be precomputed from the salt, seed, and tag, all of which are recovered statically, without executing the sample.

Reimplementation of the Octo2 DGA:

#!/usr/bin/env python3
"""Octo2 DGA.  Usage: dga.py [YYYY-MM-DD]"""
import datetime, hashlib, sys

SALT, SEED, TAG = "kurbanguly", "zw5L1P3YDKFF64s5jt5d", "chrome"
ZONES = "com net org info in ir uk au de ua ca us".split()

def dga(zone, week):
 md5 = hashlib.md5(f"{SALT}{week}{zone}{SEED}{TAG}".encode()).hexdigest()
 return f"https://{md5}.{zone}/{SEED}/"

d = datetime.date.fromisoformat(sys.argv[1]) if len(sys.argv) > 1 else datetime.date.today()
week = (d.timetuple().tm_yday - 1) // 7 + 1     # C's tm_yday is 0-based; Python's is 1-based
for z in ZONES:
 print(dga(z, week))

Network C2 Commands

Command Behavior
cmd Feeds the payload to the automation-script engine, which executes a sequence of device actions
sms Sends an SMS (number|text). The special target contacts iterates through the address book and substitutes %NAME% for each recipient
show_inject Displays the stored overlay for a given package
ussd Dials a USSD code via TelephonyManager.sendUssdRequest()
register_again Clears the registration flag and re-registers the bot with the C2
lock_on Enables the black-screen lock overlay, dims the display, and starts the foreground service
lock_off Removes the lock, restores brightness, and disables Do Not Disturb
intercept_on / intercept_off Toggles SMS interception
vnc_start Starts the VNC module; parses WS_PORT:<n> and opens a socket back to the operator
vnc_stop Stops VNC and tears down the socket
vnc_update Pushes new parameters to a running VNC session
start_keylogger / stop_keylogger Toggles the keylogger flag
push Posts a fake local notification (title|body|package)
kill_bot Adds its own package to the uninstall queue—self-removal
uninstall_apps Queues packages for removal, using accessibility to click through confirmation dialogs
open_url Opens a URL in a full-screen WebView, or in the system browser when prefixed with BROWSER|
disable_inject Deactivates the stored overlay for a package
run_app Launches an installed application by package name
syslog_start / syslog_stop Starts and stops device log collection
set_bot_mode Switches operating mode, toggles the foreground service, and optionally opens a VNC port
ask_perms Queues one or more permission-acquisition tasks
sync_injects Updates the overlay configuration sources and refreshes stored overlays
upload_inject Stores an operator-supplied HTML overlay for a package and displays it immediately
fs_download / fs_upload / fs_delete / fs_rescan File-manager module: retrieve, place, delete, and re-enumerate files

Conclusion

Our analysis indicates that the updates in this Octo2 campaign primarily focus on malformed APK evasion techniques rather than modifications to core banking trojan capabilities. Three key techniques stand out:

  1. Zero-filled decoy DEX files
  2. Invalid extra-field TLV structures in ZIP headers
  3. Payloads hidden behind paths that cannot be resolved by inspection alone

The extra-field malformation is particularly effective because strict parsers encounter corrupted record sizes and abort processing entirely. The decoy DEX entries (classes000.dex through classes0011.dex) exploit a different assumption, targeting static analyzers that search generically for classes*.dex and forcing them to fail. Meanwhile, the Android runtime safely ignores them.

These tactics highlight an asymmetry between the target environment and security tools: Android devices can install the package successfully, while static analysis frameworks fail strict metadata checks.

Furthermore, because the Stage-5 payload is loaded reflectively in memory and C2 domains are generated dynamically via native DGA libraries, static analysis limited to the DEX layer provides minimal actionable intelligence.

We will continue to monitor potential threats, including recent trends and emerging families. Stay tuned and follow our blog posts to get the most out of our sandbox. If you haven’t signed up yet, visit tria.ge to register for a free account!

Indicators of Compromise (IOCs)

Octo2 Sample

9188452fc54aeb560e24465b809de3958fbdaa16377f017fe7c344721dcc6305

DGA Constants

salt    kurbanguly
seed    zw5L1P3YDKFF64s5jt5d
tag     chrome
zones   com, net, org, info, in, ir, uk, au, de, ua, ca, us

C2 Candidates - Week 34 (August 25 2026)

06f5cab703c12b8d1af1270da681761f.com
14c45f4e8929d369d6c18b460158f359.org
1aeaa60481d1c988a34cfd150fc9b9b1.info
847a6f1d223d8464985e8a441564ba42.net
01144444ab4135bd1e2468f0f7130967.us
c50525b85b60f08d6d3ccc80e7071867.uk
fbcd6e49f48551fb33b69caff1bb4191.au
5a490f954053abbaf743b5ecfcb0e80e.ir
a0bac1110693eb4221f687b9aafb6c45.ca
43fe60c1f124f3408213621f6af30d28.in
179573063715c067b2af4eefb2a38c52.ua
b76f775d662ca32c00d3273376c1ed41.de

You may also like: