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
- SHA256:
9188452fc54aeb560e24465b809de3958fbdaa16377f017fe7c344721dcc6305 - Report Link: https://tria.ge/260825-vbp3cawdme
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:
- 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.
- DEX Loader: Once decrypted, the DEX loader serves a single purpose: dynamically loading the native library
libigv2wWx29.so.
- Native Extraction: The native library reads encrypted payload data embedded in its
.rodatasection, decrypts it with RC4, and loads it as a secondary native library.
- 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 ID0x7F020001, whichopenRawResourcethen resolves via resources.arsc touknown/pnrg5cu=1i1h.
- 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
- Stage 0 (APK Container): Contains 12 x 1 MiB zero-filled decoy DEX entries, malformed ZIP extra-field TLVs, obfuscated asset paths.
- Stage 1 (
biz.similar.deer.Bcorescene): Pure Java code that readsPbxRHA.datfrom the Unicode path, decrypts it with RC4, and loads the inner DEX. - Stage 2 (
com.fnotes20_safety.cbkgwj): Loader stub that callsSystem.loadLibrary("igv2wWx29"). - Stage 3 (
libigv2wWx29.so): Unpacks and loads an embedded inner ELF shared object. - Stage 4 (Inner ELF): RC4 decrypts
uknown/pnrg5cu=1i1h, repairs the stripped DEX magic bytes, and loads the DEX into memory. - Stage 5 (Core Octo2 Payload): Core Android banking trojan functionality.
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:
- Zero-filled decoy DEX files
- Invalid extra-field TLV structures in ZIP headers
- 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