grammarly
All checks were successful
/ test_checkout (push) Successful in 1m42s

This commit is contained in:
Jean-Marie 'Histausse' Mineau 2025-09-22 06:02:08 +02:00
parent 039970904e
commit 10df431972
Signed by: histausse
GPG key ID: B66AEEDA9B645AD2
9 changed files with 240 additions and 250 deletions

View file

@ -1,76 +1,77 @@
#import "../lib.typ": paragraph, ART, DEX, APK, eg
#import "../lib.typ": todo, jfl-note, jm-note
== Limitations and Futur Works <sec:th-limits>
== Limitations and Future Works <sec:th-limits>
The method we presented in this chapter has a number of underdeveloped aspects.
In this section we will present those issues and potential avenues of improvement, related to the bytecode transformation, the dynamic analysis and DroidRA, a tool similar to our solution.
In this section, we will present those issues and potential avenues of improvement related to the bytecode transformation, the dynamic analysis and DroidRA, a tool similar to our solution.
=== Bytecode Transformation
#paragraph[Custom Classloaders][
The first obvious limitation of our bytecode transformation is that we do not know what custom classloadrs do, so we cannot accuratly reproduce statically their behavior.
We elected to fallback to the behavior of the `BaseDexClassLoader`, which is the highest Android specific classloader in the inheritance hierarchy, and whose behavior is shared by all classloaders safe `DelegateLastClassLoader`.
The current implementation of the #ART enforce some restrictions on the classloaders behavior to optimize the runtime performance by caching classes.
This gives us some garanties that custom classesloaders will keep a some coherences will the classic classloaders.
The first obvious limitation of our bytecode transformation is that we do not know what custom classloaders do, so we cannot accurately reproduce statically their behaviour.
We elected to fallback to the behaviour of the `BaseDexClassLoader`, which is the highest Android-specific classloader in the inheritance hierarchy, and whose behaviour is shared by all classloaders except `DelegateLastClassLoader`.
The current implementation of the #ART enforces some restrictions on the class loader's behaviour to optimise the runtime performance by caching classes.
This gives us some guarantees that custom class loaders will keep some coherence with the classic classloaders.
For instance, a class loaded dynamically must have the same name as the name used in `ClassLoader.loadClass()`.
This make `BaseDexClassLoader` a good estimation for legitimate classloaders, however, an obfuscated application could use the techniques discussed in @sec:cl-cross-obf, in wich case our model would be entirelly wrong.
This makes `BaseDexClassLoader` a good approximation for legitimate class loaders.
However, an obfuscated application could use the techniques discussed in @sec:cl-cross-obf, in which case our model would be entirely wrong.
It would be interesting to expore if some form of static analysis like symbolic execution could be used to extract the behavior of an ad hoc class loader and be used to model the class used appropriately.
A more reasonable approach however would be to improve the static analysis to intercept each calls of `loadClass()` of each class loaders, including implicite calls performed by the #ART.
This would allow to collect a mapping $("class loader", "class name") -> "class"$ that can then be used when renaming colliding classes.
It would be interesting to explore if some form of static analysis, like symbolic execution, could be used to extract the behaviour of an ad hoc class loader and be used to model the class used appropriately.
A more reasonable approach would be to improve the static analysis to intercept each call of `loadClass()` of each class loader, including implicit calls performed by the #ART.
This would allow us to collect a mapping $("class loader", "class name") -> "class"$ that can then be used when renaming colliding classes.
]
#paragraph[Multiple Classloaders for one `Method.invoke()`][
Although we managed to handle call to different methods from one `Method.invoke()` site, we do not handle calling methods from different classloaders with colliding classes definition.
Although we managed to handle calls to different methods from one `Method.invoke()` site, we do not handle calling methods from different classloaders with colliding class definitions.
The first reason is that it is quite challenging to compare classloaders statically.
At runtime, each object has an unique identifier that can be used to compare them over the course of the same execution, but this identifier is reset each time the application starts.
At runtime, each object has a unique identifier that can be used to compare them over the course of the same execution, but this identifier is reset each time the application starts.
This means we cannot use this identifier in an `if` condition to differentiate the classloaders.
Ideally, we would combine the hash of the loaded #DEX files, the classloader class and parent to make an unique, static identifier, but the #DEX files loaded by a classloader cannot be accessed at runtime without accessing the process memory at arbitrary locations.
For some classloaders, the string representation returned by `Object.toString()` list the location of the loaded #DEX file on the file system.
Ideally, we would combine the hash of the loaded #DEX files, the classloader class and parent to make a unique, static identifier, but the #DEX files loaded by a classloader cannot be accessed at runtime without accessing the process memory at arbitrary locations.
For some classloaders, the string representation returned by `Object.toString()` lists the location of the loaded #DEX file on the file system.
This is not the case for the commonly used `InMemoryClassLoader`.
In addition, the #DEX files are often located in the application private folder, whose name is derived from the hash of the #APK itself.
Because we modify the application, the path of the private folder also change, and so will the string representation of the classloaders.
Checking the classloader of a classes can also have side-effect on classloaders that delegate to the main application classloader:
because we inject the classes in the #APK, the classes of the classloader are now already in the main application classloader, which in most case will have priority on the other classloaders, and lead to the class beeing loaded by the application classloader instead of the original classloader.
If we check for the classloader, we would need to considere such cases en rename each classes of each classloader before reinjecting them to the in the application.
In addition, the #DEX files are often located in the application's private folder, whose name is derived from the hash of the #APK itself.
Because we modify the application, the path of the private folder also changes, and so will the string representation of the classloaders.
Checking the classloader of a class can also have side effects on classloaders that delegate to the main application classloader:
because we inject the classes in the #APK, the classes of the classloader are now already in the main application classloader, which in most cases will have priority over the other classloaders, and lead to the class being loaded by the application classloader instead of the original classloader.
If we check for the classloader, we would need to consider such cases and rename each class of each classloader before reinjecting them in the application.
This would greatly increase the risk of breaking the application during its transformation.
Instead, we elected to ignore the classloaders when selecting the method to invoque.
This leads to potential invalid runtime behaviore, as the first method that matching the class name will be called, but the alternative methods from other classloader still appears in the new application, albeit in a block that might be flagged as dead-code by a sufficiently advenced static analyser.
Instead, we elected to ignore the classloaders when selecting the method to invoke.
This leads to potential invalid runtime behaviour, as the first method that matches the class name will be called, but the alternative methods from other class loaders still appear in the new application, albeit in a block that might be flagged as dead code by a sufficiently advanced static analyser.
]
#paragraph[`ClassNotFoundException` may not be raised][
In the very specific situation where the original application tries to access a class from dynamically loaded bytecode without actually accessing this bytecode (#eg by using the wrong class loader), the patched application behavior will differ.
The original application should raise a `ClassNotFoundException`, but in the patched application, the class will be accible and the exception will not be raised.
In pactice, their is not a lot of reason to do such thing.
One could be to check if the #APK as been tempered with, but their are easier ways to do thins, like checking the application signature.
Another would be to check if the class is already available, and if not, load it dynamically, in wich case it does not matter as code loaded dynamically is already present.
In any cases, statically, because we remove neither the calls to the function that load the classes (like `ClassLoader.loadClass(..)`) nor the `try` / `catch` blocks, static analysis tools those can handle the original behavior should still be hable to access the old behavior.
In the very specific situation where the original application tries to access a class from dynamically loaded bytecode without actually accessing this bytecode (#eg by using the wrong class loader), the patched application behaviour will differ.
The original application should raise a `ClassNotFoundException`, but in the patched application, the class will be accessible and the exception will not be raised.
In practice, there is not a lot of reason to do such a thing.
One could be to check if the #APK has been tampered with, but there are easier ways to do this, like checking the application signature.
Another would be to check if the class is already available, and if not, load it dynamically, in which case it does not matter, as code loaded dynamically is already present.
In any case, statically, because we remove neither the calls to the function that load the classes (like `ClassLoader.loadClass(..)`) nor the `try` / `catch` blocks, static analysis tools that can handle the original behaviour should still be able to access the old behaviour.
]
=== Dynamic Analysis
#paragraph[Anti Evasion][
Our dynamic analysis does not permform any kind of anti-evasive technique.
Our dynamic analysis does not perform any kind of anti-evasive technique.
Any application implementing even basic evasion will detect our environment and will probably not load malicious bytecode.
Running the dynamic analysis in a appropriate sandbox such as DroidDungeon should improve the results significantly.
Running the dynamic analysis in an appropriate sandbox such as DroidDungeon should improve the results significantly.
]
#paragraph[Code Coverage][
In @sec:th-dyn-failure, we saw that our dynamic analysis performed poorly.
It may be due to our experimental setup, and it is possible that a better sandbox will fix the issue.
However their is a larger code coverage issue.
We tried to manually analysed a few applications marked as malware on MalwareBazaar to test our method.
Although we did confirm statically that the applications where using reflection and dynamic code loading, we did not managed to trigger this behavior at runtime, and other obfuscation technique make it verry difficult to determine statically the required condition to trigger them.
Thus, we believe that techniques to improve code coverage are indeed needed when analysing application.
This could mean better exploration techniques such as the one implemented by Stoat and GroddDroid, or more intrusive approched such as forced excecution.
However, there is a larger code coverage issue.
We tried to manually analyse a few applications marked as malware on MalwareBazaar to test our method.
Although we did confirm that the applications were using reflection and dynamic code loading with a static analysis, we did not manage to trigger this behaviour at runtime, and other obfuscation techniques make it very difficult to determine statically the required condition to trigger them.
Thus, we believe that techniques to improve code coverage are indeed needed when analysing applications.
This could mean better exploration techniques, such as the one implemented by Stoat and GroddDroid, or more intrusive approaches, such as forced execution.
]
=== Comparision to DroidRA
=== Comparison with DroidRA
It would be very interesting to compare our tool to DroidRA.
DroidRA is a tool that compute reflection information using static analysis and patch the application to add those calls to the application.
Beyond the classic comparison static vs dynamic, DroidRA has a similar goal and strategy to ours.
DroidRA is a tool that computes reflection information using static analysis and patches the application to add those calls.
Beyond the classic comparison of static versus dynamic, DroidRA has a similar goal and strategy to ours.
Two notable comparison criteria would be the failure rate and the number of edges added to an application call graph.
The first criterion indicate how much the results can be used by other tools, while the second indicate how effective the approaches are.
The first criterion indicates how much the results can be used by other tools, while the second indicates how effective the approaches are.