Reversing HackEx - An android game

Hello peeps. I’m sp0re. This is my first post on 0x00sec, you can find more about me on my website. Today we are going to reverse engineer the network protocol of an android game so that we can automate the game, and earn unlimited money while drinking sodas and eating doritos. When looking online, they’ve had issue with cheaters for years now. But is it really that easy to cheat in this game? Let’s find out.

The game in question is HackEx It’s an online “hacking” simulator game for mobile. The goal is quite simple, you “hack” into other players in order to steal their money, drop some spam viruses or spyware, so you are able to upgrade your software by buying better versions or stealing better versions from others.

Environment Setup

Before tinkering, we need to set our environment up and running. For android reversing, I usually use android studio as well as an x86_64 android emulator. x86_64 mainly because I’m more comfortable reading it over ARM.

First, open android studio and create a random project.

Once done, click on tools>AVD Manager and spin up a new emulator. When it asks you to select a system image, click on x86 Images and take one with (Google APIs). For some reasons, it will allow you to have a rooted emulator.

I will take Oreo (Api level 26) because why not.

Once configured, you need to start it using emulator in order to get a writable system endpoint. it will be useful later.

$ ~/Android/Sdk/emulator/emulator -list-avds
Pixel_2_API_26
$ ~/Android/Sdk/emulator/emulator -writable-system -avd Pixel_2_API_26

Now we also need Android Debug Bridge in order to communicate easily with our emulator. On arch linux, you can pull the binary using the following command:

pacman -S android-tools

Once ADB is installed, and your emulator is running, you can check that everything is setup correctly by running

adb devices

If you get an output, you are good to go! But, just to be proactive, we will restart adbd on the emulator as root, by using the following command:

adb root

Application Discovery

Before starting the leet stuff, let’s have a first glance of the game. I downloaded the apk from the google store on my phone, then pulled the apk on my computer.

From there, once your emulator is running, you need to run the following command to install the APK on it:

adb install /path/to/app.apk

Nice! The apk is installed, now we can just open it and play with it.

So the game is pretty simple, you scan for victims, then “hack” them, and it will show you a progress bar in the processes window. Once complete, you can connect to their system. Once connected, you can download their stuff, upload some of your stuff, or attempt to crack their bank password. each of these actions take time and are represented as progress bars in the process window.

Now since this game is multiplayer. It is safe to assume that all these actions are sent to the server for synchronizing with all the player. Say if someone “hacks” my system, and flushes my bank account, I have to see it. So let’s try to determine how the application is communicating with the server.

Network Analysis

Network discovery

In order to intercept the communication between the app and it’s server, we will push tcpdump to the emulator. I suggest taking it from this Static-binaries repo found on github.

$ adb push tcpdump /data/local/tmp/
tcpdump: 1 file pushed. 66.0 MB/s (2377024 bytes in 0.034s)
$ adb shell
# cd /data/local/tmp && chmod 700 tcpdump
# ./tcpdump -i any -s0 -n -w /sdcard/out.pcap

Now all we have to do it playing a bit the game, and then exit tcpdump. It will create a pcap located at /sdcard.

Now we extract the pcap:

$ adb pull /sdcard/out.pcap . 

And open it with wireshark:

There’s a lot of packets from this dump. The first goal here is to identify the server’s IP in order to filter the traffic. In order to do that, I assumed that some of the packet would contain “hackex” as this is the name of the application. Let’s checkk that with strings:

$ strings -n 6 out.pcap | grep -i "hackex"
 hackex
hackex
hackex
hackex
api.hackex.net
*.hackex.net
[...]

This seems like the DNS entries of a cert file. Good thing is that we have an api endpoint to poke at, and the protocol is HTTP. The bad thing is that the traffic is probably encrypted using SSL.

to verify that, in wireshark, we can click on edit > Find Packet… and search for the string api.hackex.net

it returns a Client Hello packet from TLS1.2. It’s not as easy as we thought!

Network decryption

We therefore need a way to decrypt SSL traffic, in order to get the http content.
For that, the best way it to proxy all the traffic to burp, and install burp’s certificate to the emulator.

Unfortunately, since a recent version of Android, user certificates are not used by applications without consent. There’s 2 workarounds for that:

  • We install the certificate as a system certificate, but we need root
  • We patch the apk to use our user certificate.

Obviously, as we are root, we are going to set burp certificate as a system cert!

Fire up burp, go to proxy tab, then the Options subtab and add a new listener. It needs to listener on all interface, using a different port than the default one

Now export the certificate on your computer. In order for the emulator to accept it, you need to transform the der file to pem, and rename it using subject_hash_old:

openssl x509 -inform DER -in cacert.der -out cacert.pem  
openssl x509 -inform PEM -subject_hash_old -in cacert.pem |head -1  
mv cacert.pem <hash>.0  

my cert is 9a5ba575.0. so I use this command to push the cert to the emulator:

$ adb push 9a5ba575.0 /sdcard

now we need to add the certificate to the /system/etc/security/cacerts/ location. to do that, we need to remount the partition as rw . To do that, connect to the emulator shell and run the following command:

$ adb shell
# mount -o rw,remount /system

Once done, move the file to the above path:

# mv /sdcard/9a5ba575.0 /system/etc/security/cacerts/
# chmod 644 /system/etc/security/cacerts/9a5ba575.0

And finally, reboot the emulator using adb reboot.

Now if you go to Settings, browse to Security & location > Encryption & credentials > Trusted credentials you should see PortSwigger CA.

Success! We can now get the traffic using burp! But to do that, we need to proxy the traffic to our burp. For this, you will need to restart the emulator with the following command:

$ ~/Android/Sdk/emulator/emulator -writable-system -avd Pixel_2_API_26 -http-proxy 127.0.0.1:8080

Boom! SSL Decryption!

Now let’s start the game again, and play a bit, we should see some communication.

I can quicky see a lot of traffic, but each request is made with 2 parameters, sig and sig2
It seems that the application is signing each request before sending them. So the server is sure that the request is coming from the application, and not from other means. We can verify that by editing a request in burp repeater:

Legit request:

Wrong request:

YES, THE SERVER IS DUMB

So if we want to be able to use the api without the game, we need the logic to construct the sig parameters.

Reversing Time

Static analysis

We will decompile the APK using jadx. This will let us see the sources and the assets of the app.

$ mkdir jadx_out
$ jadx -d jadx_out apkdir/base.apk

Once finished, in the jadx_out folder, you’ll find the sources and the resources. Go to the sources, and grep for “api.hackex.net”:

$ grep -r "api.hackex.net" .
com/byeline/hackex/k/e.java:    public static String f1971a = "https://api.hackex.net/v8/";
com/byeline/hackex/k/e.java:        f1971a = "https://api.hackex.net/v8/";
com/byeline/hackex/d/a.java:        return (c) new retrofit2.m.a().a("https://api.hackex.net/v8/").a(aVar.a(Collections.singletonList(y.HTTP_1_1)).a()).a((retrofit2.e.a) retrofit2.a.a.a.a(configure)).a((retrofit2.c.a) g.a()).a().a(c.class);

In the file com/byeline/hackex/k/e.java, the api url is defined, we can go and analyse this file to see what we do with the string.

We can see many methods that apparently handle the rest communication. For instance, we can see in the following method that the server is handling the user_bank endpoint, probably when the user checks his bank account in the game:

We can also see the code that creates the sig parameter. Though we see that it doesn’t create the sig2 parameter anywhere.

For the sig param, we create an empty hashMap, and give it to SettingsManager.flux(). I suspect the hash_map contains the request parameters if there are any. Let’s check another function:

This is indeed the case. We can see above that we give the hashmap the values victim_user_id as well as String.valueOf(i2).

This make me believe that SettingsManager.flux() handles the signature generation. Let’s find it using grep.

$ grep -r "flux" .
./com/byeline/hackex/settings/SettingsManager.java:    public static native String flux(Map<String, String> map);
./com/byeline/hackex/settings/SettingsManager.java:        sb.append(flux(c(str)));

Checking this file, we see that flux is a native function. This doesn’t sound good! Does it? It means the function is probably coming from a compiled library. We can confirm it by cating the file and see the following code:

static {
    System.loadLibrary("HackEx");
}

Therefore, the code for generating the signature is stored in a lib named libHackEx.so.

# find / -name "*libHackEx.so*" 2>/dev/null                                                                                                                         
/data/app/com.byeline.hackex-E-Wrx3wEw35mpVavUJ0_RA==/lib/x86_64/libHackEx.so

thankfuly it’s in x86_64. We can pull it and analyse it.

# cd /data/app/com.byeline.hackex-E-Wrx3wEw35mpVavUJ0_RA==/lib/x86_64/
# file libHackEx.so
libHackEx.so: ELF shared object, 64-bit LSB x86-64, for Android 21, built by NDK r18 (5002713), BuildID=b70b329b6121afd5ba59f2c716a8a56bef9fb294, stripped
$ adb pull /data/app/com.byeline.hackex-E-Wrx3wEw35mpVavUJ0_RA==/lib/x86_64/libHackEx.so .

Now we have the library locally which is good news. The library is a native library for android, which means it follows the Java Native Interface convention (JNI). We won’t talk much about JNI itself here because there’s too much to be said about it, and it’s not the point of this article. Key things to know are:

  • The exported function have the following name: Java_(packageName)(className)(functionName)
  • There’s a lot of code handling interfact between JAVA and library code. For instance, if we feed a HashMap to a native library, obviously the library won’t know how to handle that, so the JNI will translate it transparently.

That being said, we are looking for a function called Java_com_byeline_hackex_settings_SettingsManager_flux. So let’s open the file in radare2

Now we type afl to list all functions. Since the binary is stripped, we won’t get a lot of function names, but the interesting one should still be shown as it has to be named to be exprted.

[0x0000e680]> afl
(...)
0x0000e6e0   68 2870 -> 2822 sym.Java_com_byeline_hackex_settings_SettingsManager_flux
(...)

Nice, let’s check it’s content using pdf @sym.Java_com_byeline_hackex_settings_SettingsManager_flux

[0x0000e680]> pdf @sym.Java_com_byeline_hackex_settings_SettingsManager_flux

The output is 736 line of assembly code, a nightmare to reverse statically! But still, we can see some interesting stuff in it:

0x0000e70f      e8acf8ffff     call sym.imp.clock_gettime

So it gets a timestamp. That’s probably for the sig2= part as we saw earlier that it has the format of a timestamp in milisecond.

0x0000e7db      488d358ffd01.  lea rsi, [0x0002e571]       ; "sig2"

The “sig2” string is hardcoded in, which confort the idea of this being the function we are looking for.

0x0000eb3b      660f2805cd05.  movapd xmm0, xmmword [str.69540016560011007D2D2B63370A30573E3A607A730207381E3F070E38150414001B060273067F31] ; [0x2f110:16]=-1 ; "69540016560011007D2D2B63370A30573E3A607A730207381E3F070E38150414001B060273067F31"
0x0000eb82      4c8d25470502.  lea r12, str.P3eoc0cO5zHSbzZeSrQL8f3wMGbIrZcpEkEh1OFv ; 0x2f0d0 ; "P3eoc0cO5zHSbzZeSrQL8f3wMGbIrZcpEkEh1OFv"

Those are 2 interesting strings. It is probably used to create the actual signature at the end. Also, the first string has a length of 80, and it’s composed of hex, the second string has a length of 40. I think there’s a high chance that for each char of the second string, we take 2 chars of the first one, get the hex value of it and do something that will result in a new string of length 40. The following block confirms my assumption:

First we load in rdi (the address of rsp+rbx+0x160). then, we load a string in rsi, another one in rdx, and perform a scanf. We then xor a byte coming from rbp+r12 and mov it to rsp+rbx+0x130. We increment some counters, compare that one of the counter is equals to 0x50 (80) and jump back to the begining is it’s below this value.

So as we increment rbx by 2 at each loop, I assume that the first string above is located at rsp+rbx+0x160, and the second string is located at rbp+r12. The resulting string is therefore probably in rsp+rbp+0x130.

Let’s continue.

After this block, I do not spot a lot of easily decodable logic. Nonetheless, there’s still some interesting stuff.

We can see a sequence of sprintfs. 20 to be exact. this makes a string of exactly 40 chars.

Finally, we can see the following interesting lines:

0x0000f0d3      488d3509f501.  lea rsi, [0x0002e5e3]       ; "sig"
0x0000f11f      488d35c1f401.  lea rsi, str.sig2           ; 0x2e5e7 ; "&sig2="

So at this point we construct the signature to be returned to the java class.

From here, we have greatly increased our knowledge of this function already. We can assume that from the 2 strange strings, we create a new one after a loop of xoring, and then we use it to sprintf something which is used to construct our final return string. But still, we now know where the parameters from the hashmap are coming from, and also that the timestamp is somehow involved. I think it’s time to move to dynamic analysis.

Dynamic Analysis

There’s a lot of way to perform dynamic analysis. But for a native function like that, nothing beat GDB!

Let’s setup gdb in our emulator to analyze this function at runtime. But before that, I’d like an easier setup than analyzing the actual function using the game apk. Let’s create a simple apk and link our library to it, so we can debug it more easily.

Fire up android studio, create a new projet with an empty activity. Then it is important to use the same package name as the game. So we will use com.byeline.hackex

Now for the sake of easyness, let’s add a button, and we’ll trigger the flux function each time we click on it. in activity_main.xml, add a button, and change its id to btnFlux. Now in MainActivity.java file, edit the onCreate method and add the following code:

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
    
Button btn = findViewById(R.id.btnFlux);

btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view)
    {
        // Do something 
    }
});

Now we have to link our .so file to the project. For that you need to put the .so file in the following structure:

jniLibs_struct

Now create a package named settings inside of com.byeline.hackex and create a new class named SettingsManager in said class.

The code has to be the following:

package com.byeline.hackex.settings;

import java.util.Map;

public class SettingsManager {

    public static native String flux(Map<String, String> map);

    static {
        System.loadLibrary("HackEx");
    }
}

Good! Now let’s edit MainActivity.java and add the following code in our button click listener:

HashMap<String, String> lol = new HashMap<>();
lol.put("parama", "valuea");
String ret = SettingsManager.flux(lol);
Log.i("REVERSING", ret);

We will also put the same code before our click listener as it will let android map the library before we attach with gdb.

We are good to go, press Alt+F10 and it should run on your emulator! At each click, if you check the logcat output in android studio, you should see a signature string:

We are ready to debug. Now we need to get gdb setup and ready to go. On the emulator, GDB server is already present, and since it’s x86_64, I can just use my normal GDB to connect to it, which is awesome.
If we just start the gdb server, and attach to it, we will not get any symbols as my computer doesn’t know about my emulator’s libraries, for this, we will have to pull them locally first. Let’s create a new folder called fs, and pull some stuff in it.

$ mkdir fs
$ cd fs
$ mkdir -p sysroot/system
$ adb pull /system/lib sysroot/system/
/system/lib/: 313 files pulled. 44.8 MB/s (140367100 bytes in 2.988s)
$ adb pull /system/bin sysroot/system
/system/bin/: 299 files pulled. 44.3 MB/s (79616683 bytes in 1.712s)
$ chmod 755 sysroot/system/bin/*

And finally, let’s add libHackEx.so in sysroot/system/lib folder

$ cp ../libHackEx.so sysroot/system/lib/

Now, let’s forward a port to our computer in order to connect gdb to the emulator’s process

$ adb forward tcp:12345 tcp:12345
12345

Now let’s start gdb locally. I use gdb peda for my reversing/exploit dev needs as it greatly increase the output of gdb, I suggest you do the same if you use stock gdb.

$ gdb -q
gdb-peda$

Now run the following commands to prepare gdb:

gdb-peda$ set sysroot sysroot
gdb-peda$ set solib-search-path sysroot/system/lib/
gdb-peda$ handle SIG33 nostop noprint

You can add these commands in your ~/.gdbinit file so you don’t have to retype them everytime you start gdb during this reversing study.

Finally:

gdb-peda$ file sysroot/system/bin/app_process
Reading symbols from sysroot/system/bin/app_process...
Reading symbols from .gnu_debugdata for /home/void/articles/reversing_hackex/fs/sysroot/system/bin/app_process...
(No debugging symbols found in .gnu_debugdata for /home/void/articles/reversing_hackex/fs/sysroot/system/bin/app_process)

Open a new terminal, make sure the apk is running on the emulator and use the following command to get it’s pid

$ adb shell ps |grep "hackex"
u0_a87       10941  1492 1483224  81688 ep_poll    74cbba3792ba S com.byeline.hackex

Perfect, finally let’s use gdbserver to attach to it:

$ adb shell
# gdbserver64 --attach :12345 10941
Attached; pid = 10941
Listening on port 12345

Locally, run this command in gdb:

gdb-peda$ target remote :12345
(Reading symbols...)

And we arrive at this screen:

And now let’s set a breakpoint at Java_com_byeline_hackex_settings_SettingsManager_flux

gdb-peda$ b Java_com_byeline_hackex_settings_SettingsManager_flux
Breakpoint 1 at 0x74cb9fb916e0
gdb-peda$ c
Continuing.

Now click the button and wabam! We break in the function! Now we could just step instruction by instruction and inspect the registers and the stack to understand the logic, but as we performed static analysis earlier, we can just break at some key point that we found interesting to gain some time and not go insane. For instance, we can break at the address base + 0xeb90 which is the first instruction of our string creation loop we saw earlier. To get the base address of our library, we can use vmmap to get the list of mapped addresses:

gdb-peda$ vmmap libHackEx.so
0x000074cb9fb83000 0x000074cb9fbb9000 r-xp	/data/app/com.byeline.hackex-hii-krBwjRPa-wkgUi9Ldw==/lib/x86_64/libHackEx.so
0x000074cb9fbba000 0x000074cb9fbbe000 r--p	/data/app/com.byeline.hackex-hii-krBwjRPa-wkgUi9Ldw==/lib/x86_64/libHackEx.so
0x000074cb9fbbe000 0x000074cb9fbbf000 rw-p	/data/app/com.byeline.hackex-hii-krBwjRPa-wkgUi9Ldw==/lib/x86_64/libHackEx.so

Let’s break at 0x000074cb9fb83000+0x3b90 and continue:

gdb-peda$ b *0x000074cb9fb83000+0x3b90
Breakpoint 2 at 0x74cb9fb86b90
gdb-peda$ c

Ok, so we hit breakpoint 2, and we are where we want to be! Although we don’t actually care about this loop right? We just want it’s output. Let’s put the breakpoint at base+0xebca instead, and continue the execution. We end up at the instruction lea rbx,[rsp+0x130] so let’s step one time and check the string at rbx:

RBX: 0x7ffe3c8ebd30 ("9gey50rOHWc0Upj2mH16Kd4OSxeGJOgdEpCjBI9G")

w00t! We have our final string! We can confirm that using the past 2 strings, if we xor P with 0x69, we get char 9. Yes!

Now let’s carry on. There’s still a lot of assembly code before the end of that function, and I don’t want to step instruction by instruction until the end, so let’s disassemble this function in gdb, and find a spot to break in.

gdb-peda$ disas Java_com_byeline_hackex_settings_SettingsManager_flux

Remember all those sprintfs ? Since they are at the end of the function, we can assume their purpose is to create the final signature string, let’s check what’s the instruction before them to see if we could break somewhere and see what the sprintf are printing.

Here we can see a function taskClassifier_hash being called, so the signature is a hash of something else? Let’s verify that by runing hashid on a signature string:

$ hashid
6f632ea0f207f2426ea8cef93cd0e6ba76ed608e
Analyzing '6f632ea0f207f2426ea8cef93cd0e6ba76ed608e'
[+] SHA-1 
[+] Double SHA-1 
[+] RIPEMD-160 
[+] Haval-160 
[+] Tiger-160 
[+] HAS-160 
[+] LinkedIn 
[+] Skein-256(160) 
[+] Skein-512(160)

Of course it’s a sha1 string! So the sprintf are just printing a sha1 hash, and the hash is probably made by this taskClassifier_hash function! Let’s break on this call:

gdb-peda$ b *0x000074cba190fe2c
Breakpoint 2 at 0x74cba190fe2c
gdb-peda$ c

And tadam!

RDI: 0x74cba1658aa0 ("sig21568473060002paramavaluea9gey50rOHWc0Upj2mH16Kd4OSxeGJOgdEpCjBI9G")

So, are we really hashing this string to get the signature? Let’s continue, and grab the final signature from android-studio logcat

e2727dd3c39aaf42a75f217c8b21975e72f58a7d&sig2=1568473060002

now let’s sha1 hash the above string manually:

echo -n "sig21568473060002paramavaluea9gey50rOHWc0Upj2mH16Kd4OSxeGJOgdEpCjBI9G" | sha1sum 
e2727dd3c39aaf42a75f217c8b21975e72f58a7d  -

W00t! We got the same hash!

So let’s resume. The function takes the parameters from the hashmap, and construct such a string:

sig2{timestamp}{paramkey}{paramvalue}9gey50rOHWc0Upj2mH16Kd4OSxeGJOgdEpCjBI9G

Then hash it using sha1, and return the hash.

Let’s edit our MainActivity.java code, change the parameters, and click the button:

HashMap<String, String> lol = new HashMap<>();
lol.put("somethingsomething", "chivatoiloveu");
String ret = SettingsManager.flux(lol);
Log.i("REVERSING", ret);

btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view)
    {
        HashMap<String, String> lol = new HashMap<>();
        lol.put("somethingsomething", "chivatoiloveu");
        String ret = SettingsManager.flux(lol);
        Log.i("REVERSING", ret);
    }
});

The logcat output:

I/REVERSING: e4247c3fd5804429ec6388aa748cb3e0cf668d1d&sig2=1568474023095

So, the same way we did earlier, we can confirm if we sha1sum the following string:

sig21568474023095somethingsomethingchivatoiloveu9gey50rOHWc0Upj2mH16Kd4OSxeGJOgdEpCjBI9G

$ echo -ne "sig21568474023095somethingsomethingchivatoiloveu9gey50rOHWc0Upj2mH16Kd4OSxeGJOgdEpCjBI9G" |sha1sum 
4d2fddb7dad43719bff07aa002eaf2272eb6db92  -

Huh, wasn’t that too easy? Let’s fire up gdb and see the hashed string again

0x74cba166a380 ("somethingsomethingchivatoiloveusig215684743411239gey50rOHWc0Upj2mH16Kd4OSxeGJOgdEpCjBI9G")

so now , the params are at the beginning of the string? would it be related to the params length?

After many tests, it seems that if the first char of the parameter is between s and z, and if the length of the parameter is above 2, then the param/value pair is put at the begining of the string, and if it isn’t, then it is put after the timestamp.

So to get the above sha1, we need to hash the following string instead:

somethingsomethingchivatoiloveusig215684740230959gey50rOHWc0Upj2mH16Kd4OSxeGJOgdEpCjBI9G

Which give the following hash:

e4247c3fd5804429ec6388aa748cb3e0cf668d1d

Yay!

Now let’s see what happens when we add multiple parameters. Let’s edit our hashmap and add some more params:

HashMap<String, String> lol = new HashMap<>();
lol.put("somethingsomethingg", "chivatoiloveu");
lol.put("somethingelse", "ilovemealso");
String ret = SettingsManager.flux(lol);
Log.i("REVERSING", ret);

We get:

somethingsomethinggchivatoiloveusomethingelseilovemealsosig215684770776879gey50rOHWc0Upj2mH16Kd4OSxeGJOgdEpCjBI9G

If the first letter of the first param is before s then we get:

somethingelseilovemealsosig21568477173996aomethingsomethinggchivatoiloveu9gey50rOHWc0Upj2mH16Kd4OSxeGJOgdEpCjBI9G

Nice, we are ready to write our script to bot the s**t out of this game. We will try to list all users using the user_victim api endpoint. It requires only one parameter, victim_user_id. My python script therefore is the following:

#!/usr/bin/env python3

import time
import requests
from datetime import datetime
from hashlib import sha1

APIKEY="0A8E520C-0F8A-9F36-8E89-F86E61A45BB1"
url = "https://api.hackex.net/v8/"

def signature(params):
    before_sig2_params = []; after_sig2_params = []

    for p in params:
        k,v=p
        if len(k) >= 2 or k[0] in 'stuvwxyz':
            before_sig2_params += [p]
        else:
            after_sig2_params += [p]
    ts = str(int(datetime.now().timestamp()*1000))

    raw = ""
    for p in before_sig2_params:
        k,v=p
        raw += f"{k}{v}"
    raw += f"sig2{ts}"
    for p in after_sig2_params:
        k,v=p
        raw += f"{k}{v}"

    raw += f"9gey50rOHWc0Upj2mH16Kd4OSxeGJOgdEpCjBI9G"

    return sha1(raw.encode('utf-8')).hexdigest() + f"&sig2={ts}"

def main():

    s = requests.Session()
    s.headers.update({"X-API-KEY": APIKEY})

    i = 7534914
    while True:
        i-=1
        uid = str(i)
        params = [("victim_user_id", uid)]
        req=url +f"user_victim?victim_user_id={uid}&sig="+signature(params)
        try:
            _r = s.get(req)
            r = _r.json()
        except AttributeError:
            print(req)
            exit()
        print(f"id: {uid} | name: {r['user']['username']} | ip: {r['user']['ip']} | money: ${r['user_bank']['checking']}")
        for so in r['user_software']:
            print(f"{so['name']} -> {so['software_level']} |", end="")
        print()

if __name__=='__main__':
    main()
$ python pwn.py 
id: 7534913 | name: R Pay Too | ip: 15.251.62.189 | money: $0
Spyware -> 1 |Firewall -> 1 |Bypasser -> 2 |Password Encryptor -> 1 |
id: 7534912 | name: satheesan 8 | ip: 2.199.98.145 | money: $0
Firewall -> 1 |Bypasser -> 1 |Password Encryptor -> 1 |
id: 7534911 | name: _FatalError_ | ip: 17.107.179.41 | money: $0
Firewall -> 2 |Bypasser -> 1 |Password Cracker -> 10 |Password Encryptor -> 1 |
id: 7534910 | name: janari janari | ip: 234.10.89.19 | money: $0
Spyware -> 1 |Firewall -> 1 |Bypasser -> 2 |Password Encryptor -> 1 |
id: 7534909 | name: 47heck | ip: 12.240.217.17 | money: $0
Antivirus -> 1 |Spyware -> 2 |Firewall -> 1 |Bypasser -> 5 |Password Cracker -> 1 |Password Encryptor -> 1 |
id: 7534908 | name: narashima | ip: 42.230.184.13 | money: $0
Firewall -> 1 |Bypasser -> 1 |Password Encryptor -> 1 |

Conculsion

I hope the little reversing entertained you well. The developers of this game had issues with cheaters for years now, and they keep obfuscating this signature algorithm more and more, but obscurity != security. As long as they don’t change their design, people will be able to cheat in this game.

41 Likes

Damn this is insane!

Really tells the tail of not hardcoding signature strings and relying on something (abeit hard to find!) stored on the client side.

Awesome writeup mate!!! So glad to have you on 0x00sec and thanks for sharing your knowledge to the community! I’m sure everybody here will find this writeup enlightening :facepunch:t3:

5 Likes

Thank you!

I assume the developers though it is so hidden deep inside the guts or the application, nobody would try to get it, but they forgot some people find it fun to reverse such things :slight_smile:

4 Likes

This is insanely well written!
Beautifully organized content and flow, good one :slight_smile:

5 Likes

Great writeup.
A friend tried a similar thing with a .net/nodejs app.
Decompiles just as easily. The goal there was to figure out how it licensed the software. Allowing the software to work without a license it is easy enough, but the license generation is more interesting. You confirmed that one needs to just make another C# app with the functions copied and then printout the intermediate results.

1 Like

damn… awesome write up… thanks for sharing it… we should keep doing this…

2 Likes

Great post. Lot of useful information.

Thanks!

1 Like

OMG! what a write up. @everyone get in here… great job!

2 Likes

I usually read the articles without signing in. I just signed in to like this quality article :stuck_out_tongue: . Just a quick note for anyone that doesn’t have rooted phone you can use this app to download the plain official apk. (It is unmaintained but it still works however)

1 Like

Thank you for this amazing write-up. I enjoyed it !

Great work, big lesson for me.

On a side tangent, some companies and high end businesses, nowadays, actually restrict rooted phones and will not hire you, if you do have one. There is a specific security test that checks to see if the phone is rooted. I have even tested it on some of my own devices - it has not failed in showing which devices are rooted and which are not. How do you plan to get around that with a rooted phone?

-Archangel

I’m not sure you read his comment.

1 Like

Not sure if this is related to my answer. However, I wouldn’t either want to work for a company that checks if your phone is rooted before hiring you and I won’t answer your question to discourage anyone for working for them. Hope it makes sense.

1 Like

I read his comment. Just trying to put this convo onto a new path and to see what @r3llun opinion was. That is why I said “On a side tangent”. I probably should have added “unrelated to your response” to clarify.