secure-software-engineering / FlowDroid

FlowDroid Static Data Flow Tracker
GNU Lesser General Public License v2.1
1.05k stars 298 forks source link

CallGraph.edgesInto() doesn't work on permission required methods #525

Closed ZephyrusZhang closed 1 year ago

ZephyrusZhang commented 1 year ago

Hi, Steven! I'm learning to use Flowdroid recently. And now i'm trying to get the call chain of a specified method. I start from the specified function and analyze it backwards, using CallGraph.edgeInto to get the call edges into the function, and get the corresponding caller, and repeat the process until dummyMain. However, there is some problem when i use edgesInto. If the specified method requires permission when running, edgesInto will fail to get any call edges into the method, although it works well when it is applied on methods defined by me or other android api that does not require any permission when running. Since I am still unfamiliar with FlowDroid, I don't if this is a bug or I just set the wrong option or use a inappropriate way to get call chain of a specified method. I would be much grateful if you can offer me some help.

Here is the codes

SootMethod method = Scene.v().getMethod("<android.telecom.TelecomManager: boolean isInCall()>"); // this api requires android.permission.READ_PHONE_STATE perssion
Iterator<Edge> iterator = cg.edgesInto(method);
while (iterator.hasNext()) {
    Edge edge = iterator.next();
    System.out.println(edge.toString());
}
//the code above doesn't print anything

SootMethod method = Scene.v().getMethod("<edu.zephyrus.exampleapp.Clazz: void fun()>");
Iterator<Edge> iterator = cg.edgesInto(method);
while (iterator.hasNext()) {
    Edge edge = iterator.next();
    System.out.println(edge.toString());
}
//the code above print edge infomation correctly

SootMethod method = Scene.v().getMethod("<edu.zephyrus.exampleapp.Clazz: void fun()>");
Iterator<Edge> iterator = cg.edgesInto(method);
while (iterator.hasNext()) {
    Edge edge = iterator.next();
    System.out.println(edge.toString());
}
//the code above print edge infomation correctly

SootMethod method = Scene.v().getMethod("<android.content.ContextWrapper: android.content.Context getBaseContext()>");
Iterator<Edge> iterator = cg.edgesInto(method);
while (iterator.hasNext()) {
    Edge edge = iterator.next();
    System.out.println(edge.toString());
}
//the code above print edge infomation correctly

App code fragment

package edu.example.exampleapp;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Clazz.fun();

        TelecomManager telecom = (TelecomManager) getBaseContext().getSystemService(Context.TELECOM_SERVICE);
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            System.out.println();
        }
        System.out.println(telecom.isInCall());
    }
}

// file Clazz.java
public class Clazz {

    public static void fun() {
        System.out.println("Hello World!");
    }

}
StevenArzt commented 1 year ago

Your approach for obtaining a possible call graph (there might be multiple paths to the same method) is correct. The missing edges are not related to permissions. Instead, these methods are virtual calls on Android system services. You obtain an instance of the TelecomManager class through a factory method getSystemService. This methid is part of the Android SDK, which is not analyzed together with your app. Therefore, our callgraph algorithm never sees an allocation site, i.e., a constructor call, for this variable. Consequently, no type information can be propagated (which class exactly is returned - might be a subclass - , and therefore where the call exactly goes) and thus all edges from instance invocations on that variable are missing.

Analyzing the entire Android SDK together with every app would be a solution, but doesn't scale. Since you're not interested in the concrete class (you have the TelecomManager as public API, you don't care whether it's that class, some internal subclass, etc. that you get back from getSystemService). For such API methods (everything that's android.*), I would simply iterate over all units in all methods in all classes of the app, if the current unit is an invocation statement, check the target via InvokeExpr.getMethod(). You then trace back from that method. Conceptually, we're simply looking at the caller side rather than the callee side.

We could also inject such fake edges into the callgraph at some point. FlowDroid doesn't need them, though, because we have other abstractions for the Android API, e.g., the StubDroid summaries.

ZephyrusZhang commented 1 year ago

Thanks, Steven, but I'm still confused about why Flowdroid doesn't construct call edge to the method I metioned above(isInCall()). Actually, for all methods mentioned in this file, https://github.com/sqlab-sustech/APER-mapping/blob/master/API31/annotations-Mappings.txt, CallGraph.edgesInto() doesn't get approprite results. You said This methid is part of the Android SDK, which is not analyzed together with your app, but other android sdk methods, such as ContextCompact.checkSelfPermission() and Navigation.findNavController(), their call edges are constructed successfully.

Besides, I alse check jmple code of example above, find out that call site of isInCall() does exist, and type of variable telecom is also TelecomManager.

App code fragment

    private AppBarConfiguration appBarConfiguration;
    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Clazz.fun();

        TelecomManager telecom = (TelecomManager) getBaseContext().getSystemService(Context.TELECOM_SERVICE);
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            System.out.println();
        }
        System.out.println(telecom.isInCall());

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        setSupportActionBar(binding.toolbar);

        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
        appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);

        binding.fab.setOnClickListener(view -> Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                .setAction("Action", null).show());
    }

