R2Pay Under the Microscope: Runtime Protections

Analyzing and Bypassing Android Runtime Protections Using Python and GDB

Safety & Legal Notice : This content is provided for educational, research, and authorized security testing purposes only. Apply these techniques only to systems or applications you own or have explicit permission to assess, and only within isolated lab environments. The author disclaims any responsibility for misuse or illegal use of this material.

I. Introduction

The OWASP MASTG Android UnCrackable L4 (r2Pay) challenge implements multiple layers of runtime protections, including obfuscation, anti‑debugging checks, integrity verification, and defensive native code logic intended to resist analysis. These mechanisms are designed to slow down reverse engineering and block dynamic inspection of sensitive routines.

In this article, we focus exclusively on bypassing these protections at the native x86_64 layer. Using GDB and Python‑assisted debugging, we identify where each defensive control is enforced and show how to neutralize or patch it to restore full observability of the program’s execution. The objective is to present a clear, practical workflow for disabling protection mechanisms in hardened Android native libraries.

II. Adversary Model

The protection mechanisms analyzed in this work are designed to defend against a skilled software adversary with near full control over the execution environment. Specifically, the assumed adversary is capable of:

  • Debugging and runtime tracing, including the use of native debuggers (e.g., GDB or LLDB) to inspect memory, registers, and control flow at both the Java and native layers. 
  • Dynamic instrumentation and function hooking, using frameworks such as Frida. 
  • Executing the application in modified environments, including rooted devices or systems with altered runtime components. 
  • Static and dynamic reverse engineering, including disassembly and analysis of native libraries and binary patching. 
  • Analyzing obfuscated native code, where the native library exposes JNI_OnLoad but avoids exporting Java_* JNI symbols, instead registering methods at runtime via JNINativeMethod and using indirect dispatch.

The adversary is assumed to have no access to source code or private signing keys, but may freely observe, modify, and interfere with the application at runtime. Network‑level attacks and backend compromise are considered out of scope; the focus of this work is strictly on local, runtime‑based attacks against the client application.

In the context of this analysis, the adversary is realized by attaching GDB to the Android Zygote process, enabling observation and control of the application from process creation through native library initialization.

III. Protection Architecture

R2Pay application applies a multi-layered runtime protection strategy designed to defend the application throughout its entire lifecycle from native library loading to critical user interactions.

1
Native-Level Protections

Executed in init_array Segment.

2
Java-Level Protections

Root detection using libtool-checker.so.

3
Runtime and UI-Level

UI display and libnative-lib.so integrity check.

4
User Interaction Protections

Button click triggers protections.

crash
INTEGRITY VIOLATION

1. Native-Level Protections (libnative-lib.so)

The core security mechanisms are implemented in libnative-lib.so and are triggered at the init_array execution stage, ensuring protections are enforced as early as possible in the app lifecycle.
These early checks are specifically designed to disrupt instrumentation and debugging tools, causing tools such as Frida and GDB to crash or fail during attachment.

At this stage, the following protections are executed:

  • Anti-Debug detection
  • Anti-Frida detection
  • Anti-Root detection
  • Self-integrity check of libnative-lib.so
  • Execution Environment Validation.

This guarantees that the application starts only in a trusted runtime environment.

2. Java-Level Protections (libtool-checker.so)

In parallel, Java-level protections are applied using libtool-checker.so, which is dedicated exclusively to root detection. This layer provides an additional safeguard at the Java runtime level, complementing the native checks and increasing resistance against bypass attempts.

3. Runtime and UI-Level Enforcement

During application initialization, root detection is first performed at the native level through libnative-lib.so. When the GUI launch sequence starts, the root detection checks are executed again to ensure that no changes have occurred since startup. After the full user interface is displayed, libnative-lib.so conducts a self-integrity verification to confirm that the native library has not been patched or modified in memory.

4. User Interaction Protections

At the final and most critical stage—when the user clicks a sensitive action button (e.g., GENERATE R2COIN)—the application invokes native protections again via libnative-lib.so, including:

  • Anti-Debug checks
  • Anti-Frida checks
  • Self-integrity verification

