
Zyrox: LLVM 기반의 컴파일 타임 난독화 플러그인.
why not ¯\_(ツ)_/¯
제 가장 큰 프로젝트 중 하나로, LLVM 내부, 바이너리 포맷, 어셈블리, 난독화 기법에 대해 많이 배운 프로젝트입니다.
무언가를 만들면서 배우는 것이 가장 좋은 학습 방법이라고 믿기 때문에, 이 프로젝트를 통해 이 주제들을 더 깊게 배우고자 만들었습니다.
저는 Zyrox의 개념을 설명하는 블로그 4개를 작성했습니다:
이 글들은 이 README보다 더 깊이 다루며, 주제에 관심이 있다면 꼭 읽어볼 가치가 있습니다.
Zyrox를 _빠르게 테스트_하거나 CMake 프로젝트에 통합하는 방법을 배우려는 사람을 위한 것입니다.
Zyrox Template 저장소의 단계를 따르세요.
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가 설치되어 있는지 확인하세요.
# Create a virtual environment
python3 -m venv .venv
# Activate the env
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를 실행하여 점프 테이블을 암호화하세요:
# if you installed dependencies in a virtual environment, activate it first:
source .venv/bin/activate
# then run with:
python PyPlugin.py --in=<input_file> [--out=<output_file>] [--tables=<zyrox_tables_file>] [--android]
CMake 통합 예제는 Zyrox Template 저장소를 확인하세요.
이 주제가 복잡하다는 점을 알고 있습니다. 이 프로젝트는 주로 교육 목적으로 만들어졌으며, BSD Brawl을 지원하기 위해서도 만들어졌습니다.
질문이 있거나 그냥 대화하고 싶다면 언제든지 연락하세요:
@s.b[email protected] 또는 [email protected]풀 리퀘스트나 이슈를 통한 모든 도움에 감사드립니다!
ZyroxPlugin.cpp는 패스를 등록한 다음 siphash를 링크하고(자세한 내용은 나중에) StringEncryption을 호출하여 문자열을 암호화합니다.
문자열을 초기에 암호화하는 이유는 나중에 복호화 로직도 함께 난독화되도록 하기 위해서입니다.
그런 다음 ModuleUtils::ExpandCustomAnnotations와 QuickConfig::RegisterPasses를 호출하여 모든 __attribute__((annotate("..."))) 표현식을 파싱하고 QuickJs 구성을 실행합니다 (ZyroxConfig.js에 위치).
모든 함수는 ZyroxCore.cpp에 있는 Zyrox::RunOnFunction을 호출하여 난독화됩니다. 이에 대한 자세한 문서는 추후 제공될 예정입니다.
스위치는 점프 테이블을 만들고 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 // dispatcher label jumps to first condition block, label condition A
LABEL_CA if state == 1: goto LABEL_A
// if not 1, go to check if it is label B (fallback)
LABEL_CB if state == 2: goto LABEL_B
LABEL_CC if state == 3: goto LABEL_CC
// unreachable
goto LABEL_D
LABEL_A: bool b = x == 2;
// IF EQ: goto LABEL_B
// goto LABEL_C
state = 2 if b else 3 // update state for the block we want and back to dispatcher
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,
});
옵션을 하나씩 살펴보겠습니다: