본문 바로가기
Java

Finalizer attack

by ybs 2021. 1. 17.
반응형

이펙티브자바 item8 "finalizer 와 cleaner 사용을 피하라" 를 보면 finalizer 를 쓰지 말라고 한다.

cf) 자바9부터는 finalizer 대신 cleaner로 대체됐지만 이것도 사용하지 말라고 함

 

다양한 이유로 finalizer 를 사용하지 말라고 하는데, 그중 fanalizer 공격이 가능해 보안 문제를 일으킬 수 있다고 하는 부분에서 자세한 설명이 없어서 이해가 잘 안됐다.

 

과연 finalizer 로 어떻게 공격이 가능한걸까?

finalizer 는 객체 생성할 때 취약점이 존재한다. finalizer 는 java 메서드가 os로 반납해야되는 리소스를 해제 할 수있게 한다. 그런데 finalizer 메서드 에서 자바 코드가 실행될 수 있으므로 아래와 같은 코드도 허용된다.

public class Zombie {
	static Zombie zombie;

	public void finalize() {
		zombie = this;
	}
}


만약 Zombie 클래스의 finalizer 메서드가 호출되면 zombie static 변수에 this가 저장된다. 그러므로 객체는 다시 접근할 수 있게 됐고 gc 대상이 안된다.

더 교활한 버전도 있다. 부분적으로 구성된 오브젝트조차도 부활시킬 수 있다. Zombie2 생성자에는 value 값에 대한 유효성 검사로직이 들어있지만 검사 조건을 충족 못시켜도 finalizer로 작성할 수 있다. finalize() 메서드 존재로 인해 value 인수에 대한 검사의 결과는 무효화된다.

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;
    }
}

 

물론 위와 같은 코드를 직접 작성하는 사람은 아무도 없다. 그러나 클래스가 subclassed된 경우 취약점이 발생할 수 있다.

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());
    }
}
public 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!");
        }
    }
}

AttackVulnerable 클래스 main 메서드에서 새로운 AttackVulnerable 객체 생성을 시도한다. value가 -1이기 때문에 Exception이 발생하고 catch 블록으로 온다. `System.gc()`와 `System.runFinalization()` 호출은 vm이 gc를 실행하고 일부 finalizer를 실행하도록 한다.

 

이러한 호출은 공격이 성공하는 데 반드시 필요한 것은 아니지만 공격의 최종 결과를 보여준다. 즉, 값이 잘못된 Vulnerable 객체가 만들어진다. 위 코드를 실행하면 다음과 같은 결과가 나온다. 왜 Vulnerable value가 -1가 아니라 0일까? Vulnerable 생성자에서 인자 검사전까지는 value 할당을 하지 않기 때문이다. 그래서 value는 초기값 0이다.

java.lang.IllegalArgumentException: Vulnerable value must be positive
Vulnerable object 0 created!

Process finished with exit code 0

 

이러한 종류의 공격은 명시적인 보안 검사를 우회하는 데 사용될 수도 있다. 아래의 예제는 현재 디렉토리에 write 권한이 없을 때 SecurityException이 발생하도록 설계됐다.

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());
    }
}
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!");
        }
    }
}

결과

java -Djava.security.manager AttackInsecure
java.security.AccessControlException: Access denied (java.io.FilePermission index write)
Insecure object 0 created!

 

공격을 피하는 기존 방법

Java Language Specification(JLS) 세번째 edition 까지는 initialized flag, subclassing 금지, final finalizer 생성 세가지였다. 하지만 완전히 해결하기엔 부족하다.

 

initialized flag
객체가 정상적으로 생성될 때 flag값을 셋팅하고 메서드 호출 때마다 제일 먼저 저 flag 값을 검사하는 방법이다. 이 코딩 기법은 작성하기 번거롭고 실수로 생략하기 쉽다. 그리고 subclassing을 통한 공격을 막을 수는 없다.

 