Jimple code of above code

    protected void onCreate(android.os.Bundle)
    {
        android.content.Context $r2;
        android.os.Bundle $r1;
        androidx.navigation.ui.AppBarConfiguration$Builder $r11;
        androidx.navigation.ui.AppBarConfiguration $r13;
        androidx.navigation.NavController $r10;
        com.google.android.material.floatingactionbutton.FloatingActionButton r14;
        int $i0;
        androidx.navigation.NavGraph $r12;
        boolean $z0;
        java.io.PrintStream $r5;
        androidx.coordinatorlayout.widget.CoordinatorLayout $r8;
        android.view.LayoutInflater $r6;
        edu.example.exampleapp.MainActivity r0;
        android.telecom.TelecomManager $r4;
        edu.example.exampleapp.databinding.ActivityMainBinding $r7;
        androidx.appcompat.widget.Toolbar $r9;
        java.lang.Object $r3;
        edu.example.exampleapp.MainActivity$$ExternalSyntheticLambda0 $r15;

    r0 := @this: edu.example.exampleapp.MainActivity;

    $r1 := @parameter0: android.os.Bundle;

    specialinvoke r0.<androidx.appcompat.app.AppCompatActivity: void onCreate(android.os.Bundle)>($r1);

    staticinvoke <edu.example.exampleapp.Clazz: void fun()>();

    $r2 = virtualinvoke r0.<edu.example.exampleapp.MainActivity: android.content.Context getBaseContext()>();

    $r3 = virtualinvoke $r2.<android.content.Context: java.lang.Object getSystemService(java.lang.String)>("telecom");

    $r4 = (android.telecom.TelecomManager) $r3;

    $i0 = staticinvoke <androidx.core.app.ActivityCompat: int checkSelfPermission(android.content.Context,java.lang.String)>(r0, "android.permission.READ_PHONE_STATE");

    if $i0 == 0 goto label1;

    $r5 = <java.lang.System: java.io.PrintStream out>;

    virtualinvoke $r5.<java.io.PrintStream: void println()>();

 label1:
    $r5 = <java.lang.System: java.io.PrintStream out>;

    $z0 = virtualinvoke $r4.<android.telecom.TelecomManager: boolean isInCall()>();

    virtualinvoke $r5.<java.io.PrintStream: void println(boolean)>($z0);

    $r6 = virtualinvoke r0.<edu.example.exampleapp.MainActivity: android.view.LayoutInflater getLayoutInflater()>();

    $r7 = staticinvoke <edu.example.exampleapp.databinding.ActivityMainBinding: edu.example.exampleapp.databinding.ActivityMainBinding inflate(android.view.LayoutInflater)>($r6);

    r0.<edu.example.exampleapp.MainActivity: edu.example.exampleapp.databinding.ActivityMainBinding binding> = $r7;

    $r8 = virtualinvoke $r7.<edu.example.exampleapp.databinding.ActivityMainBinding: androidx.coordinatorlayout.widget.CoordinatorLayout getRoot()>();

    virtualinvoke r0.<edu.example.exampleapp.MainActivity: void setContentView(android.view.View)>($r8);

    $r7 = r0.<edu.example.exampleapp.MainActivity: edu.example.exampleapp.databinding.ActivityMainBinding binding>;

    $r9 = $r7.<edu.example.exampleapp.databinding.ActivityMainBinding: androidx.appcompat.widget.Toolbar toolbar>;

    virtualinvoke r0.<edu.example.exampleapp.MainActivity: void setSupportActionBar(androidx.appcompat.widget.Toolbar)>($r9);

    $r10 = staticinvoke <androidx.navigation.Navigation: androidx.navigation.NavController findNavController(android.app.Activity,int)>(r0, 2131231035);

    $r11 = new androidx.navigation.ui.AppBarConfiguration$Builder;

    $r12 = virtualinvoke $r10.<androidx.navigation.NavController: androidx.navigation.NavGraph getGraph()>();

    specialinvoke $r11.<androidx.navigation.ui.AppBarConfiguration$Builder: void <init>(androidx.navigation.NavGraph)>($r12);

    $r13 = virtualinvoke $r11.<androidx.navigation.ui.AppBarConfiguration$Builder: androidx.navigation.ui.AppBarConfiguration build()>();

    r0.<edu.example.exampleapp.MainActivity: androidx.navigation.ui.AppBarConfiguration appBarConfiguration> = $r13;

    staticinvoke <androidx.navigation.ui.NavigationUI: void setupActionBarWithNavController(androidx.appcompat.app.AppCompatActivity,androidx.navigation.NavController,androidx.navigation.ui.AppBarConfiguration)>(r0, $r10, $r13);

    $r7 = r0.<edu.example.exampleapp.MainActivity: edu.example.exampleapp.databinding.ActivityMainBinding binding>;

    r14 = $r7.<edu.example.exampleapp.databinding.ActivityMainBinding: com.google.android.material.floatingactionbutton.FloatingActionButton fab>;

    $r15 = <edu.example.exampleapp.MainActivity$$ExternalSyntheticLambda0: edu.example.exampleapp.MainActivity$$ExternalSyntheticLambda0 INSTANCE>;

    virtualinvoke r14.<com.google.android.material.floatingactionbutton.FloatingActionButton: void setOnClickListener(android.view.View$OnClickListener)>($r15);

    return;
}

I will be very grateful if you could solve my doubts, and offer me some hlep on how to make FlowDroid construct correct call edge for the methods I want.

StevenArzt commented 1 year ago

The problem is the allocation site. The delcared type is practically irrelevant. Assume the following code:

A a = ?
a.foo();

We don't know the precise type of variable a. It might be A, but it might also be som type derived from A, i.e., a (transitive) subclass. That's why the SPARK callgraph algorithm propagates types from the allocation site. Consider the modified example:

A a = new B();
a.foo();

We now know that the edge is from the call site a.foo() to method B.foo(). Note that an edge to A.foo() would be wrong, because the (overridden) method in B is called.

This leads to a problem when we don't see the allocation site. In the example with the TelecomManager, we never see the constructor call. The object is "magically" returned from a black-box method for which we don't have the code (because it's in the SDK). Therefore, no type information is propagated. Obviously, we could fall back to the declared type TelecomManager, but that is not the case at the moment. Therefore, all calls on the TelecomManager instance are missing from the CG.

I suggested some techniques for resolving the problem above.

ZephyrusZhang commented 1 year ago

Thanks, Steven, I've already understood. After I set call graph construciton algorithm to CHA, it now works well.