This ensures that even if an attacker manages to bypass earlier layers, runtime tampering or instrumentation is detected at the moment of sensitive operations.

IV. Analyzing and Bypassing Protections

In this article, we focus on the x86_64 version of libnative-lib.so, since our analysis is performed on an Android emulator, which runs x86_64 binaries. The detailed steps, along with a Python script to detect and capture execution traces using GDB, are available on our GitHub repository.

1. Native-Level Protections

As discussed in the previous section, libnative-lib.so initializes its protection mechanisms from the .init_array segment. Analysis of this segment revealed three functions of interest.

The first function,_datadiv_decode16813968421045149467, is responsible for decoding a list of keywords that are later reused by the protection logic. The two remaining functions, sub_AE00 and sub_12CAD0, implement core parts of the protection mechanisms.

A closer inspection of these two functions shows that they invoke pthread_create to spawn multiple threads. Each thread executes the function sub_2EB00 or sub_174510, which acts as a monitoring routine responsible for running the various protection checks during the initialization phase.

As a result, a straightforward way to neutralize these protections is to prevent the creation of these threads for example, by patching or intercepting calls to pthread_create, effectively disabling the monitoring routines at initialization time. In this article, we will examine in detail where each protection is triggered and discuss the techniques used to disable it.

1.1. Debugger Detection

The debugger detection mechanism relies on the TracerPid field read from /proc/self/status. This protection is enforced at two stages: during application initialization and upon user interaction via a button click.

A. Debugger Detection during Initialization

The detection is performed in sub_2EB00 at loc_4B80B, where the TracerPid value is compared against 0. A non‑zero value indicates the presence of a debugger and triggers the protection. The relevant assembly code and the corresponding bypass are presented below.

04B80B loc_4B80B:
04B80B mov      eax, 0A9B79A4Dh
04B810 mov      ecx, 0C7079727h
04B815 mov      dl, 1
04B817 xor      esi, esi
04B819 mov      rdi, cs:y_102_ptr
04B820 mov      r8, cs:x_101_ptr
04B827 mov      r9d, [rbp+var_1C6C]  ; Get TracerPID
04B82E mov      r10, [rbp+var_2A08]
04B835 mov      rsp, r10
04B838 mov      [rbp+var_1EA4], r9d  ; Save TracerPID
04B83F cmp      [rbp+var_1EA4], 0    ; Compare TracerPID with 0
04B846 setnz    r11b                 
04B84A and      r11b, 1
04B84E mov      [rbp+var_1E89], r11b

To bypass this protection, it is sufficient to patch the instruction setnz r11b by replacing it with xor r11b, r11b followed by a nop, thereby forcing the condition to evaluate as false.

B. Debugger Detection on Button Interaction

Within the APK, we observed that the button triggers a native function named gXftm3iswpkVgBNDUp(). This function does not appear directly in IDA Pro because it is registered dynamically through the JNINativeMethod structure. To locate its actual implementation, we need to analyze the JNI registration mapping to resolve the corresponding code address.

The JNINativeMethod structure maps a Java method to its native implementation. It contains the method name (name), its signature (signature), and a function pointer (fnPtr) that references the actual C/C++ code. As shown in the figure below, a breakpoint at the call rax instruction in loc_177E1A reveals that the $rdx register holds a pointer to a JNINativeMethod entry.

r2pay APK button click in GDB showing $rdx pointing to a JNINativeMethod entry, which maps the Java method to its native function.
R2PAY Click Function Analysis in GDB

To locate the actual implementation of gXftm3iswpkVgBNDUp, the memory pointed to by $rdx must be examined:

  • Method name pointer: $rdx + 0x0

  • Method signature pointer: $rdx + 0x8

  • Native function pointer (target): $rdx + 0x10

The following GDB command can be used to dump these fields and recover the real function address: x/3gx $rdx

After executing this command, we located the function at sub_1780F0 and the debugger detection routine at loc_1A418C.

