
Zyrox: LLVM based, compile-time obfuscator plugin.
क्यों नहीं ¯\_(ツ)_/¯
मेरी सबसे बड़ी परियोजनाओं में से एक, जहाँ मैंने LLVM के आंतरिक, बाइनरी फ़ॉर्मेट, असेंबली और अस्पष्टीकरण तकनीकों के बारे में बहुत कुछ सीखा।
मेरा मानना है कि निर्माण करते हुए सीखना सबसे अच्छा तरीका है, इसलिए मैंने इन विषयों के बारे में और अधिक जानने के लिए यह परियोजना बनाई।
मैंने Zyrox के पीछे की अवधारणाओं को समझाते हुए 4 ब्लॉग लिखे हैं:
ये भाग इस रीडमे से अधिक गहराई में जाते हैं, और यदि आप इस विषय में रुचि रखते हैं तो निश्चित रूप से पढ़ने लायक हैं।
यह उन लोगों के लिए है जो 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 इंस्टॉल है।
# एक वर्चुअल एनवायरनमेंट बनाएं
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 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 नोड्स बनाते हैं, इनसे निपटना कष्टप्रद है, इसलिए हम इन्हें क्रमशः समतल करने (if स्टेटमेंट में) और अवनत करने के लिए FunctionUtils और BasicBlockUtils का उपयोग करते हैं।
अरे यार, मैं कहाँ से शुरू करूँ
सभी 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
}
जब इस कॉन्फ़िग के साथ बेसिक ब्लॉक स्प्लिटर का उपयोग किया जाता है:
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) को एक अद्वितीय डिस्पैचर स्थिति (state) दी जाती है, उदाहरण: (सरलीकृत)
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
// अप्राप्य (unreachable)
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,
});
आइए विकल्पों पर एक-एक करके चर्चा करें:
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: जाँच को किसी ऐसे रूपांतरण में अस्पष्ट करें जो केवल एक विशिष्ट स्थिति के लिए सत्य देगा:
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]));
// ...
जब इस पास का उपयोग किया जाता है, तो प्लगइन एक zyrox_tables.txt फ़ाइल आउटपुट करेगा जिसका उपयोग PyPlugin.py द्वारा किया जाएगा।
PyPlugin.py जंप टेबल को एन्क्रिप्ट करेगा और रिलोकेशन एंट्री को पैच करेगा, फिर रिलोकेटर को प्रत्येक तालिका के jump_table[0] की ओर इंगित करेगा। एक रिलोकेटर मूल रूप से यह करता है:
target.writePointer(base.add(value)), इसलिए value को 0 पर सेट करके, हम रिलोकेटर को हमें बेस एड्रेस देने और रनटाइम पर इसे जंप टेबल में डालने देते हैं, और हम goto के साथ इसका उपयोग करके रनटाइम एड्रेस उत्पन्न करते हैं। arm32 थंब मोड पर पास डिक्रिप्शन के बाद स्वचालित रूप से | 1 जोड़ता है।
PyPlugin.py का उपयोग करने के लिए बस निम्नलिखित करें:
(यदि venv का उपयोग कर रहे हैं, तो पहले इसे सक्रिय करें)
python3 PyPlugin.py --in <out_obfuscated_file> --android
यदि आप arm64 संस्करण को लक्षित कर रहे हैं तो --android पास करना महत्वपूर्ण है क्योंकि x86_64 संस्करण में एक अलग रिलोकेटर सिग्नेचर है।
आप --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 (मिश्रित बूलियन अंकगणित प्रतिस्थापन) के रूप में भी जाना जाता है, सरल ऑपरेशनों को जटिल ऑपरेशनों में बदलता है जो समान आउटपुट देते हैं। यह पूर्व-परिभाषित सेट का उपयोग करता है। उदाहरण:
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 पर बेसिक ब्लॉक स्प्लिटर 1 बार चलाएँ जिसमें न्यूनतम आकार = 15, अधिकतम आकार = 30 और संभावना = 100।
आप पासों को भी जोड़ सकते हैं:
__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 फ़ंक्शन को क्लोन करें और यदि संभव हो तो इनलाइन करने का प्रयास करें, जिससे इसके लिए एक से अधिक सिब्लिंग बनते हैं, जिससे एक ही फ़ंक्शन को हुक करना पर्याप्त नहीं होता। इसका उपयोग करना अत्यधिक पसंद किया जाता है क्योंकि यह केवल बाइनरी आकार बढ़ाता है और प्रदर्शन को प्रभावित नहीं करता।