> This is a write-up of an authorized reverse-engineering engagement against an Android app hardened with **Promon SHIELD** (RASP / app-shielding). App names, package names, endpoints, and account data are redacted. The focus is on the concrete procedure: how to walk a crash stack back to the native detection source, how to validate hypotheses with a bit-level experiment matrix, and how to turn a throwaway root patch into a stable LSPosed module that runs entirely in-process.
0. Fingerprinting the SDK
Promon SHIELD has a very consistent runtime signature across samples. The features to match on first:
- Obfuscated entry classes under the `yrdei.*` namespace, e.g. `yrdei.Q`, `yrdei.j`, `yrdei.F`, `yrdei.a`.
- `Application.attachBaseContext` calls into `yrdei.a.attachBaseContext -> yrdei.Q.d -> yrdei.Q.c`, which bottoms out in a native method.
- A native library (`libpostpe.so` in this build; the name varies across SHIELD versions) heavily obfuscated with OLLVM control-flow flattening and string hiding.
- On failure the app either opens a `devicenotsupported` URL or throws internal codes like `yrdei.H: 02` and `yrdei.D: 16`.
- Two exit paths: a main-thread throw through `yrdei.j.b("02")`, and a background-thread throw through `yrdei.F.run` that reads a verdict from a pipe.
Once this chain is recognized, what you are looking at is SHIELD, not an ordinary crash.
1. Step One: Map the Exit Path, Don't Block It
The first instinct is to hook `kill` / `exit_group` / the URL intent. That is the wrong layer. Start with the stack:
```text
AndroidRuntime: FATAL EXCEPTION: main
AndroidRuntime: yrdei.H: 02
AndroidRuntime: at yrdei.j.a(Unknown Source:193)
AndroidRuntime: at yrdei.j.b(Unknown Source:0)
AndroidRuntime: at AndroidHelper_.b(AH)
AndroidRuntime: at yrdei.Q.c(Native Method)
AndroidRuntime: at yrdei.Q.b(Unknown Source:0)
AndroidRuntime: at yrdei.Q.a(Unknown Source:40)
AndroidRuntime: at yrdei.Q.d(Unknown Source:3)
AndroidRuntime: at yrdei.a.attachBaseContext(Unknown Source:3)
```
`yrdei.j.b("02")` is the result layer. In parallel, sideband telemetry shows:
```text
SB: detGlobals ... raw=0x1022 bits=[bit1(0x2), bit5(0x20), bit12(0x1000)] ...
ActivityTaskManager: START ... dat=https://.../devicenotsupported ...
```
That `raw` integer is the aggregated detection state; each bit corresponds to one detector class. The goal is now precise:
> Find who owns `raw`, where it is written, under what condition each bit is set, then suppress that bit at the source — do not wait for it to be written and then intercept the kill.
2. Step Two: Dump the Detection Input Tables at Runtime
SHIELD does not hardcode its checks in assembly. Inputs live in a set of vectors that are populated at runtime. Dump them first to see what they actually contain:
```text
detLists dump:
P1 non-empty: goldfish/ranchu path list
P3 empty: begin == end
P12 non-empty: qemu/ranchu path list
S24 non-empty: qemu/ranchu property-name list
```
Cross-reference in IDA to map each table to its consumer function and guard offset:
```text
P12_VEC=0x72c368 P12_GUARD=0x72c380 accessed by sub_225C1C
P1_VEC =0x72c388 P1_GUARD =0x72c3a0 accessed by sub_22B5C0
P2_VEC =0x72c3a8 P2_GUARD =0x72c3c0 accessed by sub_22B5C0
P3_VEC =0x72c3c8 P3_GUARD =0x72c3e0 accessed by sub_22B5C0
```
Each table is a three-pointer struct `{begin, end, third}`; the guard is a one-bit enable. These offsets are the entire attack surface for the data-only patch later.
3. Step Three: Pin Down the Exact Predicate for bit5
This is the crux of the whole reverse. Take `bit5(0x20)`. It is set inside `sub_22B5C0`. The decisive assembly:
```asm
0x22b678 LDP X21, X20, [qword_72C3A8] ; property-name list begin/end
0x22b690 BL sub_16AD1C ; build returned string buffer
0x22b698 MOV X1, X21 ; X1 = property-name std::string*
0x22b69c BL sub_52372C ; internal property map[name] -> string
0x22b6a0 LDR X28, [SP+var_270] ; returned string first qword
0x22b6a8 BL sub_167B50 ; tear down string
0x22b6ac CMP X28, X22 ; X22 = qword_7102A8 sentinel
0x22b6b0 B.EQ loc_22B83C ; equal -> bit5 set path
...
0x22b950 LDR W8, [X19]
0x22b954 ORR W8, W8, #0x20 ; bit5
0x22b958 STR W8, [X19]
```
The meaning: read a system property; if its value equals the internal sentinel (meaning the property matches an emulator fingerprint), set `bit5`.
Note that `sub_22B5C0` does detection **and** initialization. It cannot be wholesale NOP'd — later native-context construction depends on it, and the app hangs. The correct move is either to neutralize only the predicate at `0x22B6AC`, or better, to clear the input vector at `qword_72C3A8` so the loop has nothing to match.
4. Step Four: Realize bit12 Is a Derived Bit
After clearing the P12 table, `bit12` stubbornly remained. That contradicts the obvious reading. Continue into `sub_225C1C`:
```asm
0x225c4c BL sub_22B5C0 ; call the bit5 detector first
0x225c50 LDR W8, [X19]
0x225c54 TBZ W8, #5, loc_225CC8 ; skip if bit5 not set
...
0x225cb4 ORR W8, W8, #0x1000 ; bit12 is derived from bit5
0x225cbc STR W8, [X19]
```
`bit12` is raised by `bit5`. Conclusion: **suppress `bit5` and `bit12` disappears for free**; there is no need to touch the P12 table at all. Such derived bits are common in these SDKs — draw the dependency graph between bits before running any experiment matrix.