
Analisador de bytecode Java personalizável via regras JSON
Analisador de bytecode Java customizável através de regras JSON. É uma ferramenta de linha de comando que recebe um caminho contendo um ou mais arquivos Jar ou War, os analisa usando as regras fornecidas e gera relatórios HTML com os resultados.
usage: java -jar cba-cli.jar [OPTIONS] -a DIRECTORY_TO_ANALYZE
-a,--analyze <pathToAnalyze> Caminho do diretório para executar a
análise.
-c,--checks <checks...> Lista separada por espaços de verificações
personalizadas que serão executadas na análise.
-f,--custom-file <customFile> Especifica um arquivo em formato JSON para
executar regras personalizadas. Leia mais em
https://github.com/fergarrui/custom-bytecode-analyzer.
-h,--help Exibe esta mensagem.
-i,--items-report <maxItems> Número máximo de itens por relatório. Se o
número de problemas encontrados exceder este
valor, o relatório será dividido em diferentes
arquivos. Útil se forem esperados muitos
problemas no relatório. Padrão: 50.
-o,--output <outputDir> Diretório para salvar o relatório. Aviso -
se já houver relatórios salvos neste
diretório, eles serão sobrescritos.
O padrão é "report".
-v,--verbose-debug Aumenta a verbosidade para modo debug.
-vv,--verbose-trace Aumenta a verbosidade para modo trace - torna-o mais lento, use apenas quando necessário.
O arquivo de regras pode ser especificado usando o argumento -f,--custom-file. O arquivo está no formato JSON e possui a seguinte estrutura:
finalVocê também pode consultar net.nandgr.cba.custom.model.Rules.java para ver a estrutura em código Java.
Já existem várias regras no diretório examples . De qualquer forma, abaixo estão listados exemplos para cada regra.
Se precisarmos encontrar classes com desserialização personalizada, podemos fazê-lo facilmente. Uma classe define desserialização personalizada implementando private void readObject(ObjectInputStream in). Portanto, precisamos apenas encontrar todas as classes onde esse método está definido. Bastaria definir uma regra como:
{
"rules": [{
"name": "Desserialização personalizada",
"methods": [{
"name": "readObject",
"visibility": "private",
"parameters" : [{
"type" : "java.io.ObjectInputStream"
}]
}]
}]
}
Isso reportará métodos com visibilidade private, nome readObject e um parâmetro do tipo java.io.ObjectOutputStream. Parâmetros são um array; se mais de um for especificado, todos devem corresponder para serem reportados. Como temos apenas uma regra, será criado um relatório chamado: custom-deserialization-0.html.
Neste caso, uma regra com dois métodos deve ser definida. A mesma do exemplo anterior para desserialização, e uma nova para corresponder a private void writeObject(ObjectOutputStream out). Conforme mostrado na estrutura JSON acima, a propriedade rules.rule.methods é um array de métodos, então podemos escrever uma regra assim:
{
"rules": [{
"name": "Serialização e desserialização personalizadas",
"methods": [{
"name": "readObject",
"visibility": "private",
"parameters" : [{
"type" : "java.io.ObjectInputStream"
}]
},{
"name": "writeObject",
"report": "false",
"visibility": "private",
"parameters" : [{
"type" : "java.io.ObjectOutputStream"
}]
}]
}]
}
A propriedade report foi definida como false para evitar reportar duas vezes a mesma regra. Estamos usando o segundo método apenas como condição, mas reportar apenas os métodos readObject deve ser suficiente para o propósito desta regra.
Se uma propriedade não for definida, ela sempre corresponderá como verdadeiro. Por exemplo, esta regra retornaria todas as definições de métodos:
{
"rules": [{
"name": "Definições de métodos",
"methods": [{
}]
}]
}
Invocações de métodos também podem ser encontradas. O JSON neste caso seria:
{
"rules": [{
"name": "String equals",
"invocations": [{
"owner": "java.lang.String",
"method": {
"name": "equals"
}
}]
}]
}
A propriedade owner especifica a classe que contém o método.
Outro exemplo de invocação de método um pouco mais útil que o anterior:
{
"rules": [{
"name": "Invocação de método por reflexão",
"invocations": [{
"owner": "java.lang.reflect.Method",
"method": {
"name": "invoke"
}
}]
}]
}
É o mesmo que qualquer invocação de método, mas o nome do método neste caso deve ser <init>.
{
"rules": [{
"name" : "Instanciação de String",
"invocations" : [{
"owner" : "java.lang.String",
"method" : {
"name" : "<init>"
}
}]
}]
}
Esta regra encontrará ocorrências de:
[...]
String s = new String("foo");
[...]
Neste exemplo, queremos encontrar usos de desserialização (não classes que definem comportamentos de serialização como nos exemplos anteriores). A desserialização ocorre quando ObjectInputStream.readObject() é invocado. Por exemplo, neste trecho de código:
ObjectInputStream in = new ObjectInputStream(fileInputStream);
Object o = in.readObject();
Portanto, precisamos encontrar invocações de método de ObjectInputStream chamado readObject. Mas isso encontrará muitos falsos positivos em um contexto de pesquisa, porque quando uma classe define desserialização personalizada, ela faz uma invocação a este método dentro de um método private void readObject(ObjectInputStream in), e isso poluiria demais o relatório. Se quisermos excluir esses casos e manter apenas a desserialização genuína, a propriedade notFrom pode ser usada:
{
"rules": [{
"name": "Uso de desserialização",
"invocations": [{
"owner": "java.io.ObjectInputStream",
"method": {
"name": "readObject"
},
"notFrom": {
"name": "readObject",
"visibility": "private"
}
}]
}]
}
Este arquivo encontrará invocações de java.io.ObjectInputStream.readObject() se a invocação não for feita dentro do método private void readObject(ObjectInputStream in).
Uma classe compilada com este código não será reportada:
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
Object o = in.readObject();
}
Mas esta será reportada:
public Object deserializeObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
Object o = in.readObject();
return o;
}
A propriedade from pode ser definida em invocações exatamente da mesma forma que notFrom, mas o resultado será o oposto: só corresponderá se a invocação for feita a partir do método definido.
A propriedade superClass pode ser usada neste caso. Se quisermos encontrar todas as classes que estendem javax.servlet.http.HttpServlet, uma regra pode ser:
{
"rules": [{
"name": "Java servlets",
"superClass" : "javax.servlet.http.HttpServlet"
}]
}
Uma regra pode ser escrita para encontrar classes que implementam um array de interfaces. Se mais de uma interface for definida na regra, a classe deve implementar todas elas para ser reportada. Se quisermos encontrar classes que implementam javax.net.ssl.X509TrustManager, a regra seria:
{
"rules": [{
"name": "Implementações de X509TrustManager",
"interfaces" : ["javax.net.ssl.X509TrustManager"]
}]
}
Observe que interfaces é um array, portanto certifique-se de adicionar as strings entre colchetes, ex: ["interface1", "interface2", ...].
Anotações também são suportadas. Múltiplas propriedades de anotações podem ser definidas em uma regra (encontrando anotações de classe), em métodos ou variáveis (parâmetros ou variáveis locais). Se todas forem encontradas na classe analisada, ela será reportada.
Por exemplo, se quisermos encontrar endpoints Spring, procuraríamos por classes ou métodos anotados com org.springframework.web.bind.annotation.RequestMapping. Então, a regra pode ser:
{
"rules": [{
"name": "Endpoint Spring - anotação de classe",
"annotations" : [{
"type" : "org.springframework.web.bind.annotation.RequestMapping"
}]
},
{
"name": "Endpoint Spring - anotação de método",
"methods" : [{
"annotations" : [{
"type" : "org.springframework.web.bind.annotation.RequestMapping"
}]
}]
}]
}
A propriedade rule.fields pode ser usada para encontrar campos de classe. Se quisermos encontrar campos String privados com nomes de senha, uma regra como esta poderia ser usada:
{
"rules": [{
"name" : "Campos de senha",
"fields" : [
{
"visibility" : "private",
"type" : "java.lang.String",
"nameRegex" : "(password|pass|psswd|passwd)"
}
]
}]
}
Para encontrar variáveis, rule.variables pode ser usado. Esta propriedade reportará variáveis locais e variáveis de argumentos de métodos.
Se quisermos encontrar todas as variáveis do tipo javax.servlet.http.Part, uma regra poderia ser:
{
"rules": [{
"name" : "Arquivo de upload Servlet",
"methods" : [{
"variables" : [{
"type" : "javax.servlet.http.Part"
}]
}]
}]
}
Múltiplas regras podem ser definidas no mesmo arquivo JSON. Elas serão processadas e reportadas separadamente e não afetarão umas às outras. Podemos combinar alguns dos exemplos anteriores:
{
"rules": [{
"name": "Desserialização personalizada",
"methods": [{
"name": "readObject",
"visibility": "private",
"parameters": [{
"type" : "java.io.ObjectInputStream"
}]
}]
},{
"name": "Invocação de método por reflexão",
"invocations": [{
"owner": "java.lang.reflect.Method",
"method": {
"name": "invoke"
}
}]
}]
}
Aqui, temos duas regras ("Desserialização personalizada" e "Invocação de método por reflexão"). Elas serão processadas como se fossem duas execuções separadas. E um relatório por regra será gerado. Se as regras tiverem o mesmo nome, elas serão reportadas no mesmo arquivo.
O projeto pode ser baixado e compilado para adicionar regras personalizadas mais complexas em código Java que não são cobertas pelo formato JSON. Já existem três exemplos no pacote net.nandgr.cba.visitor.checks. São eles CustomDeserializationCheck, DeserializationCheck e InvokeMethodCheck. Você pode criar suas próprias regras estendendo net.nandgr.cba.custom.visitor.base.CustomAbstractClassVisitor.
Como mencionado acima, os relatórios são criados por padrão na pasta report. Cada regra terá um arquivo separado, a menos que tenham o mesmo nome.
Se o relatório for muito grande, você pode dividi-lo usando o parâmetro -i,--items-report <maxItems>; cada um conterá o argumento especificado ou menos (se for o último).
Cada item reportado especifica o jar onde foi encontrado, o nome da classe e o nome do método (se relevante). Também mostra a versão decompilada da classe para facilitar uma verificação visual rápida.
Exemplo de como os itens são mostrados para uma regra que encontra instanciações de java.io.File:

Ao pesquisar por bugs de segurança, é muito útil ter um grafo de chamadas. Atualmente, um arquivo simples compatível com DOT é criado no diretório report.
O grafo contém todos os fluxos possíveis a partir dos quais os problemas encontrados podem ser invocados. Por exemplo, se uma regra para encontrar desserialização for usada,
será gerado um grafo contendo todos os caminhos possíveis que levam ao método que chama a desserialização.
O arquivo é call-graph.dot e teria a seguinte aparência (este é um exemplo extremamente simples):
graph callGraph {
"demo.callgraph.Class1:method1" -- "demo.callgraph.Class2:method2"
"demo.callgraph.Class3:method3" -- "demo.callgraph.Class2:method2"
}
Para exibi-lo visualmente, DOT pode ser usado (ou qualquer software compatível). Por exemplo, para converter o arquivo para svg:
dot -Tsvg call-graph.dot -o call-graph.svg
Isso é feito automaticamente por padrão se o DOT for encontrado no PATH do sistema. Caso contrário, o DOT pode ser instalado em sistemas baseados em Debian usando sudo apt-get install graphviz.
Isso criará um arquivo SVG chamado call-graph.svg que pode ser convertido para PNG ou visualizado usando programas como inkscape ou apenas firefox.
Um exemplo muito simples do arquivo call-graph.dot acima seria:

