r2-pay: anti-debug, anti-root & anti-frida (part 1)

Introduction

This series of blog posts explains one way to resolve the r2-pay challenge released during the r2con2020 conference. This first part is about the anti-analysis tricks used to hinder reverse-engineering while the second part will be more focused on breaking the whitebox.

The resolution took me more than a week-end but it covers nice topics that worth it: obfuscation & whitebox. It was also the opportunity to practice attacks against whiteboxes and to test SideChannelMarvels/JeanGrey developed by Philippe Teuwen (aka. @doegox).

The challenge has been resolved with the AArch64 version on a device running on Android 9 and rooted with Magisk.

Overview

When opening the application on a non-tempered device (or with Magisk hide enabled), we are asked to enter a PIN and an amount that is used to generate a token.

To resolve the challenge, we have to find the master key that is used to generate the token. Few days before the CTF I was told that one of the challenges would involve an obfuscated whitebox…

The main interface of the APK is located in the Java class re.pwnme.MainActivity which forwards the user inputs (PIN & amount) to a JNI function named gXftm3iswpkVgBNDUp. This function takes the concatenated input $PIN\ ||\ Amount$ and returns the token as a byte array.

The static constructor of the class loads the “native-lib” library which is available for the architectures: arm64-v8a, armeabi-v7a, and x86_64. Unsurprisingly, this library is obfuscated and some symbols suggest that it has been compiled with a fork of O-LLVM 1.

re.pwnme.MainActivity in r2pay

In addition, the library does not export the expected symbol Java_re_pwnme_MainActivity_gXftm3iswpkVgBNDUp but prefers to use the JNI_OnLoad technique 2. JNI_OnLoad() is also obfuscated along with control-flow-flattening.

The main task of the challenge is to understand the logic of the gXftm3iswpkVgBNDUp function to figure out how the token is generated.

Anti-Root & Anti-Frida

Along with the libnative-lib.so library, the applications embeds another library libtool-checker.so whose name sounds quite familiar: it comes from the open-source project rootbeer which is used to detect if the device is rooted.

Some of the root-checks are done in the MainActivity class and if the device is rooted the application raises an exception by dividing a number with 0.

On this point, we can disable the check by using Frida on the rootbeer’s functions involved in the detection:

// frida -U -l ./bypass-root.js --no-pause -f re.pwnme
Java.perform(function () {
  var RootCheck = Java.use('\u266b.\u1d64');

  RootCheck['₤'].implementation = function () {
    console.log("Skip root");
    return false;
  }

  RootCheck['θ'].overload().implementation = function () {
    console.log("Skip root");
    return false;
  }
})

Nevertheless, the application still crashes as soon as it starts and generates the following backtrace:

F libc    : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xfa929095 in tid 8875 (re.pwnme), pid 8849 (re.pwnme)
F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
F DEBUG   : Build fingerprint: 'google/taimen/taimen:9/PQ3A.190801.002/5670241:user/release-keys'
F DEBUG   : Revision: 'rev_10'
F DEBUG   : ABI: 'arm64'
F DEBUG   : pid: 8849, tid: 8875, name: re.pwnme  >>> com.google.android.gms <<<
F DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xfa929095
F DEBUG   :     x0  0000007f041f6610  x1  0000007f2565c800  x2  0000007f25600000  x3  000000000000001d
F DEBUG   :     x4  000000000000005c  x5  0000000000000001  x6  0000000000000001  x7  0000000000000000
F DEBUG   :     x8  0000007f041f6610  x9  0000007f041f6600  x10 00000000fa929095  x11 00000000000035b2
F DEBUG   :     x12 00000000e34d79ac  x13 00000000fffffff7  x14 00000000a139577d  x15 0000000000000001
F DEBUG   :     x16 0000007fa66af220  x17 0000007fa65e3608  x18 0000000000000000  x19 0000007f041f6680
F DEBUG   :     x20 0000000000000000  x21 0000000000000000  x22 0000229100002291  x23 0000000000000000
F DEBUG   :     x24 0000007f041ff570  x25 0000007f04102000  x26 0000007fab1ad5e0  x27 0000007f0421a690
F DEBUG   :     x28 0000007f04209080  x29 0000007f041ff490
F DEBUG   :     sp  0000007f041f65f0  lr  0000007f0423de04  pc  0000007f0423f980
F DEBUG   :
F DEBUG   : backtrace:
F DEBUG   :     #00 pc 000000000003f980  /data/app/re.pwnme-7O3ynhSmMsg2_E5_uqbQxQ==/lib/arm64/libnative-lib.so

