「ソースないけどバグ直して」をランタイムで可能にするJavaバイトコードプログラミング

#はじめに
この記事は[シスコ同志によるAdvent Calendar](https://qiita.com/advent-calendar/2020/cisco)の25日目として投稿しています。 
幸いタイトルのような無茶振りには遭ったことはありませんが、シスコではJavaバイトコードを操作する技術を使った監視製品を開発しているので、その技術について本記事でサンプルを交えて紹介します。

## この記事を読んで出来ること
![BCI_demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62311/10bc51e1-5fdf-137b-6b34-ee7430933487.gif)
おわかりでしょうか？HelloWorldは**Alice**に対して挨拶するプログラムだったのに、`-javaagent:agent.jar`というオプションを付けて実行すると、**Mallory**に挨拶するプログラムに変わってしまいました。

このカラクリは**JVMが読み込んだJavaバイトコードを実行時に書きかえ**、変数の値をAliceからMalloryに変えています。HelloWorldのソースコードも、コンパイル後の.classファイルも変えていないにも関わらず、実行時に処理を変更できてしまうのです。

## バイトコードインジェクション
これが本記事で紹介する**バイトコードインジェクション（BCI: Byte Code Injection/Instrumentation）**という技術です。ソースコードやビルド済みのアプリを変えることなく、ランタイムでアプリの挙動を変更できるという非常に強力なテクニックであり、うまく使用すると以下のようなことが可能です。

1. ソースコードに**ログコードを入れずに**、変数の値やパフォーマンスを取得する
2. 本番環境にデプロイしたJavaアプリを**動かしたまま処理を変更する**
3. 脆弱性が見つかったライブラリを**ランタイムで変更し、セキュアに実行する**

バイトコードインジェクションがどのようなしくみでJavaバイトコードを書きかえるのか、サンプルコードとともに紹介します。


## Dockerイメージ
本記事で使用するJVM環境と、サンプルコードが入ったDockerイメージ・ソースコードを公開しています。環境はOpenJDK1.8.0_275とUbuntu 20.10のシンプル構成です。

- DockerHubレポジトリ：https://hub.docker.com/repository/docker/cilto/bytecode-injection
- GitHubレポジトリ：https://github.com/cilto/bytecode-injection

# バイトコードを操作する技術

## JavaでHello Worldプログラムが実行されるまで
Javaのプログラムはコンパイル時に、**Javaバイトコード**というJVMが解釈できる中間言語の形に変換されます。JVMはバイトコードを読み込み、実行環境に応じたマシンコードに変換し、実行します。

![how_jvm_runs_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62311/bd802bc6-334b-af14-8902-1c7c0da5419b.png)


## バイトコードを実行時に書きかえるAPI
Java1.5より、**JVM TI**（Java Virtual Machine Tool Interface）という仕様が策定され、JVM上のアプリケーションを外からコントロールしたり、デバッグできるAPIが導入されました。その仕様では**java.lang.instrument**パッケージを使って、**JVMにロードされたバイトコードを実行時に変更する**機能を提供しています。

`-javaagent:xxx.jar`というJVMオプションを指定すると、アプリケーションをロードする際に、**指定されたプログラム**がバイトコードを読み込み、自在に書きかえ、アプリケーションの動作を変えることができます。

そのバイトコード操作を担うプログラムを**エージェント**と呼び、アプリケーションと独立して動くプログラムとなります。このエージェントがどのようなしくみで動くのか、デモプログラムのHello Worldを例に見てみましょう。


## バイトコードを書きかえる仕組み
以下の図がバイトコードを置きかえる際の流れになります。一つずつ追って見ていきましょう。

![BCI-flow-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62311/9a2d0259-cfbd-3ecc-741c-e05fa2e09ba6.png)


1. javaagentオプションが指定されると、JVMは**アプリケーションよりも前に**javaagentで指定したエージェントを読み込み、エージェントが定義するpremainメソッドを実行します。（"pre"mainという名前の通りの動作です）
2. premainメソッドでは、**バイトコードを書きかえるTransformerインスタンス**を生成し、JVMに**コールバック関数として登録**します。

3. エージェントの処理が終了すると、JVMはアプリケーションのバイトコード（HelloWorld.class）のロードを始めます。
4. 新しいクラスをロードする度に、JVMで**ClassFileLoadHookイベント**が発火し、登録されたTransformerをコールバックとして呼び出します。（JVM TIの仕様です）
5. Transformerには**ロードされたバイトコードがclassfileBufferという変数で渡されます**。この変数を通してクラス定義を読み込み、書きかえることができます。エージェントはアプリケーションよりも前に読み込まれるため、アプリケーションのバイトコード全てをロード時に読み込み、置きかえることが可能です。これがバイトコードインジェクションの肝となります。

### ClassFileLoadHookイベントの中身
5つ目のステップで渡されるClassFileLoadHookの中身は以下となります（Open JDK実装）。class_dataの先にロードされたバイトコードが保存されており、そのデータをJVMはclassfileBufferという変数としてTransformerに渡します。

Transformerは書きかえたバイトコードを返し、そのデータがnew_class_dataに入ります。JVMは、new_class_dataにデータが存在する場合、こちらの置換されたバイトコードを実行します。

```cpp:ClassFileLoadHook
ClassFileLoadHook(jvmtiEnv *jvmti_env,
            JNIEnv* jni_env,
            jclass class_being_redefined,
            jobject loader,
            const char* name,
            jobject protection_domain,
            jint class_data_len,
            const unsigned char* class_data,
            jint* new_class_data_len,
            unsigned char** new_class_data)
```

# エージェントを自作してバイトコードを書きかえてみよう
バイトコードインジェクションでは普段意識しないJVMの内部処理が関わってくるため、直感的に理解しづらいかと思います。なので、最低限の機能をもったエージェントをつくり、最初のデモのようにHello Worldプログラムを書きかえ、ハンズオンでインジェクションの処理を理解してみましょう。

環境を汚したくない方のために、JVM環境とサンプルコードが入ったDockerイメージを配布しています。[Dockerイメージとソースコードのリンクはこちら](#Dockerイメージとソースコード)

## まずはサンプルを走らせてみる

以下のコマンドを実行すると、Ubuntuベースのコンテナ内でサンプルアプリをビルド・実行できます。

```console
docker run -it cilto/bytecode-injection:latest bash
（コンテナ内で）./build.sh
```
このような出力になっていれば正常に動いています。

```console
root@myagent-7fb7fbb849-jv89j:/tmp/instrument_example# ./build.sh 
マニフェストが追加されました
instrument/MyAgent.classを追加中です(入=507)(出=311)(38%収縮されました)
=== Javaバイトコードが変更されました ===
Hello, I'm Mallory
root@myagent-7fb7fbb849-jv89j:/tmp/instrument_example# 
```

## サンプルコードの構成

コンテナイメージのデフォルトディレクトリ配下の構成と、各ファイルの役割は以下のとおりです。

```
root/
   ├ build.sh             エージェントのビルドやアプリの実行を一括で行うシェルスクリプト
   └ instrument/
      ├ HelloWorld.java   BCI対象のアプリケーション
      ├ MyAgent.java      BCIを行うエージェント
      ├ MANIFEST.MF       エージェントをJarに圧縮するためのマニフェストファイル　
      └ lib/
         └ javassist.jar  BCIの補助ライブラリ

```

## アプリケーションプログラム

Hello Worldプログラムは、名前を引数にとってsayHello関数を呼び出し、`Hello, I'm <name>`と出力する単純なプログラムです。
`javac instrument/Greetings.java && java instrument.HelloWorld`でコンパイル・実行すると、`Hello, I'm Alice`と出力されているはずです。


```java:HelloWorld.java
package instrument;

public class HelloWorld {
	public static void main(String[] args) {
		sayHello("Alice");
	}
	public static void sayHello(String name) {
		System.out.println("Hello, I'm " + name);
	}
}
```

```console
Hello, I'm Alice
```

ではこのコンパイル後のHelloWorld.classを**全く変えずに**、実行時に名前を`Mallory`に変えるエージェントを実装していきましょう。

## エージェントプログラム

`-javaagent`オプションで起動するBCIエージェントを実装するためには、以下の２つが必須要件となります。
**1. エージェントクラスでpremainメソッドを実装する
2. JARのマニフェストに属性Premain-Classを定義し、エージェントクラスの名前を値とする**

### premainメソッドを実装する
premainはアプリケーションより先に呼ばれ、Instrumentationインスタンスを受けとります。このインスタンスから`Transformer`を登録できます。

```java:MyAgent.java
public class MyAgent {
	public static void premain(String agentArgs, Instrumentation instrumentation) {
		instrumentation.addTransformer(
		...
```

この`Transformer`はClassFileTransformerクラスの実装です。`transform`関数は、JVMがクラスをロードする際にコールバックとして呼び出され、置換したバイトコードを返します。JVMはこのバイトコードを実行するため、transoform関数の実装がバイトコードを置きかえるロジックのコア部分といえます。

transform関数の**className**にはロードされたクラス名が入っており、サンプルのHello Worldの場合`instrument/HelloWorld`という文字列になります。続いて、**classfileBuffer**には、クラス定義部分のバイトコードデータが保存されており、読み出せばどのような関数や変数が定義されているかが分かります。
最後に、関数の返り値として置換したバイトコードを返します。

```java:MyAgent.java
		instrumentation.addTransformer(
			new ClassFileTransformer() {
				public byte[] transform(ClassLoader loader,
					String className,
					Class<?> classBeingRedefined,
					ProtectionDomain protectionDomain,
					byte[] classfileBuffer) {
	                    ....
	                return transformedByteCode;
	             }
	    });
```

#### バイトコードを再構築する
あとはバイトコードを書きかえるロジックを実装するのみです。しかし、バイトコードは人間に読める形であるものの、生のバイトデータを文字列処理するのは大変です。幸い、バイトコードを簡易に操作できるライブラリがサードパーティから提供されているので、本記事ではその一つであるjavassistというライブラリを使います。

以下のコードがバイトコード書きかえを行う部分です。デモのようにsayHello関数の引数を書きかえるため、まずは元のクラスのバイトコードをJavassistのクラス定義のインスタンス`ctClass`に読み直します。

`getDeclaredMethods`でHelloWorldのメソッドを取得し、sayHelloの場合に分岐させます。

`$1`という変数が、sayHelloの第１引数`name`に当たります。よって、`"$1=\"Mallory\";"`ではname変数にMalloryという文字列を代入しており、そのバイトコードを**動的に生成**しています。
このバイトコードをsayHelloの**一番最初に挿入**することで、Helloの相手をMalloryに書きかえています。

これでエージェントの動作として全ての処理が完了しました。

```java:MyAgent.java
		ClassPool classPool = ClassPool.getDefault();
		CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(
			classfileBuffer));
		CtMethod[] methods = ctClass.getDeclaredMethods();

		for (CtMethod method : methods) {
			if (method.getName().equals("sayHello")) {
				method.insertBefore("$1=\"Mallory\";");
			}
		}
		byteCode = ctClass.toBytecode();
		...
```


### マニフェストファイルを用意
エージェントはjar形式ですので、マニフェストファイルが必要です。通常のJavaアプリケーションでは`Main-Class`という属性でエントリーポイントを明示しますが、エージェントでは`Premain-Class`にエージェントのクラスを指定します。

```MANIFEST.MF
Premain-Class: instrument.MyAgent
...
```

### エージェントの生成
以下のコマンドでMyAgentをコンパイルし、jarファイルに圧縮します。これでエージェントの準備が整いました。

```console
javac -classpath instrument/lib/javassist.jar instrument/MyAgent.java 
jar cvfm agent.jar instrument/MANIFEST.MF instrument/MyAgent.class
```

### 自作エージェントでHello Worldを書きかえる
それではこの自作エージェントを使って、実行時にバイトコードが書きかわることを確認してみましょう
`-javaagent:agent.jar`パラメーターを入れ、Hello Worldを実行します。
すると以下のように、変数がAliceからMalloryに置きかわっているはずです。

```console
/tmp/instrument_example# java instrument.HelloWorld
Hello, I'm Alice
/tmp/instrument_example# java -javaagent:agent.jar instrument.HelloWorld
=== Javaバイトコードが変更されました ===
Hello, I'm Mallory
```


## 簡易プロファイラをBCIで作ってみる
今度は少し実用的な例として、メソッドの実行時間を測定する簡易的なプロファイラを、バイトコードインジェクションを使って実装してみます。先程のデモをほんの少し変更するだけで作れます。

### 走らせてみる
同様にサンプルコードを入れたDockerイメージを公開しているので、まず動かしてみましょう。

```console
docker run -it cilto/bytecode-injection-profiler:latest bash
（コンテナ内で）./build.sh
```

結果は以下のようになります。最後の２行をみると、HelloWorld.javaの各関数mainとsayHelloの実行秒数が表示されていることが分かります。

```console
root@7f4caf47ab89:/tmp/instrument_example# ./build.sh 
マニフェストが追加されました
instrument/MyAgent.classを追加中です(入=507)(出=311)(38%収縮されました)
=== Javaバイトコードが変更されました ===
Hello, I'm Alice
*[Injected code] function:sayHello executed. Duration is 119usec
*[Injected code] function:main executed. Duration is 285usec
```

### プロファイラのバイトコード
変更したのは`transform`関数の以下の部分のみです。今回は全てのメソッドに対して、バイトコードインジェクションを行います。

まずメソッドの実行時の時間を保持する`startTime`という変数を定義し、メソッドの始めに現時刻を入れておきます。メソッドの終わりでは、現在時刻を取得し、最初の時刻から引いた値を出力するバイトコードを入れています。このように単純ですが、メソッドの最初と最後にバイトコードを入れるだけで簡易プロファイラができてしまいます。

```java:MyProfilerAgent.java
	for (CtMethod method : methods) {
		method.addLocalVariable("startTime", CtClass.longType);
		method.insertBefore("startTime = System.nanoTime();");
		method.insertAfter("System.out.println(\"*[Injected code] function:" + method.getName() + " executed. Duration is \" + (System.nanoTime() - startTime)/1000 + \"usec\"); ");
	}
```

# バイトコードインジェクションはどう活用されているのか
バイトコードインジェクションは仕組み自体は少し複雑ですが、`-javaagent`オプションを追加するだけで使えるため、非常に強力で手軽に利用できるツールとして、様々な用途で使われています。

## アプリケーションパフォーマンス監視：APM
バイトコードインジェクションを活用すると、ログのコードを追加することなく、システムのパフォーマンスを取得したり、データフローを可視化することができます。これをエンドツーエンドで一括に監視・可視化できるのがアプリケーションパフォーマンス監視（APM）というツールです。

APMはシステムにオブザーバビリティ（可観測性）を付加し、サービス全体の状態やデータフローをリアルタイムに把握できます。複雑なシステムでも、データフローを見ながら修正や機能追加を行えるため、安全に高速に開発サイクルを回せます。また問題が起きた箇所を横断的に確認できるため、運用のオペレーションコストが減り、DevOpsを推進する基盤となります。

例えば、`(アプリケーション 1)->(メッセージキュー)->(アプリケーション 2)->(AWS S3)`というデータフローのシステムがあったとします。このシステムのアプリケーション1と2にエージェントを導入すると、以下のようなグラフがリアルタイムで自動的に可視化されるようになります。

実際に実行されたコードを**トレース**して可視化するため、一見するだけで**動いたデータの流れ**が分かり、レスポンスタイムやコードレベルでのエラーが一見して分かるため、非常に強力なツールです。

![Screen Shot 2020-12-20 at 23.07.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62311/423a1b02-5d82-b623-6f84-74c9132f05fa.png)

今回はJavaのBCIのみ取り上げましたが、.NetやJavaScriptでも同様のことができ、商用APMベンダーではたいていのメジャーな言語に対応しています。特に分散化されたシステムには欠かせないツールで、最近ではCNCFのSandBoxプロジェクトとしてOpen telemetryのようなOSS実装のエージェントも出てくるなど、注目度が高くなっています。


## ランタイムで自己防御するセキュリティ：RASP
脆弱性があるコードをランタイムに検知し**自己改変**する防御手法をRun-time Application Self-Protection（RASP）といい、すばやくアプリケーションを保護する手法として注目されています。

このRASPにおいて、検知・自己改変の部分にはバイトコードインジェクションが使われており、理論だけの技術ではなく[この会社の例のように](https://docs.contrastsecurity.com/en/java.html)実際にプロダクトとして販売している会社もあります。ドキュメントにある通り、`-javaagent`でエージェントを読み込ませています。エージェントはロードされたクラス情報にアクセスできるため、脆弱性があるクラスや、意図とは違う処理が行われそうになると検知し、バイトコードを改変することで実行を防ぐことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62311/f5e11c3a-935d-d994-b060-20c11dc9ca63.png)


## 本番緊急パッチ
もし本番環境を落とせず（ロードバランサ等もなく）、コードのバグが致命的で緊急に直さなければならない、という場合にもBCIは使えます。

たとえば、[『本番環境でやらかしちゃったアドベントカレンダー』](https://qiita.com/advent-calendar/2020/yarakashi-production)で取り上げられているような、APIの向き先が間違ってしまった場合には、そのURLの変数のバイトコードをエージェントで書きかえることが可能です。あまり行儀のいい方法ではありませんが、取れる選択肢が少ない状況下で意外と有用な方法になりうるかもしれません。

## JVM自体のデバッグ
JVMの動作やクラスローダーをデバッグする際には、一つ一つ直しながらビルドするよりも、非常に早くコードを改変して実行することができます。たとえば、クラスローダーの順番やそのロード解決を追いたい場合には、各ローダーの名前やローダーが読み込んだクラス名をprintするだけのエージェントは簡単に作ることができます。

最近ではJavaはモジュール化され軽量になっていますが、依然とビルドには時間がかかるため、こういったサイドカー的なデバッグ手法は強力です。

# もっと詳しく知りたい方へ
## BCI技術についてのドキュメント
- [OpenJDKでのInstrument実装ソースコード](https://github.com/openjdk/jdk/blob/master/src/java.instrument/share/classes/sun/instrument/InstrumentationImpl.java)：特に89-111行目のaddTransformerの処理をさかのぼっていくとTransformerがどう登録され、呼ばれるのかが分かります。
- [ClassFileLoadHookイベントに対してJVMがコールバック関数を呼び出す仕組みについて](https://www.oracle.com/technical-resources/articles/javase/jvm-tool-interface.html)：特にEvent Callbacksの項でcbClassFileLoadHookに注目です。cbはコールバックの意。
- [OpenJDKでのAgent実装ソースコード](https://github.com/openjdk/jdk/blob/master/src/java.instrument/share/native/libinstrument/JPLISAgent.c)：上記の仕組みのソースコードを見たければこちら。ClassFileLoadHookイベントをJVMがどう処理しているのかが分かります。ソースコード自体はエージェント全般の実装となっています。
- [BCIを仕様策定したJVM TIのリファレンス](https://docs.oracle.com/en/java/javase/15/docs/specs/jvmti.html#bci)


## 本記事で使用・引用した文献
- [Instrumentパッケージの概要について](https://docs.oracle.com/en/java/javase/15/docs/api/java.instrument/java/lang/instrument/package-summary.html)
- [Instrumentationインターフェースの詳細について](https://docs.oracle.com/en/java/javase/15/docs/api/java.instrument/java/lang/instrument/Instrumentation.html)
- [ClassFileTransformerインターフェイス](
https://docs.oracle.com/en/java/javase/15/docs/api/java.instrument/java/lang/instrument/ClassFileTransformer.html)
- [Javassistライブラリ](https://www.javassist.org/)　本ライブラリは東京大学の千葉滋教授の開発となります。
- [JVM TIが策定されたJSR-163の内容（JSR: Java Specification Requests）](https://jcp.org/en/jsr/detail?id=163)
- [IBMでのContrast Securityの実運用イメージ](https://www.ibm.com/blogs/cloud-archive/2017/09/detecting-security-vulnerabilities-with-contrast-security/)

## お仕事で触りたい方は
バイトコードインジェクションの実用例で挙げた、APMベンダーのAppDynamics（Cisco）という製品部門で日本語サポートエンジニアを募集しています。筆者はそのチームを立ち上げ、マネージャーを担当しています。興味をもった方やこんな技術で遊びたいという方は[こちらのリンク](https://jobs.cisco.com/jobs/ProjectDetail/AppD-Customer-Support-Engineer/1314703)から応募してみてください。

# 補足事項
- 「バイトコードインジェクション」という訳については、Javaの正式名称はByte Code Instrumentationとなっています。Instrumentationというのは「器械を使用・導入する」や「計測」という意味で、バイトコードという「道具」を「入れて」動かすことを指していますが、より直感的な言葉としてByte Code **Injection**とも言われてます。[Oracleの日本語翻訳ドキュメント](https://docs.oracle.com/javase/jp/6/api/java/lang/instrument/Instrumentation.html)ではインストゥルメンテーションとそのままカナにしており、また日本語ではいい訳語がないため、本記事では「バイトコードインジェクション」という言葉を使っています。
- ハンズオンで取り上げたBCIは、JVM起動時にエージェントを読み込む場合の流れですが、メジャーなJVM実装では、動作しているJavaプロセスに対してもエージェントを適用することができます。その場合はManifestの内容や関数名が異なります。
- Java以外の言語ではJavaバイトコードに当たる中間コードの名前は異なります。例えば.Netでは共通中間言語（CLI: Common Intermediate Language）という名前で、仕様については[ECMA-335](https://www.ecma-international.org/publications/standards/Ecma-335.htm)に記述があります。

## 他のシスコシステムズアドベントカレンダーはこちら
2017年版: https://qiita.com/advent-calendar/2017/cisco
2018年版: https://qiita.com/advent-calendar/2018/cisco
2019年版: https://qiita.com/advent-calendar/2019/cisco
2020年版: https://qiita.com/advent-calendar/2020/cisco
2020年版(2枚目): https://qiita.com/advent-calendar/2020/cisco2

## 免責事項
本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、シスコの意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、シスコや他の関係者による推奨や表明を目的としたものではありません。各利用者は、本Webサイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本Webサイトの利用に関するあらゆる責任からシスコを免責することに同意したものとします。