Existem algumas limitações, como por exemplo, se o item pesquisado estiver em um método java.lang.Runnable.run() ou similar, não encontrará de onde a thread foi executada.
Além disso, o grafo está limpando ciclos para evitar StackOverflowErrors; isso é feito de maneira um pouco conservadora para que a memória do sistema não seja drenada durante a análise de um diretório grande.
Mais opções serão adicionadas em versões futuras.
java -jar cba-cli-<version>.jar -a /caminho/com/jars -f /caminho/com/arquivo/json/regras.json
Para usar regras Java personalizadas, os nomes das classes devem ser especificados como argumentos de -c.
java -jar cba-cli-<version>.jar -a /caminho/com/jars -c DeserializationCheck
Aceita uma lista separada por espaços, então múltiplas regras personalizadas podem ser definidas (cada regra criará um relatório separado):
java -jar cba-cli-<version>.jar -a /caminho/com/jars -c DeserializationCheck InvokeMethodCheck CustomDeserializationCheck SuaRegraPersonalizada
java -jar cba-cli-<version>.jar -a /caminho/com/jars -f /caminho/com/arquivo/json/regras.json -c SuaRegraPersonalizada1 SuaRegraPersonalizada2
Para encontrar erros, a verbosidade pode ser aumentada. Nível de debug:
java -jar cba-cli-<version>.jar -a /caminho/com/jars -c SuaRegraPersonalizada1 -v
Nível de trace:
java -jar cba-cli-<version>.jar -a /caminho/com/jars -c SuaRegraPersonalizada1 -vv
No momento, o APK deve ser convertido para JAR primeiro para ser analisado.
d2j-dex2jar.sh -f -o app_to_analyze.jar app_to_analyze.apk-a o diretório contendo o arquivo jar convertido.Já existe um arquivo jar executável no diretório bin em: https://github.com/fergarrui/custom-bytecode-analyzer/blob/master/bin/cba-cli-0.1-SNAPSHOT.jar . Se você quiser fazer modificações ou adicionar regras personalizadas, o projeto pode ser compilado fazendo:
git clone https://github.com/fergarrui/custom-bytecode-analyzer.git
cd custom-bytecode-analyzer
mvn clean package
Dois jars serão gerados na pasta target. cba-cli-<version>.jar contém todas as dependências e é executável. Pode ser executado usando java -jar cba-cli-<version>.jar