
Analyseur de bytecode Java personnalisable via des règles JSON
Analyseur de bytecode Java personnalisable via des règles JSON. C'est un outil en ligne de commande qui reçoit un chemin contenant un ou plusieurs fichiers Jar ou War, les analyse en utilisant les règles fournies et génère des rapports HTML avec les résultats.
usage: java -jar cba-cli.jar [OPTIONS] -a DIRECTORY_TO_ANALYZE
-a,--analyze <pathToAnalyze> Path of the directory to run the
analysis.
-c,--checks <checks...> Space separated list of custom checks
that are going to be run in the analysis.
-f,--custom-file <customFile> Specify a file in JSON format to run
custom rules. Read more in
https://github.com/fergarrui/custom-bytecode-analyzer.
-h,--help Print this message.
-i,--items-report <maxItems> Max number of items per report. If the
number of issues found exceeds this
value, the report will be split into
different files. Useful if expecting too
many issues in the report. Default: 50.
-o,--output <outputDir> Directory to save the report. Warning -
if there are already saved reports in
this directory they will be overwritten.
Default is "report".
-v,--verbose-debug Increase verbosity to debug mode.
-vv,--verbose-trace Increase verbosity to trace mode - makes it slower, use it only when you need.
Le fichier de règles peut être spécifié en utilisant l'argument -f,--custom-file. Le fichier est au format JSON et a la structure suivante :
finalVous pouvez également consulter net.nandgr.cba.custom.model.Rules.java pour voir la structure en code Java.
Il existe déjà plusieurs règles dans le répertoire examples. Quoi qu'il en soit, voici des exemples pour chaque règle.
Si nous avons besoin de trouver des classes avec désérialisation personnalisée, nous pouvons le faire assez facilement. Une classe définit une désérialisation personnalisée en implémentant private void readObject(ObjectInputStream in). Nous avons donc seulement besoin de trouver toutes les classes où cette méthode est définie. Il suffirait de définir une règle comme :
{
"rules": [{
"name": "Custom deserialization",
"methods": [{
"name": "readObject",
"visibility": "private",
"parameters" : [{
"type" : "java.io.ObjectInputStream"
}]
}]
}]
}
Il rapportera les méthodes avec la visibilité private, readObject comme nom et un paramètre de type java.io.ObjectOutputStream. Les paramètres sont un tableau ; si plus d'un est spécifié, tous doivent correspondre pour être signalés. Comme nous n'avons qu'une seule règle, un rapport nommé custom-deserialization-0.html sera créé.
Dans ce cas, une règle avec deux méthodes doit être définie. La même que dans l'exemple précédent pour la désérialisation, et une nouvelle pour correspondre à private void writeObject(ObjectOutputStream out). Comme indiqué dans la structure JSON ci-dessus, la propriété rules.rule.methods est un tableau de méthodes, donc une règle comme celle-ci peut être écrite :
{
"rules": [{
"name": "Custom serialization and deserialization",
"methods": [{
"name": "readObject",
"visibility": "private",
"parameters" : [{
"type" : "java.io.ObjectInputStream"
}]
},{
"name": "writeObject",
"report": "false",
"visibility": "private",
"parameters" : [{
"type" : "java.io.ObjectOutputStream"
}]
}]
}]
}
La propriété report a été définie à false pour éviter de signaler deux fois la même règle. Nous utilisons la deuxième méthode juste comme condition, mais signaler uniquement les méthodes readObject devrait suffire pour l'objectif de cette règle.
Si une propriété n'est pas définie, elle correspondra toujours à vrai. Par exemple, cette règle retournerait toutes les définitions de méthodes :
{
"rules": [{
"name": "Method definitions",
"methods": [{
}]
}]
}
Les invocations de méthodes peuvent également être trouvées. Le JSON dans ce cas serait :
{
"rules": [{
"name": "String equals",
"invocations": [{
"owner": "java.lang.String",
"method": {
"name": "equals"
}
}]
}]
}
La propriété owner spécifie la classe contenant la méthode.
Un autre exemple d'invocation de méthode un peu plus utile que le précédent :
{
"rules": [{
"name": "Method invocation by reflection",
"invocations": [{
"owner": "java.lang.reflect.Method",
"method": {
"name": "invoke"
}
}]
}]
}
C'est la même chose que n'importe quelle invocation de méthode, mais le nom de la méthode dans ce cas doit être <init>.
{
"rules": [{
"name" : "String instantiation",
"invocations" : [{
"owner" : "java.lang.String",
"method" : {
"name" : "<init>"
}
}]
}]
}
Cette règle trouvera les occurrences de :
[...]
String s = new String("foo");
[...]
Dans cet exemple, nous voulons trouver les utilisations de la désérialisation (pas les classes définissant des comportements de sérialisation comme dans les exemples précédents). La désérialisation se produit lorsque ObjectInputStream.readObject() est invoqué. Par exemple dans cet extrait de code :
ObjectInputStream in = new ObjectInputStream(fileInputStream);
Object o = in.readObject();
Nous devons donc trouver les invocations de méthodes depuis ObjectInputStream nommées readObject. Mais cela trouvera beaucoup de faux positifs dans un contexte de recherche, car lorsqu'une classe définit une désérialisation personnalisée, elle fait une invocation à cette méthode à l'intérieur d'une méthode private void readObject(ObjectInputStream in), et cela polluerait trop le rapport. Si nous voulons exclure ces cas et ne conserver que la désérialisation authentique, la propriété notFrom peut être utilisée :
{
"rules": [{
"name": "Deserialization usage",
"invocations": [{
"owner": "java.io.ObjectInputStream",
"method": {
"name": "readObject"
},
"notFrom": {
"name": "readObject",
"visibility": "private"
}
}]
}]
}
Ce fichier trouvera les invocations de java.io.ObjectInputStream.readObject() si l'invocation n'est pas faite à l'intérieur de la méthode private void readObject(ObjectInputStream in).
Une classe compilée avec ce code ne sera pas signalée :
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
Object o = in.readObject();
}
Mais celle-ci sera signalée :
public Object deserializeObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
Object o = in.readObject();
return o;
}
La propriété from peut être définie dans les invocations exactement de la même manière que notFrom, mais le résultat sera l'opposé : elle correspondra uniquement si l'invocation est faite depuis la méthode définie.
La propriété superClass peut être utilisée dans ce cas. Si nous voulons trouver toutes les classes étendant javax.servlet.http.HttpServlet, une règle peut être :
{
"rules": [{
"name": "Java servlets",
"superClass" : "javax.servlet.http.HttpServlet"
}]
}
Une règle peut être écrite pour trouver les classes implémentant un tableau d'interfaces. Si plus d'une interface est définie dans la règle, la classe doit toutes les implémenter pour être signalée. Si nous voulons trouver les classes implémentant javax.net.ssl.X509TrustManager, la règle serait :
{
"rules": [{
"name": "X509TrustManager implementations",
"interfaces" : ["javax.net.ssl.X509TrustManager"]
}]
}
Veuillez noter que interfaces est un tableau, alors assurez-vous d'ajouter les chaînes entre crochets, par exemple : ["interface1", "interface2", ...].
Les annotations sont également prises en charge. Plusieurs propriétés d'annotations peuvent être définies dans une règle (trouver des annotations de classe), dans des méthodes ou des variables (paramètres ou variables locales). Si toutes sont trouvées dans la classe analysée, elle sera signalée.
Par exemple, si nous voulons trouver des points de terminaison Spring, nous chercherions des classes ou des méthodes annotées avec org.springframework.web.bind.annotation.RequestMapping. Ainsi, la règle peut être :
{
"rules": [{
"name": "Spring endpoint - class annotation",
"annotations" : [{
"type" : "org.springframework.web.bind.annotation.RequestMapping"
}]
},
{
"name": "Spring endpoint - method annotation",
"methods" : [{
"annotations" : [{
"type" : "org.springframework.web.bind.annotation.RequestMapping"
}]
}]
}]
}
La propriété rule.fields peut être utilisée pour trouver des champs de classe. Si nous voulons trouver des champs String privés avec des noms de mot de passe, une règle comme celle-ci pourrait être utilisée :
{
"rules": [{
"name" : "Password fields",
"fields" : [
{
"visibility" : "private",
"type" : "java.lang.String",
"nameRegex" : "(password|pass|psswd|passwd)"
}
]
}]
}
Pour trouver des variables, rule.variables peut être utilisé. Cette propriété signalera les variables locales et les variables d'arguments de méthode.
Si nous voulons trouver toutes les variables de type javax.servlet.http.Part, une règle pourrait être :
{
"rules": [{
"name" : "Servlet upload file",
"methods" : [{
"variables" : [{
"type" : "javax.servlet.http.Part"
}]
}]
}]
}
Plusieurs règles peuvent être définies dans le même fichier JSON. Elles seront traitées et signalées séparément et ne s'affecteront pas mutuellement. Nous pouvons combiner certaines des règles des exemples précédents :
{
"rules": [{
"name": "Custom deserialization",
"methods": [{
"name": "readObject",
"visibility": "private",
"parameters": [{
"type" : "java.io.ObjectInputStream"
}]
}]
},{
"name": "Method invocation by reflection",
"invocations": [{
"owner": "java.lang.reflect.Method",
"method": {
"name": "invoke"
}
}]
}]
}
Ici, nous avons deux règles ("Custom deserialization" et "Method invocation by reflection"). Elles seront traitées comme si vous le faisiez en deux exécutions séparées. Et un rapport par règle sera généré. Si les règles ont le même nom, elles seront signalées dans le même fichier.
Le projet peut être téléchargé et construit pour ajouter des règles personnalisées plus complexes en code Java qui ne sont pas couvertes par le format JSON. Il existe déjà trois exemples dans le package net.nandgr.cba.visitor.checks. Ce sont CustomDeserializationCheck, DeserializationCheck et InvokeMethodCheck. Vous pouvez créer vos propres règles en étendant net.nandgr.cba.custom.visitor.base.CustomAbstractClassVisitor.
Comme mentionné ci-dessus, les rapports sont créés par défaut dans le dossier report. Chaque règle aura un fichier séparé à moins qu'elles aient le même nom.
Si le rapport est trop volumineux, vous pouvez le diviser en utilisant le paramètre -i,--items-report <maxItems>, chacun contiendra l'argument spécifié ou moins (s'il s'agit du dernier).
Chaque élément signalé spécifie le jar où il est trouvé, le nom de la classe et le nom de la méthode (si pertinent). Il montre également la version décompilée de la classe pour faciliter une vérification visuelle rapide.
Exemple de la manière dont les éléments sont affichés pour une règle trouvant les instanciations de java.io.File :

