[Android] Arm64でAnti-Disassembly/Decompile

昨日、このような素晴らしい記事を見かけました

https://tech-blog.sterrasec.com/entry/2024/04/12/151113

こちらの記事ではPLDという命令を使用してアンチディスアセンブルを施していましたが、こちらでも言及されている通り、Arm64では存在しない命令です。

そこで、Arm64ではアンチディスアセンブルを施すことはできないのかと気になったので自分なりに調べてまとめてみました。



# 初めに

ArmにはDCBディレクティブという1バイト以上のメモリを割り当て、メモリの実行時の初期内容を定義するものがあります。

https://developer.arm.com/documentation/dui0473/m/directives-reference/dcb?lang=en

> The DCB directive allocates one or more bytes of memory, and defines the initial runtime contents of the memory.

そのほかにも種類があり、

|DCB|DCW|DCD|DCQ|
|:--:|:--:|:--:|:--:|
|BYTE (1 Bytes)|WORD (2 Bytes)|DWORD (4 Bytes)|QWORD (8 bytes)|

というものがあります。

(以後これらを総じてDCXと呼びます)

DCX命令の使用例としては次の通りです

```cpp:LOAD
C_string   DCB  "C_string",0
```

```:.got.plt
off_XXXXX     DCQ fprintf
```

```:.data.rel.ro
`vtable for'Sample
            DCQ `typeinfo for'
off_XXXX    DCQ sub_XXXXX    
```

(あまり意識してなかったけどそこら中にいるな.....)

DCXは一般的にReadOnlyなデータ領域に置かれるようですが、どうやらtext領域にも設置できるようです。
本来置かれるはずのないtext領域に設置することによりIDAのディスアセンブルを妨害するということですね。

# データ埋め込み (DCQ)

インラインアセンブラを使用してDCQを設置することが出来るようです。
.longに任意の4バイトを続けて直接書きます

```cpp
extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    __asm__(R"(
        .long 0x12345678
        .long 12345678
    )");

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}
```
コンパイルしてIDAにかけてみましょう
![dcb_demo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/fbaa16ec-c3de-2705-c261-61a9914b943c.png)

無事にデータ扱いされてますね。F5もCreate Functionも効きません。

Ghidraにもかけてみましょう

![スクリーンショット 2024-04-20 233838.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/82510bc0-b3d4-d728-f6be-d93dffc2ccbf.png)

Ghidraは頑張ってデコンパイルしてくれますが、何だかダメそうです。

しかしながら、これではとある問題が発生します。

関数内に埋め込んだデータは他の命令と同じように実行されます。
が、**埋め込んだデータは有効な命令ではありません。**
なので実行するとクラッシュします。

```cpp
Fatal signal 4 (SIGILL), code 1 (ILL_ILLOPC), fault addr 0x7c70bd523c in tid 8419 (.arm64_studying), pid 8419 (.arm64_studying)
```

# B命令 + DCQ

