Reverse engineering of the Nike Run Club application using Frida

Reverse engineering of the Nike Run Club application using Frida

The ultimate goal of the study was to extract the authentication tokens that Nike generates at login. It was another project that I put off “for later” and what could be better to remember about it when I learned something new?



I am an avid athlete. Most of my friends and relatives know it. When I started jogging, I immediately found myself in the Nike Run club. Previously, I carefully recorded every run to compare the results of my efforts. It lasted for 2 years until I found out that most of my friends used Strava. I decided to switch to Strava, but I was very upset when I found out that there was no way to export data to NRC.

On the Internet documented Nike API and it allows you to export data manually in JSON format, but I wanted something more automated. So I decided to do what any other crazy technician would do in my place and started my way to reengineer Nike Run Club APK. I decided to study the sources and see if I could implement the login process and generate tokens in a fully automated way.

This article will teach you the basics of using Frida and how you can open and study the various APKs. But before we move on to the next article you need to know how I decided to reverse the application, not just attack MITM using mitmproxy.

NRC Traffic Capture

When I caught fire with this idea, I decided to track traffic using mitmproxy. You can download the NRC APK from APKMirror, to explore it. After downloading, rename the file nike.apk so that the other commands in this manual are version independent and universal.

Then, use adb to install this apk into the emulator:

$ adb install nike.apk

Now you need to run mitmproxy. If you can’t configure the emulator or proxy, please refer to my Previous article where I explained in detail how to do it. To run mitmproxy, I used the following command:

$ mitmproxy -set block_global=false --showhost

The last thing to do is to install the certificate mitmproxy, as a system on the Android emulator, following of the official documentation and adding the emulator to mitmproxy.


After this setting, I opened NRC and started tracking requests to mitmproxy. I was a bit surprised and frightened by the number of requests with analytics that NRC sent. It lacked SSL pinning so I didn’t have to do anything special before all the queries started to appear. All these 68 queries appeared before I logged in:


When I tried to enter, I saw the following query:


What does client_id mean? All other parameters seem clear and I can set them myself, but client_id seems pretty unique . Where does it come from .

I checked a couple of requests before and after this particular API call and could not find client_id anywhere. It had to be created inside the APK itself. I tried to repeat the request several times, and got an error on 2-3 attempts. The answer page looked like this:

  <TITLE>Access Denied</TITLE>
</HEAD> Access Denied</TITLE> </HEAD>
  <H1>Access Denied</H1>
    You don't have permission to access " on this server.
    Reference #18.2eaf3817.1593493058.26b69156

There was something dynamic in the query, and I just had to find out what. This was the ideal occasion to start a deeper study of NRC.

By the way, the NRC application actively uses HTML. The login page is actually an HTML document and is mapped out from the server. You can access it by to this address.

Decompilation NRC APK

I hope you have already downloaded the NRC APK with APKMirror and renamed it nike.apk. I downloaded the APK to this online version of jadx and decompiled it. I have to find client_id and the first thing I do when I try to find a line in the raw form is to use grep:

$ grep -r `client_id` ./.

Oh, I really got a lot of data. This seems to be the most interesting line:

./resources/res/values/strings.xml:    <string name="unite_client_id"> IFc97q8fSoR84EHfevnzBNivAiT6H+i8vmVZDnCAax/ZjSGxw5ejdekfXtCrzrtJrQfJnj30JeK+MsyruZi8sW6iUBfe//NGZlpQJXUbz8LuPEXnLMAlxKdLV6BrBgKHqNI94nfSHCCr0xW3HOZk/XyFdevndG52zmZR0XXym0yW5d8n/XvLGDCtVyryFLYoYwHYrDC9JZ+GfAacPKE5S437fT9Af+Z/AeZgqPplm9mCaPBoOc0Co4+h3nT8TvXMsU4Vy8pRTuW0skMU0uwUkq7R/UN06daQ8AkAaYt7KWG0S36tSbHuR03ji7om8ebOJqOzgFyOp/KfkHkVX5+PVk2lG7lk1hBltitrBND8njmHIPisC6+W7Ul1an0mRiNTQVFFSJpyNUVE1D17NQ==< /string>

grep also showed me where it was found:

./sources/com/nike/plusgps/login/ UniteConfig uniteConfig = new UniteConfig(this.userAgentHeader. getUserAgent(), this.appResources.getString(C5369R.string.unite_experience_id), this.obfuscator.decrypt(this.appResources.getString(C5369R.string.unite_client_id)));

Based on previous experience with encryption, it looks like an AES-encrypted string. But I need to make sure that it is the exact client_id that I have seen in the query before. It’s time to use Frida, inject the APK and find out what the value of the decrypted string is.

Enterface in Frida

According to official website , Frida is

Dynamic tools for developers, reverse engineers and security professionals.
It can be used to inject scripts into native applications and thus track what is going on inside them. We will use it to track input data in methods in the NRC APK, as well as for user calls to different procedures. Before we move on, let’s install Frida first.

Set Frida

Frida is divided into two parts (as far as I know). Frida-Client and Frida-Server. The client runs on the host, and the server runs on the Android / iOS device. To simplify the testing, it will be much easier to use the Android emulator with Frida.

Install the Python client packages

For the client side we can install two separate packages / libraries pip. One is called frida, and the other is called frida-tools. frida allows us to import and use frida in our code, while frida-tools provides some really useful command line tools to help us with the reverse engineering process.

Let’s create a new directory and virtual environment, and install both of these packages.

$ mkdir nike_project
$ cd nike_project
$ python -m venv env
$ source env/bin/activate
$ pip install frida frida-tools

If everything was installed and configured correctly, the frida-ps -h command will print something like this:

$ frida-ps -h
Usage: frida-ps [options]

  --version show program's version number and exit
  -h, --help show this help message and exit
  -D ID, --device=ID connect to device with the given ID
  -U, --usb connect to USB device
  -R, -remote connect to remote frida-server
  -H HOST, --host=HOST connect to remote frida-server on HOST
  -O FILE, --options-file=FILE
                        text file containing additional command line options
  -a, --applications list only applications
  -i, --installed include all installed applications
  -j, --json output results as JSON

Settings frida-server on Android

Now we need to install the frida-server on the Android emulator (you can also use your Android device, but I prefer the emulator for testing). At the time of writing this article, the latest version of the frida-server was 12.10.4. You can check the latest version on GitHub. Just change the version number in the commands below and it should work:

unxz frida-server-12.10.4-android-x86_64.xz
adb push frida-server-12.10.4-android-x86_64 /data/local/tmp/frida-server
adb shell chmod 755 /data/local/tmp/frida-server

With the commands above we downloaded and copied the frida server to the emulator, now we have to start the server. Open the adb shell:

adb shell

And run in adb shell:

/data/local/tmp/frida-server &

Frida listens

Once we run frida-server, we can start preparing Javascript code for the injection.

Write code using Frida

I looked at the NRC source code, looked at the breadcrumbs and found that the NativeObfuscator file has the magic of decryption. I recognized the NRC process name frida-utils -Ua and then wrote the following code to test my hypothesis:

import frida, sys

jscode = ""
Java.perform(function (){
    var MainActivity = Java.use('');
    var ConfFactory = Java.use('');
    var String = Java.use("java.lang.String");
    var obfuscator = Java.use("");
    var resources = Java.use("android.content.res.Resources");
    var logger = Java.use("");
    var strRes = "IFc97q8fSoR84EHfevnzBNivAiT6H+i8vmVZDnCAax/ZjSGxw5ejdekfXtCrzrtJrQfJnj30JeK+MsyruZi8sW6iUBfe//NGZlpQJXUbz8LuPEXnLMAlxKdLV6BrBgKHqNI94nfSHCCr0xW3HOZk/XyFdevndG52zmZR0XXym0yW5d8n/XvLGDCtVyryFLYoYwHYrDC9JZ+GfAacPKE5S437fT9Af+Z/AeZgqPplm9mCaPBoOc0Co4+h3nTv8TvXMsU4Vy8pRTuW0skMU0uwUkq7R/UN06daQ8AkAaYt7KWG0S36tSbHuR03ji7om8ebOJqOzgFyOp/KfkHkvX5+PVk2lG7lk1hBltitrBND8njmHIPisC6+W7Ul1an0mRiNTQVFFSJpyNUVE1D17NQ==";
    var context = Java.use('').currentApplication().getApplicationContext();    
    var log = logger.$new();
    var obs = obfuscator.$new(context, log);

process = frida.get_usb_device().attach('')
script = process.create_script(jscode)

I saved it as and ran it with Python:

$ python

The conclusion was exactly what was sent as part of the request. Thus, it seems that client_id is not dynamically generated.

This was the first time I used Frida for a real application, so I wanted to practice a little bit of interception. So, let’s ask Frida to intercept the decrypt method, and display the input that is transferred to it in the console every time it is called. This is possible by overloading the methods with Frida:

jscode = "".
  setTimeout(function() {
      Java.perform(function (){
          var obfuscator = Java.use("");
          obfuscator.decrypt.overload('java.lang.String').implementation = function (str){.
              console.log("******* start ******");
              console.log('input str: ' + str);
              var output = this.decrypt(str);
              console.log('output str: ' + output);
              console.log("******* end ******\\n");
              return output;

Now restart the NRC application, – in the terminal should appear method calls:

******* start ******
input str: FwWEAP2r615reIRlMz45fzIVs4OQqq+IVpyN7d1M1kQ3tYV5Fo6VlOjM435cvAfI0zq+
output str: Basic ZmE2NzJlOTUtYmQwNi00YWZhLWExZjYtYTczNGUzYjhkNmI5OnFidDJ5clJzc
******* end ******

******* start ******
input str: 4sk+vdohXtRY2UkXX2piy+oON0fs/0DjGNfyitJBXc+lIw57oSGIEJLreAPfzf/9Kbdk
output str: AQzIBimI3XFvsMKXXjFREYpjfS43McGw
******* end ******

So far everything is fine, but let’s take a little closer look.

Binding to JNI calls

In the code, I noticed that Nike put all the encryption and decryption code in .so file and used JNI to access it. I found out about this because the NativeObfuscator class contained this code:


I also learned that, as the name suggests, the .so file was obfuscated. Fortunately, Frida provides us with super-powerful possibilities to connect to native function calls. To do this, you must use Interceptor and know which function calls you want to connect. As far as I remember, I found that the .so file had been obfuscated, as well as the actual functions inside by uploading it to Ghidra and let Ghidra do wonders. You can see a list of functions in the right column:

The decryption function name was Java_com_nike_clientconfig_NativeObfuscator_decrypt and I connected to it with this code:

Interceptor.attach(Module.findExportByName("", "Java_com_nike_clientconfig_NativeObfuscator_decrypt"), {code> and I connected to it with this code.
    onEnter: function (args) 
    console.log("inside decrypt");
    onLeave: function(args)
        console.log("outside decrypt");

I also had to configure my code to make sure that frida restarts the NRC application every time the script runs, because the decryption happens exactly when the APK is loaded for the first time. I did this by replacing the code at the bottom with the following:

app_name = ''.
device = frida.get_usb_device()
pid = device.spawn(app_name)
process = device.attach(pid)
script = process.create_script(jscode)

The function arguments in .so point to memory cells, and we can print the value of that pointer by changing our Interceptor code:

Interceptor.attach(Module.findExportByName("", "Java_com_nike_clientconfig_NativeObfuscator_decrypt"), {
    onEnter: function (args) 
        console.log("inside decrypt");
        var data = Memory.readByteArray(args[0], 10);
        console.log("Memory data: ");
    onLeave: function(args)
        console.log("outside decrypt");

I do not know the input size, so I manually inserted 10 in this case. The output was the same for all function calls:

inside decrypt
Memory data:
           0 1 2 3 4 5 5 6 7 7 8 9 A B C D E F 0123456789ABCDEF
00000000 10 43 5f c6 4a 72 00 00 00 4c .C_.Jr...L
outside decrypt

That was good, my theory about client_id static was correct, and I proved it by connecting to actual calls to Java methods. I tried to figure out the .so code using Ghydra, but I soon gave up. The pointers were scrambled, the inputs did not match the actual pointers of the input parameters – there was a big mess. I realized that I don’t need to go through the effort because this file doesn’t generate anything dynamic.

That’s where my APK reverse engineering journey ended, and I decided to put the “reverse engineering .so file” in the background… I still had to figure out how the login worked so that I could create a tool to automatically extract the token and maintain the activity.

Note. There is also a program jnitrace based on Frida which should print all JNI calls, but NRC hung up whenever I tried to use jnitrace. And for those of you who really want to be thrilled, try reversing the .so file.

Side Enpoints

Remember I told you at the beginning that the NRC application uses HTML and loads the login page from its server? I continued testing this endpoint. I looked at the actual request in more detail and saw some cookies that I found interesting. There was a cookie file called _abck and another cookie I just forgot. I did some online research and found that Akamai Bot Manager used the same cookies when filtering bots.

I searched the Internet for resources, hoping to find information that someone could reverse this bot manager, but found nothing useful. Those who may have done it don’t publish their research on the Internet, because then Akamai will just get them patched up. I found old repositories at GitHub, but they are so old that they don’t carry anything useful.

Now we are outside the scope of this article, so I will just give you a summary of what I did next. The main problem was that I needed access to Bearer tokens in automatic mode so that I could eventually use them to execute Nike API calls. I already knew how to extract it manually, but wanted to provide users with an automated tool. I searched and found another URL that can be automated with Selenium, seleniumwire and geckodriver.

And after several sleepless nights I wrote a script called nrc-exporter.


It will allow you to export mileage data from Nike and convert them to GPX format. It is still in its initial stage and there is a lot of shit code. If you are interested in its development, please feel free to improve it and send requests. I am always happy about it.


I hope you have learned something new in this article. I have certainly learned a lot of new things. It’s always interesting to see what some APKs do under the hood. This article just touched on what Frida is capable of. If you want to learn more about it, read the resources listed at the end of this article. What about me and my interest in this APK?


The automation of tokens used by nrc-exporter is likely to be blocked as soon as I publish this article, but now you know how to generate them manually, so everything is fine.


Source –

WARNING! All links in the articles may lead to malicious sites or contain viruses. Follow them at your own risk. Those who purposely visit the article know what they are doing. Do not click on everything thoughtlessly.


0 0 vote
Article Rating
Notify of
Inline Feedbacks
View all comments

Do NOT follow this link or you will be banned from the site!
Would love your thoughts, please comment.x

Spelling error report

The following text will be sent to our editors: