-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Java: Insecure Loading of Class in Android App without Package Signature Checking #14752
Conversation
Minor correction. I referred #5435 to when I meant to refer to #4947, in CWE-094, which also identified vulnerability in https://github.com/oversecured/ovaa. |
This blog https://blog.oversecured.com/Android-arbitrary-code-execution-via-third-party-package-contexts/ gives an overview for the type of vulnerability this PR meant to identify. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @masterofnow, thanks for your contribution.
First of fall, I made some suggestions below simply about code optimization. Note that I:
- renamed the deprecated
MethodAccess
to the newMethodCall
. - Used
hasQualifiedName
to combine package, type and name checks forMethod
s. Also used string lists instead of disjunctions. - Used the predicates' result types to avoid unnecessary
instanceof
checks and casts. - Removed superfluous predicates.
- Removed unnecessary
exists
variables where possible.
Now, the query will still need work after all that. You're combining global flow, local flow, syntactic AST variable relations, and CFG relations to determine flow between two expressions in several predicates (namely, getClassLoaderReachableMethodAccess
, getDangerousReachableMethodAccess
, isSignaturesChecked
, and the select
clause). I'd recommend picking one mechanism and sticking to that everywhere, since the logic is the same all the time. If you don't want to use several global flow configurations (which would admittedly be overkill), you can stick to 1 global flow configuration + local flow.
Also, take another look at your SignaturePackageConfig
, because I don't think it's doing what you want it to do (the source being an argument doesn't look right to me).
If you have doubts, we can further discuss all that once you've applied this first round of suggestions.
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.ql
Outdated
Show resolved
Hide resolved
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.ql
Outdated
Show resolved
Hide resolved
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.ql
Show resolved
Hide resolved
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.ql
Outdated
Show resolved
Hide resolved
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.ql
Outdated
Show resolved
Hide resolved
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.ql
Outdated
Show resolved
Hide resolved
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.ql
Outdated
Show resolved
Hide resolved
Added suggestion from atorralba. Co-authored-by: Tony Torralba <atorralba@users.noreply.github.com>
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.ql
Outdated
Show resolved
Hide resolved
MethodCall maCreatePackageContext, LocalVariableDeclExpr lvdePackageContext, | ||
Expr sinkPackageContext, MethodCall maGetMethod, MethodCall maInvoke | ||
MethodAccess maCreatePackageContext, LocalVariableDeclExpr lvdePackageContext, | ||
DataFlow::Node sinkPackageContext, MethodAccess maGetMethod, MethodAccess maInvoke |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed Expr sinkPackageContext
to DataFlow::Node sinkPackageContext
, due to the following error:
Error: Failed to run query: ERROR: Expr::Expr is not compatible with DataFlowNodes::Public::Node
TaintTracking::localExprTaint(lvdePackageContext.getAnAccess(), sinkPackageContext) and | ||
getClassLoaderReachableMethodCall(sinkPackageContext) = maGetMethod and | ||
getGetMethodMethodCall(maGetMethod) = maInvoke | ||
TaintTracking::localTaint(DataFlow::exprNode(lvdePackageContext.getAnAccess()), sinkPackageContext) and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
TaintTracking::localExprTaint(lvdePackageContext.getAnAccess(), sinkPackageContext) and
to
TaintTracking::localTaint(DataFlow::exprNode(lvdePackageContext.getAnAccess()), sinkPackageContext) and
to resolve the following error
Error: Failed to run query: ERROR: DataFlowNodes::Public::Node is not compatible with Expr::Expr
Hi @atorralba, thanks for your code review and suggestion. I have commited all your suggestion. As a result, I had to do some minor adjustment and push a new commit. I have added some comments in the code change to explain why they were needed. My codeql version in vscode is as follows:
|
(maCreatePackageContext.getCallee().getDeclaringType().getQualifiedName() = "android.content.ContextWrapper" or | ||
maCreatePackageContext.getCallee().getDeclaringType().getQualifiedName() = "android.content.Context") and | ||
maCreatePackageContext.getCallee().getName() = "createPackageContext" and | ||
sink.asExpr() = maCreatePackageContext.getArgument(0) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(maCreatePackageContext.getCallee().getDeclaringType().getQualifiedName() = "android.content.ContextWrapper" or | |
maCreatePackageContext.getCallee().getDeclaringType().getQualifiedName() = "android.content.Context") and | |
maCreatePackageContext.getCallee().getName() = "createPackageContext" and | |
sink.asExpr() = maCreatePackageContext.getArgument(0) | |
) | |
maCreatePackageContext.getMethod().hasQualifiedName("android.content", ["ContextWrapper", "Context"], "createPackageContext") and | |
sink.asExpr() = maCreatePackageContext.getArgument(0) | |
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, now that the code is somewhat tidy (and please, remember to autoformat your code in VSCode using > Format Document
), let's reason about your intentions.
The pattern you're trying to model is:
Context appContext = context.createPackageContext(packageName, ...); // source
ClassLoader classLoader = appContext.getClassLoader(); // step
try {
Object result = classLoader.loadClass("some.package.SomeClass") // suggested sink
.getMethod("someMethod")
.invoke(null); // your sink
where the signature of packageName
hasn't been verified.
We can express this as a dataflow problem, where the source is the Context
obtained from a createPackageContext
call with a certain packageName
argument. So a MethodAccess
that calls the method android.content.Context.createPackageContext
(or an override).
predicate isSource(DataFlow::Node src) {
exists(Method m | m = src.asExpr().(MethodAccess).getMethod() |
m.getDeclaringType().getASourceSupertype*() instanceof TypeContext and
m.hasName("createPackageContext")
)
}
Now we can track it to the qualifier of any problematic access of the ClassLoader
class, such as ClassLoader.loadClass
, and that'll be our sink:
predicate isSink(DataFlow::Node sink) {
exists(MethodAccess ma |
ma.getMethod().hasQualifiedName("java.lang", "ClassLoader", "loadClass")
|
sink.asExpr() = ma.getQualifier()
)
}
Of course we could make it an intermediate step, and keep tracking the Class
object until it reached the qualifier of another dangerous access, like getMethod
, getDeclaredMethod
, etc — and repeat until we found invoke
or similar. But that unnecessarily complicates the query — if a class is loaded from an untrusted ClassLoader
, that's bad enough, and we can assume that some interaction with it will happen, which would cause the vulnerability.
Nonetheless the source is a Context
object, and the sink is a ClassLoader
object, so we do need to model how the flow can go from one to the other. That can be done with an additional flow step:
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
exists(MethodAccess ma, Method m |
ma.getMethod() = m and
m.getDeclaringType().getASourceSupertype*() instanceof TypeContext and
m.hasName("getClassLoader")
|
node1.asExpr() = ma.getQualifier() and
node2.asExpr() = ma
)
}
Now, to model the safe cases so that we don't report FPs. This is an example of a validation that would prevent this vulnerability:
PackageManager packageManager = context.getPackageManager();
for (PackageInfo info : packageManager.getInstalledPackages(0)) {
String packageName = info.packageName;
if (packageName.startsWith("some.package.")
&& packageManager.checkSignatures(packageName, context.getPackageName()) == PackageManager.SIGNATURE_MATCH) {
Context appContext = context.createPackageContext(packageName, ...);
// ...
}
There are many ways of modeling this, but since we modeled our source around Context.createPackageContext
, we can add the restriction that if the used packageName
argument is trusted, then this call isn't a valid source.
To model the validation, you need to define a Guard
:
import semmle.code.java.controlflow.Guards
class CheckSignaturesGuard extends Guard instanceof MethodAccess {
CheckSignaturesGuard() {
super.getMethod().hasQualifiedName("android.content.pm", "PackageManager", "checkSignatures")
}
Expr getCheckedExpr() { result = super.getArgument(0) }
}
And now we can tell that any access to the variable of the first argument of this call (packageName
) that is controlled by the guard in its true
branch (that is, where checkSignatures
has been necessarily executed and its result is true
) is safe, and can't be used to build a source:
import semmle.code.java.dataflow.SSA
predicate signatureChecked(Expr safe) {
exists(CheckSignaturesGuard g, SsaVariable v |
v.getAUse() = g.getCheckedExpr() and
safe = v.getAUse() and
g.controls(safe.getBasicBlock(), true)
)
}
We can simply add this as a condition to our previous predicate isSource
:
predicate isSource(DataFlow::Node src) {
exists(Method m | m = src.asExpr().(MethodAccess).getMethod() |
m.getDeclaringType().getASourceSupertype*() instanceof TypeContext and
m.hasName("createPackageContext")
and not signatureChecked(ma.getArgument(0))
)
}
Note that this does only SSA analysis to determine whether the first argument of createPackageContext
is safe. If you wanted this to be tracked across callables (i.e. global flow), you'd need to use a secondary DataFlow::Global
configuration (similar to your SigPkgCfg
but with some adjustments). I leave this as an exercise to you if you want to further improve the query.
For now, try to fit the pieces I gave you into a new TaintTracking::Global
configuration, and discard your current
predicates getClassLoaderReachableMethodAccess
, getDangerousReachableMethodAccess
, and isSignaturesChecked
. Try to write your select
clause following the style of one of the many existing path-problem
queries, and adapt your query metadata accordingly.
To make sure the query works as you expected, I'd recommend writing some tests too. See https://github.com/github/codeql/blob/main/docs/supported-queries.md for some guidance about writing unit tests. You can also get inspiration from other experimental queries in this regard.
I can't recall entirely but I remember one of the reason my query ended up combining data flow with CFG etc was that a plain data flow somehow could not identify the vulnerability. Not sure if it captures what you had in mind correctly, but I tried implement the following but it also return empty result.
|
The reason the query isn't working for you is because of how you defined Now, I made a mistake when defining the class CheckSignaturesGuard extends Guard instanceof EqualityTest {
MethodAccess 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())
)
} |
Also tweak your select clause and metadata like this if you want to see pretty-printed alerts: /*
* ...
* @kind path-problem
* ...
*/
// ...
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" |
It works now, thanks! |
Before we do the bounty evaluation, do you intend to add unit tests (see last paragraph of this comment) or are you happy with the current state of the query? |
QHelp previews: java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.qhelpLoad 3rd party classes or code ('unsafe reflection') without signature checkIf a vulnerable loads classes or code of any app based solely on the package name of the app without first checking the package signature of the app, this could 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. RecommendationVerify that the signature of an app in addition to the package name before loading the classes or code. References
|
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.ql
Fixed
Show fixed
Hide fixed
- Query ID - MethodAccess -> MethodCall - Redundant import - Formatting
3981f8e
to
3970852
Compare
…LoadClassNoSignatureCheck
I have added the following unit test files:
Because I am not familiar with the unit test tools. I was not able to properly run them using codeql-cli. So, what I did was I manually create a database via |
The reason the tests don't compile for you is that you're importing Android classes but you didn't specify where the compiler should look for them. You can use the
Once you've done that you'll be able to run the test with
Now that we're at it, and that you have a good and a bad example, I recommend you add them to the QHelp file as |
It worked! Thanks again! |
QHelp previews: java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.qhelpLoad 3rd party classes or code ('unsafe reflection') without signature checkIf 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. RecommendationVerify the package signature in addition to the package name before loading any classes or code from another application. ExampleThe 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());
}
}
}
} The 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());
}
}
}
} References
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some final docs suggestions. Also note that you need to include the BadClassLoader.java
and GoodClasLoader.java
files in the java/ql/src/experimental/Security/CWE/CWE-470/
directory too for the QHelp to render correctly.
You can verify that the QHelp is correct with the following command:
codeql generate query-help --format=markdown -- "java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.qhelp"
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.qhelp
Outdated
Show resolved
Hide resolved
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.qhelp
Outdated
Show resolved
Hide resolved
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.qhelp
Outdated
Show resolved
Hide resolved
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.qhelp
Outdated
Show resolved
Hide resolved
java/ql/src/experimental/Security/CWE/CWE-470/LoadClassNoSignatureCheck.ql
Outdated
Show resolved
Hide resolved
Update to documentation. Co-authored-by: Tony Torralba <atorralba@users.noreply.github.com>
All done! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
Thanks @atorralba! |
The evaluation workflow is the following: So after query review, the SecLab takes over again, and they'll make the final decision about your bounty. Can't give you a deadline because of the holidays, but at least there's nothing else you need to do, other than waiting for a bit :) |
If a vulnerable loads classes or code of any app based solely on the package name of the app without first checking the package signature of the app, this could 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.
Although both uses OVAA as a target application, this QL is different from the one in #5435 that created UnsafeReflection.ql as they target different category of vulnerability.