preventing subclssing
클래스를 final로 선언함으로써 subclass 자체를 막아버린다. 하지만 이 기법은 확장성 자체를 없애버린다.

 

create a final finalizer
final finalizer 메서드를 생성함으로써 subclass에서 finalizer 메서드 재사용을 금지시킨다. 하지만 이 방식은 단점이 있는데 위의 Insecure 클래스를 예로 설명하겠다. Insecure 클래스에 finalize() 메서드를 만들어야 하고(엄밀히 말하면 Object 클래스의 finalize 메서드를 오버라이딩 해야하고) finalize 메서드를 쓴 객체는 안쓴 객체보다 더 오래 살아있게 된다. 이게 왜 단점이냐면 일반적으로 레퍼런스가 끊긴 객체는 jvm gc 의해 수집되야 하는데 finalizer 구현이 있는 객체는 그게 실행될 때까지 gc 수집이 안된다. 자바 언어 스펙은 어떤 스레드가 finalizers 실행할지(finalize 메서드를 실행할지) 보장하지 않는다. 즉 언제 실행될지, 반드시 실행되는지에 대한 보장이 없다.

공격을 피하는 더 좋은 방법

추가 코드나 제한(restirctions)없이 이러한 종류의 공격을 막기위해 자바 설계자(Java designers)는 JLS를 수정했다. 그래서  java.lang.Object 가 생성되기 전에 생성자에서 exception이 발생하면 finalize 메서드가 실행되지 않는다.

 

그런데 java.lang.Object가 생성되기 전에 어떻게 exception이 발생할 수 있을까? 결국 모든 생성자의 첫행은 this() 또는 super()에 대한 호출이어야 한다. 만약 생성자에 이러한 명시적인 호출이 포함되어 있지 않으면 super() 호출이 암묵적으로 추가된다. 따라서 객체가 생성되기 전에 동일한 클래스 또는 슈퍼 클래스의 다른 객체가 만들어져야 한다. 결국 생성되고 있는 메서드의 코드가 실행되기 전에 java.lang.Object 자체의 생성과 모든 subclass의 생성이 이뤄진다.

 

java.lang.Object가 생성되기 전에 예외를 throw하는 방법을 이해하려면, Object 생성 순서를 정확하게 이해해야한다. JLS는 순서를 명시적으로 작성했다.

 

Object가 생성될 때 JVM은 다음과 같이 동작한다.

1. 객체를 위한 공간을 할당한다.

2. 객체의 모든 인스턴스 변수를 기본값으로 설정. 여기에는 객체의 수퍼 클래스에 있는 인스턴스 변수가 포함된다.

3. 객체에 대한 파라미터 변수를 지정한다.

4. 명시적 또는 암시적 생성자 호출(생성자에서 this() 또는 super() 호출)을 처리한다.

5. 클래스의 변수를 초기화한다.

6. 생성자의 나머지 부분을 실행한다.

 

요점은 생성자 안의 모든 코드가 처리되기 전에 생성자의 파라미터가 처리된다는 것이다. 즉, 파라미터를 처리하는 동안 유효성 검사를 수행하면 예외를 throw하여 클래스가 finalize 되지 않도록 할 수 있다.

public 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));
    }
}

위 코드를 보면 생성자 Invulnerable에서 checkValues 메서드를 수행하고 그 결과로 private 생성자를 호출한다. 이 메서드는 생성자가 super class의 생성자를 호출하기 전에 호출된다. 따라서 checkValues에 예외가 발생하면 Invulnerable 객체는 finalize 되지 않는다.

public 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");
        }
    }
}

결과

java.lang.IllegalArgumentException: Invulnerable value must be positive
Attack failed

 

이 글은 IBM 개발자 페이지에 있는 finalizer attack 문서를 기반으로 작성했다. 하지만 이제 더이상 원문 pdf 링크를 제공하지 않는다. 새롭게 개편된 페이지 에서도 finalizer attack 검색 결과가 나오지 않는다.

 

반응형