Attack on APK: Reversing an annoying online gambling APK that showed in my phone as unwanted ads!!

Do not get me wrong, the title is not somewhat japenese light novel or manga but perfectly fit to describe what annoy me this current weeks : unsolicited online gambling ads!

Disclaimer: The site that Yukari want to investigate already reported. Unfortunately, there no real action. Which is there plenty time for us to toying with them. Here the Report.

You still can get the apk here: https://ad-dl-300209[.]idgamefun11[.]online/apk/300209/pp7_300209.apk (do not download it in android).

But if you want to try it with different APK that they serve, here the list https://ad-dl-3002090[.]idgamefun11[.]online/index.js (the another link served as JSON list inside that js file). They very resourceful.

The Artifact

The site is name (double P with 7) which is an online gambling that targeted Android user by the way the showing the invitation. They lure user to download the android APK and the victim will install it, poor users.

Hypothese

Despite the web is alive or blocked. Yukari assuming the APK connect to different domain that serve API Endpoint. Why?

  • The current name domain like stolen or one-time use that would trashed anytime, unstable for API that need stable name.
  • As programmer my self, API should hosted differently to share burden toward user facing page and API server, so when one down by doss, the incident could localize.
  • Yukari figure out, reporting the website doesn't stop the gamble activity since the site enforce user migrate to the APK. If only report the website, then it not address to embbeded domain inside the APK and the activity still going.

So, here Yukari plan:

  • Decompile APK and look what chrunchy yet interesting inside it (Reverse Engineering Time), it recommended to use laptop to prevent accidentally install the APK.
  • Looking for the code that programmed by the developer (skip any third party library that name with google, firebase, etc), to find the entry point, analyze and predict what they behaivor.
  • Find any string that start with URL or anything that cryptic, since Yukari hunt what address the connecting to remote server domain (and to know what their server).

Execution

First, lets decompile APK. For reducing our need to install tools, Yukari will go with https://www.decompiler.com/, then upload there.

FYI: Android APK is simple ZIP archive, it can easily to open. But our main goal is to get the reversed source code which compiled into bytecode inside classes.dex and classes2.dex, so the website will get the job done (if you want do manually, install JADX).

Here the result when we done:

the file tree

Now, let dive into sources/com, in here Yukari will determine where the interesting place:

where insteresting path

Everything looks legit, except ppid7 and SSFun.

  • ppid7, bit similar with the gambling site name.
  • SSFun, IDK but invinting to explore.

Let download the result as zip then visit the ppid7 via VSCode, and here we get the entrypoint ("MainActivity.java"):

package com.ppid7.prod2025;

import android.os.Bundle;
import com.ssfun.commonss.SSFunContext;
import com.ssfun.commonss.SSFun_PoolMainActivity;

public class MainActivity extends SSFun_PoolMainActivity {
    /* access modifiers changed from: protected */
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        OnCoreCreate();
        SSFunContext.SSFun_SetActivity(this);
        SSFunContext.SSFun_SetContext(this);
        Init();
    }
}

From code above, what we got is they redirect to com.ssfun.commonss. It's common practice to make entry point small, but I got bad feeling about this.

what inside Init() function

The code above shown that it use Firebase, constant or string value likely written somewhere (in this case android resources file, my guts tell me because how the function invocation same as call string from resources file.) -> SSFunContext.GetMetaData(SSFunConst.K_META_CHANNEL_ID). They wrapper to accessing metadata and resource, read sources/com/ssfun/commonss/SSFunContext.java for the details.

Yukari opinion: for a gambling app they tooks design pracitce quite well also intentionally separating layer can be smart way to obfuscate they app flow. Yukari conclude SSFuns is somewhat template that will changing the MainActivity but the behaivour still same.

I'am stuck, just for you information that gotten to next paragraf of this article, roughly through ~ an hour or more about finding alternative way and mess around with the code.

In other word, tracing via entrypoint is could be difficult as the developer intented. So, Yukari switching to hunting interesting string to jump off from the developer hand.

To do so, lets looking for constant or any string that indicating URL.

finding any thing related to URL

Gotcha, we found interesting pattern (think like f(x)=blah.x.blah, that we need find the x, where the function is called and what inside it).

Go to sources/com/ssfun/commonss/SSFun_PoolHandler.java line 50 ~ 58 (focus at the two last line):

public void SSFun_CheckOneApi(List<SSFun_PoolListItem> list, int i, int i2) {
    final int size = list.size();
    Log.i(SSFun_Utils.TAG, String.format("CheckOneApi Start!!  apiPoolSize=%d, index=%d, retry=%d", new Object[]{Integer.valueOf(size), Integer.valueOf(i), Integer.valueOf(i2)}));
    if (size <= i) {
        SSFun_CheckFallbackApi();
        return;
    }
final String SSFun_getDomain = list.get(i).SSFun_getDomain();
String str = "https://" + SSFun_getDomain + "/v1/site/domains?rand=" + System.currentTimeMillis();

Now our task is figuring out what SSFun_getDomain string is, but first let me explain bit to you:

  • This function Yukari assume to checking API to remote address.
  • list is collection of SSFUN_PoolListItem class, Nice we got data structure which base of lead to understand how appliction treat data.
    • FYI: They handle API connection by pooling, They must really good at code this implement this pattern manually, usually this done by using third party alone.
  • SSFun_getDomain() likely getter/setter function that generated from Kotlin, which mean it actually function to access an attribute.

Yukari found this way good indication, the SSFUN_PoolListItem is somehow is important to the app. It had been 65 times invoked and used in com.ssfuns.commonss.

where the SSFUN_PoolListItem invoked

The file definiton is at : sources/com/ssfun/commonss/SSFun_PoolListItem.java:

what SSFUN_PoolListItem class

Found what Yukari wanted: domain. Now we need to find where this class serialize data or transforming certain format data into that class object. Lets investigate the search result of SSFUN_PoolListItem one by one, look for code that encode or decode into that class object.

After carefully looking (randomly, that to VSCode preview that save a lot of click), Yukari landed to sources/com/ssfun/commonss/SSFun_XmlPoolHelper.java:

package com.ssfun.commonss;

import com.alibaba.fastjson.JSON;
import java.util.List;

public class SSFun_XmlPoolHelper {
    private static final String XML_KEY = "e12m83+IhY@2s#UB";

    public static List<SSFun_PoolListItem> SSFun_getXmlList() {
        return JSON.parseArray(SSFun_EncryptUtil.SSFun_decrypt(SSFunContext.SSFun_GetResString(SSFunConst.K_STRING_POOL_DATA), XML_KEY), SSFun_PoolListItem.class);
    }
}

Okay we find the code, let Yukari explain:

  • The code above serialize JSON into SSFun_PoolListItem.
  • There invocation of SSFun_decrypt which sign we will dealing with crypthography (but that latter ok?).
  • Luckly, the key is there.
  • It accessing string resource named SSFunConst.K_STRING_POOL_DATA, which if Yukari diving it founded as (look at sources/com/ssfun/commonss/SSFunConst.java):
public static final String K_STRING_POOL_DATA = "pool_data";
  • SSFunContext is basically is just Context from main application that abstracted into SSFunMainApplication, so GetRes_String() basically, context.GetResources().GetString('key') alternative to getString(R.string.key).
  • The pool_data resource value can be found at resources/res/values/strings.xml, with value:
bO4PkHAM8CsULa5JuvXTWl....<trimmed>..egE5cbUGIMMq8=

Then how we figure what inside it?

Let take a look at SSFun_decrypt at sources/com/ssfun/commonss/SSFun_EncryptUtil.java.

the decryption function

It use Base64 encoding with AES, which the latter is quite. AES have different implementation especially in Java, we have the ciphertext and the key, but not IV (Initialization Vector). The Java make that simple by SecretKeySpec.

To handle that and Yukari not want to much bother, Yukari write Java code to handling decryption in JDoodle online compiler with custom main function to print the decrypted result in contained manner like this:

scrapped decryption function in online compiler

Mind you, Yukari change "AES/ECB/PKCS7Padding" into "AES/ECB/PKCS5Padding", this because it not supported by current JDK or else. Yukari figure out when surfing in stackoverflow and found out the commonly use PKCS5Padding instead PKCS7Padding.

the decrypted result

TADA!!! We got the hidden domain address to API Endpoint.

[
  {
    "domain": "id-game-app-0000000001-client-ep.fungame123.online",
    "domainType": 1
  },
  {
    "domain": "id-game-app-0000000002-client-ep.fungame123.pro",
    "domainType": 1
  },
  {
    "domain": "id-game-app-0000000003-client-ep.gameid321.store",
    "domainType": 1
  },
  {
    "domain": "id-game-app-0000000004-client-ep.gameid456.store",
    "domainType": 1
  },
  {
    "domain": "id-game-app-0000000005-client-ep.gameid789.store",
    "domainType": 1
  },
  {
    "domain": "api-id-game-server-ep-0000000001a.fungame123.online",
    "domainType": 2
  },
  {
    "domain": "api-id-game-server-ep-0000000002a.fungame123.pro",
    "domainType": 2
  },
  {
    "domain": "api-id-game-server-ep-0000000003a.gameid321.store",
    "domainType": 2
  },
  {
    "domain": "api-id-game-server-ep-0000000004a.gameid456.store",
    "domainType": 2
  },
  {
    "domain": "api-id-game-server-ep-0000000005a.gameid789.store",
    "domainType": 2
  },
  {
    "domain": "id-game-api-server-ep-0000000006.idfun321.online",
    "domainType": 2
  },
  {
    "domain": "id-game-api-server-ep-0000000007.idfun321.pro",
    "domainType": 2
  },
  {
    "domain": "id-game-api-server-ep-0000000008.idfun321.store",
    "domainType": 2
  },
  {
    "domain": "client-id-game-app-ep-0000000006c.idfun321.online",
    "domainType": 1
  },
  {
    "domain": "client-id-game-app-ep-0000000007c.idfun321.pro",
    "domainType": 1
  },
  {
    "domain": "client-id-game-app-ep-0000000008c.idfun321.store",
    "domainType": 1
  },
  {
    "domain": "pp7-game-prod.oss-ap-southeast-5.aliyuncs.com",
    "domainType": 2,
    "isFallback": 1
  }
]

Conclusion

The developer of the apps is pretty smart to obfuscate the APK domain URL to make difficult for outsider figuring out by scrambling it with AES encryption and extra abstraction layer in the apps itself. They took great care in the app code it self.

Also the result of APK analysis prove that Yukari hipotheses correct:

  • The APK had a lot of backup domain for API Endpoint to preventing them from taken down, they seriously thinking about avaibility and also the domain not same as the websites.
  • From this result, Yukari conclude that APK more reliable that websites because it complex mechanism to failsafe to another domain in order to fetching data.

So, blocking online gambling is not enough just to block the domain and call it a day. We need to address underlying APK that store dozen of URL for effectively do the blocking.

Now let report it to aduankonten.id.