Analyzing Akamai BMP 4.1.3 - Part 2
| PART 1 | App showcase: Iberia 14.81.0 | IDA Pro: 9.3 |
1. Analyzing the post-decompress lib
The decompiler often misidentified the number of arguments and return values for the polymorphic dispatcher, i used a secret technique to get around this using asm.
As we saw earlier, sub_25E0AC is a large polymorphic dispatcher.
sub_25E0AC(string_ptr) → strlen or string copy
sub_25E0AC(string_ptr, 0x8641, 0xFFFFFFFF) → string deobfuscation
sub_25E0AC(plaintext, output, len, key, iv) → AES-128-CBC encrypt
sub_25E0AC(qword_2466A8) → MT19937 extract

JNI Entry Points
| VA | Java Name | Purpose |
|---|---|---|
| 0x9D394 | SensorDataBuilder.buildN | Main entry: serialize + encrypt sensor data |
| 0x9D074 | SensorDataBuilder.encryptKeyN | Generate session ID (20-char base62 → base64) |
| 0xA0144 | addOne | Set MT flag dword_247A38 |
| 0xA0150 | sampleTest | Set MT flag dword_247A3C |
| 0xA015C | presentData | Set MT flag dword_247A40 |
| 0xA0168 | testOne | Set MT flag dword_247A44 |
Internal Functions (Called by buildN)
| VA | Size | Name | Purpose |
|---|---|---|---|
0x9ED74 |
0xAFC | sub_9ED74 |
Core encrypt+format: MT → AES → HMAC → b64 → header assembly |
0x9EAF0 |
0x34 | sub_9EAF0 |
Crypto context singleton getter |
0x9EB24 |
0x250 | sub_9EB24 |
One-time key initialization |
0x9E840 |
0x1D0 | sub_9E840 |
LCG-based string deobfuscation |
0x9E660 |
0xF8 | sub_9E660 |
RSA_public_encrypt wrapper |
0x9E594 |
0x90 | sub_9E594 |
HMAC-SHA256 wrapper |
0x9E620 |
0x24 | sub_9E620 |
RAND_bytes wrapper |
0x9E75C |
0xE0 | sub_9E75C |
Base64 encode (OpenSSL BIO) |
0x9FAD0 |
0x130 | sub_9FAD0 |
MT19937 bounded random |
0x9F91C |
0xFC | sub_9F91C |
C++ stringstream initialization |
0x9FDF8 |
0xD0 | sub_9FDF8 |
Stringstream write (string) |
0x1CFD2C |
0x158 | sub_1CFD2C |
Stringstream write (integer) |
0x9DBD0 |
0x98 | sub_9DBD0 |
JNI CallIntMethodV wrapper |
1.1. Serialization Pipeline
Function: Java_com_cyberfend_cyfsecurity_SensorDataBuilder_buildN @ 0x9D394
| Property | Value |
|---|---|
| Size | 0x83C (2108 bytes) |
| Input | ArrayList<Pair<String, String>> — 28 entries from Java |
| Output | Encrypted header string "6,a,{rsa1},{rsa2}${b64}${timing}" |