The backtrace suggests that other checks are performed in the native library. By looking at the ELF’s constructors, we can notice two functions that differ from those generated by the obfuscator:

ELF constructors involved in the detection

By tracing these functions with QBDI, we quickly understand that sub_9080 iterates over /proc/self/maps with the syscalls openat/read that are located at the addresses 0x009870 and 0x00b448.

Then, we observe the following sequence:

0x011fb0: syscall: openat(0xffffffffffffff9c, '/system/lib64/libc.so')

0x012884: syscall: read(51, 0x7ffc006c58, 64): 'ELF@)@8@'

0x013170: syscall: lseek(51, 0x112918, 0)

0x0145f8: syscall: read(51, 0x7ffc006c18, 64)
0x0145f8: syscall: read(51, 0x7ffc006c18, 64)
0x0145f8: syscall: read(51, 0x7ffc006c18, 64): '/ '
0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'B88'
0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'J>'
0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'RoP)'
0x0145f8: syscall: read(51, 0x7ffc006c18, 64): '\o(('
0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'io'
0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'xo0'
0x0145f8: syscall: read(51, 0x7ffc006c18, 64)
0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'Bxx`-'
0x0145f8: syscall: read(51, 0x7ffc006c18, 64): 'PP`'

0x0151f4: malloc(0x18): 0x7f0c21f4c0
0x0156e4: syscall: lseek(51, 0x1a650, 0)
0x015a68: malloc(0x1e60): 0x7f0acb2000
0x015fa0: syscall: read(51, 0x7f0acb2000, 0x1e60): '{n@b r@ v@ z@ ~@ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ A A" AB Ab A A A A "A &A" *AB .Ab 2A 6A :A >A BA FA" JAB NAb RA VA ZA ^A bA fA" jAB nAb rA vA zA ~A A A" AB Ab A A A A A A" AB Ab A A A A A A" AB Ab A A A A A A" AB Ab A A A A B B" BB Bb B B B B "B &B" *BB .Bb 2B 6B :B >B BB FB" JBB NBb RB VB ZB ^B bB fB" jBB nBb rB vB zB ~B B B" BB Bb B B B B B B" B ...'
0x016cfc: free(0x7f0acb2000) -> {n@b    r@ v@ z@ ~@ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ @ @" @B @b @ @ @ @ A A" AB Ab A A A A "A &A" *AB .Ab 2A 6A :A >A BA FA" JAB NAb RA VA ZA ^A bA fA" jAB nAb rA vA zA ~A A A" AB Ab A A A A A A" AB Ab A A A A A A" AB Ab A A A A A A" AB Ab A A A A B B" BB Bb B B B B "B &B" *BB .Bb 2B 6B :B >B BB FB" JBB NBb RB VB ZB ^B bB fB" jBB nBb rB vB zB ~B B B" BB Bb B B B B B B" B ...
0x017118: syscall: close(51)

From this output, we can infer the following logic:

  1. 0x011fb0: the function opens the libc
  2. 0x012884: it reads the ELF header
  3. 0x013170: it jumps to the ELF sections table
  4. 0x0145f8: it looks for the .plt section
  5. 0x015a68, 0x015fa0: it reads the content of the .plt section

These operations suggest that the function checks if the .plt of /system/lib64/libc.so is not tampered with. In particular, if we use Frida on a libc’s function this check won’t pass.

After this check, the function sub_9080 spawns a thread:

0x0195dc: pthread_create(0xf1079f10, 0x0, 0x1a690, 0x0)

The libc integrity check makes more sense as it is probably used to protect the library against a hook of pthread_create().

The thread’s routine sub_1a690 starts by making two calls to the mathematical function tan():

0x01b774: tan(0.): 0.
0x01b79c: tan(-7832.0): -0.00951489
0x01cc74: memcpy(0x7ffc006598, libnative-lib.so!0x1267f0, 80) -> !7Nl
0x01ceb8: rand()
0x01f774: tan(0.): 0.
0x01f79c: tan(-7832.0): -0.00951489

My understanding of these calls is that the application tries to protect against tools that would not support floating-point instructions such as FCMP or FMOV. In addition, I think that if we mock the behavior of tan() with a constant value it would trigger a crash.

tan

Then it follows a check of TracerPid value in /proc/self/status. This value is set when the process is ptrace-debugged (which is the case with gdb). Dynamically, we observe syscalls that open /proc/self/status and read the content byte-per-byte:

0x020ee0: syscall: openat(0xffffffffffffff9c, '/proc/self/status'): 51
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'N'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'a'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'm'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'e'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): ':'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1)
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'r'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'e'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): '.'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'p'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'w'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'n'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'm'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'e'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1)
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'S'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 't'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'a'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 't'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): 'e'
0x0231e8: syscall: read(51, 0x7ffc00656c, 1): ':'
...

