
前年在的上一家公司,制造的機(jī)器里應(yīng)用裝有不良廣告,嚴(yán)重影響了兒童客戶使用者的思想健康,導(dǎo)致被人投訴。于是乎,就有了想研發(fā)一款類似于360廣告屏蔽的應(yīng)用的念頭。嗯,事情就是這樣,現(xiàn)在切入主題。
目前市場(chǎng)上有很多安全軟件,它們攔截第三方應(yīng)用廣告的方式都不一樣,比如說有 以so 注入方式來攔截彈出廣告。
現(xiàn)在我們來看下這種方式的詳細(xì)情況:
要做到攔截,首先我們得知道廣告是怎么出來的,原來第三方應(yīng)用大部分是以加入廣告jar形式加入廣告插件,然后在AndroidManifest中聲明廣告service或者在程序中執(zhí)行廣告Api,廣告插件再通過Http請(qǐng)求去加載廣告。在java中,有四種訪問網(wǎng)絡(luò)的接口,如apache的http庫(kù)(如下介紹),這幾種方式首先都會(huì)通過getaddrinfo函數(shù)獲取域名地址,然后通過connect函數(shù)連接到服務(wù)器讀取廣告信息。
- WebView(源碼文件在frameworks/base/core/java/android/webkit/WebView.java)。通過WebView類的void loadUrl(String url)、void postUrl(String url, byte[] postData)、void loadData(String data, String mimeType, String encoding)、void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl)、void evaluateJavascript(String script, ValueCallback resultCallback)等加載網(wǎng)頁(yè)。
- apache-http(源碼目錄在external/apache-http/ , HttpGet 和 HttpPost類)。通過external/apache-http/src/org/apache/http/impl/client/DefaultRequestDirector.java中的DefaultRequestDirector類的HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context)方法執(zhí)行訪問的網(wǎng)絡(luò)的動(dòng)作。
- okhttp(源碼目錄在external/okhttp/)。通過external/okhttp/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java中的HttpEngine類的private void connect(Request request) throws IOException方法連接網(wǎng)絡(luò)。
- URL(源碼在libcore/luni/src/main/java/java/net/URL.java)。通過libcore/luni/src/main/java/java/net/URL.java中的URL類的URLConnection openConnection() throws IOException方法和URLConnection openConnection(Proxy proxy) throws IOException方法連接網(wǎng)絡(luò)。
然后再來說說動(dòng)態(tài)庫(kù)注入,具體什么是動(dòng)態(tài)庫(kù)注入,以及如何注入,網(wǎng)上有很多文章,這里就不介紹。動(dòng)態(tài)庫(kù)注入攔截呢,主要是攔截getaddrinfo,根據(jù)條件返回錯(cuò)誤URL(源碼在libcore/luni/src/main/java/java/net/URL.java)。通過libcore/luni/src/main/java/java/net/URL.java中的URL類的URLConnection openConnection() throws IOException方法和URLConnection openConnection(Proxy proxy) throws IOException方法連接網(wǎng)絡(luò)。攔截網(wǎng)絡(luò)請(qǐng)求,達(dá)到攔截作用。不過需要注意一點(diǎn)就是攔截之前要確定你所攔截的動(dòng)態(tài)庫(kù)是否是你需要攔截的庫(kù)?例如A程序調(diào)用了動(dòng)態(tài)庫(kù)BO和CO,而BO和CO都調(diào)用了connect函數(shù),此時(shí)需要攔截BO的請(qǐng)求,需要注入到BO動(dòng)態(tài)庫(kù)并修改GOT表,而不是注入到CO中。
攔截HTTP方式廣告在多數(shù)廣告包中,應(yīng)用程序首先會(huì)通過apache的http庫(kù)或JDK中的http方法先將廣告數(shù)據(jù)下載過來,然后通過WebView顯示。這種方式通過注入攔截進(jìn)程的/system/lib/libjavacore.so可實(shí)現(xiàn)廣告地址攔截。
攔截WebView方式廣告廣告插件也可以直接通過WebView加載URL,通過分析WebView加載流程可知它的網(wǎng)絡(luò)處理過程均交給libchromium_net庫(kù)來完成。因此通過注入libjavacore.so是無法實(shí)現(xiàn)攔截,而是需要注入到/system/lib/libchromium_net.so。
通過這種方式已經(jīng)完全能夠攔截掉第三方APP廣告,但存在一些問題:
1.廣告商可以通過JNI方式調(diào)用系統(tǒng)getaddrinfo與connect實(shí)現(xiàn)自己的解析與連接過程的動(dòng)態(tài)庫(kù),從而跳過libjavacore.so導(dǎo)致攔截?zé)o效。 2.攔截WebView方式廣告雖然能夠不顯示廣告,但通常仍然會(huì)有浮動(dòng)框顯示”網(wǎng)頁(yè)無法打開”,從而影響美觀。 3.最重要的是我們機(jī)器是沒有root權(quán)限的??!
第三個(gè)問題直接導(dǎo)致了放棄了這種注入做法。 來來去去一段時(shí)間后,目前是采用android 系統(tǒng)本地掃描第三方應(yīng)用廣告形式。具體怎么做,請(qǐng)往下看!
如果對(duì)這種方式不了解的話,建議先看下這篇 Android系統(tǒng)掃描帶廣告應(yīng)用的做法。
所以具體廣告插件掃描方案是匹配包名+類名形式的: 1.掃描本地所有第三方應(yīng)用,列出一個(gè)應(yīng)用中的所有類,將包名+類名方式與廣告插件特征庫(kù)進(jìn)行匹配; 2.將匹配出來的應(yīng)用所帶廣告特征,通過系統(tǒng)提供傳入接口,將這些規(guī)則設(shè)置進(jìn)去。(當(dāng)然,系統(tǒng)代碼是需要改的,做了一些處理,主要是在上面介紹中的幾種訪問網(wǎng)絡(luò)方式上做了判斷處理)。
這種方案的關(guān)鍵在于廣告特征庫(kù)的完善,廣告插件特征庫(kù)收集越全,掃描出來的廣告插件就可以越準(zhǔn)確。所幸,公司有幾位大神,做過類似的事情,所以工作簡(jiǎn)單了多些。
獲取第三方應(yīng)用:
/** * 查詢機(jī)器內(nèi)非本公司應(yīng)用 */
public List<PackageInfo> getAllLocalInstalledApps() {
List<PackageInfo> apps = new ArrayList<PackageInfo>();
if(pManager == null){
return apps;
}
//獲取所有應(yīng)用
List<PackageInfo> paklist = pManager.getInstalledPackages(0);
for (int i = 0; i < paklist.size(); i++) {
PackageInfo pak = (PackageInfo) paklist.get(i);
//屏蔽掉公司內(nèi)部應(yīng)用
//...
//判斷是否為非系統(tǒng)預(yù)裝的應(yīng)用程序
if ((pak.applicationInfo.flags & pak.applicationInfo.FLAG_SYSTEM) <= 0) {
// customs applications apps.add(pak);
}
}
return apps;
}
============================
獲取某個(gè)應(yīng)用的廣告特征:
public static List<String> getClassNameByDex(Context context, String packageName) {
List<String> datalist = new ArrayList<String>();
String path = null;
try {
path = context.getPackageManager().getApplicationInfo(packageName, 0).sourceDir;
// 獲得某個(gè)程序的APK路徑
} catch (NameNotFoundException e) {
e.printStackTrace();
}
try {
if(TextUtils.isEmpty(path)){
return datalist;
}
DexFile dexFile = new DexFile(path);// get dex file of APK
Enumeration<String> entries = dexFile.entries();
while (entries.hasMoreElements()) {// travel all classes
String className = (String) entries.nextElement();
String totalname = packageName + "."+className;
datalist.add(totalname);
}
} catch (IOException e) {
e.printStackTrace();
}
return datalist;
}
=======================================
將應(yīng)用中的所有類名與特征庫(kù)進(jìn)行匹配:
for (PackageInfo info : infolsit) {
if (info == null) {
continue;
}
data = getClassNameByDex(context,info.packageName);
if(data == null){
Log.d(TAG,"getAdFlagForLocalApp() 類名解析出錯(cuò)"+info.packageName);
continue;
}
sgPgmap = new HashMap<String, String>();
for (String clsname : data) {
for (ADSInfo adinfo : flaglist) {
String flag = adinfo.getAdFlag(); //廣告樣本庫(kù)的某一標(biāo)識(shí)
String adpg = adinfo.getAdName(); //廣告樣本庫(kù)的某一包名
if (clsname.contains(adpg)) { //匹配類名與廣告特征庫(kù)里的匹配符,看是否包含關(guān)系
sgPgmap.put(flag,info.packageName);
}
}
}
if(sgPgmap.size() > 0){ //AdsPgInfo 一個(gè)對(duì)應(yīng)應(yīng)用里包含了多少個(gè)標(biāo)識(shí)
adspginfo = new AdsPgInfo(info.packageName, sgPgmap);
pglist.add(adspginfo);
}
}
ps: 在匹配時(shí),有一個(gè)很注意的點(diǎn),有時(shí)候單單類名匹配不準(zhǔn),或者會(huì)漏掉某些廣告,所以應(yīng)該加上包名,再去匹配特征庫(kù)里的匹配符,這樣才能百無一漏。
在此舉例一個(gè)指智廣告的特征(特征顯示形式可自定義,只要符合自己的解析策略即可):
ads.banner.zhidian#指智廣告#com/adzhidian/#ad.zhidian3g.cn
ads.banner.zhidian 為該類型廣告標(biāo)識(shí),主要是為了匹配時(shí)應(yīng)用對(duì)應(yīng)標(biāo)識(shí)的簡(jiǎn)潔性,不用直接跟著一群特征到處跑。。
指智廣告 該廣告名稱
com/adzhidian/ 該廣告用來匹配應(yīng)用中類名的匹配符,當(dāng)應(yīng)用中某一(包名+類名)包含該匹配符時(shí),說明了該應(yīng)用包含該廣告
ad.zhidian3g.cn 需要傳給系統(tǒng)的一個(gè)規(guī)則特征。
匹配出所有應(yīng)用的所屬規(guī)則特征后,接下來需要傳給系統(tǒng)了,系統(tǒng)將滿足需求的幾個(gè)接口提供出來。這邊涉及到修改系統(tǒng)層代碼,我就主要講下實(shí)現(xiàn)思路,會(huì)貼出關(guān)鍵的幾個(gè)代碼。 實(shí)現(xiàn)思路:系統(tǒng)根據(jù)應(yīng)用層傳入的應(yīng)用包名以及規(guī)則,將其緩存,在webview或http處請(qǐng)求時(shí),對(duì)其進(jìn)行判斷處理。
添加某應(yīng)用規(guī)則接:
/** * add Adblock url of package pkgName */
private boolean addAdblockUrlInner(String pkgName, String url) {
synchronized (mAdblockEntries) {
HashMap<String, UrlEntry> pkgEntry = mAdblockEntries.get(pkgName);
if (pkgEntry == null) {
pkgEntry = new HashMap<String, UrlEntry>();
if (pkgEntry == null) {
Slog.e(TAG, "addAdblockUrl():new HashMap<String, UrlEntry>() fail!");
return false;
}
mAdblockEntries.put(pkgName, pkgEntry);
}
UrlEntry entry = pkgEntry.get(url);
if (entry == null) {
pkgEntry.put(url, new UrlEntry(0, false));
} else {
entry.deleted = false;
}
}
return true;
}
==============================
WebView類postUrl處判斷處理:
/**
* Loads the given URL. * * @param url the URL of the resource to load
*/
public void loadUrl(String url) {
checkThread();
if (!isAddressable(url)) {
return;
}
if (DebugFlags.TRACE_API)
Log.d(LOGTAG, "loadUrl=" + url);
if(!isChromium && url.startsWith("file://")){
Log.e("WebView.java", "loadurl setLocalSWFMode");
mProvider.setLocalSWFMode();
}
/**
* Returns true if the url is not included by adblock service
*/
private boolean isAddressable(String url) {
boolean addressable = true;
AdblockManager adblockManager = AdblockManager.getInstance();
if (adblockManager != null) {
String adblockUrl = adblockManager.containedAdblockUrl(ActivityThread.currentPackageName(), url);
if (adblockUrl != null) {
addressable = false;
adblockManager.increaseNumberOfTimes(ActivityThread.currentPackageName(), adblockUrl);
}
}
return addressable;
}
由于系統(tǒng)代碼這部分的改動(dòng)并非是我改的,更深細(xì)節(jié)處的理論就不清楚了。 應(yīng)用層的廣告特征庫(kù)為了可以持續(xù)更新,建議可以做成網(wǎng)絡(luò)更新方式。 據(jù)此,廣告攔截功能實(shí)現(xiàn)就完成了,可能會(huì)有瑕疵,不過持續(xù)優(yōu)化中。
如果覺得此文不錯(cuò),麻煩幫我點(diǎn)下“喜歡”。么么噠!