Javassist를 이용한 Byte-Code Injection
📄

Javassist를 이용한 Byte-Code Injection

Created
Oct 28, 2020 01:48 PM
Tags
java
bci
 
만약 이미 배포해버린 자바 코드. 즉, .class 파일을 수정하고 싶다면 어떻게 해야 할까?
 
Decompile도 하나의 방법이겠지만... 이보다는 BCI(Byte-code Injection)를 사용하는 것이 조금 더 효율적인 방법이다.
 
Java는 각각의 클래스 파일을 메모리에 Load할 때 java.lang.ClassLoader 라는 객체를 이용해 Dynamic하게, 필요할 때 불러온다.
 
이를 이용하여 클래스 파일을 Modify 할 수 있는데, 이를 BCI라고 한다.
 
여러가지 라이브러리가 있지만, 여기에서는 가장 빠르게 학습하기 쉬운 Javassist 라이브러리를 이용하여 BCI를 어떻게 하는지 보도록 하겠다.
 
방법은 크게 어렵지 않다. 라이브러리에서 강력하고 다양한 API를 제공하기에 이를 이용하면 된다. 거의 기존의 Java 코딩과 다를 것이 없는 수준.
 
예제를 하나 보자.
 
import javassist.ClassPool; import javassist.CtClass; ClassPool cp = ClassPool.getDefault(); CtClass cc = cp.get("test.Rectangle");
 
위 코드는 javassist 라이브러리를 이용하여 test.Rectangle 이라는 클래스를 Load한 것이다.
 
여기서 다음과 같은 코드로 test.Rectangle 클래스의 Superclass를 지정하도록 할 수 있다.
 
// cc := test.Rectangle class file (CtClass type) cc.setSuperclass(cp.get("test.Point")); cc.writeFile();
 
간단하지 않은가? 단 네 줄의 코드로 이미 컴파일까지 마친 test.Rectangle 클래스에 대해, test.Point 라는 클래스를 Load해 test.Rectangle 클래스의 Superclass로 만들어주게 되었다.
 
참고로 마지막 writeFile() 은 수정을 마친 test.Rectangle 클래스를 다시 원본 클래스 파일에 Write한다는 의미. 따라서 위의 연산을 진행한 이후 test.Rectangle 클래스를 사용하게 되면, Superclass로 test.Point 클래스를 가진 test.Rectangle 클래스가 Load될 것이다.
 

Reading and Writing Bytecode

 
이렇게 javassist.CtClass 라는 객체를 이용해 클래스 파일을 나타내게 되고, 이 객체는 javassist.ClassPool 이라는, 클래스 파일들을 관리하는 Class Pool 객체를 이용해서 가져올 수 있게 된다.
 
Class Pool. 다시말해 앞서 Load한 클래스 파일(CtClass)들을 캐싱 한다는 말.
 
// cc := CtClass type Loaded Class file byte[] buf = cc.toBytecode(); Class clazz = cc.toClass();
 
이와 같이 CtClass 는 클래스 파일에 대해 수정한 뒤 Bytecode 또는 java.lang.Class 타입의 객체로 Convert 또한 가능하다.
 
참고로 아예 클래스나 인터페이스 자체를 만들수도 있는데 이건 논외. 여기서는 BCI에 대해서만 다루도록 하겠다.
 

Frozen Class

 
위와 같이 Converting 과정을 거친 CtClass 객체는 Frozen Class라고 불리며, 이후 해당 클래스 파일을 수정할 수 없게 된다.
 
물론 영원히 불가능한 것은 아니고, defrost() 메서드를 이용해 다시 수정할 수 있도록 만들어 줄 수 있다.
 
CtClass cc = cp.get("test.Rectangle"); cc.writeFile(); // frozen cc.setSuperclass(/* ... */); // ERROR cc.defrost(); // defrost cc.setSuperclass(/* ... */); // OK
 

Class Search Path

 
지금까지 ClassPool.getDefault() 메서드를 이용해 ClassPool 인스턴스를 받아온 뒤, ClassPool.get() 메서드를 이용해 클래스 파일을 Load하는 과정을 보았다. 그럼 이 클래스 파일은 어디서 가져오는 것일까?
 
기본적으로 ClassPool.getDefault() 메서드는 JVM과 동일한 경로에 위한 파일을 기준으로 클래스 파일을 탐색하게 된다. 따라서, Tomcat과 같은 WAS(Web App Server)를 사용하게 되면 예상하지 못한 클래스 파일을 가져오거나 아예 가져오지 못하는 경우가 발생하게 된다.
 