Step-by-step from Assembly
Phase 1: JNI Environment Setup (0x9D3CC–0x9D54C)
The function begins by resolving all required JNI references prior to data extraction:
FindClass("android/os/Build$VERSION")
GetStaticFieldID("SDK_INT", "I")
GetStaticIntField() → must be >= 1 (sanity check)
FindClass("java/util/ArrayList")
GetMethodID("size", "()I") → pair_count
GetMethodID("get", "(I)Ljava/lang/Object;")
FindClass("android/util/Pair")
GetFieldID("first", "Ljava/lang/Object;")
GetFieldID("second", "Ljava/lang/Object;")
Phase 2: Pair Vector Extraction (0x9D554–0x9D6A8)
Each element of the input ArrayList is extracted via a JNI iteration loop over indices $i \in [0,\ \text{pair\_count} - 1]$:
pair = ArrayList.get(i)
key = GetStringUTFChars(pair.first)
value = GetStringUTFChars(pair.second)
Pairs are stored in a C++ vector as 48-byte structs with the layout:
\[\text{struct PairEntry} = \underbrace{\text{key}}_{\text{std::string, 24 B}} \;\|\; \underbrace{\text{value}}_{\text{std::string, 24 B}}\]The 24-byte std::string layout corresponds to the standard libc++ small-string-optimized (SSO) representation on AArch64.
Phase 3: Output Initialization from First Pair (0x9D6F8–0x9D724)
The value field of the first pair (index 0) is used as the initial content of the serialized output buffer; its key is discarded. In the observed execution context, this value is the SDK version string "4.1.3".
Phase 4: Separator String Deobfuscation (0x9D728–0x9D768)
The separator literal is stored obfuscated in .rodata and decoded at runtime. The encoded form "WUfOL#f}+" is passed to a .pb dispatcher alongside the constant 0x8641, which drives a substitution-based decode:
9d728 ADRL X9, aWufolF ; load encoded string "WUfOL#f}+"
9d738 STRB W8, [SP, #var_90] ; SSO length field = 9
9d744 STUR X9, [SP, #var_90+1] ; copy encoded bytes onto stack
9d748 ADD X8, SP, #var_78 ; destination buffer = var_78
9d750 MOV W1, #0x8641 ; deobfuscation constant
9d754 MOV W2, #0xFFFFFFFF ; flag
9d758 BL sub_25E0AC ; decode → "-1,2,-94," stored in var_78
Phase 5: Pair Serialization Loop (0x9D76C–0x9D84C)
Pairs at indices $i \in [1,\, \mathsf{pair\_count} - 1]$ are serialized in order. Each iteration appends to the output buffer with the pattern:
\[\mathtt{output} \mathrel{+}= \mathtt{separator} \;\Vert\; \mathtt{key}_i \;\Vert\; \text{","} \;\Vert\; \mathtt{value}_i\]The struct stride is 48 bytes (ADD X22, X22, #0x30).
9d7b0 ADD X0, SP, #var_60 ; output string ptr
9d7b4 BL sub_25E0AC ; append(output, separator)
9d7d4 LDRB W9, [X8] ; load key SSO flag (offset +0)
9d7f0 BL sub_1CDC48 ; append(output, key)
9d7f8 MOV X1, X20 ; X20 = "," literal @ 0x51163
9d7fc BL sub_1CE08C ; append(output, ",")
9d81c LDRB W9, [X8, #0x18] ; load value SSO flag (offset +24)
9d83c BL sub_1CDC48 ; append(output, value)
9d840 ADD X22, X22, #0x30 ; advance to next struct (stride = 48)
9d844 ADD X24, X24, #1 ; i++
9d84c B.NE loop_start
Phase 6: SECURITY_PATCH Field Appended via JNI (0x9D850–0x9D92C)
After exhausting the input vector, the function retrieves Build.VERSION.SECURITY_PATCH directly from the Android runtime via JNI reflection, bypassing the Java-side pair list entirely:
9d888 ADRL X2, aSecurityPatch ; field name "SECURITY_PATCH"
9d890 ADRL X3, aLjavaLangStrin ; descriptor "Ljava/lang/String;"
9d8a4 BLR X8 ; GetStaticFieldID
9d8bc BLR X8 ; GetStaticObjectField
9d8d4 BLR X8 ; GetStringUTFChars → X20 = e.g. "2025-04-01"
The value is appended with a dedicated tag identifier -164:
The native-side injection of SECURITY_PATCH — absent from the Java-supplied pair list — constitutes an integrity signal that cannot be trivially spoofed by intercepting the Java layer alone.
Phase 7: Encryption and Formatting (0x9D930–0x9D96C)
The assembled plaintext is passed to the shared cryptographic context singleton and encrypted via sub_9ED74, which implements the same AES-128-CBC + HMAC-SHA256 pipeline.
9d930 BL sub_9EAF0 ; acquire crypto context singleton
9d964 ADD X8, SP, #var_90 ; output buffer
9d968 ADD X1, SP, #var_B0 ; plaintext string
9d96c BL sub_9ED74 ; encrypt + assemble → "6,a,{rsa1},{rsa2}${b64}${timing}"
Phase 8: Return to Java (0x9D99C–0x9D9AC)
9d99c BLR X8 ; NewStringUTF(output_cstr)
9d9ac ; return jstring
Serialization Format
4.1.3-1,2,-94,-90,{val}-1,2,-94,-91,{val}-1,2,-94,-70,-1,2,-94,-80,...-1,2,-94,-164,{SECURITY_PATCH}
- First: SDK version (pair 0 value only)
- Then:
{separator}{key},{value}for each remaining pair - Last:
{separator}-164,{SECURITY_PATCH}(from JNI)
2. Cryptographic Pipeline — sub_9ED74
Function: sub_9ED74 @ 0x9ED74
| Property | Value |
|---|---|
| Size | 0xAFC (2812 bytes) |
| Calling convention | __usercall — X8 = output ptr, X0 = crypto context, X1 = plaintext |
| Input | Crypto context (from sub_9EAF0), serialized sensor plaintext |
| Output | "6,a,{rsa1},{rsa2}${base64(IV+cipher+HMAC)}${timing}" |
2.1 Phase 1: Separator Decode
Employs the same deobfuscation routine as buildN: the encoded string "WUfOL#f}+" is decoded with constant 0x8641, yielding the separator "-1,2,-94,".
2.2 Phase 2: MT19937 PRNG Initialization
Two atomic guard flags gate one-time initialization of the Mersenne Twister state:
byte_2466A0— controls MT state array initializationbyte_247A30— controls seeding fromclock_gettime(CLOCK_REALTIME)
The seeding routine implements the standard MT19937 initialization recurrence:
\[\mathtt{state}[i] = i + 1812433253 \cdot \bigl(\mathtt{state}[i-1] \oplus (\mathtt{state}[i-1] \gg 30)\bigr), \quad i \in [1, 623]\]2.3 Phase 3: Verification Value Generation
Five sampling ranges are loaded from .rodata:
| Variable | Range | Conditional Flag | Semantic |
|---|---|---|---|
var_188 |
[1, 1000] | — (unconditional) | base values |
var_190 |
[1, 6] | dword_247A38 (addOne) |
optional |
var_198 |
[1, 7] | dword_247A3C (sampleTest) |
optional |
var_1A0 |
[1, 8] | dword_247A40 (presentData) |
optional |
var_1A8 |
[1, 4] | dword_247A44 (testOne) |
optional |
Four verification values are derived through a chained XOR construction:
\[\text{val}_1 = v_{14} + 7 \cdot v_{13}\] \[\text{val}_2 = \left(v_{16} + 8 \cdot v_{15}\right) \oplus \text{val}_1\] \[\text{val}_3 = \left(v_{18} + 9 \cdot v_{17}\right) \oplus \text{val}_2\] \[\text{val}_4 = \left(v_{20} + 5 \cdot v_{19}\right) \oplus \text{val}_3\]2.4 Phase 4: AES-128-CBC Encryption
A fresh 16-byte IV is generated per invocation via RAND_bytes(). The output layout is:
2.5 Phase 5: HMAC-SHA256 Authentication
The authentication tag is computed over the full IV || ciphertext blob:
2.6 Phase 6: Final Assembly
The authenticated ciphertext is assembled into a contiguous binary blob:
\[\mathtt{output\_bin} = \mathrm{IV}_{16} \;\Vert\; \mathtt{ciphertext}_{n} \;\Vert\; \mathrm{HMAC}_{32}\]Total length: $n + 48$ bytes. The blob is then Base64-encoded.
Final header format:
\[\mathtt{header} = \mathrm{Segment\_A} \;\Vert\; \textrm{"\$"} \;\Vert\; \mathtt{b64} \;\Vert\; \textrm{"\$"} \;\Vert\; \mathrm{Segment\_B}\]3. Mersenne Twister Verification
3.1 MT19937 Implementation
The native code employs a standard MT19937 PRNG, confirmed by three structural markers: a 624-element state array at qword_2466A8, the initialization multiplier 1812433253, and a twist operation matching the reference implementation.
3.2 Bounded Random Sampling
int mt_rand_range(int lo, int hi) {
int range = hi - lo + 1;
int result;
do {
result = mt_extract() % range;
} while (result >= range);
return lo + result;
}
3.3 Flag Behavior
The four control flags are set by the JNI entry points addOne, sampleTest, presentData, and testOne. Their static value in the binary is 0xFFFFFFFF (not equal to 1), rendering all conditional terms zero by default.
4. Key Initialization — sub_9EB24
Invoked when ctx[40] == 0 (uninitialized). Executes once per process lifetime.
4.1 Key Buffer Allocation
9eb5c BL sub_25E0AC ; ctx[0] = malloc(17) → AES key buffer (16 B + null)
9ec54 BL sub_20AF7C ; ctx[8] = malloc(17) → IV buffer (16 B + null)
9ec64 BL sub_25E0AC ; ctx[16] = malloc(33) → HMAC key buffer (32 B + null)
4.2 RSA Public Key Deobfuscation
268 bytes of obfuscated key material are loaded from off_245030 and decoded via the LCG substitution cipher with seed 63.
4.3 Session Key Generation
; AES key: 16 random bytes, RSA-encrypted, Base64-encoded → ctx[24]
9ec38 BL sub_9E660 ; RSA_public_encrypt(pem_key, rand_16)
9ec48 BL sub_9E75C ; base64(encrypted) → ctx[24]
; HMAC key: 32 random bytes, RSA-encrypted, Base64-encoded → ctx[32]
9ec9c BL sub_9E660 ; RSA_public_encrypt(pem_key, rand_32)
9eca8 BL sub_9E75C ; base64(encrypted) → ctx[32]
5. String Deobfuscation
5.1 Native: LCG Substitution Cipher
def build_charset():
return [ch for ch in range(32, 127) if ch not in (34, 39, 92)]
def lcg_decode(data: bytes, seed: int) -> bytes:
charset = build_charset()
n = len(charset)
lcg = seed
out = []
for byte in data:
idx = charset.index(byte)
shift = ((lcg >> 8) & 0xFFFF) % n
out.append(charset[(idx - shift + n) % n])
lcg = (lcg * 65793 + 4282663) & 0x7FFFFF
return bytes(out)
5.2 Java: XOR Table Cipher
def build_table(size: int = 32768) -> list[int]:
table, prev = [0] * size, 3
for i in range(size):
val = prev ^ i
prev = (prev + val + 88) % 63
table[i] = prev
return table
def decode_kpR(encoded: str) -> str:
table = build_table()
return ''.join(chr(ord(c) ^ table[i]) for i, c in enumerate(encoded))
6. Java-Side Architecture
6.1 CYFManager.buildSensorData()
The primary Java entry point orchestrates the full sensor data pipeline:
- Evaluate event count thresholds to select fast path or full path
- Collect data from 12+ sensor subsystems
- Assemble a
LinkedHashMap<String, String>with ~25 entries - Convert to
ArrayList<Pair<String, String>> - Invoke native
buildN(pairs)→ encrypted header - Append
$[3]..$[6]sections: Proof-of-Work, CCA token, signal, metadata
6.2 Fast Path (event count < 16)
When both GA and IIT accumulators hold fewer than 16 events and EG.D.isValid() is true, the function returns cached sensor data from EG.D.get().
6.3 Sensor Collector Classes
| Class | Alias | Data Collected |
|---|---|---|
C0005GA |
EG.GA |
Accelerometer, gyroscope, magnetometer (orientation) |
C0009GN |
EG.GN/McP.IIT |
Motion analysis, jerk derivatives (9 axes) |
C0022KG |
EG.KG |
Touch events (DOWN/MOVE/UP with coordinates) |
C0018K |
EG.K |
Text input events (keystroke timing) |
C0054Z |
EG.Z |
EditText field metadata |
C0008GK |
EG.GK |
Activity lifecycle (resume/pause events) |
C0006GE |
EG.GE |
Device info (40+ fields) |
C0051Vw |
EG.Vw/KWS |
System fingerprint, device ID |
C0001C |
EG.C |
DCI/JavaScript bridge (WebView challenges) |
C0002D |
EG.D |
CPR signal cache |
C0042U |
EG.U |
Proof-of-Work responses |
C0038M |
EG.M |
CCA challenge tokens |
I’m going to rest here, wait for part 3.