1A418C loc_1A418C:
1A418C mov      eax, 0A9B79A4Dh
1A4191 mov      ecx, 0C7079727h
1A4196 mov      dl, 1
1A4198 mov      rsi, cs:y_102_ptr
1A419F mov      rdi, cs:x_101_ptr
1A41A6 mov      r8d, [rbp+var_102C]  ; Get TracerPID
1A41AD mov      r9, [rbp+var_1CA8]
1A41B4 mov      rsp, r9
1A41B7 mov      [rbp+var_1264], r8d  ; Save TracerPID
1A41BE cmp      [rbp+var_1264], 0    ; Compare TracerPID with 0
1A41C5 setnz    r10b                 
1A41C9 and      r10b, 1
1A41CD mov      [rbp+var_1249], r10b

To bypass this protection, it is sufficient to patch the instruction setnz r10b by replacing it with xor r10b, r10b followed by a nop, thereby forcing the condition to evaluate as false.

1.2. Frida Instrumentation Detection

The anti‑Frida detection relies on two complementary techniques. The first method inspects thread names for known Frida‑related keywords such as gmain and gum-js-loop. These names are obtained by parsing the thread status files located at /proc/self/task/[tid]/status. If any thread name matches one of these identifiers, the application assumes the presence of Frida.

The second method scans process‑related information retrieved from /proc/self/pd/%s for additional Frida indicators, specifically the keywords frida_agent_main and linjector. The detection is triggered when any of these strings are found, indicating that a Frida agent has likely been injected into the process. Below is the pseudocode illustrating the Frida verification and detection logic.

// Synchronized Search: Increments both on match, resets on failure
while (scanning_buffer) {

    // STAGE 1: Check the first character
    if (compare(buffer_char, target_keyword_char)) { 
        /* PATCH POINT: Force failure here to prevent reaching Stage 2 */

        // STAGE 2: If equal, increment both and check the next characters
        if (compare_next_sequence(buffer_inc, target_inc)) {
            return KEYWORD_FOUND;
        }

        // If sequence check fails, keyword resets to index 0
    }
    
    move_to_next_buffer_char();
}
A. Anti-Frida detection during Initialization

All anti-Frida detection routines are implemented within the sub_2EB00 function.

Anti-Frida #1

The two blocks loc_6C21E and loc_6D9E1 implement the verification of thread names against the gum-js-loop identifier. To bypass this protection, it is sufficient to patch the first-stage check so that it always follows the path where the comparison fails.

This can be achieved by replacing the conditional instruction: cmovnz eax, ecx with an unconditional move: mov eax, ecx; nop

This modification forces the logic to always behave as if the keyword comparison failed, effectively disabling the detection.

.06C21E loc_6C21E:      ; stage 1
.06C21E mov    eax, 0D79E378Bh
.06C223 mov    ecx, 0B7D8CAA9h
.06C228 movsx  edx, [rbp+var_16DA]
.06C22F movsx  esi, [rbp+var_16D9]
.06C236 cmp    edx, esi
.06C238 cmovnz eax, ecx
.06C23B mov    [rbp+var_16EC], eax
.06C241 jmp    loc_70227
.06D9E1 loc_6D9E1:      ; stage 2
.06D9E1 mov    eax, 70571F28h
.06D9E6 mov    ecx, 0C8E8CD64h
.06D9EB mov    rdx, [rbp+var_1660]
.06D9F2 movsx  esi, byte ptr [rdx]
.06D9F5 mov    rdx, [rbp+var_1668]
.06D9FC mov    rdi, rdx
.06D9FF add    rdi, 1
.06DA03 mov    [rbp+var_1668], rdi
.06DA0A mov    r8d, byte ptr [rdx]
.06DA0E cmp    esi, r8d
.06DA11 cmovnz eax, ecx
.06DA14 mov    [rbp+var_1674], eax
.06DA1A jmp    loc_6F419
Anti-Frida #2

In the second anti-Frida mechanism, the blocks loc_71A39 and loc_72FC0 verify the presence of the gmain identifier in thread names. As with the previous protection, bypassing this check only requires patching the first stage of the verification logic. This can be achieved by replacing the conditional instruction: cmovnz eax, ecx with an unconditional move: mov eax, ecx; nop

