R2Pay Under the Microscope: Runtime Protections
Analyzing and Bypassing Android Runtime Protections Using Python and GDBSafety & 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.
- Anti-Debugging
- Anti-Root
- Anti-Instrumentation
- Self-Integrity Verification
- Execution Environment Validation
- Anti-Root
Executed in init_array Segment.
Root detection using libtool-checker.so.
UI display and libnative-lib.so integrity check.
Button click triggers protections.
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.
To locate the actual implementation of gXftm3iswpkVgBNDUp, the memory pointed to by $rdx must be examined:
Method name pointer:
$rdx + 0x0Method signature pointer:
$rdx + 0x8Native 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.
if (rb.m1162() || (rb.m1155() && rb.m1150())) { // TRAP: ArithmeticException (Div by 0) int i = 1337 / 0; this.f508 = (byte)(this.f508 | 15); }
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