pool.insertClassPath("/usr/local/javalib"); pool.insertClassPath(new URLClassPath( "www.javassist.org", 80, "/java/", "org.javassist." ); // www.javassist.org:80/java/org/javassist pool.insertClassPath(new ByteArrayClassPath("ClassName", buf));
 
따라서 필요한 경우 위와 같이 Classpath를 Insert하는 방법으로 원하는 클래스 파일을 Load할 수 있도록 지정해주도록 하자.
 

Class Loader

 
Java는 java.lang.ClassLoader 를 이용해 클래스 파일을 가져온다고 했다.
 
이를 이용해 클래스 파일을 Load할 때 수정(BCI)할 수 있으나, 여기서는 이보다는 조금 더 쉬운 방법은 javassist.Loader 를 이용해 BCI를 하는 방법을 보도록 하겠다.
 
참고로 지금까지 진행했던 방법은 클래스 파일을 직접 지정해서 Load한 뒤 BCI를 했던 것이고, 지금 볼 방법은 Class Loader를 통해 Load되는 클래스 파일에 대해 BCI를 하는 것이다. 이 차이를 알아두자.
 
public class Main { public static void main(String[] args) throws Throwable { ClassPool cp = ClassPool.getDefault(); Loader cl = new Loader(cp); // javassist.Loader // 지금까지 보았던 방법 CtClass ct = cp.get("test.Rectangle"); ct.setSuperclass(cp.get("test.Point")); // Class Loader를 이용해 클래스를 가져오는 방법 Class c = cl.loadClass("test.Rectangle"); Object rect = c.getDeclaredConstructor().newInstance(); } }
 
방법은 크게 다르지 않다. 단, 위와 같이 javassist.Loader 를 이용하는 경우 클래스 파일을 Load할 때(loadClass()) BCI를 하도록 일종의 이벤트를 등록해놓을 수 있다.
 
public interface Translator { public void start(ClassPool cp) throws NotFoundException, CannotCompileException; public void onLoad(ClassPool cp, String classname) throws NotFoundException, CannotCompileException; }
 
이벤트 리스너의 인터페이스는 위와 같으며, 이를 구현하는 방식으로 이벤트의 등록이 가능하다.
 
public class MyTranslator implements Translator { @Override public void start(ClassPool cp) throws NotFoundException, CannotCompileException { } @Override public void onLoad(ClassPool cp, String classname) throws NotFoundException, CannotCompileException { CtClass cc = cp.get(classname); cc.setModifiers(Modifier.PUBLIC); // modified class modifier // ... } }
 
이런 식이다. 각 메서드의 의미는 다음을 참고.
 
  • start() : Translator 가 등록될 때 호출
  • onLoad() : javassist.Loader 가 클래스를 Load하기 전 호출
 
따라서 일반적으로 onLoad() 메서드를 오버라이드 하는 방식으로 구현한다.
 
public class Main { public static void main(String[] args) throws Throwable { ClassPool cp = ClassPool.getDefault(); Loader cl = new Loader(cp); Translator t = new MyTranslator(); cl.addTranslator(cp, t); cl.loadClass("test.Rectangle"); // emit => run MyTranslator } }
 
사용은 위와 같이 Loader.addTranslator() 메서드를 이용하면 된다.
 

Introspection and Customization

 
지금까지는 클래스에 대해서만 다루었으나, 메서드 역시 수정이 가능하다.
 
메서드를 표현하는 javassist.CtMethod 라는 객체를 이용하며, insertBefore() , insertAfter() , insertAt() 과 같은 API를 이용해 메서드에 대한 BCI가 가능하다. 단, 들어가는 매개변수는 String 이나, 다음 세 가지 형태만이 가능하다.
 
  • Inline: System.outPrintln("Hello");
  • Block: { System.out.println("Hello"); }
  • Statement: if (i < 0) { i = -1; }
 
또한 다음의 Special Character를 이용해 BCI의 타깃이 되는 메서드의 Parameters에 대한 참조 역시 가져올 수 있다.
 
  • $0 : this Context
  • $1 , $2 , ... : Parameters
  • $_ : Return Value
 
다른 것들도 많으나, 주로 사용되는 것은 위와 같다. 실제 사용 예는 다음 코드를 참고.
 
// cm := CtMethod type object cm.insertAfter(String.join("\n", "System.out.println(\"Hello!\");", "if ($1 == true) {", " System.out.println(\"Hello2\");", "}" );
 
Field 역시 생성이 가능. 이 때는 CtField.make() 메서드를 이용한다.
 
// cc := CtClass type Object cc.addField(CtField.make("private boolean isValid = true;", cc));
 
실제 위와 같은 코드가 클래스 파일 내에 BCI 되는 것이다.
 
여기까지가 javassist 라이브러리를 이용한 튜토리얼이며, 굉장히 다양한 방법과 API를 제공하니 필요하다면 문서를 참고하도록 하자.