071A39 loc_71A39:               ; stage 1
071A39 mov      eax, 0D79E378Bh
071A3E mov      ecx, 0B7D8CAA9h
071A43 movsx    edx, [rbp+var_1632]
071A4A movsx    esi, [rbp+var_1631]
071A51 cmp      edx, esi
071A53 cmovnz  eax, ecx
071A56 mov      [rbp+var_1644], eax
071A5C jmp      loc_75935
072FC0 loc_72FC0:               ; stage 2
072FC0 mov      eax, 70571F28h
072FC5 mov      ecx, 0C8E8CD64h
072FCA mov      rdx, [rbp+var_15B8]
072FD1 movsx    esi, byte ptr [rdx]
072FD4 mov      rdx, [rbp+var_15C0]
072FDB mov      rdi, rdx
072FDE add      rdi, 1
072FE2 mov      [rbp+var_15C0], rdi
072FE9 movsx    r8d, byte ptr [rdx]
072FED cmp      esi, r8d
072FF0 cmovnz  eax, ecx
072FF3 mov      [rbp+var_15CC], eax
072FF9 jmp      loc_74A63
Anti-Frida #3

The third anti-Frida check targets the linjector keyword using a two-stage verification, with the first stage at loc_92C42 and the second at loc_9433F. As in previous cases, patching the first stage by replacing cmovnz eax, ecx with mov eax, ecx; nop forces the check to always fail, effectively disabling the detection.

092C42 loc_92C42:               ; stage 1
092C42 mov      eax, 0D79E378Bh
092C47 mov      ecx, 0B7D8CAA9h
092C4C movsx    edx, [rbp+var_134A]
092C53 movsx    esi, [rbp+var_1349]
092C5A cmp      edx, esi
092C5C cmovnz  eax, ecx
092C5F mov      [rbp+var_135C], eax
092C65 jmp      loc_968C8
09433F loc_9433F:               ; stage 2
09433F mov      eax, 70571F28h
094344 mov      ecx, 0C8E8CD64h
094349 mov      rdx, [rbp+var_12D0]
094350 movsx    esi, byte ptr [rdx]
094353 mov      rdx, [rbp+var_12D8]
09435A mov      rdi, rdx
09435D add      rdi, 1
094361 mov      [rbp+var_12D8], rdi
094368 movsx    r8d, byte ptr [rdx]
09436C cmp      esi, r8d
09436F cmovnz  eax, ecx
094372 mov      [rbp+var_12E4], eax
094378 jmp      loc_95A84
Anti-Frida #4

The final anti‑Frida check targets the frida_agent_main keyword and follows the same two‑stage verification logic. The first stage is implemented at loc_1028DE, while the second stage resides at loc_102937. As with the previous protections, bypassing this check only requires patching the first stage by replacing cmovnz eax, ecx with mov eax, ecx; nop, forcing the verification to always fail.

1028DE loc_1028DE:               ; stage 1
1028DE mov      eax, 0D980C88Bh
1028E3 mov      ecx, 79D2DAC2h
1028E8 mov      rdx, [rbp+var_A38]
1028EF movsx    esi, byte ptr [rdx]
1028F2 mov      rdx, [rbp+var_A28]
1028F9 movsx    edi, byte ptr [rdx]
1028FC cmp      esi, edi
1028FE cmovz    eax, ecx
102901 mov      [rbp+var_A4C], eax
102907 jmp      loc_103B34
102937 loc_102937:               ; stage 2
102937 mov      eax, 0F1C10BAEh
10293C mov      ecx, 0FCB9F06h
102941 mov      rdx, [rbp+var_A48]
102948 movsx    esi, byte ptr [rdx]
10294B mov      rdx, [rbp+var_A28]
102952 movsxd   rdi, [rbp+var_A3C]
102959 movsx    r8d, byte ptr [rdx+rdi]
10295E cmp      esi, r8d
102961 cmovz    eax, ecx
102964 mov      [rbp+var_A4C], eax
10296A mov      [rbp+var_A4D], 0
102971 jmp      loc_103B34
B. Anti-Frida detection on Button Interaction

At this stage, the application applies the first three anti‑Frida protections using the same checking logic. To bypass them, it is sufficient to patch the same instruction in each corresponding block, specifically at loc_21B494, loc_22646F, and loc_1DEC18 at function sub_1780F0.

1.3. Root Detection Mechanisms

As explained earlier, the init_array segment contains three functions. The last one, sub_12CAD0, spawns a new thread and uses sub_174510 as the thread entry point. Analyzing this function reveals three distinct blocks dedicated to root detection.

These blocks rely on the open, faccessat, and openat system calls to check for the presence of common su binaries at the following locations:

  • /data/local/su

  • /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

  • /dev/su

The presence of any of these binaries is treated as an indicator of a rooted device.

174F83 loc_174F83:
174F83 xor      esi, esi
174F85 lea      rax, off_319090
174F8C mov      rcx, [rbp+var_150]
174F93 movsxd   rcx, dword ptr [rcx]
174F96 mov      rdi, [rax+rcx*8] ; file
174F9A mov      al, 0
174F9C call     _open
174FA1 mov      esi, 7E4B3BE5h
174FA6 mov      edx, 0B86B8D84h
174FAB mov      r8b, 1
174FAE xor      r9d, r9d
174FB1 mov      rcx, cs:y_154_ptr
174FB8 mov      rdi, cs:x_153_ptr
174FBF cmp      eax, 0
174FC2 setnl    r10b
174FC6 and      r10b, 1
174FCA mov      [rbp+var_141], r10b
174FD1 mov      eax, [rdi]
174FD3 mov      r11d, [rcx]
174FD6 mov      ebx, r9d
174FD9 sub      ebx, eax
174FDB mov      r14d, r9d
174FDE sub      r14d, 0FFFFFFFFh
174FE2 add      ebx, r14d
174FE5 sub      r9d, ebx
174FE8 imul     eax, r9d
174FEC and      eax, 1
174FEF cmp      eax, 0
174FF2 setz     r10b
174FF6 cmp      r11d, 0Ah
174FFA setl     r15b
174FFE mov      r12b, r10b
175001 xor      r12b, 0FFh
175005 mov      r13b, r15b
175008 xor      r13b, 0FFh
17500C xor      r8b, 0
175010 or       r12b, r13b
175013 or       r8b, 0
175017 xor      r12b, 0FFh
17501B and      r12b, r8b
17501E mov      r8b, r10b
175021 xor      r8b, 0FFh
175025 mov      r13b, r15b
175028 and      r13b, r8b
17502B xor      r15b, 0FFh
17502F and      r10b, r15b
175032 or       r13b, r10b
175035 mov      r8b, r12b
175038 and      r8b, r13b
17503B xor      r12b, r13b
17503E or       r8b, r12b
175041 test     r8b, 1
175045 cmovnz   esi, edx
175048 mov      [rbp+var_168], esi
17504E jmp      loc_175960
17557E loc_17557E:
17557E mov      eax, 10Dh
175583 mov      edi, eax
175585 lea      rcx, off_319090
17558C mov      rdx, [rbp+var_150]
175593 movsxd   rdx, dword ptr [rdx]
175596 mov      rcx, [rcx+rdx*8]
17559A mov      [rbp+var_114], 0
1755A4 mov      [rbp+var_120], rcx
1755AB mov      [rbp+var_124], 4
1755B5 mov      [rbp+var_128], 0
1755BF movsxd   rsi, [rbp+var_114]
1755C6 mov      rdx, [rbp+var_120]
1755CD movsxd   rcx, [rbp+var_124]
1755D4 movsxd   r8, [rbp+var_128]
1755DB call     sub_263640 ; faccessat
1755E0 xor      r9d, r9d
1755E3 mov      r10d, 0C9275064h
1755E9 mov      r11d, 2916AB64h
1755EF mov      ebx, eax
1755F1 cmp      r9d, ebx
1755F4 cmovz    r10d, r11d
1755F8 mov      [rbp+var_168], r10d
1755FF jmp      loc_175960
175260 loc_175260:
175260 mov      eax, 101h
175265 mov      edi, eax
175267 lea      rcx, off_319090
17526E mov      rdx, [rbp+var_150]
175275 movsxd   rdx, dword ptr [rdx]
175278 mov      rcx, [rcx+rdx*8]
17527C mov      [rbp+var_12C], 0FFFFFF9Ch
175286 mov      [rbp+var_138], rcx
17528D mov      [rbp+var_13C], 0
175297 mov      [rbp+var_140], 0
1752A1 movsxd   rsi, [rbp+var_12C]
1752A8 mov      rdx, [rbp+var_138]
1752AF movsxd   rcx, [rbp+var_13C]
1752B6 movsxd   r8, [rbp+var_140]
1752BD call     sub_263640 ; openat
1752C2 mov      r9d, 313B2917h
1752C8 mov      r10d, 0F463275Eh
1752CE mov      r11d, eax
1752D1 cmp      r11d, 0
1752D5 cmovge   r9d, r10d
1752D9 mov      [rbp+var_168], r9d
1752E0 jmp      loc_175960

