
なんでって? ¯\_(ツ)_/¯
私が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つずつ見ていきましょう: