Android污点分析工具flowdroid源码简析
flowdroid是一款對Android app進行風險分析的應用,下面深入其源碼對其工作的流程進行相關的探究。
1、準備
a)下載相關源碼(包括soot、heros、jasmin、soot-infoflow、soot-infoflow、soot-infoflow-android)到同一文件夾中,使用eclipse將源碼依次導入就完成了整體項目的導入,盡量使用最新版eclipse,如果版本太老,導入后可能會出現各種問題;完成導入后整體項目結構如下所示:
b)本次測試使用的APK是flowdroid本身提供的一個apk:enriched1.apk,位于soot-infoflow-android/testAPKs目錄下,該應用包含一個主Activity,下圖展示了Mainfest.xml文件的具體內容:
主要的操作是在主Activity的onCreate方法中獲取設備ID(DeviceId)然后通過短信的形式發送出去,使用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(風險產生點)與sink(風險觸發點)點,使用flowdroid源碼中提供的:SourceAndSink.txt,位于soot-infoflow-android根目錄下;下面展示本次使用的source點、sink點,從中可以看出source、sink的定義不僅包含了Jimple格式的方法聲明,也包含了調用該方法需要聲明的權限,從上圖的Manifest文件中可以看出,這兩個方法需要使用的權限均已聲明;
<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)回調函數(執行某些回調操作將調用的函數)使用soot-infoflow-android根目錄下的AndroidCallbacks.txt;
e)污染易傳播點,表示當source(風險產生點)經過該函數后,新產生的變量將會成為新的source點,如list = List.add(source1),list將會成為新的source源;該文件使用soot-infoflow根目錄下的EasyTaintWrapperSource.txt文件;
f)本地運行的主函數位于soot-infoflow-android/src/soot.jimple.infoflow.android.TestApps目錄下的Test.java文件中,在Run Configurations中配置待掃描apk文件地址及android.jar目錄地址,配置詳情如下圖所示:
至此一切配置就緒,點擊上圖Run按鈕運行,就會看到flowdroid運行起來了,經過不到一分鐘(這是demo的時間,復雜的應用就呵呵吧)的分析,就能夠看到運行輸入的結果,先來一張運行結果圖:
由于結果太長,不能完全展示結果的內容,下面將結果拷貝下來,用文字進行展示:
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
至此我們已經完成了項目導入及運行一個demo程序,下面正式進入源碼的分析。
2、收集source點、sink點、入口點(Entrypoint)
這個題目是根據執行的第一個關鍵函數(如下)取的,但是這個函數實際的作用其實有一些名不副實,該函數位于soot-infoflow-android/src/soot.jimple.infoflow.android.TestApps/Test.java的642行:
app.calculateSourcesSinksEntrypoints("SourcesAndSinks.txt");
從main()函數的起始點到該函數之間的一些操作,主要是變量初始化、賦值的一些操作,在此不再詳述;進入該函數,該函數主要進行一個操作,根據source、sink文件定義類型,對文件中的內容進行提取,然后進入calculateSourcesSinksEntrypoints(parser)函數,其參數parser就是對source、sink點進行解析后的變量,解析函數:
parser = PermissionMethodParser.fromFile(sourceSinkFile)
這個函數是對txt文件進行解析;進入calculateSourcesSinksEntrypoints這個函數,大致瀏覽下該函數,函數應該是這個本次操作的主函數,對其操作進行拆解,首先執行操作的代碼如下所示:
? ProcessManifest processMan = new ProcessManifest(apkFileLocation);
? this.appPackageName = processMan.getPackageName();
? this.entrypoints = processMan.getEntryPointClasses();
上面代碼的主要作用是反編譯并解析該APK的Manifest.xml文件,生成變量processMan,獲取其packagename并賦值給this.appPackageName=”de.ecspride.reflectionprivacyleak1″,并獲取其程序的入口點,Android應用的入口點實際上就是其定義的四大組件(Activity、Broadcast、Provider、Service),此應用只定義了一個Activity,因此
this.entrypoints=[de.ecspride.reflectionprivacyleak1.ReflectionPrivacyLeak1];
然后使用ARSCFileParser類反編譯apk文件,并賦值給變量resParser,然后進入calculateCallbackMethods(resParser, lfp)函數,其參數resParser表示剛反編譯的apk文件,lfp表示對布局文件的反編譯,屬于類LayoutFileParser,進入calculateCallbackMethods函數,首先看到的就是對soot的一些操作,如下所示:
soot.G.reset();
initializeSoot(true);
createMainMethod();
第一個語句soot.G.reset()是一個標準的soot操作,用于清空soot之前所有操作遺留下的緩存值,后面的代碼中將會多次使用該操作;
第二個語句initializeSoot(true)的完整函數如下所示,參數true表示是否構建程序調用圖(控制流圖);該函數主要作用是反編譯.dex文件,反編譯生成jimple文件,為查看方便,函數的說明將會在代碼注解中詳細給出;
private void initializeSoot(boolean constructCallgraph) {
? ? ? //相當于soot命令:-no-bodies-for-excluded,表示不加載未被包含的類
? ? ? Options.v().set_no_bodies_for_excluded(true);
? ? ? //相當于soot命令:-allow-phantom-refs,表示是否加載未被解析的類
? ? ? Options.v().set_allow_phantom_refs(true);
? ? ? //相當于soot命令:-f FORMAT -output-format FORMAT,設置輸出的格式,此處設置為不輸出,因此不會輸出反編譯后的jimple文件
? ? ? Options.v().set_output_format(Options.output_format_none);
? ? ? //相當于soot命令:-w -whole-program,以全局應用的模式運行
? ? ? Options.v().set_whole_program(constructCallgraph);
? ? ? //相當于soot命令:-process-path DIR -process-dir DIR,待反編譯文件所在的文件夾,此處是apk文件地址
? ? ? Options.v().set_process_dir(Collections.singletonList(apkFileLocation));
? ? ? if (forceAndroidJar)
? ? ? ? ?//相當于soot命令:-android-jars PATH,表示在該路徑下尋找android.jar文件
? ? ? ? ?Options.v().set_force_android_jar(androidJar);
? ? ? else
? ? ? ? ?//相對于soot命令:-force-android-jar PATH,表示強制在該路徑下尋找android.jar文件
? ? ? ? ?Options.v().set_android_jars(androidJar);
? ? ? //相當于soot命令:-src-prec FORMAT,表示反編譯后文件的生成文件類型,此處為jimple類型
? ? ? Options.v().set_src_prec(Options.src_prec_apk_class_jimple);
? ? ? //是否記錄代碼所在行
? ? ? Options.v().set_keep_line_number(false);
? ? ? //是否記錄代碼偏移量
? ? ? Options.v().set_keep_offset(false);
? ? ??
? ? ? //設置上述另一的變量類型,該設置需要保證在soot反編譯之前進行
? ? ? if (sootConfig != null)
? ? ? ? ?sootConfig.setSootOptions(Options.v());
? ? ??
? ? ? Options.v().set_soot_classpath(getClasspath());
? ? ? Main.v().autoSetOptions();
? ? ??
? ? ? // 構建控制流圖選項,默認是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文件,并將反編譯后的文件加載到內存中
? ? ? Scene.v().loadNecessaryClasses();
? ?}
初始化soot完成后,進入第三個語句createMainMethod(),其代碼如下所示;其主要的操作是構造一個虛擬的main方法,并將入口點(entrypoint)相關類添加到這個虛方法中;
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方法中的時候,由于entryponit是Android的四大組件,因此在塞入main方法中的時候需要對組件進行建模,建模的方法是根據組件的生命周期(onCreate、onStart、onResume、onPause、onStop、onRestart、onDestory),依次塞入其相關的方法,下面給出生成的dummyMainMethod方法主體函數,可以從中感受下。
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;
? ? }
接下來便是進行回調函數的收集,主要的操作函數如下所示:
? 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中的回調函數添加到this.callbackMethods變量中,這里執行的函數在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)>]},
從中可以看出添加的回調函數主要是該Activity生命周期中的函數,這樣在上面生成的虛main方法調用到onCreate這些方法時,就會到this.callbackMethods中尋找相關的方法;隨后還會進入xml文件中分析相關的回調函數,這里沒有涉及。
calculateCallbackMethods(resParser, lfp)函數執行完成后,跳轉回calculateSourcesSinksEntrypoints()函數,至此完成了對Entry point、回調函數的收集,下一步就是完成source、sink的收集,這一步的操作并不涉及soot的相關操作,只是將SourceAndSink.txt文件中包含的source、sink點,封裝到sourceSinkManager中,具體在代碼中搜索source、sink點,在數據流追蹤中完成;
? ? ? {
? ? ? ? ?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());
? ? ? }
到此已經完成了第一步的操作,主要是做一些分析前的準備工作,包括收集入口點(entrypoint)、回調函數(callback)、source點、sink點,為后面的數據流分析做準備。
3、數據流分析
數據流的分析主要依賴heros工具,可能大家有些時候對heros、jasmin與soot的關系理不大清,heros、jasmin是基于soot開發的工具,相當于soot的插件,不能獨立運行,因為沒有自己的main調用方法,目前下載最新版的編譯后的soot.jar里面默認是包含這兩個工具的。
上面進行初步的分析工作,下面將正式進行數據流的分析,即執行完Test.java/runAnalysis()方法的642行app.calculateSourcesSinksEntrypoints(“SourcesAndSinks.txt”)后,繼續向下執行,然后執行到數據流分析的入口點,651行:
final InfoflowResults res = app.runInfoflow(new MyResultsAvailableHandler()),
進入runInfoflow()函數,函數的前半部分主要進行一些賦值、初始化的操作,其中比較重要的是實例化一個info變量,該變量是Infoflow類的實例對象,然后使用info.computeInfoflow(apkFileLocation, path, entryPointCreator, sourceSinkManager)進行實際的分析,四個參數的含義為:
? ?apkFileLocation:待分析apk文件的地址;
? ?path:android.jar文件的地址,用于后面反編譯使用;
? ?entryPointCreator:前面獲得的應用的入口函數;
? ?sourceSinkManager:從SourceAndSink.txt文件中獲取的source點與sink點,一共包括89個source點、133個sink點;
進入該函數,其代碼如下所示,從diamante中可以看出,其操作跟2中的操作相類似:初始化soot,然后構造虛擬main方法并設置為入口點,最后使用runAnalysis()方法進行分析。
?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);
? ?}
進入runAnalysis函數,首先第一個比較重要的操作就是使用soot構造控制流圖:constructCallgraph();先進入該函數,為方便說明,將在代碼中對關鍵部分進行注解;通過該方法生成控制流圖(callgraph)后,獲取控制流圖變量:
CallGraph appCallGraph = Scene.v().getCallGraph();
控制流圖在整個分析中非常關鍵,后面將有獨立的章節介紹控制流圖。
?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)的類轉化成虛類
? ? ? ? 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)的關鍵步驟
? ? ? ? ? ?PackManager.v().getPack("cg").apply();
? ? ? }
? ? ??
? ? ? // If we don't have a FastHierarchy, we need to create it
? ? ? // 構造整個應用的層級關系
? ? ? hierarchy = Scene.v().getOrMakeFastHierarchy();
? ? ??
? ? ? // Run the preprocessors
? ? ? ? for (PreAnalysisHandler tr : preProcessors)
? ? ? ? ? ? tr.onAfterCallgraphConstruction();
? ?}
下面就是構建最關鍵的ICFG圖,我把它叫做數據流圖,數據流圖的構成推薦大家看flowdroid推薦的相關論文,很經典的算法,想要把它講明白需要很大的篇幅,如果有機會,我會單獨寫一篇關于數據流圖構成的文章,原論文中的數據流圖在圖形展示上會給人造成一些誤解,容易造成混淆。生成ICFG代碼如下所示,此處不再深入此代碼。
? ?iCfg = icfgFactory.buildBiDirICFG(config.getCallgraphAlgorithm(), config.getEnableExceptionTracking());
使用ICFG進行數據流分析,其代碼是非常冗長的,但是這些代碼大部分都是一些初始化的工作,看多了很容易把你繞暈,個人覺得這里面的關鍵操作主要在于兩點:
一是對于source、sink點的統計,其代碼如下所示;遍歷應用所有的方法,然后使用scanMethodForSourcesSinks函數對方法內的source、sink進行統計,并返回該方法中包含的sink點的數量;
? ?for (SootMethod sm : getMethodsForSeeds(iCfg))
? ? ? sinkCount += scanMethodForSourcesSinks(sourcesSinks, forwardProblem, sm);
進入scanMethodForSourcesSinks方法,源碼如下所示;其主要操作是通過判斷方法是否存在方法體,如果存在方法體,則遍歷方法體中的所有語句,soot中定義為Unit對象,可以將其強制轉化為Stmt對象,可以理解為jimple形式的java語句,然后判斷該語句是否在source、sink中包含,被包含的話,如果是source點,則首先將其作為初始0向量(ICFG圖起始點)存入zeroValue中,然后保存到collectedSources容器中;如果是sink點則直接存儲到collectedSinks容器中,這個尋找source點的方法,在后面介紹控制流圖的時候會使用到。
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;
? ?}
二是調用前向追蹤方法:forwardSolver.solve(),確認source點到sink點是否存在聯通的數據路徑,如果存在則認為是一個風險點。前向追蹤的算法主要在heros中實現,此處不再展開;最終分析的結果保存在results變量中,通過以下方法將結果打印出來,源碼如下所示:
?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);
? ? ? ? ? ? }
? ? ? ? ?}
? ? ? }
? ?}
至此我們比較簡單的介紹了flowdroid的執行流程,對于控制流圖的算法及heros的具體實現并沒有做更深入的介紹,后續可能作為獨立的文章進行分析。
4、優化
flowdroid無論從算法、實現上,還是從效果上都堪稱是一款非常牛逼的產品,但是他也有個非常大的問題就是,太耗內存,分析時間太長,實際使用的價值很低,因此我常常稱它為一個實驗室的產品。那么有什么優化方法呢,首先需要明白造成flowdroid分析耗時的原因,其實無非就是apk較大時,代碼量太大,造成數據流圖(ICFG)呈現爆炸式增長,那么很明顯的一條思路就是削減ICFG圖的體量,我曾經的一個方法是只對幾個相關聯的文件構造ICFG圖,這樣就使得ICFG圖體量呈現幾何式的下降,分析速度肯定明顯提升,但是這個方法比較適用于對風險進行驗證,并不適用于分析。
最近在研究其他源碼掃描工具(如我上篇文章的RIPS)的時候發現,這些工具在進行源碼掃描的時候并沒有進行所謂的數據流分析,更多的只是對調用關系進行分析。誠然數據流分析能夠減少很多誤報,但是這些誤報在我們進行漏洞驗證的時候可能很容易就排除掉,這樣只使用控制流圖進行風險分析看起來也是個不錯的想法。進行控制流分析首先要獲取Android應用的控制流圖,下面的代碼展示如何使用soot構造Android應用的控制流圖,相關說明上文均有提及,此處不再進行詳細說明。
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();
獲取控制流圖后,下一步就是確定圖中的兩個點是否存在連線,假設這兩個點為我們定義的source點、sink點,如果存在連線,即source點與sink點之間存在調用關系,那么就可以作為一個風險點拋出。那么如何確定是否存在調用關系,查看CallGraph的源碼中是否定義了相關方法,發現存在findEdge這個方法,該方法用于尋找某個語句對于某個方法是否存在調用調用關系,如果把第一個參數u定義為一個sink點,即調用點,第二個參數callee定義為一個source點,那么便定義了他們之間的一種調用關系。unit的獲取可以通過上面介紹的獲取應用內source、sink點的方法獲取。
?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;
? ? }
注:使用控制流分析本人并沒有親自試驗,可能存在問題,望見諒。
原文地址: http://www.freebuf.com/sectool/137435.html
總結
以上是生活随笔為你收集整理的Android污点分析工具flowdroid源码简析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: FlowDroid工具的构建与运行
- 下一篇: 细说反射,Java 和 Android