To bypass the anti-root detection, several patches were applied across the relevant blocks.
In block loc_174F83, the instruction setnl r10b at address 0x174FC2 is replaced with: mov r10b, 0 ; nop In loc_175260, the conditional move instruction cmovge r9d, r10d at address 0x1752D5 is neutralized by replacing it with four nop instructions.

Finally, in the last anti-root block at loc_17557E, the instruction cmovz r10d, r11d at address 0x1755F4 is similarly disabled by replacing it with four nop instructions.

These modifications force the root-detection logic to always follow the non-rooted execution path.

 

1.4. Application Self-Integrity Verification

After patching the previous protections and launching the application, we observed that it still terminated automatically after a short time, even when running on a non-rooted device. This behavior suggested the presence of a self-integrity check within the native library.

A. Self-Integrity Verification during Initialization

The analyzing the generated error led us to a code block located at loc_11F0F1 inside the function sub_2EB00. This block compares a dynamically generated key against a hardcoded reference value, as illustrated below.

11F0F1 loc_11F0F1:      
11F0F1 mov      edi, 6
11F0F6 mov      rax, [rbp+var_848]
11F0FD mov      rcx, [rbp+var_6048]
11F104 mov      rsp, rcx
11F107 mov      [rbp+var_60F0], rax
11F10E call     sub_2655A0
11F113 lea      rsi, unk_2881D0
11F11A mov      edi, 20h ; ' '
11F11F mov      edx, edi
11F121 lea      r9, [rbp+var_6C0]
11F128 xor      edi, edi
11F12A mov      ecx, edi
11F12C mov      r8, 0E470D9B80E2FDC38h
11F136 mov      r10, 0E8919E556BBE84Ah
11F140 mov      r1, 0CF1B529AE06B923Eh
11F14A mov      [rbp+var_8A8], rax
11F151 mov      rdi, [rbp+var_8A8]
11F158 mov      rax, [rbp+var_888]
11F15F mov      rbx, [rbp+var_890]
11F166 mov      r14, [rbp+var_888]
11F16D sub      rbx, r11
11F170 add      rbx, r8
11F173 add      rbx, r11
11F176 sub      rbx, r10
11F179 sub      rbx, r14
11F17C add      rbx, r10
11F17F sub      rcx, r8
11F182 add      rbx, rcx
11F185 mov      rcx, rax
11F188 mov      r8, rbx
11F18B call     sub_26C080
11F190 lea      rcx, unk_2881F0 ; 0x9fb14c242553cb4a
11F197 lea      rdx, [rbp+var_6C0] 
11F19E mov      [rbp+var_7A0], rdx
11F1A5 mov      [rbp+var_7A8], rcx
11F1AC mov      [rbp+var_7B0], 20h ; ' '
11F1B7 mov      rcx, [rbp+var_7B0]
11F1BE mov      [rbp+var_790], rcx
11F1C5 mov      [rbp+var_7C4], 392704ABh
11F1CF mov      [rbp+var_60F4], eax

The hardcoded reference value is loaded into the rcx register at address 0x11F190, while the generated value is loaded at 0x11F197. To bypass this check, we patch the instruction that loads the generated value: lea rdx, [rbp+var_6C0] and replace it with: mov rdx, rcx ; nop . With this modification, the rdx register is forced to contain the same value as rcx, causing the comparison to always succeed and effectively disabling the self-integrity check.

B. Self-Integrity Verification on Button Interaction

