Skip to content

Java: Insecure Loading of Class in Android App without Package Signature Checking #14752

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

Merged
merged 17 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package poc.sample.classloader;

import android.app.Application;
import android.content.pm.PackageInfo;
import android.content.Context;
import android.util.Log;

public class BadClassLoader extends Application {
@Override
public void onCreate() {
super.onCreate();
for (PackageInfo p : getPackageManager().getInstalledPackages(0)) {
try {
if (p.packageName.startsWith("some.package.")) {
Context appContext = createPackageContext(p.packageName,
CONTEXT_INCLUDE_CODE | CONTEXT_IGNORE_SECURITY);
ClassLoader classLoader = appContext.getClassLoader();
Object result = classLoader.loadClass("some.package.SomeClass")
.getMethod("someMethod")
.invoke(null);
}
} catch (Exception e) {
Log.e("Class loading failed", e.toString());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package poc.sample.classloader;

import android.app.Application;
import android.content.pm.PackageInfo;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;

public class GoodClassLoader extends Application {
@Override
public void onCreate() {
super.onCreate();
PackageManager pm = getPackageManager();
for (PackageInfo p : pm.getInstalledPackages(0)) {
try {
if (p.packageName.startsWith("some.package.") &&
(pm.checkSignatures(p.packageName, getApplicationContext().getPackageName()) == PackageManager.SIGNATURE_MATCH)
) {
Context appContext = createPackageContext(p.packageName,
CONTEXT_INCLUDE_CODE | CONTEXT_IGNORE_SECURITY);
ClassLoader classLoader = appContext.getClassLoader();
Object result = classLoader.loadClass("some.package.SomeClass")
.getMethod("someMethod")
.invoke(null);
}
} catch (Exception e) {
Log.e("Class loading failed", e.toString());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>

<overview>
<p>
If an application loads classes or code from another app based solely on its package name without
first checking its package signature, this could allow a malicious app with the same package name
to be loaded through "package namespace squatting".
If the victim user install such malicious app in the same device as the vulnerable app, the vulnerable app would load
classes or code from the malicious app, potentially leading to arbitrary code execution.
</p>
</overview>

<recommendation>
<p>
Verify the package signature in addition to the package name before loading any classes or code from another application.
</p>
</recommendation>

<example>
<p>
The <code>BadClassLoader</code> class illustrates class loading with the <code>android.content.pm.PackageInfo.packageName.startsWith()</code> method without any check on the package signature.
</p>
<sample src="BadClassLoader.java" />
<p>
The <code>GoodClassLoader</code> class illustrates class loading with correct package signature check using the <code>android.content.pm.PackageManager.checkSignatures()</code> method.
</p>
<sample src="GoodClassLoader.java" />
</example>


<references>
<li>
<a href="https://blog.oversecured.com/Android-arbitrary-code-execution-via-third-party-package-contexts/">
Oversecured (Android: arbitrary code execution via third-party package contexts)
</a>
</li>
</references>

</qhelp>
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @name Load 3rd party classes or code ('unsafe reflection') without signature check
* @description Loading classes or code from third-party packages without checking the
* package signature could make the application
* susceptible to package namespace squatting attacks,
* potentially leading to arbitrary code execution.
* @problem.severity error
* @precision high
* @kind path-problem
* @id java/android/unsafe-reflection
* @tags security
* experimental
* external/cwe/cwe-470
*/

import java
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.controlflow.Guards
import semmle.code.java.dataflow.SSA
import semmle.code.java.frameworks.android.Intent

class CheckSignaturesGuard extends Guard instanceof EqualityTest {
MethodCall checkSignatures;

CheckSignaturesGuard() {
this.getAnOperand() = checkSignatures and
checkSignatures
.getMethod()
.hasQualifiedName("android.content.pm", "PackageManager", "checkSignatures") and
exists(Expr signatureCheckResult |
this.getAnOperand() = signatureCheckResult and signatureCheckResult != checkSignatures
|
signatureCheckResult.(CompileTimeConstantExpr).getIntValue() = 0 or
signatureCheckResult
.(FieldRead)
.getField()
.hasQualifiedName("android.content.pm", "PackageManager", "SIGNATURE_MATCH")
)
}

Expr getCheckedExpr() { result = checkSignatures.getArgument(0) }
}

predicate signatureChecked(Expr safe) {
exists(CheckSignaturesGuard g, SsaVariable v |
v.getAUse() = g.getCheckedExpr() and
safe = v.getAUse() and
g.controls(safe.getBasicBlock(), g.(EqualityTest).polarity())
)
}

module InsecureLoadingConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node src) {
exists(Method m | m = src.asExpr().(MethodCall).getMethod() |
m.getDeclaringType().getASourceSupertype*() instanceof TypeContext and
m.hasName("createPackageContext") and
not signatureChecked(src.asExpr().(MethodCall).getArgument(0))
)
}

predicate isSink(DataFlow::Node sink) {
exists(MethodCall ma |
ma.getMethod().hasQualifiedName("java.lang", "ClassLoader", "loadClass")
|
sink.asExpr() = ma.getQualifier()
)
}

predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
exists(MethodCall ma, Method m |
ma.getMethod() = m and
m.getDeclaringType().getASourceSupertype*() instanceof TypeContext and
m.hasName("getClassLoader")
|
node1.asExpr() = ma.getQualifier() and
node2.asExpr() = ma
)
}
}

module InsecureLoadFlow = TaintTracking::Global<InsecureLoadingConfig>;

import InsecureLoadFlow::PathGraph

from InsecureLoadFlow::PathNode source, InsecureLoadFlow::PathNode sink
where InsecureLoadFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "Class loaded from a $@ without signature check",
source.getNode(), "third party library"
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package poc.sample.classloader;

import android.app.Application;
import android.content.pm.PackageInfo;
import android.content.Context;
import android.util.Log;

public class BadClassLoader extends Application {
@Override
public void onCreate() {
super.onCreate();
for (PackageInfo p : getPackageManager().getInstalledPackages(0)) {
try {
if (p.packageName.startsWith("some.package.")) {
Context appContext = createPackageContext(p.packageName,
CONTEXT_INCLUDE_CODE | CONTEXT_IGNORE_SECURITY);
ClassLoader classLoader = appContext.getClassLoader();
Object result = classLoader.loadClass("some.package.SomeClass")
.getMethod("someMethod")
.invoke(null);
}
} catch (Exception e) {
Log.e("Class loading failed", e.toString());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package poc.sample.classloader;

import android.app.Application;
import android.content.pm.PackageInfo;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;

public class GoodClassLoader extends Application {
@Override
public void onCreate() {
super.onCreate();
PackageManager pm = getPackageManager();
for (PackageInfo p : pm.getInstalledPackages(0)) {
try {
if (p.packageName.startsWith("some.package.") &&
(pm.checkSignatures(p.packageName, getApplicationContext().getPackageName()) == PackageManager.SIGNATURE_MATCH)
) {
Context appContext = createPackageContext(p.packageName,
CONTEXT_INCLUDE_CODE | CONTEXT_IGNORE_SECURITY);
ClassLoader classLoader = appContext.getClassLoader();
Object result = classLoader.loadClass("some.package.SomeClass")
.getMethod("someMethod")
.invoke(null);
}
} catch (Exception e) {
Log.e("Class loading failed", e.toString());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
edges
| BadClassLoader.java:15:42:16:75 | createPackageContext(...) : Context | BadClassLoader.java:17:47:17:56 | appContext : Context |
| BadClassLoader.java:17:47:17:56 | appContext : Context | BadClassLoader.java:17:47:17:73 | getClassLoader(...) : ClassLoader |
| BadClassLoader.java:17:47:17:73 | getClassLoader(...) : ClassLoader | BadClassLoader.java:18:37:18:47 | classLoader |
nodes
| BadClassLoader.java:15:42:16:75 | createPackageContext(...) : Context | semmle.label | createPackageContext(...) : Context |
| BadClassLoader.java:17:47:17:56 | appContext : Context | semmle.label | appContext : Context |
| BadClassLoader.java:17:47:17:73 | getClassLoader(...) : ClassLoader | semmle.label | getClassLoader(...) : ClassLoader |
| BadClassLoader.java:18:37:18:47 | classLoader | semmle.label | classLoader |
subpaths
#select
| BadClassLoader.java:18:37:18:47 | classLoader | BadClassLoader.java:15:42:16:75 | createPackageContext(...) : Context | BadClassLoader.java:18:37:18:47 | classLoader | Class loaded from a $@ without signature check | BadClassLoader.java:15:42:16:75 | createPackageContext(...) | third party library |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.ql
Original file line number Diff line number Diff line change
@@ -1 +1 @@
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/springframework-5.3.8/
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/springframework-5.3.8/:${testdir}/../../../../stubs/google-android-9.0.0