Anti-Frida #1

Still in the thread’s routine sub_1a690, the function checks if Frida is running by looking at all the values of /proc/self/task/<tid>/status and by checking if one of the names is gmain. It turns out that it’s the case when Frida is used in the application :-)

0x0368e4: snprintf('/proc/self/task/9719/status', '/proc/self/task/%s/status'): '/proc/self/task/9719/status'
0x036a1c: syscall: openat(0xffffffffffffff9c, '/proc/self/task/9719/status')
0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'N'
0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'a'
0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'm'
0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'e'
0x03897c: syscall: read(73, 0x7ffc400af4, 1): ':'
0x03897c: syscall: read(73, 0x7ffc400af4, 1)
0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'g'
0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'm'
0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'a'
0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'i'
0x03897c: syscall: read(73, 0x7ffc400af4, 1): 'n'
0x03897c: syscall: read(73, 0x7ffc400af4, 1)
0x03897c: closedir()
# Crash!

To bypass this check, one can statically patch the syscall or we can dynamically change the behavior of snprintf(..., '/proc/self/task/%s/status') in order to always returns the same status (e.g. /proc/self/task/123/status). Concretely, it could be done by hooking snprintf and by forcing the output string to /proc/self/task/123/status.

Anti-Frida #2

Still in the sub_1a690 function, the anti-frida checks continue by inspecting the file descriptors of the process. It iterates over /proc/self/fd/%s and looks at the underlying symlink.

Frida server — which is running globally on the device — and Frida agent — which is injected in the process — communicate with named pipes that are associated with a file descriptor.

If Frida server is running, we can observe the following values:

0x04308c: lstat('/proc/self/fd/32')
0x043448: syscall: readlinkat(0xffffffffffffff9c, '/proc/self/fd/32', 0x7ffbffdc10, 256): 'anon_inode:[eventfd]'
0x041844: readdir('33')
0x043078: snprintf('/proc/self/fd/33', '/proc/self/fd/%s'): '/proc/self/fd/33'
0x04308c: lstat('/proc/self/fd/33')
0x043448: syscall: readlinkat(0xffffffffffffff9c, '/proc/self/fd/33', 0x7ffbffdc10, 256): 'anon_inode:[eventfd]'

0x041844: readdir('34')
0x043078: snprintf('/proc/self/fd/34', '/proc/self/fd/%s'): '/proc/self/fd/34'
0x04308c: lstat('/proc/self/fd/34')
0x043448: syscall: readlinkat(0xffffffffffffff9c, '/proc/self/fd/34', 0x7ffbffdc10, 256): '/data/local/tmp/re.frida.server/linjector-500'
# Crash!

In this case, the file descriptor 34 is associated with /data/local/tmp/re.frida.server/linjector-500 which triggers the detection and the application crashes.

As for /proc/self/task/<tid>/status, one can disable this check by statically patching the syscalls or by dynamically changing the result of readlinkat(). For instance, we can use QBDI to instrument syscall instructions and process the result of readlinkat() in an user callback:


vm.addMnemonicCB("SVC", POST_INST,
  [] (VMInstanceRef vm, GPRState* gprState, FPRState*, void* data) {
    if (gprState->x8 != __NR_readlinkat) {
      return VMAction::CONTINUE;
    }

    std::string buf = reinterpret_cast<char*>(gprState->x2);
    if (buf.find("re.frida.server") != std::string::npos) {
      static const std::string FAKE_VALUE = "anon_inode:[eventfd]";
      // Bypass Frida detection!
      memcpy(
        reinterpret_cast<void*>(gprState->x2),
        reinterpret_cast<void*>(FAKE_VALUE.c_str()),
        FAKE_VALUE.size() + 1
      );
      gprState->x0 = FAKE_VALUE.size() + 1;
    }

    return VMAction::CONTINUE;

  }, ctx);

Anti-Frida #3 ?

I’m not sure if the following calls sequence is used to check the libc’s integrity against Frida but at the end of the thread’s routine, we can observe these syscalls:

0x048ff0: syscall: openat(0xffffffffffffff9c, '/proc/self/maps'): 51
0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '1'
0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '2'
0x04ae6c: syscall: read(51, 0x7ffc006578, 1): 'c'
0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '0'
0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '0'
0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '0'
0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '0'
0x04ae6c: syscall: read(51, 0x7ffc006578, 1): '0'
...
0x0513f8: sscanf('7fa65c0000-7fa65dc000 r-xp 00000000 08:07 1275/system/lib64/libc.so', '%lx-%lx %s %s %s %s %s')
0x056034: syscall: close(51)

The result of sscanf() could be used to check the page permissions (e.g. rwxp) or to the libc’s base address (to check if it is consistent).

Anti-Root

In addition to the root-beer detection, the library embeds another root detection located in the second ELF constructor. This constructor — sub_77D14 — performs the same early checks as the first constructor on the libc’s .plt integrity before spawning another thread routine, sub_98c00.

0x08861c: pthread_create(0xfa780b70, 0x0, 0x98c00, 0x0)

ELF constructors: anti-frida and anti-root

By tracing the thread’s routine, we notice that it checks if su files are present on the device through three different calls:

  1. One call to open(): 0x099180: open(’/system/xbin/su’)
  2. One syscall to openat(): 0x0992a4: syscall: openat(…, ’/data/su’)
  3. One syscall to faccessat(): 0x0993f0: syscall: faccessat(’/sbin/su’)
0x099180: open('/data/local/su'): -1
0x0992a4: syscall: openat(0xffffffffffffff9c, '/data/local/su'): -2
0x0993f0: syscall: faccessat('/data/local/su'): -2
0x099180: open('/data/local/bin/su'): -1
0x0992a4: syscall: openat(0xffffffffffffff9c, '/data/local/bin/su'): -2
0x0993f0: syscall: faccessat('/data/local/bin/su'): -2
0x099180: open('/data/local/xbin/su'): -1
0x0992a4: syscall: openat(0xffffffffffffff9c, '/data/local/xbin/su'): -2
0x0993f0: syscall: faccessat('/data/local/xbin/su'): -2
0x099180: open('/sbin/su'): 51
0x0992a4: syscall: openat(0xffffffffffffff9c, '/sbin/su'): 52
0x0993f0: syscall: faccessat('/sbin/su'): 52
Crash!

By forcing the results of these functions to -1 or -2, we can disable the checks.

Here is the list of the su-files that are used in this detection:

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

At the end of the thread’s routine, we can also observe the following calls that are probably used to check if the application is running on a real Android system.

0x099e30: syscall: faccessat('/system')
0x099e30: syscall: faccessat('/system/bin')
0x099e30: syscall: faccessat('/system/sbin')
0x099e30: syscall: faccessat('/system/xbin')
0x099e30: syscall: faccessat('/vendor/bin')
0x099e30: syscall: faccessat('/sbin')
0x099e30: syscall: faccessat('/etc')

Static bypass with LIEF

In the previous sections, we described the anti-root, anti-debug and anti-frida checks made in the ELF constructors. The same dynamic checks are also performed in the gXftm3iswpkVgBNDUp function at the following locations:

  • 0x09f2f8: /proc/self/status
  • 0x0d4840: /proc/self/fd/
  • 0x0dec8c: /proc/self/task/<tid>/status