Lors de la recherche de bugs de sécurité, il est très utile d'avoir un graphe d'appel. Actuellement, un simple fichier compatible DOT est créé dans le répertoire report.
Le graphe contient tous les flux possibles à partir desquels les problèmes trouvés peuvent être invoqués. Par exemple, si une règle pour trouver la désérialisation est utilisée, un graphe contenant tous les chemins possibles menant à la méthode qui appelle la désérialisation sera généré.
Le fichier est call-graph.dot et il ressemblerait à ceci (c'est un exemple extrêmement simple) :
graph callGraph {
"demo.callgraph.Class1:method1" -- "demo.callgraph.Class2:method2"
"demo.callgraph.Class3:method3" -- "demo.callgraph.Class2:method2"
}
Pour l'afficher visuellement, DOT peut être utilisé (ou tout logiciel compatible). Par exemple, pour convertir le fichier en svg :
dot -Tsvg call-graph.dot -o call-graph.svg
Ceci est fait automatiquement par défaut si DOT est trouvé dans le PATH du système. Sinon, DOT peut être installé sur les systèmes basés sur Debian en utilisant sudo apt-get install graphviz.
Il créera un fichier SVG nommé call-graph.svg qui peut être converti en PNG ou visualisé à l'aide de programmes comme inkscape ou simplement firefox.
Un exemple très simple du fichier call-graph.dot ci-dessus serait :

Il y a certaines limitations, par exemple, si l'élément recherché se trouve dans une méthode java.lang.Runnable.run() ou similaire, il ne trouvera pas d'où le thread est exécuté.
De plus, le graphe nettoie les cycles pour éviter les StackOverflowError, il est fait de manière un peu conservatrice pour que la mémoire du système ne soit pas épuisée lors de l'analyse d'un grand répertoire.
Plus d'options seront ajoutées dans les versions futures.
java -jar cba-cli-<version>.jar -a /path/with/jars -f /path/with/json/file/rules.json
To use custom java rules, class names have to be specified as arguments of -c.
java -jar cba-cli-<version>.jar -a /path/with/jars -c DeserializationCheck
Accepts a space separated list, so multiple custom rules can be defined (each of the rules will create a separate report):
java -jar cba-cli-<version>.jar -a /path/with/jars -c DeserializationCheck InvokeMethodCheck CustomDeserializationCheck YourCustomRule
java -jar cba-cli-<version>.jar -a /path/with/jars -f /path/with/json/file/rules.json -c YourCustomRule1 YourCustomRule2
Pour trouver des erreurs, la verbosité peut être augmentée. Niveau Debug :
java -jar cba-cli-<version>.jar -a /path/with/jars -c YourCustomRule1 -v
Niveau Trace :
java -jar cba-cli-<version>.jar -a /path/with/jars -c YourCustomRule1 -vv
Actuellement, l'APK doit d'abord être converti en JAR pour être analysé.
d2j-dex2jar.sh -f -o app_to_analyze.jar app_to_analyze.apk-a.Il existe déjà un fichier jar exécutable dans le répertoire bin à l'adresse : https://github.com/fergarrui/custom-bytecode-analyzer/blob/master/bin/cba-cli-0.1-SNAPSHOT.jar. Si vous souhaitez faire des modifications ou ajouter des règles personnalisées, le projet peut être construit en faisant :
git clone https://github.com/fergarrui/custom-bytecode-analyzer.git
cd custom-bytecode-analyzer
mvn clean package
Deux jars seront générés dans le dossier target. cba-cli-<version>.jar contient toutes les dépendances et est exécutable. Peut être exécuté en utilisant java -jar cba-cli-<version>.jar