Android污点分析工具flowdroid源码简析
flowdroid是一款對(duì)Android app進(jìn)行風(fēng)險(xiǎn)分析的應(yīng)用,下面深入其源碼對(duì)其工作的流程進(jìn)行相關(guān)的探究。
1、準(zhǔn)備
a)下載相關(guān)源碼(包括soot、heros、jasmin、soot-infoflow、soot-infoflow、soot-infoflow-android)到同一文件夾中,使用eclipse將源碼依次導(dǎo)入就完成了整體項(xiàng)目的導(dǎo)入,盡量使用最新版eclipse,如果版本太老,導(dǎo)入后可能會(huì)出現(xiàn)各種問(wèn)題;完成導(dǎo)入后整體項(xiàng)目結(jié)構(gòu)如下所示:
b)本次測(cè)試使用的APK是flowdroid本身提供的一個(gè)apk:enriched1.apk,位于soot-infoflow-android/testAPKs目錄下,該應(yīng)用包含一個(gè)主Activity,下圖展示了Mainfest.xml文件的具體內(nèi)容:
主要的操作是在主Activity的onCreate方法中獲取設(shè)備ID(DeviceId)然后通過(guò)短信的形式發(fā)送出去,使用Jimple代碼表示如下:
$r6 = virtualinvoke $r4.<android.telephony.TelephonyManager: java.lang.String getDeviceId()>()?
virtualinvoke $r10.<android.telephony.SmsManager: void sendTextMessage(java.lang.String,java.lang.String,java.lang.String,android.app.PendingIntent,android.app.PendingIntent)>($r6, $r11, $r12, $r13, $r14)
c)source(風(fēng)險(xiǎn)產(chǎn)生點(diǎn))與sink(風(fēng)險(xiǎn)觸發(fā)點(diǎn))點(diǎn),使用flowdroid源碼中提供的:SourceAndSink.txt,位于soot-infoflow-android根目錄下;下面展示本次使用的source點(diǎn)、sink點(diǎn),從中可以看出source、sink的定義不僅包含了Jimple格式的方法聲明,也包含了調(diào)用該方法需要聲明的權(quán)限,從上圖的Manifest文件中可以看出,這兩個(gè)方法需要使用的權(quán)限均已聲明;
<android.telephony.SmsManager: void sendTextMessage(java.lang.String,java.lang.String,java.lang.String,android.app.PendingIntent,android.app.PendingIntent)> android.permission.SEND_SMS -> _SINK_?
<android.telephony.TelephonyManager: java.lang.String getDeviceId()> android.permission.READ_PHONE_STATE -> _SOURCE_
d)回調(diào)函數(shù)(執(zhí)行某些回調(diào)操作將調(diào)用的函數(shù))使用soot-infoflow-android根目錄下的AndroidCallbacks.txt;
e)污染易傳播點(diǎn),表示當(dāng)source(風(fēng)險(xiǎn)產(chǎn)生點(diǎn))經(jīng)過(guò)該函數(shù)后,新產(chǎn)生的變量將會(huì)成為新的source點(diǎn),如list = List.add(source1),list將會(huì)成為新的source源;該文件使用soot-infoflow根目錄下的EasyTaintWrapperSource.txt文件;
f)本地運(yùn)行的主函數(shù)位于soot-infoflow-android/src/soot.jimple.infoflow.android.TestApps目錄下的Test.java文件中,在Run Configurations中配置待掃描apk文件地址及android.jar目錄地址,配置詳情如下圖所示:
至此一切配置就緒,點(diǎn)擊上圖Run按鈕運(yùn)行,就會(huì)看到flowdroid運(yùn)行起來(lái)了,經(jīng)過(guò)不到一分鐘(這是demo的時(shí)間,復(fù)雜的應(yīng)用就呵呵吧)的分析,就能夠看到運(yùn)行輸入的結(jié)果,先來(lái)一張運(yùn)行結(jié)果圖:
由于結(jié)果太長(zhǎng),不能完全展示結(jié)果的內(nèi)容,下面將結(jié)果拷貝下來(lái),用文字進(jìn)行展示:
Found a flow to sink virtualinvoke $r10.<android.telephony.SmsManager: void sendTextMessage(java.lang.String,java.lang.String,java.lang.String,android.app.PendingIntent,android.app.PendingIntent)>($r6, $r11, $r12, $r13, $r14), from the following sources:
- $r6 = virtualinvoke $r4.<android.telephony.TelephonyManager: java.lang.String getDeviceId()>() (in <de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onCreate(android.os.Bundle)>)
Maximum memory consumption: 141.144008 MB
Analysis has run for 5.030271649 seconds
至此我們已經(jīng)完成了項(xiàng)目導(dǎo)入及運(yùn)行一個(gè)demo程序,下面正式進(jìn)入源碼的分析。
2、收集source點(diǎn)、sink點(diǎn)、入口點(diǎn)(Entrypoint)
這個(gè)題目是根據(jù)執(zhí)行的第一個(gè)關(guān)鍵函數(shù)(如下)取的,但是這個(gè)函數(shù)實(shí)際的作用其實(shí)有一些名不副實(shí),該函數(shù)位于soot-infoflow-android/src/soot.jimple.infoflow.android.TestApps/Test.java的642行:
app.calculateSourcesSinksEntrypoints("SourcesAndSinks.txt");
從main()函數(shù)的起始點(diǎn)到該函數(shù)之間的一些操作,主要是變量初始化、賦值的一些操作,在此不再詳述;進(jìn)入該函數(shù),該函數(shù)主要進(jìn)行一個(gè)操作,根據(jù)source、sink文件定義類(lèi)型,對(duì)文件中的內(nèi)容進(jìn)行提取,然后進(jìn)入calculateSourcesSinksEntrypoints(parser)函數(shù),其參數(shù)parser就是對(duì)source、sink點(diǎn)進(jìn)行解析后的變量,解析函數(shù):
parser = PermissionMethodParser.fromFile(sourceSinkFile)
這個(gè)函數(shù)是對(duì)txt文件進(jìn)行解析;進(jìn)入calculateSourcesSinksEntrypoints這個(gè)函數(shù),大致瀏覽下該函數(shù),函數(shù)應(yīng)該是這個(gè)本次操作的主函數(shù),對(duì)其操作進(jìn)行拆解,首先執(zhí)行操作的代碼如下所示:
? ProcessManifest processMan = new ProcessManifest(apkFileLocation);
? this.appPackageName = processMan.getPackageName();
? this.entrypoints = processMan.getEntryPointClasses();
上面代碼的主要作用是反編譯并解析該APK的Manifest.xml文件,生成變量processMan,獲取其packagename并賦值給this.appPackageName=”de.ecspride.reflectionprivacyleak1″,并獲取其程序的入口點(diǎn),Android應(yīng)用的入口點(diǎn)實(shí)際上就是其定義的四大組件(Activity、Broadcast、Provider、Service),此應(yīng)用只定義了一個(gè)Activity,因此
this.entrypoints=[de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1];
然后使用ARSCFileParser類(lèi)反編譯apk文件,并賦值給變量resParser,然后進(jìn)入calculateCallbackMethods(resParser, lfp)函數(shù),其參數(shù)resParser表示剛反編譯的apk文件,lfp表示對(duì)布局文件的反編譯,屬于類(lèi)LayoutFileParser,進(jìn)入calculateCallbackMethods函數(shù),首先看到的就是對(duì)soot的一些操作,如下所示:
soot.G.reset();
initializeSoot(true);
createMainMethod();
第一個(gè)語(yǔ)句soot.G.reset()是一個(gè)標(biāo)準(zhǔn)的soot操作,用于清空soot之前所有操作遺留下的緩存值,后面的代碼中將會(huì)多次使用該操作;
第二個(gè)語(yǔ)句initializeSoot(true)的完整函數(shù)如下所示,參數(shù)true表示是否構(gòu)建程序調(diào)用圖(控制流圖);該函數(shù)主要作用是反編譯.dex文件,反編譯生成jimple文件,為查看方便,函數(shù)的說(shuō)明將會(huì)在代碼注解中詳細(xì)給出;
private void initializeSoot(boolean constructCallgraph) {
? ? ? //相當(dāng)于soot命令:-no-bodies-for-excluded,表示不加載未被包含的類(lèi)
? ? ? Options.v().set_no_bodies_for_excluded(true);
? ? ? //相當(dāng)于soot命令:-allow-phantom-refs,表示是否加載未被解析的類(lèi)
? ? ? Options.v().set_allow_phantom_refs(true);
? ? ? //相當(dāng)于soot命令:-f FORMAT -output-format FORMAT,設(shè)置輸出的格式,此處設(shè)置為不輸出,因此不會(huì)輸出反編譯后的jimple文件
? ? ? Options.v().set_output_format(Options.output_format_none);
? ? ? //相當(dāng)于soot命令:-w -whole-program,以全局應(yīng)用的模式運(yùn)行
? ? ? Options.v().set_whole_program(constructCallgraph);
? ? ? //相當(dāng)于soot命令:-process-path DIR -process-dir DIR,待反編譯文件所在的文件夾,此處是apk文件地址
? ? ? Options.v().set_process_dir(Collections.singletonList(apkFileLocation));
? ? ? if (forceAndroidJar)
? ? ? ? ?//相當(dāng)于soot命令:-android-jars PATH,表示在該路徑下尋找android.jar文件
? ? ? ? ?Options.v().set_force_android_jar(androidJar);
? ? ? else
? ? ? ? ?//相對(duì)于soot命令:-force-android-jar PATH,表示強(qiáng)制在該路徑下尋找android.jar文件
? ? ? ? ?Options.v().set_android_jars(androidJar);
? ? ? //相當(dāng)于soot命令:-src-prec FORMAT,表示反編譯后文件的生成文件類(lèi)型,此處為jimple類(lèi)型
? ? ? Options.v().set_src_prec(Options.src_prec_apk_class_jimple);
? ? ? //是否記錄代碼所在行
? ? ? Options.v().set_keep_line_number(false);
? ? ? //是否記錄代碼偏移量
? ? ? Options.v().set_keep_offset(false);
? ? ??
? ? ? //設(shè)置上述另一的變量類(lèi)型,該設(shè)置需要保證在soot反編譯之前進(jìn)行
? ? ? if (sootConfig != null)
? ? ? ? ?sootConfig.setSootOptions(Options.v());
? ? ??
? ? ? Options.v().set_soot_classpath(getClasspath());
? ? ? Main.v().autoSetOptions();
? ? ??
? ? ? // 構(gòu)建控制流圖選項(xiàng),默認(rèn)是SPARK
? ? ? if (constructCallgraph) {
? ? ? ? ?switch (config.getCallgraphAlgorithm()) {
? ? ? ? ?case AutomaticSelection:
? ? ? ? ?case SPARK:
? ? ? ? ? ? Options.v().setPhaseOption("cg.spark", "on");
? ? ? ? ? ? break;
? ? ? ? ?case GEOM:
? ? ? ? ? ? Options.v().setPhaseOption("cg.spark", "on");
? ? ? ? ? ? AbstractInfoflow.setGeomPtaSpecificOptions();
? ? ? ? ? ? break;
? ? ? ? ?case CHA:
? ? ? ? ? ? Options.v().setPhaseOption("cg.cha", "on");
? ? ? ? ? ? break;
? ? ? ? ?case RTA:
? ? ? ? ? ? Options.v().setPhaseOption("cg.spark", "on");
? ? ? ? ? ? Options.v().setPhaseOption("cg.spark", "rta:true");
? ? ? ? ? ? Options.v().setPhaseOption("cg.spark", "on-fly-cg:false");
? ? ? ? ? ? break;
? ? ? ? ?case VTA:
? ? ? ? ? ? Options.v().setPhaseOption("cg.spark", "on");
? ? ? ? ? ? Options.v().setPhaseOption("cg.spark", "vta:true");
? ? ? ? ? ? break;
? ? ? ? ?default:
? ? ? ? ? ? throw new RuntimeException("Invalid callgraph algorithm");
? ? ? ? ?}
? ? ? }
? ? ? //使用soot反編譯dex文件,并將反編譯后的文件加載到內(nèi)存中
? ? ? Scene.v().loadNecessaryClasses();
? ?}
初始化soot完成后,進(jìn)入第三個(gè)語(yǔ)句createMainMethod(),其代碼如下所示;其主要的操作是構(gòu)造一個(gè)虛擬的main方法,并將入口點(diǎn)(entrypoint)相關(guān)類(lèi)添加到這個(gè)虛方法中;
private void createMainMethod() {
? ? ? // Always update the entry point creator to reflect the newest set
? ? ? // of callback methods
? ? ? SootMethod entryPoint = createEntryPointCreator().createDummyMain();
? ? ? Scene.v().setEntryPoints(Collections.singletonList(entryPoint));
? ? ? if (Scene.v().containsClass(entryPoint.getDeclaringClass().getName()))
? ? ? ? ?Scene.v().removeClass(entryPoint.getDeclaringClass());
? ? ? Scene.v().addClass(entryPoint.getDeclaringClass());
? ? ??
? ? ? // addClass() declares the given class as a library class. We need to
? ? ? // fix this.
? ? ? entryPoint.getDeclaringClass().setApplicationClass();
? ?}
此處在將entrypoint塞入虛擬main方法中的時(shí)候,由于entryponit是Android的四大組件,因此在塞入main方法中的時(shí)候需要對(duì)組件進(jìn)行建模,建模的方法是根據(jù)組件的生命周期(onCreate、onStart、onResume、onPause、onStop、onRestart、onDestory),依次塞入其相關(guān)的方法,下面給出生成的dummyMainMethod方法主體函數(shù),可以從中感受下。
public static void dummyMainMethod(java.lang.String[])
? ? {
? ? ? ? java.lang.String[] $r0;
? ? ? ? int $i0;
? ? ? ? de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1 $r1;
? ? ? ? $r0 := @parameter0: java.lang.String[];
? ? ? ? $i0 = 0;
? ? ?label1:
? ? ? ? if $i0 == 0 goto label5;
? ? ? ? $r1 = new de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1;
? ? ? ? specialinvoke $r1.<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void <init>()>();
? ? ? ? if $i0 == 1 goto label5;
? ? ? ? virtualinvoke $r1.<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onCreate(android.os.Bundle)>(null);
? ? ?label2:
? ? ? ? virtualinvoke $r1.<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onStart()>();
? ? ?label3:
? ? ? ? virtualinvoke $r1.<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onResume()>();
? ? ? ? virtualinvoke $r1.<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onPause()>();
? ? ? ? if $i0 == 3 goto label3;
? ? ? ? virtualinvoke $r1.<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onStop()>();
? ? ? ? if $i0 == 4 goto label4;
? ? ? ? virtualinvoke $r1.<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onRestart()>();
? ? ? ? if $i0 == 5 goto label2;
? ? ?label4:
? ? ? ? virtualinvoke $r1.<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onDestroy()>();
? ? ?label5:
? ? ? ? if $i0 == 7 goto label1;
? ? ? ? return;
? ? }
接下來(lái)便是進(jìn)行回調(diào)函數(shù)的收集,主要的操作函數(shù)如下所示:
? for (Entry<String, Set<SootMethodAndClass>> entry : jimpleClass.getCallbackMethods().entrySet()) {
? ? ? ? ? ? Set<SootMethodAndClass> curCallbacks = this.callbackMethods.get(entry.getKey());
? ? ? ? ? ? if (curCallbacks != null) {
? ? ? ? ? ? ? ?if (curCallbacks.addAll(entry.getValue()))
? ? ? ? ? ? ? ? ? hasChanged = true;
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ?this.callbackMethods.put(entry.getKey(), new HashSet<>(entry.getValue()));
? ? ? ? ? ? ? ?hasChanged = true;
? ? ? ? ? ? }
? ? ? }
? ? ??
? ? ? if (entrypoints.addAll(jimpleClass.getDynamicManifestComponents()))
? ? ? ? ?hasChanged = true;
? ?}
? ?// Collect the XML-based callback methods
? ?collectXmlBasedCallbackMethods(resParser, lfp, jimpleClass);
其操作是遍歷entrypoint,然后將entry中的回調(diào)函數(shù)添加到this.callbackMethods變量中,這里執(zhí)行的函數(shù)在else中,添加到this.callbackMethods中的值為:
{de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1=
[<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onStart()>,?
<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onDestroy()>,?
<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onPause()>,?
<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onRestart()>,?
<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onResume()>,?
<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onStop()>,?
<de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1: void onCreate(android.os.Bundle)>]},
從中可以看出添加的回調(diào)函數(shù)主要是該Activity生命周期中的函數(shù),這樣在上面生成的虛main方法調(diào)用到onCreate這些方法時(shí),就會(huì)到this.callbackMethods中尋找相關(guān)的方法;隨后還會(huì)進(jìn)入xml文件中分析相關(guān)的回調(diào)函數(shù),這里沒(méi)有涉及。
calculateCallbackMethods(resParser, lfp)函數(shù)執(zhí)行完成后,跳轉(zhuǎn)回calculateSourcesSinksEntrypoints()函數(shù),至此完成了對(duì)Entry point、回調(diào)函數(shù)的收集,下一步就是完成source、sink的收集,這一步的操作并不涉及soot的相關(guān)操作,只是將SourceAndSink.txt文件中包含的source、sink點(diǎn),封裝到sourceSinkManager中,具體在代碼中搜索source、sink點(diǎn),在數(shù)據(jù)流追蹤中完成;
? ? ? {
? ? ? ? ?Set<SootMethodAndClass> callbacks = new HashSet<>();
? ? ? ? ?for (Set<SootMethodAndClass> methods : this.callbackMethods.values())
? ? ? ? ? ? callbacks.addAll(methods);
? ? ? ? ?sourceSinkManager = new AccessPathBasedSourceSinkManager(
? ? ? ? ? ? ? ?this.sourceSinkProvider.getSources(),
? ? ? ? ? ? ? ?this.sourceSinkProvider.getSinks(),
? ? ? ? ? ? ? ?callbacks,
? ? ? ? ? ? ? ?config.getLayoutMatchingMode(),
? ? ? ? ? ? ? ?lfp == null ? null : lfp.getUserControlsByID());
? ? ? ? ?sourceSinkManager.setAppPackageName(this.appPackageName);
? ? ? ? ?sourceSinkManager.setResourcePackages(this.resourcePackages);
? ? ? ? ?sourceSinkManager.setEnableCallbackSources(this.config.getEnableCallbackSources());
? ? ? }
到此已經(jīng)完成了第一步的操作,主要是做一些分析前的準(zhǔn)備工作,包括收集入口點(diǎn)(entrypoint)、回調(diào)函數(shù)(callback)、source點(diǎn)、sink點(diǎn),為后面的數(shù)據(jù)流分析做準(zhǔn)備。
3、數(shù)據(jù)流分析
數(shù)據(jù)流的分析主要依賴(lài)heros工具,可能大家有些時(shí)候?qū)eros、jasmin與soot的關(guān)系理不大清,heros、jasmin是基于soot開(kāi)發(fā)的工具,相當(dāng)于soot的插件,不能獨(dú)立運(yùn)行,因?yàn)闆](méi)有自己的main調(diào)用方法,目前下載最新版的編譯后的soot.jar里面默認(rèn)是包含這兩個(gè)工具的。
上面進(jìn)行初步的分析工作,下面將正式進(jìn)行數(shù)據(jù)流的分析,即執(zhí)行完Test.java/runAnalysis()方法的642行app.calculateSourcesSinksEntrypoints(“SourcesAndSinks.txt”)后,繼續(xù)向下執(zhí)行,然后執(zhí)行到數(shù)據(jù)流分析的入口點(diǎn),651行:
final InfoflowResults res = app.runInfoflow(new MyResultsAvailableHandler()),
進(jìn)入runInfoflow()函數(shù),函數(shù)的前半部分主要進(jìn)行一些賦值、初始化的操作,其中比較重要的是實(shí)例化一個(gè)info變量,該變量是Infoflow類(lèi)的實(shí)例對(duì)象,然后使用info.computeInfoflow(apkFileLocation, path, entryPointCreator, sourceSinkManager)進(jìn)行實(shí)際的分析,四個(gè)參數(shù)的含義為:
? ?apkFileLocation:待分析apk文件的地址;
? ?path:android.jar文件的地址,用于后面反編譯使用;
? ?entryPointCreator:前面獲得的應(yīng)用的入口函數(shù);
? ?sourceSinkManager:從SourceAndSink.txt文件中獲取的source點(diǎn)與sink點(diǎn),一共包括89個(gè)source點(diǎn)、133個(gè)sink點(diǎn);
進(jìn)入該函數(shù),其代碼如下所示,從diamante中可以看出,其操作跟2中的操作相類(lèi)似:初始化soot,然后構(gòu)造虛擬main方法并設(shè)置為入口點(diǎn),最后使用runAnalysis()方法進(jìn)行分析。
?public void computeInfoflow(String appPath, String libPath,
? ? ? ? ?IEntryPointCreator entryPointCreator,
? ? ? ? ?ISourceSinkManager sourcesSinks) {
? ? ? if (sourcesSinks == null) {
? ? ? ? ?logger.error("Sources are empty!");
? ? ? ? ?return;
? ? ? }
? ? ? initializeSoot(appPath, libPath, entryPointCreator.getRequiredClasses());
? ? ? // entryPoints are the entryPoints required by Soot to calculate Graph - if there is no main method,
? ? ? // we have to create a new main method and use it as entryPoint and store our real entryPoints
? ? ? Scene.v().setEntryPoints(Collections.singletonList(entryPointCreator.createDummyMain()));
? ? ? // Run the analysis
? ? ? ? runAnalysis(sourcesSinks, null);
? ?}
進(jìn)入runAnalysis函數(shù),首先第一個(gè)比較重要的操作就是使用soot構(gòu)造控制流圖:constructCallgraph();先進(jìn)入該函數(shù),為方便說(shuō)明,將在代碼中對(duì)關(guān)鍵部分進(jìn)行注解;通過(guò)該方法生成控制流圖(callgraph)后,獲取控制流圖變量:
CallGraph appCallGraph = Scene.v().getCallGraph();
控制流圖在整個(gè)分析中非常關(guān)鍵,后面將有獨(dú)立的章節(jié)介紹控制流圖。
?protected void constructCallgraph() {
? ? ? // Allow the ICC manager to change the Soot Scene before we continue
? ? ? ipcManager.updateJimpleForICC();
? ? ? // Run the preprocessors
? ? ? ? for (PreAnalysisHandler tr : preProcessors)
? ? ? ? ? ? tr.onBeforeCallgraphConstruction();
? ? ? ??
? ? ? ? // Patch the system libraries we need for callgraph construction
? ? ? ? LibraryClassPatcher patcher = new LibraryClassPatcher();
? ? ? ? patcher.patchLibraries();
? ? ??
? ? ? ? // To cope with broken APK files, we convert all classes that are still
? ? ? ? // dangling after resolution into phantoms
? ? ? ? // 將所有未定義(dangling)的類(lèi)轉(zhuǎn)化成虛類(lèi)
? ? ? ? for (SootClass sc : Scene.v().getClasses())
? ? ? ? ?if (sc.resolvingLevel() == SootClass.DANGLING) {
? ? ? ? ? ? sc.setResolvingLevel(SootClass.BODIES);
? ? ? ? ? ? sc.setPhantomClass();
? ? ? ? ?}
? ? ? ??
? ? ? // We explicitly select the packs we want to run for performance
? ? ? ? // reasons. Do not re-run the callgraph algorithm if the host
? ? ? ? // application already provides us with a CG.
? ? ? if (config.getCallgraphAlgorithm() != CallgraphAlgorithm.OnDemand
? ? ? ? ? ? && !Scene.v().hasCallGraph()) {
? ? ? ? ? ?PackManager.v().getPack("wjpp").apply();
? ? ? ? ? ?// 生成控制流圖(callgraph)的關(guān)鍵步驟
? ? ? ? ? ?PackManager.v().getPack("cg").apply();
? ? ? }
? ? ??
? ? ? // If we don't have a FastHierarchy, we need to create it
? ? ? // 構(gòu)造整個(gè)應(yīng)用的層級(jí)關(guān)系
? ? ? hierarchy = Scene.v().getOrMakeFastHierarchy();
? ? ??
? ? ? // Run the preprocessors
? ? ? ? for (PreAnalysisHandler tr : preProcessors)
? ? ? ? ? ? tr.onAfterCallgraphConstruction();
? ?}
下面就是構(gòu)建最關(guān)鍵的ICFG圖,我把它叫做數(shù)據(jù)流圖,數(shù)據(jù)流圖的構(gòu)成推薦大家看flowdroid推薦的相關(guān)論文,很經(jīng)典的算法,想要把它講明白需要很大的篇幅,如果有機(jī)會(huì),我會(huì)單獨(dú)寫(xiě)一篇關(guān)于數(shù)據(jù)流圖構(gòu)成的文章,原論文中的數(shù)據(jù)流圖在圖形展示上會(huì)給人造成一些誤解,容易造成混淆。生成ICFG代碼如下所示,此處不再深入此代碼。
? ?iCfg = icfgFactory.buildBiDirICFG(config.getCallgraphAlgorithm(), config.getEnableExceptionTracking());
使用ICFG進(jìn)行數(shù)據(jù)流分析,其代碼是非常冗長(zhǎng)的,但是這些代碼大部分都是一些初始化的工作,看多了很容易把你繞暈,個(gè)人覺(jué)得這里面的關(guān)鍵操作主要在于兩點(diǎn):
一是對(duì)于source、sink點(diǎn)的統(tǒng)計(jì),其代碼如下所示;遍歷應(yīng)用所有的方法,然后使用scanMethodForSourcesSinks函數(shù)對(duì)方法內(nèi)的source、sink進(jìn)行統(tǒng)計(jì),并返回該方法中包含的sink點(diǎn)的數(shù)量;
? ?for (SootMethod sm : getMethodsForSeeds(iCfg))
? ? ? sinkCount += scanMethodForSourcesSinks(sourcesSinks, forwardProblem, sm);
進(jìn)入scanMethodForSourcesSinks方法,源碼如下所示;其主要操作是通過(guò)判斷方法是否存在方法體,如果存在方法體,則遍歷方法體中的所有語(yǔ)句,soot中定義為Unit對(duì)象,可以將其強(qiáng)制轉(zhuǎn)化為Stmt對(duì)象,可以理解為jimple形式的java語(yǔ)句,然后判斷該語(yǔ)句是否在source、sink中包含,被包含的話,如果是source點(diǎn),則首先將其作為初始0向量(ICFG圖起始點(diǎn))存入zeroValue中,然后保存到collectedSources容器中;如果是sink點(diǎn)則直接存儲(chǔ)到collectedSinks容器中,這個(gè)尋找source點(diǎn)的方法,在后面介紹控制流圖的時(shí)候會(huì)使用到。
private int scanMethodForSourcesSinks(final ISourceSinkManager sourcesSinks, InfoflowProblem forwardProblem, SootMethod m) {
? ? ? if (getConfig().getLogSourcesAndSinks() && collectedSources == null) {
? ? ? ? ?collectedSources = new HashSet<>();
? ? ? ? ?collectedSinks = new HashSet<>();
? ? ? }
? ? ??
? ? ? int sinkCount = 0;
? ? ? if (m.hasActiveBody()) {
? ? ? ? ?// Check whether this is a system class we need to ignore
? ? ? ? ?final String className = m.getDeclaringClass().getName();
? ? ? ? ?if (config.getIgnoreFlowsInSystemPackages()
? ? ? ? ? ? ? ?&& SystemClassHandler.isClassInSystemPackage(className))
? ? ? ? ? ? return sinkCount;
? ? ? ? ?// Look for a source in the method. Also look for sinks. If we
? ? ? ? ?// have no sink in the program, we don't need to perform any
? ? ? ? ?// analysis
? ? ? ? ?PatchingChain<Unit> units = m.getActiveBody().getUnits();
? ? ? ? ?for (Unit u : units) {
? ? ? ? ? ? Stmt s = (Stmt) u;
? ? ? ? ? ? if (sourcesSinks.getSourceInfo(s, iCfg) != null) {
? ? ? ? ? ? ? ?forwardProblem.addInitialSeeds(u, Collections.singleton(forwardProblem.zeroValue()));
? ? ? ? ? ? ? ?if (getConfig().getLogSourcesAndSinks())
? ? ? ? ? ? ? ? ? collectedSources.add(s);
? ? ? ? ? ? ? ?logger.debug("Source found: {}", u);
? ? ? ? ? ? }
? ? ? ? ? ? if (sourcesSinks.isSink(s, iCfg, null)) {
? ? ? ? ? ? ? ?sinkCount++;
? ? ? ? ? ? ? ?if (getConfig().getLogSourcesAndSinks())
? ? ? ? ? ? ? ? ? collectedSinks.add(s);
? ? ? ? ? ? ? ?logger.debug("Sink found: {}", u);
? ? ? ? ? ? }
? ? ? ? ?}
? ? ? }
? ? ? return sinkCount;
? ?}
二是調(diào)用前向追蹤方法:forwardSolver.solve(),確認(rèn)source點(diǎn)到sink點(diǎn)是否存在聯(lián)通的數(shù)據(jù)路徑,如果存在則認(rèn)為是一個(gè)風(fēng)險(xiǎn)點(diǎn)。前向追蹤的算法主要在heros中實(shí)現(xiàn),此處不再展開(kāi);最終分析的結(jié)果保存在results變量中,通過(guò)以下方法將結(jié)果打印出來(lái),源碼如下所示:
?for (ResultSinkInfo sink : results.getResults().keySet()) {
? ? ? logger.info("The sink {} in method {} was called with values from the following sources:",
? ? ? ? ? ? ? ? ?sink, iCfg.getMethodOf(sink.getSink()).getSignature() );
? ? ? for (ResultSourceInfo source : results.getResults().get(sink)) {
? ? ? ? ?logger.info("- {} in method {}",source, iCfg.getMethodOf(source.getSource()).getSignature());
? ? ? ? ?if (source.getPath() != null) {
? ? ? ? ? ? logger.info("\ton Path: ");
? ? ? ? ? ? for (Unit p : source.getPath()) {
? ? ? ? ? ? ? ?logger.info("\t -> " + iCfg.getMethodOf(p));
? ? ? ? ? ? ? ?logger.info("\t\t -> " + p);
? ? ? ? ? ? }
? ? ? ? ?}
? ? ? }
? ?}
至此我們比較簡(jiǎn)單的介紹了flowdroid的執(zhí)行流程,對(duì)于控制流圖的算法及heros的具體實(shí)現(xiàn)并沒(méi)有做更深入的介紹,后續(xù)可能作為獨(dú)立的文章進(jìn)行分析。
4、優(yōu)化
flowdroid無(wú)論從算法、實(shí)現(xiàn)上,還是從效果上都堪稱(chēng)是一款非常牛逼的產(chǎn)品,但是他也有個(gè)非常大的問(wèn)題就是,太耗內(nèi)存,分析時(shí)間太長(zhǎng),實(shí)際使用的價(jià)值很低,因此我常常稱(chēng)它為一個(gè)實(shí)驗(yàn)室的產(chǎn)品。那么有什么優(yōu)化方法呢,首先需要明白造成flowdroid分析耗時(shí)的原因,其實(shí)無(wú)非就是apk較大時(shí),代碼量太大,造成數(shù)據(jù)流圖(ICFG)呈現(xiàn)爆炸式增長(zhǎng),那么很明顯的一條思路就是削減ICFG圖的體量,我曾經(jīng)的一個(gè)方法是只對(duì)幾個(gè)相關(guān)聯(lián)的文件構(gòu)造ICFG圖,這樣就使得ICFG圖體量呈現(xiàn)幾何式的下降,分析速度肯定明顯提升,但是這個(gè)方法比較適用于對(duì)風(fēng)險(xiǎn)進(jìn)行驗(yàn)證,并不適用于分析。
最近在研究其他源碼掃描工具(如我上篇文章的RIPS)的時(shí)候發(fā)現(xiàn),這些工具在進(jìn)行源碼掃描的時(shí)候并沒(méi)有進(jìn)行所謂的數(shù)據(jù)流分析,更多的只是對(duì)調(diào)用關(guān)系進(jìn)行分析。誠(chéng)然數(shù)據(jù)流分析能夠減少很多誤報(bào),但是這些誤報(bào)在我們進(jìn)行漏洞驗(yàn)證的時(shí)候可能很容易就排除掉,這樣只使用控制流圖進(jìn)行風(fēng)險(xiǎn)分析看起來(lái)也是個(gè)不錯(cuò)的想法。進(jìn)行控制流分析首先要獲取Android應(yīng)用的控制流圖,下面的代碼展示如何使用soot構(gòu)造Android應(yīng)用的控制流圖,相關(guān)說(shuō)明上文均有提及,此處不再進(jìn)行詳細(xì)說(shuō)明。
SetupApplication app = new SetupApplication(androidPlatformPath, appPath);
? ? ?app.calculateSourcesSinksEntrypoints("SourcesAndSinks.txt");
? ? ?soot.G.reset();
? ? ?Options.v().set_src_prec(Options.src_prec_apk);
? ? ?Options.v().set_process_dir(Collections.singletonList(appPath));
? ? ?Options.v().set_android_jars(androidPlatformPath);
? ? ?Options.v().set_whole_program(true);
? ? ?Options.v().set_allow_phantom_refs(true);
? ? ?Options.v().setPhaseOption("cg.spark", "on");
? ? ?Scene.v().loadNecessaryClasses();
? ? ?SootMethod entryPoint = app.getEntryPointCreator().createDummyMain();
? ? ?Options.v().set_main_class(entryPoint.getSignature());
? ? ?Scene.v().setEntryPoints(Collections.singletonList(entryPoint));
? ? ?System.out.println(entryPoint.getActiveBody());
? ? ?PackManager.v().runPacks();
? ? ?CallGraph appCallGraph = Scene.v().getCallGraph();
獲取控制流圖后,下一步就是確定圖中的兩個(gè)點(diǎn)是否存在連線,假設(shè)這兩個(gè)點(diǎn)為我們定義的source點(diǎn)、sink點(diǎn),如果存在連線,即source點(diǎn)與sink點(diǎn)之間存在調(diào)用關(guān)系,那么就可以作為一個(gè)風(fēng)險(xiǎn)點(diǎn)拋出。那么如何確定是否存在調(diào)用關(guān)系,查看CallGraph的源碼中是否定義了相關(guān)方法,發(fā)現(xiàn)存在findEdge這個(gè)方法,該方法用于尋找某個(gè)語(yǔ)句對(duì)于某個(gè)方法是否存在調(diào)用調(diào)用關(guān)系,如果把第一個(gè)參數(shù)u定義為一個(gè)sink點(diǎn),即調(diào)用點(diǎn),第二個(gè)參數(shù)callee定義為一個(gè)source點(diǎn),那么便定義了他們之間的一種調(diào)用關(guān)系。unit的獲取可以通過(guò)上面介紹的獲取應(yīng)用內(nèi)source、sink點(diǎn)的方法獲取。
?public Edge findEdge( Unit u, SootMethod callee )
? ? {
? ? ? Edge e = srcUnitToEdge.get(u);
? ? ? while ( e.srcUnit() == u &&
? ? ? ? ? ? e.kind() != Kind.INVALID ) {
? ? ? ? ?if ( e.tgt() == callee )
? ? ? ? ? ? return e;
? ? ? ? ?e = e.nextByUnit();
? ? ? }
? ? ? return null;
? ? }
注:使用控制流分析本人并沒(méi)有親自試驗(yàn),可能存在問(wèn)題,望見(jiàn)諒。
原文地址: http://www.freebuf.com/sectool/137435.html
總結(jié)
以上是生活随笔為你收集整理的Android污点分析工具flowdroid源码简析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: FlowDroid工具的构建与运行
- 下一篇: Part 2 – Deep analys