DJI Pilot 2.5.1.17 combines two distinct protections: it hides DEX containers and moves method instructions into an external code pool. A file that opens in a decompiler has only passed the first hurdle. Intact method declarations followed by long runs of nop are a reason to follow the runtime repair path, not to declare unpacking complete.
The sample record dates to February 2024 and identifies the APK by SHA-256 642aa123437c259eea5895fe01dc4210c4a3a430842b79612074d88745f54714, with package name com.dji.industry.pilot. The complete APK, device build, and runtime dumps are not available with the record. The analysis therefore separates visible bytes, transformation relationships, and local offline checks; it does not present them as a new device experiment.

The layered body is a conceptual illustration, not a representation of the device's actual internal design.
Bootstrap code does not explain the container size
The initial class tree exposes little beyond bootstrap content:
- com
- amap.api
- autonavi
- dji.industry.pilot
- R
- secneo.apkwrapper
The bootstrap selects DexHelper-x86 or DexHelper through H.isNeedLoadX86(), then calls System.loadLibrary. The analyzed path enters libDexHelper.so. Its on-disk ELF is protected too, so the native analysis uses an image obtained at runtime. The record does not fully describe that unpacking stage and does not establish a general solution for the native protector.
decrypt_jar_128K is a functional name assigned during analysis. Its output buffers begin with the following DEX 035 marker, with eight outputs recorded along the startup path:
magic0x00–0x03version0x04–0x06035terminator0x07
Despite the sparse class tree, classes.dex occupies roughly 63 MB. Its entropy profile shows eight regions approaching 8 bits per byte, associated in the record with the first 128 KiB of eight business-code DEX files. Neither the sampling window nor the underlying series is available, so the observation remains qualitative rather than becoming a newly plotted measurement.
High entropy alone does not establish encryption; compressed data can look similar. The useful proof connects file ranges to function inputs, output lengths, and DEX headers. Exporting also requires checks against file_size, readable mappings, and the actual buffer length. Magic bytes alone do not define a valid read boundary.
Recognize RC4 and separate the first key derivation
Flattened native control flow still reveals state initialization, key scheduling (KSA), and pseudorandom generation (PRGA). The PRGA relationship is:
i = (i + 1) & 0xff
j = (j + S[i]) & 0xff
swap(S[i], S[j])
output = input ^ S[(S[i] + S[j]) & 0xff]An XOR loop is not enough. The 256-byte state, index updates, swaps, and output dependency together identify RC4. Following the output forward and the KSA input backward connects that algorithm to a particular container.
The DEX key combines a 16-byte constant with the first 16 bytes of the package name. That prefix is exactly com.dji.industry, without a trailing dot:
package = b"com.dji.industry.pilot"
assert package[:16] == b"com.dji.industry"
def derive_dex_key(constant):
if len(constant) != 16:
raise ValueError("expected 16 bytes")
return bytes(a ^ b for a, b in zip(constant, package[:16]))This describes the derivation without supplying the sample constant. Processing the first 128 KiB of each DEX exposes more classes, but some methods still lack meaningful bodies. Container decryption and method restoration remain separate checkpoints.
debug_info_off points toward an external code pool
The recorded getRequiredPermissions method retains its declaration and exception structure but contains mostly nop instructions, together with const v0, 0x8854372. The same value appears in the method's debug_info_off field.
| Field | Value |
|---|---|
| registers_size | 3 |
| ins_size | 1 |
| outs_size | 2 |
| tries_size | 1 |
| debug_info_off | 0x08854372 |
| insns_size | 0x23 16-bit code units |
In ordinary DEX semantics, debug_info_off locates debug information relative to the start of the file; it is not a method identifier. The recorded parser attempts position 142951282, beyond its 9930016 buffer limit. That position equals 0x08854372. The failure suggests a repurposed field but does not, by itself, establish the lookup algorithm. AOSP DEX format
Correlating assets/classes.dgc with the native repair function closes the relationship: a placeholder method carries an identifier, the DGC index locates an external code_item, and runtime code restores its instructions. DGC also has a high-entropy prefix of approximately 128 KiB, but derives its key differently.
The DGC key combines a data block and a deterministic sequence
The first 16 bytes observed at the DGC handler's input match the file prefix:
A second native function exhibits the same RC4 state operations. Its key comes from a 4096-byte block near the mthfilekey marker in the native library, not from the package prefix. The block is visible in the recorded on-disk library.
flowchart TD A["4096-byte data block"] --> B["MD5: 16-byte digest"] A --> C["Access block through deterministic sequence"] D["F(0)=0, F(1)=1"] --> C C --> E["Select 16 bytes"] B --> X["Bytewise XOR"] E --> X X --> K["DGC RC4 key"]
The sequence function shows F(0)=0, F(1)=1, and F(i)=F(i-2)+F(i-1). The initial values are supported by the evidence, rather than being unknown. Integer width, overflow behavior, the mapping from sequence values to block positions, and any modulus still require confirmation in the target function. "Fibonacci plus MD5" is not a complete decryption specification.
Round logic and constants support the MD5 identification. A compression function that consumes 512-bit message blocks should not be described as computing the entire digest over only 64 bytes. Block processing, padding, and digest output form a separate chain to verify.
Check index byte order separately from code_item boundaries
The DGC index view labels a code-section base of 0x00167e38 and several relative offsets. Adding 0x38 produces 0x00167e70, matching the location of a separate code-item view. The index contains byte sequences such as 00 16 7E 38; DEX's usual little-endian representation is not a reason to parse every custom DGC field as little-endian.
This checks an address relationship, not the complete index schema or every identifier pairing. The following code_item belongs to a different method: its debug_info_off = 0x046d63dd is distinct from the earlier 0x08854372.
registers_size0x167E70–0x167E713ins_size0x167E72–0x167E732outs_size0x167E74–0x167E752tries_size0x167E76–0x167E771debug_info_off0x167E78–0x167E7B0x046d63ddinsns_size0x167E7C–0x167E7F0x12
insns_size = 0x12 means 18 code units, or 36 bytes, not 18 bytes. After the 16-byte header, the instruction array ends at relative offset 0x34. Its even code-unit count requires no two-byte alignment pad before tries.
With tries_size = 1, an exception table and handler data remain to be checked. A valid header-plus-instruction range is not proof that the whole code item is valid. Building a replacement DEX also requires consistent relocated offsets, indexes, debug information, and file integrity fields, rather than a blind block overwrite.
ART supplies a conditional repair point
The sample redirects the entry of Instrumentation::InitializeMethodsCode into custom code. The functional name PatchMethodCode describes a sequence that obtains the identifier, looks up DGC data, allocates a restoration buffer, calls DecryptMethodCode, updates the method's code association, and reaches the original function through a trampoline.
flowchart TD
A["Post-verification class update"] --> B{"Runtime and current entry qualify?"}
B -->|Yes| C["InitializeMethodsCode"]
B -->|No| N["Skip this initialization branch"]
C --> P["PatchMethodCode"]
P --> D["DGC lookup and opcode restoration"]
D --> U["Update method code association"]
U --> T["Trampoline / original initialization"]
T --> R["Return to caller"]
In public ART commit 82e525a4f5f08a72ea1b6907c0a10dacb77a8a87, the loop in UpdateClassAfterVerification calls InitializeMethodsCode only when CanRuntimeUseNterp() holds and the method currently uses the quick-to-interpreter bridge. This pinned source explains the condition; it has not been matched to the sample device build.
The diagram therefore does not imply that every method on every ART version unconditionally traverses this entry. Entry initialization, class verification, and actual method execution are separate events. Restored bytes should be compared with the relevant method's code association at that point, not judged solely by whether an exported DEX opens.
Restore opcodes at instruction boundaries
The core operation in DecryptMethodCode replaces an opcode byte:
def decode_opcode(encoded_opcode, debug_info_off, substitution):
if not 0 <= encoded_opcode <= 255:
raise ValueError("opcode must fit in one byte")
if len(substitution) != 256:
raise ValueError("expected a 256-entry table")
return substitution[encoded_opcode ^ (debug_info_off & 0xff)]The substitution table resides in the native library. It is not RC4's continually permuted state array. An analyst might call both objects S, but their lifetimes and purposes are different.
Dalvik instructions use 16-bit code units, with the opcode normally in the low byte of the first unit. A walker needs the restored instruction format to advance while preserving registers, immediates, and branch offsets. Switch and array-data payloads require separate handling of identifiers, sizes, and alignment. Applying the function to every byte would corrupt operands too. AOSP Dalvik bytecode
A restored example shows coherent checkAccountManager calls and preference-writing logic. That establishes improved readability, not equivalence between every restored method and runtime execution. Check identifier uniqueness, complete instruction traversal, valid referenced indexes, and consistent control-flow and exception ranges.
Local offline checks use public RFC 6229 RC4 test material rather than a sample key. A deliberately synthetic substitution table checks the single-byte mapping, alongside parsing the header above:
PASS: 2 RC4 vectors; 65536 synthetic opcode round trips; 4 boundary rejections
DEX key prefix: com.dji.industry
DGC code_item: 0x167e70 insns bytes: 36 tries relative offset: 52These results validate the RC4 implementation, length arithmetic, and mapping shape. They do not validate missing DGC derivation parameters, a complete unpacked application, or device execution. A trustworthy restoration ultimately connects three things: where container bytes originate, where method identifiers lead, and why restored instructions agree with the runtime.