As with the previous integrity-check bypass, the protection triggered upon user interaction with the button can be disabled by applying the same patch to the same instruction. Specifically, at address 0x1C766E in block loc_1C7574, the instruction should be replaced using the same modification described earlier.

1.5. Execution Environment Validation

The function sub_174510 also verifies whether certain system partitions are mounted with read-only or read-write permissions. This check is implemented in block loc_175FCD, where the function evaluates the mount status of the following partitions:

  • /system

  • /system/bin

  • /system/sbin

  • /system/xbin

  • /vendor/bin

  • /sbin

  • /etc

00175FCD loc_175FCD:
00175FCD mov     eax, 10Dh
00175FD2 mov     edi, eax
00175FD4 lea     rcx, off_319100
00175FDB mov     rdx, [rbp+var_E8]
00175FE2 movsxd  rdx, dword ptr [rdx]
00175FE5 mov     rcx, [rcx+rdx*8]
00175FE9 mov     [rbp+var_CC], 0FFFFFF9Ch
00175FF3 mov     [rbp+var_D8], rcx
00175FFA mov     [rbp+var_DC], 2
00176004 mov     [rbp+var_E0], 0
0017600E movsxd  rsi, [rbp+var_CC]
00176015 mov     rdx, [rbp+var_D8]
0017601C movsxd  rcx, [rbp+var_DC]
00176023 movsxd  r8, [rbp+var_E0]
0017602A call    sub_263640      ; faccessat(file_name)
0017602F xor     r9d, r9d
00176032 mov     r10d, 12C474F3h
00176038 mov     r11d, 0D7431983h
0017603E mov     ebx, eax
00176040 cmp     r9d, ebx
00176043 cmovz   r10d, r11d
00176047 mov     [rbp+var_100], r10d
0017604E jmp     loc_176555

If any of these partitions are detected as writable, the device is considered compromised. Most emulators mount these partitions as read-only, so this check typically does not require patching. However, if this protection is triggered, it can be bypassed by replacing the instruction: mov ebx, eax with: mov ebx, 0xFFFFFFE2 . This works because 0xFFFFFFE2 is the hexadecimal representation of -30, the Linux error code for EROFS (Read-Only File System).

2. Java-Level Protections

At this stage of the reverse engineering process, we focus on the Java and Smali code to patch the application and bypass the anti-root protections. By analyzing MainActivity, we identified two anti-root checks: one in the onCreate method during the initialization phase, and another in the button click handler method.

[0x01] Entry Protection: onCreate()
if (rb.m1162() || (rb.m1155() && rb.m1150())) {
    // TRAP: ArithmeticException (Div by 0)
    int i = 1337 / 0; 
    this.f508 = (byte)(this.f508 | 15);
}
[0x02] Interaction Protection: onClick()
if (rb.m1162() || (rb.m1155() && rb.m1150())) {
    MainActivity mainActivity = MainActivity.this;
    mainActivity.f508 = (byte)(mainActivity.f508 | 15);
    
    // TRAP: IllegalMonitorStateException
    NullPointerException np = new NullPointerException();
    np.notify(); 
}

First, we patch the onCreate method in MainActivity.smali. As shown below, we commented out the section that invokes the anti-root detection methods in order to disable the check during application initialization.

.method public onCreate(Landroid/os/Bundle;)V
    .locals 3
    .param p1, "savedInstanceState"    # Landroid/os/Bundle;

    .line 33
    invoke-super {p0, p1}, L♫/上;->onCreate(Landroid/os/Bundle;)V

    .line 34
    const/4 v0, 0x1
    invoke-virtual {p0, v0}, Landroid/app/Activity;->setRequestedOrientation(I)V

    .line 35
    const v0, 0x7f09001c
    invoke-virtual {p0, v0}, L♫/上;->setContentView(I)V

    .line 37
    const/16 v0, -0x10
    iput-byte v0, p0, Lre/pwnme/MainActivity;->θ:B

    .line 38
    new-instance v0, L♫/ᵤ;
    invoke-virtual {p0}, Landroid/app/Activity;->getApplicationContext()Landroid/content/Context;
    move-result-object v1
    invoke-direct {v0, v1}, L♫/ᵤ;-><init>(Landroid/content/Context;)V

    .line 39
    .local v0, "rb":L♫/ᵤ;

    # ===== ROOT CHECK (BYPASS) =====
    # invoke-virtual {v0}, L♫/ᵤ;->₤()Z
    # move-result v1
    # if-nez v1, :cond_0
    #
    # invoke-virtual {v0}, L♫/ᵤ;->θ()Z
    # move-result v1
    # if-eqz v1, :cond_1
    #
    # invoke-virtual {v0}, L♫/ᵤ;->ö()Z
    # move-result v1
    # if-eqz v1, :cond_1
    #
    # :cond_0
    # const/16 v1, 0x539
    # div-int/lit8 v1, v1, 0x0   
    #
    # iget-byte v2, p0, Lre/pwnme/MainActivity;->θ:B
    # or-int/lit8 v2, v2, 0xf    
    # int-to-byte v2, v2
    # iput-byte v2, p0, Lre/pwnme/MainActivity;->θ:B
    #
    # :cond_1
    # ===== END BYPASS BLOCK =====

    .line 43
    invoke-virtual {p0}, Lre/pwnme/MainActivity;->()V

    .line 44
    return-void
.end method

To bypass the anti-root check triggered by the button click handler, we need to patch the onClick method in the class MainActivity$θ.smali, as shown below. This method contains the logic responsible for invoking the anti-root verification during the click event.

    # ... [Validation logic above] ...

    new-instance v2, L♫/ᵤ;
    iget-object v7, p0, Lre/pwnme/MainActivity$θ;->θ:Lre/pwnme/MainActivity;
    invoke-virtual {v7}, Landroid/app/Activity;->getApplicationContext()Landroid/content/Context;
    invoke-direct {v2, v7}, L♫/ᵤ;-><init>(Landroid/content/Context;)V

    # --- Anti-Root bypass (Neutralized) ---
    # invoke-virtual {v2}, L♫/ᵤ;->₤()Z
    # move-result v7
    # if-nez v7, :cond_1

    # invoke-virtual {v2}, L♫/ᵤ;->θ()Z
    # move-result v7
    # if-eqz v7, :cond_2

    # invoke-virtual {v2}, L♫/ᵤ;->ö()Z
    # move-result v7
    # if-eqz v7, :cond_2

    # :cond_1
    # iget-object v7, p0, Lre/pwnme/MainActivity$θ;->θ:Lre/pwnme/MainActivity;
    # invoke-static {v7}, Lre/pwnme/MainActivity;->θ(Lre/pwnme/MainActivity;)B
    # move-result v8
    # or-int/lit8 v8, v8, 0xf
    # int-to-byte v8, v8
    # invoke-static {v7, v8}, Lre/pwnme/MainActivity;->θ(Lre/pwnme/MainActivity;B)B
    # new-instance v7, Ljava/lang/NullPointerException;
    # invoke-direct {v7}, Ljava/lang/NullPointerException;-><init>()V
    # invoke-virtual {v7}, Ljava/lang/Object;->notify()V

    .line 90
    .end local v7    # "np":Ljava/lang/NullPointerException;
    :cond_2
    iget-object v7, p0, Lre/pwnme/MainActivity$θ;->θ:Lre/pwnme/MainActivity;

    # ... [Rest of success logic] ...

After patching the libnative-lib.so library and the Smali files as described above, the final step is to rebuild the APK, sign it, and install it on the emulator. All patching scripts, the modified library, and the rebuilt APK are available in our GitHub repository for reference and reproducibility.

V. Conclusion

This article demonstrated how to bypass Android protections at both the native and Java layers in the R2Pay challenge (Android UnCrackable L4). We showed how dynamic and static analysis techniques, combined with tools such as GDB and Python scripting, can be effectively used to defeat multiple protection mechanisms and obfuscation layers. The methodology presented highlights a practical workflow for analyzing and neutralizing advanced defensive implementations in Android applications.

References

Most Recent Posts

  • All Posts
  • Exploit Development
  • Fuzzing
  • Penetration Testing
  • Reverse Engineering
  • Vulnerability Research

Category