Fetching latest headlines…
Analyzing Akamai BMP 4.1.3 - Part 1 - For Noobs Learn
NORTH AMERICA
πŸ‡ΊπŸ‡Έ United Statesβ€’March 22, 2026

Analyzing Akamai BMP 4.1.3 - Part 1 - For Noobs Learn

0 views0 likes0 comments
Originally published byDev.to

App showcase: Iberia 14.81.0
IDA Pro: 9.3

1. Initial analysis

Well, I already had some prior knowledge of how Akamai worked, after loading the library in Ida, which I found very strange initially:

firsts functions
The library is over 2MB, and the low number of functions made me realize something was wrong. So I went to check the exports:


I try disasm addresses of the exported functions:

initializeKeyN @ 0x9d060
encryptKeyN @ 0x9d074
decryptN @ 0x9d18c
buildN @ 0x9d394 <-- prob generates the sensor data

The decompilation failed because this isn't even a functional arm64 instruction:

0x9d060: bytes=1094e857b4b328ef  -> NOT a valid ARM64 instruction                                                                                                                                                                   
0x9d394: bytes=a23908369f7bcc23  -> NOT a valid ARM64 instruction

The bytes appeared to be random data, not opcodes. The native code is encrypted on disk.

strings before:

before

2. Decompression using unicorn

IDA only recognized 8 functions in the entire binary (of ~1.5MB of .text!). Among them:

  • .init_proc @ 0x2cdc20 β€” function that runs automatically when the .so file is loaded by Android

  • sub_2CBF50 @ 0x2cbf50 β€” huge function (0x1CC8 bytes) called by .init_proc

Decompiling .init_proc revealed:

void init_proc() {                                        
      sub_2CBF50();  // <-- the unpacker                                                                                                                                                                                         
  }

2.1. Analyzing the unpacker (sub_2CBF50)

This is the centerpiece.

.spd (Section Protection Data) data structure

The table is located at address VA 0x2CDCC0 with this layout:

0x2CDCC0: checksum/magic (0x15043297f10b7863)
0x2CDCC8: outer_count = 2 (how many encrypted sections)
0x2CDCD0: start of entries

Each external entrance has:

qword[0]: section offset (VA) e.g.: 0x000000 (.text+.rodata)
qword[1]: section size e.g.: 0x2116E0
qword[2]: flags (for mprotect) e.g.: 5 (RX)
qword[3]: number of subsections e.g.: 3

Each subsection has:

14 qwords = 28 32-bit round keys (cipher keys)
1 checksum/sentinel qword
1 padding qword

Then: offset, size, and more keys for the next subsection

The encrypted sections found:

| Section                         | VA        | Size               | Subsections |
|---------------------------------|-----------|--------------------|-------------|
| Segment 1 (code + rodata)       | 0x000000  | 0x2116E0 (~2MB)    | 3           |
| Segment 2 (.pb + extras)        | 0x254000  | 0x079F68 (~490KB)  | 1           |

The cipher: Speck-like block cipher

The algorithm is a custom block cipher inspired by the NSA's Speck, operating on 64-bit blocks (2Γ—32 bits) with 28 rounds.

Each round does:

xor_val = hi XOR lo
rot3 = ROR32(xor_val, 3) // right rotation 3 bits

sub_val = (round_key XOR hi) - rot3 // subtraction mod 2^32
rot24 = ROR32(sub_val, 24) // = ROL8 (left rotation 8)
hi = rot24 XOR rot3
lo = rot24

It operates in CBC (Cipher Block Chaining) mode: each decrypted block is XORed with the previous ciphertext, creating a chain dependency.

The code has two paths:

  • NEON path: uses ARM64 SIMD instructions to process 2 blocks at a time (128 bits) β€” performance optimization

  • Scalar path: processes 1 block of 64 bits at a time β€” fallback

In addition, for some sections there is an XOR mode (when flags & 1 == 1) that uses the round keys as PRNG to generate a keystream, and applies byte-by-byte XOR (masking with & 0x7F).

Auxiliary Functions

