
Java-Bytecode-Analyzer, anpassbar über JSON-Regeln
Java-Bytecode-Analyzer, der über JSON-Regeln anpassbar ist. Es ist ein Kommandozeilen-Tool, das einen Pfad mit einer oder mehreren Jar- oder War-Dateien erhält, diese mit den bereitgestellten Regeln analysiert und HTML-Berichte mit den Ergebnissen erstellt.
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.
Die Regeldatei kann mit dem Argument -f,--custom-file angegeben werden. Die Datei ist im JSON-Format und hat die folgende Struktur:
final istDu kannst auch net.nandgr.cba.custom.model.Rules.java überprüfen, um die Struktur im Java-Code zu sehen.
Es gibt bereits mehrere Regeln im Verzeichnis examples. Nachfolgend sind dennoch Beispiele für jede Regel aufgeführt.
Wenn wir Klassen mit benutzerdefinierter Deserialisierung finden müssen, können wir das recht einfach tun. Eine Klasse definiert benutzerdefinierte Deserialisierung durch die Implementierung von private void readObject(ObjectInputStream in). Wir müssen also nur alle Klassen finden, in denen diese Methode definiert ist. Es würde ausreichen, eine Regel wie folgt zu definieren:
{
"rules": [{
"name": "Custom deserialization",
"methods": [{
"name": "readObject",
"visibility": "private",
"parameters" : [{
"type" : "java.io.ObjectInputStream"
}]
}]
}]
}
Es werden Methoden mit Sichtbarkeit private, Name readObject und einem Parameter vom Typ java.io.ObjectInputStream gemeldet. Parameter sind ein Array; wenn mehr als einer angegeben wird, müssen alle übereinstimmen, damit gemeldet wird. Da wir nur eine Regel haben, wird ein Bericht mit dem Namen custom-deserialization-0.html erstellt.
In diesem Fall muss eine Regel mit zwei Methoden definiert werden. Dieselbe wie im vorherigen Beispiel für Deserialisierung und eine neue, um private void writeObject(ObjectOutputStream out) zu finden. Wie in der JSON-Struktur oben gezeigt, ist die Eigenschaft rules.rule.methods ein Array von Methoden, sodass eine solche Regel geschrieben werden kann:
{
"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"
}]
}]
}]
}
Die Eigenschaft report wurde auf false gesetzt, um eine doppelte Meldung für dieselbe Regel zu vermeiden. Wir verwenden die zweite Methode nur als Bedingung, aber das Melden nur von readObject-Methoden sollte für den Zweck dieser Regel ausreichen.
Wenn eine Eigenschaft nicht definiert ist, wird sie immer als wahr betrachtet. Zum Beispiel würde diese Regel alle Methodendefinitionen zurückgeben:
{
"rules": [{
"name": "Method definitions",
"methods": [{
}]
}]
}
Methodenaufrufe können ebenfalls gefunden werden. Das JSON wäre in diesem Fall:
{
"rules": [{
"name": "String equals",
"invocations": [{
"owner": "java.lang.String",
"method": {
"name": "equals"
}
}]
}]
}
Die Eigenschaft owner gibt die Klasse an, die die Methode enthält.
Ein weiteres, etwas nützlicheres Beispiel für einen Methodenaufruf als das vorherige:
{
"rules": [{
"name": "Method invocation by reflection",
"invocations": [{
"owner": "java.lang.reflect.Method",
"method": {
"name": "invoke"
}
}]
}]
}
Es ist dasselbe wie bei jedem Methodenaufruf, aber der Name der Methode sollte in diesem Fall <init> sein.
{
"rules": [{
"name" : "String instantiation",
"invocations" : [{
"owner" : "java.lang.String",
"method" : {
"name" : "<init>"
}
}]
}]
}
Diese Regel findet Vorkommen von:
[...]
String s = new String("foo");
[...]
In diesem Beispiel möchten wir Deserialisierungsnutzungen finden (nicht Klassen, die Serialisierungsverhalten definieren wie in den vorherigen Beispielen). Deserialisierung tritt auf, wenn ObjectInputStream.readObject() aufgerufen wird. zum Beispiel in diesem Code-Ausschnitt:
ObjectInputStream in = new ObjectInputStream(fileInputStream);
Object o = in.readObject();
Also müssen wir Methodenaufrufe von ObjectInputStream mit dem Namen readObject finden. Aber in einem Forschungskontext wird das viele false positives erzeugen, denn wenn eine Klasse benutzerdefinierte Deserialisierung definiert, erfolgt ein Aufruf dieser Methode innerhalb einer private void readObject(ObjectInputStream in) Methode, und das würde den Bericht zu sehr verunreinigen. Wenn wir diese Fälle ausschließen und nur echte Deserialisierung behalten wollen, kann die Eigenschaft notFrom verwendet werden:
{
"rules": [{
"name": "Deserialization usage",
"invocations": [{
"owner": "java.io.ObjectInputStream",
"method": {
"name": "readObject"
},
"notFrom": {
"name": "readObject",
"visibility": "private"
}
}]
}]
}
Diese Datei findet java.io.ObjectInputStream.readObject()-Aufrufe, wenn der Aufruf nicht innerhalb einer private void readObject(ObjectInputStream in) Methode erfolgt.
Eine Klasse, die mit diesem Code kompiliert wurde, wird nicht gemeldet:
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
Object o = in.readObject();
}
Aber diese hier wird gemeldet:
public Object deserializeObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
Object o = in.readObject();
return o;
}
Die Eigenschaft from kann bei Invocations genauso wie notFrom gesetzt werden, aber das Ergebnis ist das Gegenteil: Es wird nur übereinstimmen, wenn der Aufruf von der definierten Methode aus erfolgt.
Die Eigenschaft superClass kann in diesem Fall verwendet werden. Wenn wir alle Klassen finden möchten, die von javax.servlet.http.HttpServlet erben, könnte eine Regel sein:
{
"rules": [{
"name": "Java servlets",
"superClass" : "javax.servlet.http.HttpServlet"
}]
}
Eine Regel kann geschrieben werden, um Klassen zu finden, die ein Array von Interfaces implementieren. Wenn mehr als ein Interface in der Regel definiert ist, muss die Klasse alle implementieren, um gemeldet zu werden. Wenn wir Klassen finden möchten, die javax.net.ssl.X509TrustManager implementieren, wäre die Regel:
{
"rules": [{
"name": "X509TrustManager implementations",
"interfaces" : ["javax.net.ssl.X509TrustManager"]
}]
}
Bitte beachte, dass interfaces ein Array ist, also stelle sicher, dass die Zeichenketten zwischen eckigen Klammern hinzugefügt werden, z.B: ["interface1", "interface2", ...].
Annotationen werden ebenfalls unterstützt. Es können mehrere Annotationseigenschaften in einer Regel definiert werden (Klassenannotationen finden), in Methoden oder Variablen (Parameter oder lokale Variablen). Wenn alle in der analysierten Klasse gefunden werden, wird gemeldet.
Zum Beispiel, wenn wir Spring-Endpunkte finden möchten, würden wir nach Klassen oder Methoden suchen, die mit org.springframework.web.bind.annotation.RequestMapping annotiert sind. Die Regel könnte also sein:
{
"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"
}]
}]
}]
}
Die Eigenschaft rule.fields kann verwendet werden, um Klassenfelder zu finden. Wenn wir private String-Felder mit Passwortnamen finden möchten, könnte eine Regel wie diese verwendet werden:
{
"rules": [{
"name" : "Password fields",
"fields" : [
{
"visibility" : "private",
"type" : "java.lang.String",
"nameRegex" : "(password|pass|psswd|passwd)"
}
]
}]
}
Um Variablen zu finden, kann rule.variables verwendet werden. Diese Eigenschaft meldet lokale Variablen und Methodenargument-Variablen.
Wenn wir alle Variablen vom Typ javax.servlet.http.Part finden möchten, könnte eine Regel sein:
{
"rules": [{
"name" : "Servlet upload file",
"methods" : [{
"variables" : [{
"type" : "javax.servlet.http.Part"
}]
}]
}]
}
Es können mehrere Regeln in derselben JSON-Datei definiert werden. Sie werden separat verarbeitet und gemeldet und beeinflussen sich gegenseitig nicht. Wir können einige der vorherigen Beispielregeln kombinieren:
{
"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"
}
}]
}]
}
Hier haben wir zwei Regeln ("Custom deserialization" und "Method invocation by reflection"). Sie werden so verarbeitet, als ob du sie in zwei separaten Ausführungen ausführen würdest. Und pro Regel wird ein Bericht erstellt. Haben die Regeln denselben Namen, werden sie in derselben Datei gemeldet.
Das Projekt kann heruntergeladen und gebaut werden, um komplexere benutzerdefinierte Regeln in Java-Code hinzuzufügen, die vom JSON-Format nicht abgedeckt werden. Es gibt bereits drei Beispiele im Paket net.nandgr.cba.visitor.checks. Dies sind CustomDeserializationCheck, DeserializationCheck und InvokeMethodCheck. Du kannst deine eigenen Regeln erstellen, indem du net.nandgr.cba.custom.visitor.base.CustomAbstractClassVisitor erweiterst.
Wie oben erwähnt, werden die Berichte standardmäßig im Ordner report erstellt. Jede Regel erhält eine separate Datei, es sei denn, sie haben denselben Namen.
Wenn der Bericht zu groß ist, kannst du ihn mit dem Parameter -i,--items-report <maxItems> aufteilen. Jeder Teil enthält die angegebene Anzahl oder weniger (falls es der letzte ist).
Jeder gemeldete Eintrag gibt an, in welchem Jar er gefunden wurde, den Klassennamen und den Methodennamen (falls relevant). Es wird auch die dekompilierte Version der Klasse angezeigt, um eine schnelle visuelle Überprüfung zu ermöglichen.
Beispiel, wie die Einträge für eine Regel zum Auffinden von java.io.File-Instanziierungen angezeigt werden:

Bei der Suche nach Sicherheitsfehlern ist ein Aufrufgraph sehr nützlich. Derzeit wird eine einfache DOT-kompatible Datei im Verzeichnis report erstellt.
Der Graph enthält alle möglichen Flüsse, von denen aus die gefundenen Probleme aufgerufen werden können. Wenn beispielsweise eine Regel zur Deserialisierung verwendet wird, wird ein Graph mit allen möglichen Pfaden generiert, die zu der Methode führen, die die Deserialisierung aufruft.
Die Datei ist call-graph.dot und würde so aussehen (dies ist ein extrem einfaches Beispiel):
graph callGraph {
"demo.callgraph.Class1:method1" -- "demo.callgraph.Class2:method2"
"demo.callgraph.Class3:method3" -- "demo.callgraph.Class2:method2"
}
Um es visuell darzustellen, kann DOT (oder eine kompatible Software) verwendet werden. Zum Beispiel, um die Datei in svg zu konvertieren:
dot -Tsvg call-graph.dot -o call-graph.svg
Dies geschieht standardmäßig automatisch, wenn DOT im System-PATH gefunden wird. Andernfalls kann DOT auf Debian-basierten Systemen mit sudo apt-get install graphviz installiert werden.
Es wird eine SVG-Datei mit dem Namen call-graph.svg erstellt, die in PNG konvertiert oder mit Programmen wie inkscape oder einfach firefox visualisiert werden kann.
Ein sehr einfaches Beispiel der obigen Datei call-graph.dot wäre:

Es gibt einige Einschränkungen, z.B. wenn das gesuchte Element in einer java.lang.Runnable.run()- oder ähnlichen Methode ist, wird nicht gefunden, von wo der Thread ausgeführt wird.
Außerdem bereinigt der Graph Zyklen, um StackOverflowErrors zu vermeiden. Dies geschieht auf eine etwas konservative Weise, damit der Systemspeicher während einer Analyse eines großen Verzeichnisses nicht ausgeschöpft wird.
Weitere Optionen werden in zukünftigen Versionen hinzugefügt.
java -jar cba-cli-<version>.jar -a /path/with/jars -f /path/with/json/file/rules.json
Um benutzerdefinierte Java-Regeln zu verwenden, müssen Klassennamen als Argumente von -c angegeben werden.
java -jar cba-cli-<version>.jar -a /path/with/jars -c DeserializationCheck
Akzeptiert eine durch Leerzeichen getrennte Liste, sodass mehrere benutzerdefinierte Regeln definiert werden können (jede Regel erstellt einen separaten Bericht):
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
Um Fehler zu finden, kann die Ausführlichkeit erhöht werden. Debug-Level:
java -jar cba-cli-<version>.jar -a /path/with/jars -c YourCustomRule1 -v
Trace-Level:
java -jar cba-cli-<version>.jar -a /path/with/jars -c YourCustomRule1 -vv
Derzeit muss das APK zunächst in JAR konvertiert werden, um analysiert zu werden.
d2j-dex2jar.sh -f -o app_to_analyze.jar app_to_analyze.apk-a Parameter das Verzeichnis mit der konvertierten Jar-Datei übergeben wird.Es gibt bereits eine ausführbare Jar-Datei im Verzeichnis bin unter: https://github.com/fergarrui/custom-bytecode-analyzer/blob/master/bin/cba-cli-0.1-SNAPSHOT.jar . Wenn du Änderungen vornehmen oder benutzerdefinierte Regeln hinzufügen möchtest, kann das Projekt wie folgt gebaut werden:
git clone https://github.com/fergarrui/custom-bytecode-analyzer.git
cd custom-bytecode-analyzer
mvn clean package
Es werden zwei Jars im Ordner target erstellt. cba-cli-<version>.jar enthält alle Abhängigkeiten und ist ausführbar. Kann mit java -jar cba-cli-<version>.jar ausgeführt werden.