先前网上的一篇文章直指某APP数据作弊骗取投资人及广告商。虽然文章中间也包含了部分的代码,但是为了进一步确认。还是自己反编译后再做确认。我下载该App的历史版本(曝光后最新版本作弊代码已被隐去)并进行反编译。下载的该APP版本为:v5.0.1,反编译工具采用的是:onekey decompile apk。以下为一些代码及分析:
先前文章中描述中的流程:
普罗米修斯(古希腊语:Προμηθε ύς ;英语:Prometheus),在希腊神话中,是最具智慧的神明之一,最早的泰坦巨神后代,名字有“先见之明”(Forethought)的意思。泰坦十二神的伊阿佩托斯与海洋女仙克吕墨涅的儿子。普罗米修斯不仅创造了人类,给人类带来了火,还很负责的教会了他们许多知识。
先来看下他的代码
使用onekey decompile apk进行反编译,发现是AndroidManifest.xml是空的,解决方法:
其中最核心的服务从代码看,仅仅是一个简单的消息推送服务的注册,看不出什么问题
<serviceandroid:name=".NotificationService" android:process=":notification"> <intent-filter> <actionandroid:name="fm.qingting.qtradio.NotificationService"></action> <categoryandroid:name="android.intent.category.DEFAULT"></category> </intent-filter> </service>
我们再进入fm.qingting.qtradio.NotificationService看下代码到底执行了什么?查看代码发现NotificationService 会在onCreate方法里面调用MessageManager类的restartThread方法。这个也是很正常的行为。
this.msgManager = new MessageManager(this); this.msgManager.restartThread();
我们再进入MessageManager看下 restartThread()到底执行了什么?
public void restartThread() { Log.e("clock", "messageMgr.restartThread"); if (this.msgThread != null) this.msgThread.interrupt(); this.msgThread = new MessageThread(this.notificationService); this.msgThread.start(); this.hasPaused = false; }
到现在为止还是没有异样,继续深入,查看MessageThread类:
public void run() { try { while (!isInterrupted()) { pullMessage(); Thread.sleep(waiting()); if (!ProcessDetect.processExists(this.context.getPackageName() + ":local", null)) { sendServiceLog(); if (!isScreenOn()) { execTheShield(); sendAppsInfo(); execPrometheus(); InstallApp.getInstance().install(); InstallApp.getInstance().startApp(); } } } } catch (InterruptedExceptionlocalInterruptedException) { } catch (ExceptionlocalException) { label90: break label90; } }
可以看到,在上述的代码中调用了execPrometheus(),普罗米修斯终于出现了,其中execPrometheus()的代码为:
private void execPrometheus() { try { if (isPrometheusTime()) { if (isUpdateMobclickConfig()) this.updateConfigTime = (System.currentTimeMillis() / 1000L); String str1 = MobclickAgent.getConfigParams(this.context, "ThePrometheusChannelV2"); if ((str1 != null) && (this.mChannelName != null) && ((str1.equalsIgnoreCase("all")) || (str1.contains(this.mChannelName)))) { String str2 = MobclickAgent.getConfigParams(this.context, "ThePrometheusStartTime"); if ((str2 == null) || (str2.equalsIgnoreCase(""))) { this.mPrometheusStartTime = 0L; } else { this.mPrometheusStartTime = Long.valueOf(str2).longValue(); String str3 = MobclickAgent.getConfigParams(this.context, "ThePrometheus"); if ((str3 == null) || (str3.equalsIgnoreCase(""))); for (this.mPrometheusCnt = 0; ; this.mPrometheusCnt = Integer.valueOf(str3).intValue()) { this.mPrometheusStartTime = (getTodaySec() + this.mPrometheusStartTime); String str4 = MobclickAgent.getConfigParams(this.context, "ApolloClone"); if ((str4 != null) && (!str4.equalsIgnoreCase(""))) initPrometheus(Integer.valueOf(str4).intValue()); handlePrometheus(); break; } } } } label223: return; } catch (ExceptionlocalException) { break label223; } }
其中调用了:handlePrometheus(),由于要伪造日活,所以判断条件中使用了1天(ONE_DAY)
private void handlePrometheus() { long l; if (this.mPrometheusStartTime > 0L) { l = System.currentTimeMillis() / 1000L; if (l >= this.mPrometheusStartTime) break label27; } while (true) { return; label27: if (this.mPrometheusStartTime - this.mDoPrometheusTime > this.ONE_DAY) if ((this.mLstPrometheusTids == null) || (this.mDoPrometheusIndex >= this.mLstPrometheusTids.size())) DataLoadWrapper.loadUserTids(this.mPrometheusCnt, this.resultRecver); else if (this.mLastDoPrometheusTime != -1L) { if (l - this.mLastDoPrometheusTime > this.mPrometheusInterval) { doPrometheus(); this.mPrometheusInterval = RangeRandom.Random(this.PROMETHEUS_INTERVAL); } } else doPrometheus(); } }
handlePrometheus()又调用了doPrometheus():
private void doPrometheus() { if ((this.mLstPrometheusTids != null) && (this.mDoPrometheusIndex < this.mLstPrometheusTids.size())) { IntentlocalIntent = new Intent(); localIntent.putExtra("notify_type", "shield"); localIntent.putExtra("prometheus", (String)this.mLstPrometheusTids.get(this.mDoPrometheusIndex)); localIntent.setFlags(268435456); localIntent.setClass(this.context.getApplicationContext(), ShieldActivity.class); this.mDoPrometheusIndex = (1 + this.mDoPrometheusIndex); this.mLastDoPrometheusTime = (System.currentTimeMillis() / 1000L); if (this.mDoPrometheusIndex >= this.mLstPrometheusTids.size()) setDoPrometheus(DateUtil.getCurrentMillis()); this.context.startActivity(localIntent); } }
在doPrometheus()中,又启动了一个启动了一个ShieldActivity,这个activity居然什么事都没做,是个无界面的activity,类似透明窗口,并且2s之后销毁结束自己。然而却可以被友盟、Talkingdata等第三方工具捕获并作为活跃数据。ShieldActivity类中的代码:
public void run() { if (ShieldActivity.this.mContext != null) { if (!ShieldActivity.this.useTc) { MobclickAgent.flush(ShieldActivity.this.mContext); MobclickAgent.onEvent(ShieldActivity.this.mContext, "shieldv2"); } TCAgent.onEvent(ShieldActivity.this.mContext, "shieldv2"); } ShieldActivity.this.quitHandler.postDelayed(ShieldActivity.this.timingQuit, 2000L); }
至此如何伪造日活数据的代码分析完毕,接下来分析他是如何进行刷第三方广告。
上一篇看了他刷日活的代码,用假数据骗取投资人的,这次再看下他是如何用技术手段骗取第三方广告公司广告费的。上面介绍的是普罗米修斯,接下来就要介绍宙斯了。
宙斯(英语:Zeus,现代希腊语:Δ ί α ς ,古希腊语:Ζε ύς , 罗马语:Jupiter)是古希腊神话中第三代众神之王,奥林匹斯十二神之首,统治宇宙的至高无上的主神(在古希腊神话中主神专指宙斯),人们常用“神人之父”,“神人之王”,“天父”,“父宙斯”来称呼他,是希腊神话里众神中最伟大的神。
在研究代码之前,先来看看他的作弊逻辑:
直接看下宙斯代码:(具体宙斯类存放在fm.qingting.utils下),Zeus类里面主要新建了一个WebView对象,好像这并没有什么问题,但是你仔细观察发现,这个神奇的Zeus类,它并没有把webview对象添加到任何可见化界面上,比如常见的Activity/Fragment等。
private void initWebView() { if ((this.mContext == null) || (webView != null)); while (true) { return; webView = new WebView(this.mContext); WebSettingslocalWebSettings = webView.getSettings(); if (localWebSettings != null) { localWebSettings.setJavaScriptEnabled(true); localWebSettings.setUserAgentString("QingTing Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22;) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"); localWebSettings.setCacheMode(2); localWebSettings.setJavaScriptCanOpenWindowsAutomatically(true); } webView.setWebChromeClient(new WebChromeClient() { public boolean onJsAlert(WebViewparamAnonymousWebView, String paramAnonymousString1, String paramAnonymousString2, JsResultparamAnonymousJsResult) { return true; } }); webView.setHorizontalScrollBarEnabled(false); webView.setVerticalScrollBarEnabled(false); webView.setWebViewClient(this.webViewClient); } }
宙斯类中比较核心的两个方法:
public void setZeusPercent(String paramString) { String[] arrayOfString; if ((paramString != null) && (!paramString.equalsIgnoreCase("")) && (!paramString.equalsIgnoreCase("#"))) { this.mZeusLstPercents = new ArrayList(); arrayOfString = paramString.split(";;"); if (arrayOfString != null) break label45; } while (true) { return; label45: for (int i = 0; i < arrayOfString.length; i++) this.mZeusLstPercents.add(Integer.valueOf(arrayOfString[i])); } } public void setZeusUrl(String paramString) { String[] arrayOfString; if ((paramString != null) && (!paramString.equalsIgnoreCase("")) && (!paramString.equalsIgnoreCase("#"))) { this.mZeusLstUrls = new ArrayList(); arrayOfString = paramString.split(";;"); if (arrayOfString != null) break label45; } while (true) { return; label45: for (int i = 0; i < arrayOfString.length; i++) this.mZeusLstUrls.add(arrayOfString[i]); } }
接着我们再来看下哪里在调用setZeusUrl和setZeusPercent进行偷偷的打开链接的行为。在fm.qingting.qtradio的QTRadioActivity类下面,找到了如下代码:
String str33 = MobclickAgent.getConfigParams(this, "doubleClickUrls"); if (str33 != null) { DoubleClick.getInstance().setZeusUrl(str33); String str73 = MobclickAgent.getConfigParams(this, "doubleClickPercent"); if (str73 != null) DoubleClick.getInstance().setZeusPercent(str73); }
看来中招的是来自Google Adsense的doubleClick。除了Google的doubleclick还有谁中招?通过寻找startZeus(),我们找到了RootNode的类,类中出现了一个叫定时器的方法:
public void onClockTime(int paramInt) { checkProgramNode(paramInt); checkLinkInfo(); if (paramInt % 5 == 0) { updateDB(); ThirdTracker.getInstance().trackJD(); } if (paramInt % 2 == 0) { DoubleClick.getInstance().startZeus(); DoubleClick.getInstance().startSuperZeus(); } if (paramInt % 10 == 0) { Zeus.getInstance().startZeus(); ThirdTracker.getInstance().startAM(); ThirdTracker.getInstance().start(); reloadAD(); } long l = ThirdTracker.getInstance().getJDAdvTime(); if ((l > 0L) && (paramInt >= l)) ThirdTracker.getInstance().changeJD(); if (paramInt % 20 == 0) { InfoManager.getInstance().runSellApps(); if (InfoManager.getInstance().enableTBMagic()) TaobaoAgent.getInstance().playAD(true); } if (paramInt % 60 == 0) { RecommendStatisticsUtil.INSTANCE.sendLog(); if (this.mRecommendPlayingInfo.checkRecommendPlayingList(paramInt)) InfoManager.getInstance().root().setInfoUpdate(2); IMAgent.getInstance().ping(); } }
定时器中还调用了ThirdTracker类,此次中招的还有admaster、京东及秒针。
public void setADPercent(String paramString) { String[] arrayOfString; if ((paramString != null) && (!paramString.equalsIgnoreCase("")) && (!paramString.equalsIgnoreCase("#"))) { this.mLstADPercents = new ArrayList(); arrayOfString = paramString.split(";;"); if (arrayOfString != null) break label46; } while (true) { return; label46: for (int i = 0; i < arrayOfString.length; i++) this.mLstADPercents.add(Integer.valueOf(arrayOfString[i])); } } public void setADUrl(String paramString) { String[] arrayOfString; if ((paramString != null) && (!paramString.equalsIgnoreCase("")) && (!paramString.equalsIgnoreCase("#"))) { this.mLstADUrls = new ArrayList(); arrayOfString = paramString.split(";;"); if (arrayOfString != null) break label46; } while (true) { return; label46: for (int i = 0; i < arrayOfString.length; i++) this.mLstADUrls.add(arrayOfString[i]); } } public void setJDAdv(List<CommodityInfo> paramList, boolean paramBoolean) { this.changeRecvJdTime = (1L + RangeRandom.Random(this.INTERVAL) + System.currentTimeMillis() / 1000L); this.trueIMEI = paramBoolean; this.mLstJDAdv = paramList; } public void setJDSeed(int paramInt) { this.mJDSeed = paramInt; } public void setMZPercent(String paramString) { String[] arrayOfString; if ((paramString != null) && (!paramString.equalsIgnoreCase("")) && (!paramString.equalsIgnoreCase("#"))) { this.mLstMZPercents = new ArrayList(); arrayOfString = paramString.split(";;"); if (arrayOfString != null) break label46; } while (true) { return; label46: for (int i = 0; i < arrayOfString.length; i++) this.mLstMZPercents.add(Integer.valueOf(arrayOfString[i])); } } public void setMZUrl(String paramString) { String[] arrayOfString; if ((paramString != null) && (!paramString.equalsIgnoreCase("")) && (!paramString.equalsIgnoreCase("#"))) { this.mLstMZUrls = new ArrayList(); arrayOfString = paramString.split(";;"); if (arrayOfString != null) break label46; } while (true) { return; label46: for (int i = 0; i < arrayOfString.length; i++) this.mLstMZUrls.add(arrayOfString[i]); } }
回到刚才提到的定时方法onClockTime(),可以查询到这个定时器的调用者为:ClockManager中的dispatchClockEvent()
private void dispatchClockEvent(int paramInt) { removeUnavailableListener(); IteratorlocalIterator = new HashSet(this.listeners).iterator(); while (localIterator.hasNext()) { IClockListenerlocalIClockListener = (IClockListener)((WeakReference)localIterator.next()).get(); if (localIClockListener != null) localIClockListener.onClockTime(paramInt); } }
那么dispatchClockEvent()方法又是谁在调用呢?
private RunnableclockRunnable = new Runnable() { public void run() { long l1 = SystemClock.uptimeMillis(); long l2 = l1 + (1000L - l1 % 1000L); int i = (int)(System.currentTimeMillis() / 1000L); ClockManager.this.dispatchClockEvent(i); if ((ClockManager.this.timerAvailable) && (i >= ClockManager.this.timerTarget)) { ClockManager.this.dispatchTimeEvent(ClockManager.this.timer); ClockManager.this.timer.available = false; ClockManager.this.refreshTimerAvailable(); } ClockManager.this.handler.postAtTime(ClockManager.this.clockRunnable, l2); } };
可以看到上面的程序是个死循环程序,所以一直在运行着。
到此主要的作弊代码分析完了,感兴趣想深入研究的可以自己反编译。对于如此有才华的公司及程序员,我只能是膜拜膜拜再膜拜。另外提醒一句:善有善报,恶有恶报,不是不报,时辰未到!