While the checks in gXftm3iswpkVgBNDUp can be dynamically disabled when instrumenting the function, the checks in the ELF constructors are annoying.

One way to disable the checks in the thread’s routines is to disable the pthread_create(...). It can be achieved by patching the .plt entry associated with the function:

mov x0, xzr;
ret;

Thanks to llvm-mc, we can get the raw bytes of these instructions:

$ echo "mov x0, xzr;ret;"|llvm-mc -arch=aarch64 -show-encoding

.text
mov     x0, xzr                 // encoding: [0xe0,0x03,0x1f,0xaa]
ret                             // encoding: [0xc0,0x03,0x5f,0xd6]

Finally, we can patch the .plt with LIEF:

import lief
lib = lief.parse("./libnative-lib.so")
lib.patch_address(0x5870, [0xe0,0x03,0x1f,0xaa])
lib.patch_address(0x5874, [0xc0,0x03,0x5f,0xd6])
lib.write("./libnative-lib-patched.so")

pthread_create patches

Using these patches and the Frida script exposed in the first section, we are able to load the application but the other detections are triggered in gXftm3iswpkVgBNDUp. Nevertheless, with the Frida’s stalker or QBDI we can trace the instructions and disable the other checks.

If one wants to completely bypass all the protections statically, here are the patches:

import lief
lib = lief.parse("./libnative-lib.so")

# Keys are str objects for a better understanding :)
INST = {
    "mov x0, #0":  [0xe0, 0x03, 0x1f, 0xaa],
    "ret":         [0xc0, 0x03, 0x5f, 0xd6],
    "nop":         [0x1f, 0x20, 0x03, 0xd5],
}

PATCHES = [
    # Patch the .plt entry of pthread_create
    (0x5870, INST["mov x0, #0"]),
    (0x5874, INST["ret"]),

    # Disable anti-frida checks
    (0x0d718c, INST["mov x0, #0"]), # /proc/self/fd : patch the result of readlinkat syscall
    (0x0e1940, INST["mov x0, #0"]), # /proc/self/task/<tid>/status: patch the result of read syscall

    # Disable .text integrity checks
    (0xB64D0, INST["nop"]),
]

for patch in PATCHES:
    lib.patch_address(*patch)

lib.write("libnative-lib.so")

When writing this write-up, I realized that patching the syscalls involved in the anti-frida (/proc/self/fd/ and /proc/self/task/<tid>/status) makes the application crash.

It turns out that the library seems to implement code integrity on the .text section that I didn’t notice when running the function through QBDI. Nevertheless, by tracing the basic block3 we can identify the basic block involved in the integrity check and patch it.

Code integrity patches

Regarding JNI_OnLoad(), a trace generated with QBDI’s ExecBroker leads to following result:

JNI_OnLoad() {
    0x09af3c: GetEnv(0x7fcb507460, 0x10006)
    0x09b0ac: FindClass("re/pwnme/MainActivity"): 537
    0x09b1b4: RegisterNatives()
        gXftm3iswpkVgBNDUp ([BB)[B -> "libnative-lib.so@0x9b41c"
}

Then, we can extract the function’s offset: gXftm3iswpkVgBNDUp: 0x9b41c.

Summary & Conclusion

Whilst Frida detections are usually based on sockets and library names in /proc/self/maps, this challenge introduces two detections based on named pipes:/proc/self/fd and thread status: /proc/self/task/<tid>/status which are pretty cool :-)

These checks are performed in two locations:

  1. The ELF constructors
  2. The function gXftm3iswpkVgBNDUp()

The implementation in the ELF constructors might be tricky to analyse since the functions are called before any other classical functions (which includes JNI_OnLoad()). Nevertheless, thanks to the interface of the ELF loader, it exposes the function call_array(...)4 which is handy to process the ELF constructors.

Overview of the anti-root and anti-frida

Since QBDI is not detected in this challenge, it’s a good opportunity to give it a try:

https://github.com/QBDI/QBDI

Acknowledgments

Thanks to Eduardo Novella (@enovella_) and Gautam Arvind (@darvincisec) for this interesting and realistic challenge they created!

Also thanks to Quarkslab that allowed this publication. For those who are interested in similar topics, you can take a look at the Quarkslab’s blog.

References