
なんでって? ¯\_(ツ)_/¯
私がLLVMの内部、バイナリフォーマット、アセンブリ、難読化技術について多くを学んだ最大のプロジェクトのひとつです。
作ることを通じて学ぶのが最良の学び方だと信じているので、このテーマについてもっと学ぶためにこのプロジェクトを構築しました。
Zyrox の背後にあるコンセプトを解説したブログを4つ書きました:
これらのパートはこのREADMEより深く掘り下げており、このテーマに興味があるなら一読の価値があります。
これはZyroxを_すばやくテスト_したい人、またはcmakeプロジェクトへの統合方法を学びたい人を対象としています。
Zyroxテンプレート リポジトリの手順に従ってください。
llvmをインストール:
sudo apt update
sudo apt install llvm-18 llvm-18-dev clang-18
zyroxをクローンしてコンパイル:
git clone --recurse-submodules https://github.com/PeterHackz/zyrox.git
cd zyrox
cmake -S . -B build -DCMAKE_C_COMPILER=/usr/bin/clang -DCMAKE_CXX_COMPILER=/usr/bin/clang++
cmake --build build --parallel 4
python3 と pip がインストールされていることを確認してください。
# 仮想環境を作成
python3 -m venv .venv
# 環境を有効化
source .venv/bin/activate
pip install -r requirements.txt
pip install -r requirements.txt
clang -O0 -flto=full -c main.c -o out/main.o
clang -flto=full -fuse-ld=lld -Wl,--load-pass-plugin=./build/libzyrox.so out/main.o -o out/main
難読化後、PyPlugin.py を実行してジャンプテーブルを暗号化します:
# 依存関係を仮想環境にインストールした場合は、最初に有効化してください:
source .venv/bin/activate
# その後、以下で実行:
python PyPlugin.py --in=<input_file> [--out=<output_file>] [--tables=<zyrox_tables_file>] [--android]
CMake統合の例については、Zyroxテンプレート リポジトリを確認してください。
これは複雑なテーマであり、このプロジェクトは主に教育目的であるとともに、BSD Brawl に役立てるためのものであることを理解しています。 質問がある場合や、ただチャットしたい場合でも、お気軽にご連絡ください:
@s.b[email protected] または [email protected]プルリクエストやissueによる支援はいつでも歓迎です!
ZyroxPlugin.cpp はパスを登録し、siphash をリンクして(詳細は後述)、StringEncryption を呼び出して文字列を暗号化します。
文字列を早期に暗号化するのは、復号ロジックも後で難読化されるようにするためです。
次に、ModuleUtils::ExpandCustomAnnotations と QuickConfig::RegisterPasses を呼び出して、すべての __attribute__((annotate("..."))) 式を解析し、QuickJs構成(ZyroxConfig.js に配置)を実行します。
すべての関数は ZyroxCore.cpp にある Zyrox::RunOnFunction を呼び出すことで難読化されます。これに関する詳細なドキュメントは今後提供される予定です。
switch文はジャンプテーブルを生成し、PHIノードは扱いにくいため、FunctionUtils と BasicBlockUtils を使用して、それぞれ(if文への)フラット化と降格を行います。
いやはや、どこから始めればいいのか
すべてのjsプラグイン引数は index.d.ts にあるため、このドキュメントでは説明しません。
アノテーションのドキュメントについては、こちらをクリック
このパスは、ベーシックブロックをより小さなブロックに分割してシャッフルします。次のコードがあるとします:
int __test_fn(int x)
{
if (x == 2) {
printf("x is 2\n");
} else {
printf("x is not 2!, x is: %d\n", x);
}
return x + 4 * x - 2 / 4;
}
これは次のようにコンパイルされます:
define internal i32 @__test_fn(i32 noundef %0) #0 !zyrox !8 !obfuscated !11 {
%2 = alloca i32, align 4
store i32 %0, ptr %2, align 4
%3 = load i32, ptr %2, align 4
%4 = icmp eq i32 %3, 2
br i1 %4, label %5, label %7
5: ; preds = %1
%6 = call i32 (ptr, ...) @printf(ptr noundef @.str.1)
br label %10
7: ; preds = %1
%8 = load i32, ptr %2, align 4
%9 = call i32 (ptr, ...) @printf(ptr noundef @.str.2, i32 noundef %8)
br label %10
10: ; preds = %7, %5
%11 = load i32, ptr %2, align 4
%12 = load i32, ptr %2, align 4
%13 = mul nsw i32 4, %12
%14 = add nsw i32 %11, %13
%15 = sub nsw i32 %14, 0
ret i32 %15
}
Basic Block Splitterをこの構成で使用すると:
z.RegisterPass(ObfuscationType.BasicBlockSplitter, {
PassIterations: 1,
"BasicBlockSplitter.SplitBlockChance": 100,
"BasicBlockSplitter.SplitBlockMinSize": 2,
"BasicBlockSplitter.SplitBlockMaxSize": 5,
});
次のようになります:
define internal i32 @__test_fn(i32 noundef %0) #0 !zyrox !8 !obfuscated !11 {
%2 = alloca i32, align 4
store i32 %0, ptr %2, align 4
%3 = load i32, ptr %2, align 4
%4 = icmp eq i32 %3, 2
br i1 %4, label %5, label %14
5: ; preds = %1
%6 = call i32 (ptr, ...) @printf(ptr noundef @.str.1)
br label %7
7: ; preds = %14, %5
%8 = load i32, ptr %2, align 4
%9 = load i32, ptr %2, align 4
%10 = mul nsw i32 4, %9
%11 = add nsw i32 %8, %10
br label %12
12: ; preds = %7
%13 = sub nsw i32 %11, 0
ret i32 %13
14: ; preds = %1
%15 = load i32, ptr %2, align 4
%16 = call i32 (ptr, ...) @printf(ptr noundef @.str.2, i32 noundef %15)
br label %7
}
このような小さな関数ではあまり違いはありませんが、ベーシックブロックが分割されているのがわかりますか? これは 制御フローフラットニング のような他のパスと組み合わせると役立ちます。
ああ、このパスは全部の中で最も機能が多いんですよ、笑。 まず動作の仕組みを説明し、次にその構成について説明します。次のコードがあるとします:
LABEL_A: bool b = x == 2;
IF EQ: goto LABEL_B
goto LABEL_C
LABEL_B do_stuff()
LABEL_C do_other_stuff()
goto LABEL_A
各ベーシックブロック(A、B、C)には一意のディスパッチャ状態が割り当てられます。例:(簡略化)
states = {
1: LABEL_A,
2: LABEL_B,
3: LABEL_C,
};
次に、すべてを制御するディスパッチャブロックを注入すると、コードは次のようになります:
int state = 0;
LABEL_D goto LABEL_CA // ディスパッチャラベルは最初の条件ブロック、ラベル条件Aへジャンプ
LABEL_CA if state == 1: goto LABEL_A
// 1でなければ、ラベルBかどうかのチェックへ進む(フォールバック)
LABEL_CB if state == 2: goto LABEL_B
LABEL_CC if state == 3: goto LABEL_CC
// 到達不能
goto LABEL_D
LABEL_A: bool b = x == 2;
// IF EQ: goto LABEL_B
// goto LABEL_C
state = 2 if b else 3 // 目的のブロックの状態を更新してディスパッチャに戻る
goto LABEL_D
LABEL_B do_stuff()
LABEL_C do_other_stuff()
state = 1
goto LABEL_D
これにはオブフスケーターが修正するいくつかの欠陥があります。ご覧のとおり、ディスパッチャ変数がひとつしかないため、ブロックが状態を設定した後にどこへ進むかがわかってしまい、難読化を解除するのは簡単です。修正も簡単です!
z.RegisterPass(ObfuscationType.ControlFlowFlattening, {
PassIterations: 1,
"ControlFlowFlattening.UseFunctionResolverChance": 60,
"ControlFlowFlattening.UseGlobalStateVariablesChance": 60,
"ControlFlowFlattening.UseOpaqueTransformationChance": 40,
"ControlFlowFlattening.UseGlobalVariableOpaquesChance": 80,
"ControlFlowFlattening.UseSipHashedStateChance": 40,
"ControlFlowFlattening.CloneSipHashChance": 80,
});
オプションを1つずつ見ていきましょう:
UseFunctionResolverChance: 状態をチェックする関数を注入します。つまり、if (state == expected_state) の代わりに if (injected_resolver(state)) を行います。例:
bool __fastcall cff_resolve_state_check_3585(__int64 a1)
{
return a1 == 0x288A6154F8A5E3E2LL;
}
UseGlobalStateVariablesChance: 比較する状態値をグローバル変数に保存します:
bool __fastcall cff_resolve_state_check_506(__int64 a1)
{
return a1 == qword_1B20D8;
}
UseOpaqueTransformationChance: チェックを、特定の状態でのみ true になる何らかの変換に難読化します:
bool __fastcall cff_resolve_state_check_7901(__int64 a1)
{
return ((((a1 ^ 0xEA9E45BB6099BC6ELL) + qword_1C64D8) << qword_1A63F0)
| (((a1 ^ 0xEA9E45BB6099BC6ELL)
+ qword_1C64D8) >> qword_1ACE98)) == qword_1B0B80;
}
UseGlobalVariableOpaquesChance: 上記の例でお気づきのとおり、 を行う際に数値の代わりにグローバル変数を使用します。(、、)次のコードがあるとします:
if (x == 2) goto LABEL_A
goto LABEL_B
LABEL_A: do_stuff()
LABEL_B: // ...
これは次のように変換されます:
@global jump_table = {0, &LABEL_A, &LABEL_B};
if (x == 2) goto jump_table[0] + @inline(decrypt(jump_table[1]));
goto jump_table[0] + @inline(decrypt(jump_table[2]));
// ...
このパスを使用すると、プラグインは PyPlugin.py で使用する zyrox_tables.txt ファイルを出力します。
PyPlugin.py はジャンプテーブルを暗号化して再配置エントリをパッチし、リロケータがすべてのテーブルの jump_table[0] を指すようにします。リロケータは基本的に target.writePointer(base.add(value)) を行います。つまり、value を 0 に設定することで、リロケータにベースアドレスを渡させて実行時にジャンプテーブルに入れてもらい、それを goto とともに使用して実行時アドレスを生成します。arm32 Thumb モードでは、パスは復号後に自動的に | 1 を追加します。
PyPlugin.py を使用するには、次のようにするだけです:
(venvを使用している場合は、最初に有効化してください)
python3 PyPlugin.py --in <out_obfuscated_file> --android
arm64 バージョンを対象としている場合は、x86_64 バージョンとはリロケータのシグネチャが異なるため、--android を渡すことが重要です。
--out も渡せます(デフォルトでは --in に渡した同じファイルが使用されます)。また、--tables も渡せます(デフォルトは zyrox_tables.txt です)。
間接分岐は素晴らしいように見えますが、実行時にポインタを復号化するためパフォーマンスの低下を伴います。これはパフォーマンスに影響しないシンプルなバージョンで、次のコード:
if (x == 2) goto LABEL_A
goto LABEL_B
LABEL_A: do_stuff()
LABEL_B: // ...
は次のようになります:
@stack jump_table = {&LABEL_B, &LABEL_A}
goto jump_table[!(x == 2)]
LABEL_A: do_stuff()
LABEL_B: // ...
これはシンプルで簡単に破れるように見えますが(同感です)、パフォーマンスに影響を与えずに IDA と Ghidra を壊すには十分です。
MBA Sub(Mixed Boolean Arithmetic Substitution、混合ブール演算置換)としても知られ、単純な演算を同じ出力を与える複雑な演算に変換します。事前定義されたセットを使用します。 例:
a ^ b = (~a & b) | (a & ~b)
b * c = (((b | c) * (b & c)) + ((b & ~c) * (c & ~b)))
r = rand(); c = b + r; a = a + c; a = a - r
興味があれば、完全なリストは Passes/MBASub.cpp で確認できます。
index.d.ts を確認してください。アノテーションパーサーも同じ順序を使用します。
関数をマークするには、次のようにするだけです:
__attribute__((annotate("ibr:1,100"))) void hello_world () {
some_hello ();
}
アノテーションコード:
例:
index.d.ts には次のようにあります:
{
"BasicBlockSplitter.SplitBlockMinSize"?: number;
"BasicBlockSplitter.SplitBlockMaxSize"?: number;
"BasicBlockSplitter.SplitBlockChance"?: number;
};
さて、重要なのは、最初の引数でありすべてのパスに共通する引数が PassIterations なので、それがアノテーションの最初の引数になります。
bbs で何かをアノテーションするには、次のようにします:
__attribute__((annotate("bbs:1,15,30,100"))) void hello_world () {
some_hello ();
}
これは、hello_world に対して ベーシックブロックスプリッター を、最小サイズ = 15、最大サイズ = 30、確率 = 100 で1回実行することを意味します。
パスを組み合わせることもできます:
__attribute__((annotate("bbs:1,15,30,100 ibr:1,100 sibr:1,100"))) void hello_world () {
some_hello ();
}
これは、hello_world に対して ベーシックブロックスプリッター → 間接分岐 → シンプル間接分岐 の順に実行することを意味します。
定義順に左から右へ実行されます。
UseOpaqueTransformationChanceqword_1C64D8qword_1A63F0qword_1ACE98UseSipHashedStateChance: 状態をチェックするために、小さなカスタマイズされた siphash 関数を使用します。つまり、if (state == 23872) が if (siphash(state) == 3874872081) のようなものになり、ブロックがどこへジャンプするのかを見つけるのが難しくなります。移動先の条件がハッシュ化されると、ブロックは state = 23872 を行います。各 siphash 呼び出しはランダムな値を使用するため、エミュレートがより困難になります。CloneSipHashChance: siphash 関数をクローンし、可能な場合はインライン化も試みて、その兄弟関数を複数作るため、単一の関数をフックするだけでは不十分になります。バイナリサイズが増えるだけでパフォーマンスには影響しないため、これを使用することが強く推奨されます。