Os finalizadores podem causar uma vulnerabilidade no código Java usado para criar objetos. A exploração é uma variação da técnica conhecida de usar um finalizador para ressuscitar um objeto. Quando um objeto com o método finalize() fica inacessível, ele é colocado em uma fila para ser processado posteriormente. Esta dica explica como a exploração funciona e mostra como é possível proteger o código contra ela. Todos os exemplos de código estão disponíveis para download.
A ideia dos finalizadores é permitir que um método Java libere recursos nativos que precisem ser devolvidos ao sistema operacional. Infelizmente, qualquer código Java pode ser executado em um finalizador, permitindo um o código como o da Listagem 1
Listagem 1. Classe que pode ser ressuscitada
public class Zombie {
static Zombie zombie;
public void finalize() {
zombie = this;
}
}
|
Quando o finalizador Zombie é chamado, ele pega o objeto que está sendo finalizado, — referenciado por this — , e o armazena em uma variável zombie estática. Agora, o objeto pode ser alcançado novamente e não pode ser coletado como lixo.
Uma versão mais insidiosa desse código permite que até mesmo um objeto parcialmente construído seja ressuscitado. Mesmo que um objeto não passe em algum critério de correção em seu inicializador, ele ainda pode ser criado por um finalizador, como na Listagem 2:
Listagem 2. Criando uma classe ilegal
public class Zombie2 {
static Zombie2 zombie;
int value;
public Zombie2(int value) {
if(value < 0) {
throw new IllegalArgumentException("Negative Zombie2 value");
}
this.value = value;
}
public void finalize() {
zombie = this;
}
}
|
Na Listagem 2, o efeito da verificação do argumento value é negado pela existência do método finalize() .
Naturalmente, é pouco provável que alguém escreva um código como o da Listagem 2. Mas uma vulnerabilidade pode surgir se a classe for subclassificada, como na Listagem 3:
Listagem 3. Uma classe vulnerável
class Vulnerable {
Integer value = 0;
Vulnerable(int value) {
if(value <= 0) {
throw new IllegalArgumentException("Vulnerable value must be positive");
}
this.value = value;
}
@Override
public String toString() {
return(value.toString());
}
}
|
A classe Vulnerable na Listagem 3 foi projetada para evitar que um valor não positivo de value seja configurado. Essa intenção é subvertida pelo método AttackVulnerable() , mostrado na Listagem 4:
Listagem 4. Uma classe para subverter a classe
Vulnerable
class AttackVulnerable extends Vulnerable {
static Vulnerable vulnerable;
public AttackVulnerable(int value) {
super(value);
}
public void finalize() {
vulnerable = this;
}
public static void main(String[] args) {
try {
new AttackVulnerable(-1);
} catch(Exception e) {
System.out.println(e);
}
System.gc();
System.runFinalization();
if(vulnerable != null) {
System.out.println("Vulnerable object " + vulnerable + " created!");
}
}
}
|
No método main() da classe AttackVulnerable , é feita uma tentativa de instanciar um novo objeto AttackVulnerable . Como o valor de value está fora do intervalo, é lançada uma exceção que é capturada no bloco catch . As chamadas System.gc() e System.runFinalization() incentivam a VM a executar um ciclo de coleta de lixo e os finalizadores. Estas chamadas não são necessárias para o ataque ter sucesso, mas servem para demonstrar o resultado final do ataque, que a criação de um objeto Vulnerable com valor inválido.
A execução das etapas de teste apresenta o seguinte resultado:
java.lang.IllegalArgumentException: Vulnerable value must be positive Vulnerable object 0 created! |
Por que o valor de Vulnerable é 0 e não -1? Note que no construtor Vulnerable , na Listagem 3, a atribuição a value não acontece até depois da verificação do argumento. Assim, value tem seu valor inicial, que nesse caso é 0.
Esse tipo de ataque pode ser usado até mesmo para evitar verificações de segurança explícitas. Por exemplo, a classe Insecure da Listagem 5 foi projetada para lançar a SecurityException se for executada sob um SecurityManager e o responsável pela chamada não tiver permissão de gravar no diretório atual:
Listagem 5.
Classe Insecure
import java.io.FilePermission;
public class Insecure {
Integer value = 0;
public Insecure(int value) {
SecurityManager sm = System.getSecurityManager();
if(sm != null) {
FilePermission fp = new FilePermission("index", "write");
sm.checkPermission(fp);
}
this.value = value;
}
@Override
public String toString() {
return(value.toString());
}
}
|
A classe Insecure da Listagem 5 pode ser atacada da mesma maneira que antes, como mostrado na classe AttackInsecure da Listagem 6:
Listagem 6. Ataque à classe
Insecure
public class AttackInsecure extends Insecure {
static Insecure insecure;
public AttackInsecure(int value) {
super(value);
}
public void finalize() {
insecure = this;
}
public static void main(String[] args) {
try {
new AttackInsecure(-1);
} catch(Exception e) {
System.out.println(e);
}
System.gc();
System.runFinalization();
if(insecure != null) {
System.out.println("Insecure object " + insecure + " created!");
}
}
}
|
Executar o código da Listagem 6 sob um SecurityManager apresenta o seguinte resultado:
java -Djava.security.manager AttackInsecure java.security.AccessControlException: Access denied (java.io.FilePermission index write) Insecure object 0 created! |
Até a terceira edição do JLS (Java Language Specification) ser implementada no Java SE 6, as únicas maneiras de evitar o ataque — usando um sinalizador initialized , proibindo definir como subclasse ou criando um finalizador final — , eram soluções insatisfatórias.
Usar um sinalizador initialized
Um modo de evitar o ataque é usar um sinalizador initialized , que é configurado como true , depois que o objeto tiver sido criado corretamente. Cada método na classe primeiro verifica se initialized foi configurado e lança uma exceção se não tiver sido. É cansativo desenvolver esse tipo de codificação, é fácil omiti-la por acidente e ela não impede que um invasor defina o método como subclasse.
É possível declarar a classe que está sendo criada como final. Isso significa que ninguém pode criar uma subclasse dessa classe, o que impede o ataque de funcionar. No entanto, essa técnica elimina a flexibilidade da capacidade de estender a classe a fim de especializá-la ou adicionar funcionalidades extras.
É possível criar um finalizador para a classe que está sendo criada e declará-lo como final. Isso significa que nenhuma subclasse dessa classe pode declarar um finalizador. A desvantagem dessa abordagem é que a existência do finalizador significa que o objeto é mantido ativo por mais tempo do que poderia ser.
Para tornar mais fácil evitar esse tipo de ataque sem a introdução de código extra ou restrições, os designers de Java modificaram o JLS (veja Recursos) para dizer que, se uma exceção for lançada em um construtor antes de o java.lang.Object ser construído, o método finalize() desse método não será executado.
Mas como é possível lançar uma exceção antes de java.lang.Object ser construído? Afinal, a primeira linha de qualquer construtor deve ser uma chamada a this() ou super(). Se o construtor não incluir essa chamada explícita, uma chamada super() é acrescentada implicitamente. Assim, antes de um objeto ser criado, outro objeto da mesma classe ou de sua superclasse deve ser construído. Por fim, isso resulta na criação do próprio java.lang.Object , e depois, na criação de todas as subclasses, antes de qualquer código do método que está sendo construído ser executado.
Para entender como uma exceção pode ser lançada antes de java.lang.Object ser construído, é necessário entender a sequência exata de criação do objeto. O JLS enuncia a sequência explicitamente.
Quando um objeto é criado, a JVM:
- Aloca espaço para o objeto.
- Configura todas as variáveis de instância do objeto com seus valores padrão. Isso inclui as variáveis de instância nas superclasses do objeto.
- Designa as variáveis de parâmetro do objeto.
- Processa qualquer chamada de construtor explícita ou implícita (uma chamada para
this()ousuper()no construtor). - Inicializa as variáveis na classe.
- Executa o restante do construtor.
O ponto-chave é que os parâmetros do construtor são processados antes de qualquer código dentro do construtor ser processado. Isso significa que, se for feita a validação durante o processamento dos parâmetros, é possível — lançando uma exceção — evitar que sua classe seja finalizada.
Isso resulta em uma nova versão, mostrada na Listagem 7, da classe da Listagem 3 chamada Vulnerable :
Listagem 7.
Classe Invulnerable
class Invulnerable {
int value = 0;
Invulnerable(int value) {
this(checkValues(value));
this.value = value;
}
private Invulnerable(Void checkValues) {}
static Void checkValues(int value) {
if(value <= 0) {
throw new IllegalArgumentException("Invulnerable value must be positive");
}
return null;
}
@Override
public String toString() {
return(Integer.toString(value));
}
}
|
Na Listagem 7, o construtor público de Invulnerable chama um construtor particular que chama o método checkValues para criar seu parâmetro. Esse método é chamado antes de o construtor fazer sua chamada para criar sua superclasse, que é o construtor de Object. Assim, se for lançada uma exceção em checkValues, o objeto Invulnerable não será finalizado.
O código da Listagem 8 tenta atacar Invulnerable:
Listagem 8. Tentativa de subverter a classe
Invulnerable
class AttackInvulnerable extends Invulnerable {
static Invulnerable vulnerable;
public AttackInvulnerable(int value) {
super(value);
}
public void finalize() {
vulnerable = this;
}
public static void main(String[] args) {
try {
new AttackInvulnerable(-1);
} catch(Exception e) {
System.out.println(e);
}
System.gc();
System.runFinalization();
if(vulnerable != null) {
System.out.println("Invulnerable object " + vulnerable + "
created!");
} else {
System.out.println("Attack failed");
}
}
}
with the addition of
} else {
System.out.println("Attack failed");
|
Com Java 5, que grava em uma versão mais antiga do JLS, um objeto Invulnerable é criado:
java.lang.IllegalArgumentException: Invulnerable value must be positive Invulnerable object 0 created! |
Java SE 6 (a partir do release de disponibilidade geral da JVM Oracle e SR9 da JVM da IBM), segue a última especificação, de modo que o objeto não é criado:
java.lang.IllegalArgumentException: Invulnerable value must be positive Attack failed |
Os finalizadores são recurso infeliz da linguagem Java. Embora o coletor de lixo possa recuperar automaticamente a memória que não é mais usada por objetos Java, não existe um mecanismo para reciclar recursos nativos, como a memória nativa, descritores de arquivos ou soquetes. As bibliotecas Java padrão que fornecem essa interface com esses recursos nativos geralmente têm um método close() para permitir a limpeza adequada — mas também usam finalizadores para garantir que não ocorra vazamento de recursos se um objeto não for encerrado apropriadamente.
Para outros objetos, em geral é melhor evitar os finalizadores. Não há garantia de quando um finalizador será executado, ou mesmo se ele será executado. A existência de um finalizador significa que um objeto inatingível não pode ser coletado para o lixo até que o finalizador tenha sido executado, e esse objeto talvez esteja mantendo ainda mais objetos ativos. Isso resulta em um aumento no número de objetos ativos e, portanto, no uso de heap do processo Java.
A capacidade de um finalizador reviver um objeto destinado à coleta de lixo é claramente uma consequência não intencional da maneira em que o mecanismo de finalização funciona. Implementações mais recentes da JVM agora permitem proteger seu código contra as implicações de segurança desse efeito.
| Descrição | Nome | Tamanho | Método de download |
|---|---|---|---|
| Code samples for this tip | j-fv.zip.zip | 4KB | HTTP |
Informações sobre métodos de download
Aprender
- Java Language Specification: Consulte a referência técnica da linguagem Java.
- Secure Coding Guidelines for the Java Programming Language: Leia essas diretrizes para obter outras sugestões de boas práticas de codificação.
- Effective Java, 2.ª ed. (Joshua Bloch, Prentice Hall, 2008): Esse livro inclui uma análise dos problemas com finalizadores e outras novidades.
-
Bloco de Notas do Designer de Linguagem: Veja essa série do developerWorks, escrita por Brian Goetz, sobre problemas de design de linguagem que afetam o futuro da linguagem Java.
-
Java theory and practice: Explore a longa série de Brian Goetz para o developerWorks sobre conceitos, técnicas e boa prática de programação Java.
-
Zona de tecnologia Java do developerWorks: Encontre centenas de artigos sobre cada aspecto da programação Java.
Obter produtos e tecnologias
-
Avalie os produtos IBM da maneira que for melhor para você: faça download da versão de teste de um produto, avalie um produto on-line, use-o em um ambiente de nuvem ou passe algumas horas na SOA Sandbox aprendendo como implementar Arquitetura Orientada a Serviços de forma eficiente.
Discutir
- Java security: Participe do fórum de segurança Java no developerWorks.
- Participe da comunidade do developerWorks. Entre em contato com outros usuários do developerWorks, enquanto explora os blogs, fóruns, grupos e wikis orientados ao desenvolvedor.