そこで、無条件分岐命令である[B命令](https://developer.arm.com/documentation/ddi0596/2021-06/Base-Instructions/B--Branch-?lang=en)を使用して不正な命令を飛ばしてみます。

```cpp
extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    __asm__(R"(
        b 0xc
        .long 0x12345678
        .long 12345678
    )");

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}
```

早速IDAにかけてみましょう

![スクリーンショット 2024-04-21 014543.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/e55aa686-d4ea-8fe4-f2f1-d533bbc3aed9.png)

なんと、関数として解釈されてしまっています。

![スクリーンショット 2024-04-21 130322.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/207b280c-77a8-9c6b-7ddf-50f15217bc30.png)

デコンパイルも出来てしまいました。

どうやら、B命令がIDAに解析のヒントを与えてしまったみたいです。(Ghidraも同じ結果でした)

単純なデータ埋め込みはIDAの解析を妨害することが出来ますが、実行するとクラッシュしてしまいます。
その問題を回避するためにB命令を使ってみましたが、ジャンプ先が分かってしまうためIDAに解析されてしまいました。
もう他に方法ないのでしょうか？

# 偽の制御フロー + DCQ

もちろんあります。DCQを決して実行されない分岐に配置すれば良いのです。

```cpp
extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    constexpr volatile bool flag = false;

//    __asm__(R"(
//        .long 0x12345678
//        .long 12345678
//    )");

    if (flag){
        __asm__(R"(
            .long 0x12345678
            .long 12345678
        )");
    }
    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}
```

最適化されてDCQを消されないようにflag変数に最適化抑止をつけます

IDAにかけてみると....

![スクリーンショット 2024-04-21 155451.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/e41fa480-cbc3-be98-1fd6-7e44907cbdcc.png)

きちんと妨害できてますね。

もちろん、ちゃんと実行も出来ます。

![7671.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/1f79ce04-4cfd-7a14-6fbc-d1ff6b1e1546.jpeg)

さて、これでアンチディスアセンブルを施すことが出来ましたが、
これらを組み合わせることで更に強固なディスアセンブルを施すことが出来ます。

# 偽の制御フロー + B + DCQ
```cpp
extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    constexpr volatile bool flag = false;
    

    if (flag){
        __asm__(R"(
            B 0x4
            .long 123456789
        )");
    }

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}
```

![スクリーンショット 2024-04-21 165532.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/ed5917fe-beda-e782-6acb-f5ad5fa88104.png)


なんとも歪な形になりましたね

さて、今までの手法は関数の範囲内にデータを埋め込んでそもそもデコンパイルさせないというものでしたが、実は少し工夫すると簡単に突破されてしまいます。

そこで、次はデコンパイルにより解析されることを前提として対策を講じていきたいと思います。

# BR命令による間接ジャンプ

さっきB命令でジャンプしてたやん、と思われる方もいらっしゃるかと思いますが、先程はジャンプ先がIDAにばれてしまっていたので解析されてしまいました。
なので、今回は[BR命令(Branch to Register)](https://developer.arm.com/documentation/ddi0596/2021-06/Base-Instructions/BR--Branch-to-Register-?lang=en)を使用してみます。
静的解析はレジスタの実行時の値を取得できないため、ジャンプ先のアドレスをレジスタに格納することでIDAに解析されないようにするといったところです。

```cpp
extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    __asm__(R"(
        mov x8, #0x1
        adr x9, #0x10
        mul x8, x9, x8
        .long 0x12345678
        br x8
    )");

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}
```

アドレス構造としてはこんな感じ

|アドレス|命令||
|:--|:--|:--|
|~000|adr x9, 0x10|x9に現在のアドレス + 0x10した値を格納|
|~004|mul x8, x9, x8|x8にx9とx8の乗算結果を格納|
|~008|.long 0x12345678|フェイクの命令(DCQ)|
|~00C|br x8|x8に格納されているアドレス(下の命令)にジャンプ|
|~010|(続きの命令)|ジャンプしてきた😊|

IDAのディスアセンブル結果はこんな感じ

![スクリーンショット 2024-04-21 221012.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/19329c92-d073-e7d9-fe98-fc9c69d5aac8.png)

うまく騙されてくれていますね
BR命令のところでstringFromJNI関数の終わりと判断され、次の命令からは別の関数として扱われています。

デコンパイル結果はこんな感じ

![スクリーンショット 2024-04-21 225448.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/4547f515-218d-f394-4cc9-5f9654027c7c.png)

BR先の関数の呼び出しは表示されているものの、インラインアセンブリ埋め込み箇所より上の部分のデコンパイルは失敗したようです。

このケースはうまくIDAを騙せたものの、関数の参照を切ることは出来ませんでした。

ここまで来たらデコンパイル結果を空にしたい、ということで次はret命令を使用してみましょう。

# ret命令

今回はx30、関数呼び出し命令時にリターンアドレスを保持する[リンクレジスタ](https://learn.microsoft.com/ja-jp/cpp/build/arm64-windows-abi-conventions?view=msvc-170)を使用します。

```cpp
extern "C" JNIEXPORT jstring JNICALL
Java_rec_enuwbt_arm64_1studying_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */thiz) {

    __asm__(R"(
        adr x8, #0xc
        mov x30, x8
        ret
    )");

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());
}
```

アドレス構造としてはこんな感じ

|アドレス|命令||
|:--|:--|:--|
|~000|adr x8, #0xc|x8に現在のアドレス + 0x10した値を格納|
|~004|mov x30, x8|リンクレジスタにx8に格納されているアドレスを格納|
|~008|ret|リンクレジスタが指すアドレスにリターン|
|~010|(続きの命令)|リターンしてきた😊|

勝手にリンクレジスタを書き換えていいの？　と考える方もいらっしゃると思いますが、
大丈夫です。コンパイラが破壊される前の値をスタックに保存、関数の終了時にリンクレジスタに戻す処理を挿入してくれます。

IDAのディスアセンブル結果はこんな感じ


![スクリーンショット 2024-04-22 011053.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/61ce26b5-e5b3-f9a0-7753-86d8cd5f0a08.png)

素晴らしい、傍から見たら完全に別々の関数です

デコンパイル結果はこんな感じ

![スクリーンショット 2024-04-22 011309.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/42ae210f-e2c8-03b3-7a85-e96558a9f3ef.png)

x8レジスタにアドレスを代入しているのにIDAがデコンパイル結果に関数の呼び出しを表示しないのは、ret命令だからでしょう。
ただレジスタにアドレスを代入していると判断されたみたいですね。

さて、これで遂に関数を空っぽにすることに成功しました。

# 終わり

いかがだったでしょうか。
何通りかの方法をお教えしましたが、私のお気に入りはret命令を使用したアンチディスアセンブルです。
他の命令を使用したり、組み合わせによっては更に強固なディスアセンブルを施すことが出来そうですね。
皆様も是非お試しください!!

# 参考文献

https://github.com/AppleReer/Anti-Disassembly-On-Arm64

:::note
本記事はこれで終わりとなります。ご覧いただきありがとうございました！
:::

# おまけ

ret命令を使用した場合のアンチディスアセンブルでは、ret命令の前後で分断され別々の関数として認識されていました。
それが原因でデコンパイル結果が空っぽになったのですが、関数を元に繋ぎ直したらデコンパイル結果が正常に戻るのかと気になったのでやってみたところ....

![スクリーンショット 2024-04-23 110326.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/e06bd2bd-6267-0eb5-d0df-294f946aea05.png)
![スクリーンショット 2024-04-22 011309.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/c1b609ec-f8ff-3eac-d1a9-febd49f7a3c1.png)

なんと、**変わりませんでした。** (やり方が悪かっただけ...?)

では、Ghidraはどうでしょう？
![スクリーンショット 2024-04-23 113151.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/fd39defd-0a41-bcb8-09f2-14d85467127c.png)

ふむ、Ghidraもほとんど空っぽの関数を吐き出していますね。

retの下を見てみると...

![スクリーンショット 2024-04-23 113311.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/149676e2-443d-5635-3f49-28624e0f9a90.png)

どうやらretの下の命令達はただのデータとして扱われているようです。

Dissassembleをして命令を元に戻してみましょう
![スクリーンショット 2024-04-23 113726.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/b2716b84-14a2-0ecb-b5de-e3b96a27a0bd.png)

まだデコンパイル結果は変わりませんが...

retをnopに書き換えると....
![スクリーンショット 2024-04-23 114108.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/b9b0ed98-6224-5f3c-c5bc-e8244c008c2f.png)

**お!!**
![スクリーンショット 2024-04-23 114125.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/1d1e88fa-1c88-ab6e-8c31-a8224e00ce6f.png)

**ｷﾀ―――(ﾟ∀ﾟ)―――― !!**

(ん、待てよ? IDAでも出来るんじゃ..?)

これを
![スクリーンショット 2024-04-23 114539.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/fd9a107a-ecc1-9d4a-458a-046315d9d0d6.png)


こうして...![スクリーンショット 2024-04-23 114554.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/1f9cedc8-2c56-58ab-776f-9f172b1afba4.png)

デコンパイルすると![スクリーンショット 2024-04-23 114718.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1960856/3755c509-ff65-8e15-bb30-eb48feead9d3.png)
(全てが崩れる音)


# 結論
もう何も意味をなさない。こいつらに取り込まれた時点で我々は既に負けているのである。