동료 직원 중 한 분이 백기선님의 Finalize Attack YouTube 영상을 추천해 주셔서 보게 되었는데, 매우 흥미로운 내용을 다루고 있어 포스팅하게 되었습니다. 백기선님은 송금 관련 Finalize Attack을 재미있게 설명해 주셨는데, 저는 해당 내용을 제 코드로 작성해서 포스팅해 보겠습니다.
Finalize Attack은 finalize()
메서드를 악용하여 객체의 수명 주기 동안 민감한 정보를 노출시키거나, 객체의 상태를 변경하거나, 보안에 취약점을 만들어내는 공격 기법입니다. 기본적으로 finalize()
메서드는 객체가 더 이상 참조되지 않고 가비지 컬렉터에 의해 수거되기 직전에 호출됩니다. 이를 이용해 공격자는 객체의 finalize()
메서드를 오버라이드하여 변경할 수 있습니다.
FinalizeAttackExample.java
public class FinalizeAttackExample {
// 싱글톤 인스턴스를 저장하기 위한 정적 변수
protected static FinalizeAttackExample instance;
private String importantField;
// 생성자: 중요한 필드를 초기화하며, 필드가 null이거나 비어 있으면 예외를 발생시킵니다.
public FinalizeAttackExample(String importantField) {
if (importantField == null || importantField.isEmpty()) {
throw new IllegalArgumentException("Important field must not be null or empty");
}
this.importantField = importantField;
}
// 객체의 finalize 메서드를 분리하여 FinalizeAttackFinalizer 클래스에서 오버라이드할 수 있도록 합니다.
public static FinalizeAttackExample getInstance(String importantField) {
try {
return new FinalizeAttackExample(importantField);
} catch (IllegalArgumentException e) {
// 객체 생성에 실패하면 이전 instance를 반환합니다.
return instance;
}
}
}
FinalizeAttackFinalizer.java
public class FinalizeAttackFinalizer extends FinalizeAttackExample {
// 생성자
public FinalizeAttackFinalizer(String importantField) {
super(importantField);
}
@Override
protected void finalize() throws Throwable {
super.finalize();
// finalize 메서드에서 instance를 재할당하여 객체를 다시 사용 가능하게 합니다.
// 이로 인해 객체가 가비지 컬렉션 후에도 재활용될 수 있습니다.
FinalizeAttackExample.instance = this;
}
}
main.java
public class Main {
public static void main(String[] args) {
FinalizeAttackExample example1 = FinalizeAttackExample.getInstance("Valid Field");
FinalizeAttackExample example2 = FinalizeAttackExample.getInstance(""); // 예외 발생
System.out.println(example1 == example2); // true를 출력합니다.
}
}
위 코드에서는 importantField
가 null이거나 비어 있을 때 IllegalArgumentException을 발생시킵니다. 생성자에서 예외가 발생하더라도 finalize() 메서드에서 instance를 재할당하여 객체를 다시 사용할 수 있게 만듭니다. 이는 객체가 완전히 초기화되지 않은 상태에서 다시 사용될 수 있음을 의미하며, 보안상 문제가 될 수 있을것 같습니다!
클래스에 final 키워드를 사용하면 상속을 방지할 수 있습니다. 따라서 공격자가 finalize() 메서드를 오버라이드할 수 없습니다. 혹은 finalize()
를 final 키워드를 사용하여 상속을 방지 할 수 있습니다
public final class SecureExample {
private String importantField;
// 생성자: 중요한 필드를 초기화하며, 필드가 null이거나 비어 있으면 예외를 발생시킵니다.
public SecureExample(String importantField) {
if (importantField == null || importantField.isEmpty()) {
throw new IllegalArgumentException("Important field must not be null or empty");
}
this.importantField = importantField;
}
// 기타 메서드
}
or
// 백기선님의 코드 참고
public class SecureExample {
private String importantField;
// 생성자: 중요한 필드를 초기화하며, 필드가 null이거나 비어 있으면 예외를 발생시킵니다.
public SecureExample(String importantField) {
if (importantField == null || importantField.isEmpty()) {
throw new IllegalArgumentException("Important field must not be null or empty");
}
this.importantField = importantField;
}
// final 키워드를 사용하여 finalize 메서드를 오버라이드할 수 없게 합니다.
@Override
protected final void finalize() throws Throwable {
// 자원 해제 코드
System.out.println("Finalize called");
}
// 기타 메서드
}
Java의 finalize() 메서드는 유용할 수 있지만, Finalize Attack과 같은 보안 취약점을 초래할 수 있습니다. 따라서 필요에 따라 finalize()
메서드의 사용하고, 불필요한 경우 제거 또는 더 안전한 자원 정리 방법을 사용하는 것이 좋습니다. Cleaner (Java 9 부터)와 같은 새로운 기능을 활용하여 애플리케이션의 안정성과 보안을 강화할 수 있다고 하는데 나중에 기회가 되면 포스팅 해보는걸로 하겠습니다 :)