sub_2540C0 β†’ trampoline to sub_254314 β†’ mprotect() wrapper to mark memory as RWX before writing  
sub_2540E0 β†’ trampoline to loc_25429C β†’ instruction cache flush (required on ARM after modifying code)  
sub_254148 β†’ returns the base address (0, since it is PIE)  
sub_2CBF38 β†’ returns a pointer to 0x2CDCC0 (.spd table)  
sub_2CBF44 β†’ returns a pointer to 0x2CDCC8 (entry count)

2.3. Script to get the decompressed libakamaibmp.so
I used Unicorn Engine

Unicorn script dec.py

root@xVE:$ sha256sum libakamaibmp.so
ed963b92b7cb4b7859305102f04edd627ab5c60dd7622dccc4d926d5cfba78fd
root@xVE:$ python3 dec.py
[*] Akamai BMP 4.1.3
[*] Input:  libakamaibmp.so
[*] Output: libakamaibmp_dec.so
  Mapped LOAD: file 0x0 -> VA 0x0 (0x2116e0 bytes)
  Mapped LOAD: file 0x2116e0 -> VA 0x2156e0 (0x2b928 bytes)
  Mapped LOAD: file 0x23d008 -> VA 0x245008 (0x1678 bytes)
  Mapped LOAD: file 0x248000 -> VA 0x250000 (0xa66 bytes)
  Mapped LOAD: file 0x24c000 -> VA 0x254000 (0x79f68 bytes)
[*] Emulating sub_2CBF50 @ 0x2CBF50...
[+] Emulation completed successfully!
  Patched segment VA 0x0 (0x2116e0 bytes)
  Patched segment VA 0x254000 (0x79f68 bytes)
[+] 2 segments patched
[+] Written: libakamaibmp_dec.so (2909936 bytes)
  initializeKeyN       @ VA 0x9d060 -> file 0x9d060: VALID ARM64
  encryptKeyN          @ VA 0x9d074 -> file 0x9d074: VALID ARM64
  decryptN             @ VA 0x9d18c -> file 0x9d18c: VALID ARM64
  buildN               @ VA 0x9d394 -> file 0x9d394: VALID ARM64
root@xVE:$ sha256sum libakamaibmp_dec.so
416b1e5364cb735532fd3ef476dfa394b31206dd901a58e86a1cfac3f1f35b31

3. After-Decompression

The changes are extreme; after decompression, all the strings in .rodata became visible, and hundreds of new functions became visible.

after
With the readable strings in the .rodata file, it's already possible to identify that akamaibmp doesn't depend on the APEXΒ΄ Android openssl, and has its own embedded openssl.

functions after

Well, great, it's improved significantly, but the .pb segment still has unreadable strings

.pb is much cooler than just strings, there's a gigantic polymorphic function sub_25E0AC used for everything (11719 xrefs)
The strings in the .pb are XORed using 0x55.

.pb strings decoded (only 13, have more)

  /data/local/bin/su
  /data/local/xbin/su
  /sbin/su
  /su/bin/su
  /system/bin/su
  /system/bin/.ext/su
  /system/bin/failsafe/su
  /system/sd/xbin/su
  /system/usr/we-need-root/su
  /system/xbin/su
  /cache/su
  /data/su
  /proc/self/mounts

The .pb participates in some kind of root detection, which isn't very clear yet, but sounds trivial.

.pb is certainly trivial, but during your search, I noticed a 680kb gap in .text, 680kb without functions, It seems that IDA is encountering some problems in this section; you will have to resolve the functions manually, as the auto-analysis didn't catch it.

+170 funcs solved manually, strangely this is a high-entropy block (probably nothing); we'll unravel this mystery in part 2.

This saga will have 3 parts. The first part is the simplest; we are just cleaning up the library and making it a readable environment. In part two, we will address cryptography, and in the last part, we will have a functional sensor generator. Stay tuned!

Analysis done in 37 minutes
Telegram: @vxigl
Discord: @vx7nyvek or @blits67459537

Comments (0)

Sign in to join the discussion

Be the first to comment!