
x64 PE bin2bin obfuscator जो बाइनरी में section नहीं जोड़ता
निर्माण निर्देशों के लिए 4. निर्माण देखें।
यह अस्पष्टीक यंत्र bin2bin है, जिसका अर्थ है कि यह पहले से संकलित निष्पादन योग्य बाइनरी लेता है और अस्पष्टीकरण पास लागू करके इसे पुन: उत्पन्न करता है। इसका उपयोग मूल स्रोत कोड तक पहुँच के बिना किसी एप्लिकेशन को सुरक्षित करने के लिए किया जा सकता है। फिलहाल केवल x64 PE (पोर्टेबल निष्पादन योग्य) फ़ाइलें समर्थित हैं, लेकिन भविष्य में अन्य बाइनरी प्रारूपों (जैसे ELF) के लिए समर्थन जोड़ने की योजना है।
वर्तमान में, सभी ज्ञात bin2bin अस्पष्टीक यंत्र बाइनरी के अंत में एक अनुभाग सम्मिलित करते हैं ताकि अस्पष्ट कोड या डेटा को अंदर रखा जा सके। ऐसा इसलिए है ताकि पूर्व-मौजूद अनुभागों की सामग्री को बदलने की आवश्यकता न होने से बाइनरी का मूल लेआउट संरक्षित रहे। इसे प्रबंधित करना बहुत आसान है क्योंकि यह अधिकांश RVA (सापेक्ष पतों) को मान्य रखता है।
यह परियोजना bin2bin के लिए एक अनूठा दृष्टिकोण अपनाती है, जहाँ किसी भी अस्पष्ट कोड या डेटा को बाइनरी के मूल अनुभागों के भीतर डाला जाता है। इसके लिए एप्लिकेशन में प्रत्येक एकल RVA को ट्रैक करना आवश्यक है। इस दृष्टिकोण के लाभ हैं:
यह दस्तावेज़ निष्पादन योग्य बाइनरी के पुनर्लेखन और लागू किए गए अस्पष्टीकरण तकनीकों दोनों का वर्णन करेगा। निम्नलिखित अस्पष्टीकरण तकनीकों को लागू किया गया है:
इसके अलावा, इस परियोजना में अपवाद समर्थन (C++ अपवाद और SEH) भी है और यह अपवाद प्रबंधन वाले फ़ंक्शन को अस्पष्ट करने में सक्षम है।
बाइनरी में डिसअसेंबली और कोड की खोज में सहायता के लिए, प्रतीक फ़ाइलें (PDB और MAP दोनों) वैकल्पिक रूप से स्वीकार की जाती हैं। प्रतीक फ़ाइलें प्रदान करना आवश्यक नहीं है लेकिन जटिल बाइनरी में डिसअसेंबली में सहायता करता है। कुछ सुविधाएँ जैसे अपवाद समर्थन और नियंत्रण प्रवाह समतलन के लिए प्रतीक फ़ाइल प्रदान करना आवश्यक है।
एक बाइनरी पुनर्लेखक एक निष्पादन योग्य बाइनरी लेता है और उसके अंदर कोड या डेटा को बदलता है ताकि परिवर्तनों के साथ एक आउटपुट बाइनरी तैयार हो सके।
जैसे-जैसे अस्पष्ट कोड सीधे बाइनरी के मूल अनुभागों में डाला जाता है, प्रोग्राम में सापेक्ष पतों को ट्रैक किया जाना चाहिए ताकि उनके सभी संदर्भों को समायोजित किया जा सके। ऐसा इसलिए है ताकि कोड और डेटा डालने के बाद भी संदर्भ उसी स्थान पर इंगित करते रहें। अन्यथा डेटा या कोड गलत स्थान पर एक्सेस होगा, जिससे आउटपुट बाइनरी का व्यवहार बदल जाएगा और गंभीर अस्थिरता पैदा होगी।
जब भी किसी सापेक्ष पते का कोई संदर्भ मिलता है (जैसे rip सापेक्ष ऑपरेंड युक्त निर्देश या PE डेटा निर्देशिकाएँ), इसे पुनर्लेखन के अंत में अद्यतन करने के लिए एक ट्रैकिंग सूची में जोड़ा जाता है। जहाँ संदर्भ होता है उसका RVA ट्रैक किया जाता है (यह जानने के लिए कि संदर्भ को कहाँ अद्यतन करना है) साथ ही जिस RVA का संदर्भ दिया जा रहा है उसे भी ट्रैक किया जाता है (यह जानने के लिए कि संदर्भ को किस RVA से अद्यतन करना है)।
जब भी डिसअसेंबलर को एक rip सापेक्ष निर्देश मिलता है, तो वह इसे अस्पष्टीकरण के अंत में अद्यतन किए जाने वाले संदर्भों की सूची में जोड़ देगा। यह सुनिश्चित करता है कि वे सभी निर्देश अभी भी उसी स्थान पर इंगित कर रहे हैं जो मूल रूप से थे। जंप टेबल जैसे अन्य सापेक्ष निर्देश मामलों को भी अद्यतन किए जाने वाले संदर्भों के रूप में जोड़ा जाता है।
जब भी बाइनरी में कोई बाइट्स डाले या हटाए जाते हैं, तो ट्रैक किए गए सभी RVA को समायोजित करने की आवश्यकता होती है। उदाहरण के लिए, यहाँ बाइट सम्मिलन हैंडलर दिया गया है:```cpp
void binwrite::binary_t::insert(const rva_t rva, const std::span data, const bool inclusive)
{
buffer_.insert_range(buffer_.begin() + rva.value(), data);
update_rvas(rva, static_cast<rva_t::size_type>(data.size()), inclusive);
}
`update_rvas` वह स्थान है जहाँ बाइनरी में हुए परिवर्तन को दर्शाने के लिए प्रत्येक ट्रैक किए गए RVA को अपडेट किया जाता है। यहाँ इस प्रक्रिया का एक आरेख है:

चित्र 1. सापेक्ष पता ट्रैकिंग।
डाला गया डेटा (नीला) वर्तमान डेटा (ग्रे) को स्थानांतरित करता है। निर्देश का संदर्भित RVA (नारंगी) उसी मेमोरी को इंगित करने के लिए अपडेट होता है, जो डाले गए डेटा (नीला) को ध्यान में रखता है।
## 2.2. डिसएसेम्बली
सभी संभावित कोड प्रविष्टियाँ (एक्सपोर्ट, एंट्री पॉइंट, कोड सेक्शन की ओर इशारा करने वाले रिलोकेशन, आदि) को एक डिसएसेम्बली क्यू में जोड़ा जाता है। यदि कोई सिम्बोल फ़ाइल मौजूद है, तो सिम्बोल फ़ाइल द्वारा वर्णित सभी फ़ंक्शन भी डिसएसेम्बली क्यू में जोड़े जाते हैं। क्यू में प्रत्येक प्रविष्टि को एक व्यक्तिगत बेसिक ब्लॉक के रूप में माना जाता है।
एक बेसिक ब्लॉक बिना किसी ब्रांच के निर्देशों का एक समूह है; इसका मतलब है कि यह कंट्रोल फ्लो निर्देशों (जैसे जंप, ret, int) पर समाप्त होता है। बेसिक ब्लॉक कॉल पर समाप्त नहीं होते क्योंकि अधिकांश मामलों में उनके वापस लौटने की उम्मीद होती है। कुछ फ़ंक्शन वापस नहीं लौटते (जैसे _CxxThrowException) और अब से उन्हें 'noreturn' कॉल कहा जाएगा।
जब डिसएसेम्बली क्यू से किसी बेसिक ब्लॉक को प्रोसेस किया जा रहा होता है, तो प्रत्येक निर्देश को ऊपर से शुरू करके डिसएसेम्बल किया जाता है जब तक कि निम्नलिखित में से एक न हो जाए:
- कोई अन्य पहले से विश्लेषित बेसिक ब्लॉक पहुँच जाता है, जिससे ओवरलैप होता है। “बेसिक ब्लॉक विभाजन” देखें।
- समाप्ति निर्देश मिलता है (जंप, return, int)।
- निर्देश का डिसएसेम्बली विफल हो जाता है।
- कोड पैडिंग मिल जाती है।
नीचे डिसएसेम्बली और डिसएसेम्बली क्यू में प्रविष्टि का एक आरेख है (आरेख में कोड पैडिंग जाँच छोड़ दी गई है)। यह तब तक दोहराया जाता है जब तक डिसएसेम्बली क्यू खाली नहीं हो जाता।

चित्र 2. डिसएसेम्बली प्रसंस्करण।
### 2.2.1 बेसिक ब्लॉक विभाजन
यदि दो बेसिक ब्लॉक ओवरलैप होते हैं, तो उनमें से एक को विभाजित किया जाना चाहिए। इससे दो ब्लॉक एक ही निर्देशों का वर्णन करने से रुक जाते हैं। उदाहरण के लिए:```asm
wcslen proc
or rax, 0FFFFFFFFFFFFFFFFh
loc_140001078:
inc rax
cmp word ptr [rcx+rax*2], 0
jnz short loc_140001078
retn
wcslen endp
यह wcslen का एक कार्यान्वयन है, जो एक वाइड स्ट्रिंग की लंबाई प्राप्त करता है। जब पहली इंस्ट्रक्शन 'or rax, FFFFFFFFFFFFFFFF' को एक बेसिक ब्लॉक की शुरुआत के रूप में डिसअसेंबल किया जाता है, तो यह 'retn' तक डिसअसेंबल करता रहेगा।
'jnz loc_140001078' वापस ऊपर कूदकर एक लूप बनाता है। इसे एक संदर्भ के रूप में जोड़ा जाएगा, और jnz का लक्ष्य डिसअसेंबली कतार में जोड़ा जाएगा, साथ ही फॉलथ्रू शाखा (अगली इंस्ट्रक्शन) भी। यह इंस्ट्रक्शन पहले से विश्लेषित ब्लॉक के मध्य में कूदती है, इसलिए यह केवल एक नया ब्लॉक नहीं बना सकता और फिर से 'retn' तक डिसअसेंबल नहीं कर सकता, क्योंकि इससे डुप्लिकेट प्रतिनिधित्व होगा।
'jnz' (सशर्त कूद) फॉलथ्रू शाखा को भी ले जाएगा ताकि एक नया बेसिक ब्लॉक भी बनाया जा सके। अब 4 बेसिक ब्लॉक होंगे जो इस तरह दिखते हैं:
Block A:```asm
or rax, 0FFFFFFFFFFFFFFFFh
loc_140001078:
inc rax
cmp word ptr [rcx+rax*2], 0
jnz short loc_140001078
ब्लॉक B:```asm
loc_140001078:
inc rax
cmp word ptr [rcx+rax*2], 0
jnz short loc_140001078
ब्लॉक C:```asm
retn
Block D:```asm
retn
यह मूल ब्लॉकों का गलत प्रतिनिधित्व है, क्योंकि यह 4 ब्लॉकों में समान निर्देशों की नकल करता है। इसे वर्तमान डिसअसेंबली के साथ अतिव्याप्त किसी भी मूल ब्लॉक को विभाजित करके ठीक किया जा सकता है। अब कोई भी नकली निर्देश नहीं होंगे क्योंकि प्रत्येक अतिव्यापन पर निर्देशों की नकली प्रतिनिधित्व बनाने के बजाय, मौजूदा निर्देशों को नए मूल ब्लॉक में स्थानांतरित कर दिया जाएगा। विभाजन का उपयोग करके सही प्रतिनिधित्व इस प्रकार है:
Block A:```asm
or rax, 0FFFFFFFFFFFFFFFFh
ब्लॉक B:```asm
inc rax
cmp word ptr [rcx+rax*2], 0
jnz short loc_140001078
ब्लॉक C:```asm
retn
### 2.2.2. अप्रत्यक्ष नियंत्रण प्रवाह
#### 2.2.2.1. जंप टेबल्स
जंप टेबल्स का उपयोग स्विच स्टेटमेंट के हैंडलर्स के पतों को संग्रहीत करने के लिए किया जाता है। स्विच स्टेटमेंट में प्रत्येक केस के लिए कई if स्टेटमेंट/सशर्त जंप होने के बजाय, केस स्टेटमेंट हैंडलर्स के पतों की एक तालिका रखी जाती है। यहाँ एक उदाहरण है (LLVM/CLANG binary):```cpp
std::int32_t sub_140004080(const std::int32_t a1)
{
std::int32_t result;
switch ( a1 )
{
case 0:
result = 9;
break;
case 1:
result = 4;
break;
case 2:
result = 3;
break;
case 3:
result = 1;
break;
default:
result = 0;
break;
}
return result;
}
यह स्विच स्टेटमेंट निम्नलिखित असेंबली में संकलित होता है:```asm
; ecx = a1
cmp ecx, 3 ; check if above bounds, must be default case
ja short def_140004097 ; goto default if a1 above 3
mov ecx, ecx
mov eax, ecx
lea rcx, jpt_140004097
movsxd rax, ds:(jpt_140004097 - 140004100h)[rcx+rax*4] ; select correct index of jump table for a1
add rax, rcx
jmp rax ; goto index specified - the case handler
jpt_140004097:
dd offset loc_140004099 - 140004100h ; address of handler for first case
dd offset loc_1400040D5 - 140004100h ; address of handler for second case
dd offset loc_1400040BD - 140004100h ; address of handler for third case
dd offset loc_1400040C9 - 140004100h ; address of handler for fourth case
'a1' मान की तुलना केस मानों के अधिकतम मान से की जाती है, और यदि यह उससे ऊपर है तो यह सीधे डिफ़ॉल्ट हैंडलर पर जाएगा। यदि a1 केस रेंज के भीतर है, तो यह जंप तालिका में अपनी एंट्री तक पहुँचता है और गणित हैंडलर पते पर जंप करता है।
जंप तालिका एंट्री तथा जंप तालिका के संदर्भ दोनों को ट्रैक किया जाता है ताकि वे बरकरार रहें।
##### 2.2.2.1.1. सीमित जंप तालिकाएं
यदि कोई जंप तालिका उन सभी मान श्रेणियों का वर्णन नहीं करती जिनका उपयोग स्विच स्टेटमेंट कर सकता है, तो वह सीमाओं की जाँच करने के लिए एक सीमित तालिका का उपयोग करेगा। ऐसा स्विच को डिफ़ॉल्ट स्टेटमेंट पर रीडायरेक्ट करने के लिए किया जाता है यदि वह सीमा से बाहर है। केस स्टेटमेंट की संख्या को पार्स करने के लिए, एंट्री की संख्या ज्ञात करने हेतु तुलना निर्देश की जाँच की जाती है। उदाहरण के लिए, निर्देश 'cmp ecx, 3' दर्शाता है कि जंप तालिका में एंट्री की संख्या 3 है।
##### 2.2.2.1.2. असीमित जंप तालिकाएं
यदि कोई जंप तालिका केस स्टेटमेंट हो सकने वाले सभी संभावित मानों को भरती है (जैसे UINT8 प्रकार के लिए UINT8 न्यूनतम मान से UINT8 अधिकतम मान तक), तो वह बिना सीमा जाँच के एक असीमित तालिका का उपयोग करेगा। ऐसा इसलिए है क्योंकि कंपाइलर जानता है कि जंप तालिकाएं सभी संभावित मानों को कवर करती हैं। जंप तालिका एंट्री की संख्या का संकेत देने के लिए कोई तुलना निर्देश नहीं है, इसलिए एंट्री को बलपूर्वक खोजा जाना चाहिए। तालिका के आधार को एक कोड सेक्शन में मान्य RVA के लिए क्रमिक रूप से जाँचा जाता है, और प्रत्येक मान्य एंट्री को ट्रैक किया जाता है। यह उतना सुरक्षित नहीं है क्योंकि यह अन्य डेटा/निर्देशों को जंप तालिका एंट्री के रूप में पार्स कर सकता है, यही कारण है कि जहाँ संभव हो सीमित जंप तालिका जाँच का उपयोग किया जाता है।
##### 2.2.2.1.3. विभिन्न प्रकार की जंप तालिकाएं
बाइनरी रीराइटर MSVC (बहु-स्तरीय तालिकाओं सहित), LLVM/CLANG और GCC निर्मित बाइनरी पर जंप तालिकाओं का समर्थन करता है।
MSVC जंप तालिकाओं के 2 रूप होते हैं, सामान्य और बहु-स्तरीय। MSVC के लिए सामान्य जंप तालिकाएं RVAs की एक एरे होती हैं। प्रत्येक RVA केस स्टेटमेंट हैंडलर की ओर इशारा करता है।
MSVC की बहु-स्तरीय तालिकाओं का उपयोग उन स्विच स्टेटमेंट के लिए किया जाता है जिनमें बड़ी संख्या में केस स्टेटमेंट होते हैं जो हैंडलर साझा करते हैं। बहु-स्तरीय संस्करण में 2 तालिकाएँ होती हैं, एक हैंडलर RVAs की एरे के लिए और दूसरी केस मानों को पहली तालिका में इंडेक्स से मिलाने के लिए। यह पहली तालिका में RVAs की पुनरावृत्ति को रोकता है, क्योंकि प्रत्येक केस मान को 4-बाइट RVA के बजाय केवल 1 बाइट इंडेक्स का वर्णन करने की आवश्यकता होती है। यहाँ एक बहु-स्तरीय जंप तालिका का उदाहरण दिया गया है:```asm
lea rdx, cs:140000000h
movsxd rax, edi ; load value
movzx eax, ds:(byte_1400023D8 - 140000000h)[rdx+rax] ; get handler index by value
mov ecx, ds:(jpt_14000209D - 140000000h)[rdx+rax*4] ; get RVA of handler by handler index
add rcx, rdx
jmp rcx
jpt_14000209D dd offset loc_14000209F - 140000000h
dd offset loc_1400020AB - 140000000h
dd offset loc_1400020B7 - 140000000h
dd offset loc_1400020CF - 140000000h
dd offset loc_1400020E7 - 140000000h
dd offset loc_1400020F3 - 140000000h
dd offset loc_1400020FF - 140000000h
dd offset loc_14000210B - 140000000h
dd offset loc_140002117 - 140000000h
dd offset loc_140002123 - 140000000h
dd offset loc_1400020C3 - 140000000h
dd offset loc_14000213B - 140000000h
dd offset loc_140002147 - 140000000h
dd offset loc_140002153 - 140000000h
dd offset loc_14000216B - 140000000h
dd offset loc_140002177 - 140000000h
dd offset loc_140002183 - 140000000h
dd offset loc_14000219B - 140000000h
dd offset loc_1400021A7 - 140000000h
dd offset loc_1400021B3 - 140000000h
dd offset loc_1400021BF - 140000000h
dd offset loc_1400021CB - 140000000h
dd offset loc_1400021D7 - 140000000h
dd offset loc_1400021E3 - 140000000h
dd offset loc_1400021EF - 140000000h
dd offset loc_1400021FB - 140000000h
dd offset loc_140002207 - 140000000h
dd offset loc_140002213 - 140000000h
dd offset loc_14000221F - 140000000h
dd offset loc_14000222B - 140000000h
dd offset loc_140002237 - 140000000h
dd offset loc_140002243 - 140000000h
dd offset loc_14000224F - 140000000h
dd offset loc_14000225B - 140000000h
dd offset loc_140002267 - 140000000h
dd offset loc_140002273 - 140000000h
dd offset loc_14000227F - 140000000h
dd offset loc_14000228B - 140000000h
dd offset loc_140002294 - 140000000h
; ... more handler addresses
byte_1400023D8:
db 0, 2Bh, 1, 2, 2Bh, 3, 2Bh, 4, 2Bh, 5, 6, 7, 8, 9, 0Ah, 0Bh, 0Ch, 0Dh
db 2Bh, 0Eh, 0Fh, 10h, 2Bh, 11h, 12h, 13h, 14h, 15h, 2Bh, 16h, 2Bh, 17h
db 18h, 19h, 1Ah, 1Bh, 1Ch, 2Bh, 1Dh, 1Eh, 1Fh, 20h, 21h, 22h, 2Bh, 23h
db 24h, 25h, 26h, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh
; ... more indexes to handler table
LLVM एक तालिका का उपयोग करता है जिसमें तालिका के आधार के सापेक्ष ऑफ़सेट होते हैं। तालिका के आधार के पते को प्रविष्टि द्वारा वर्णित ऑफ़सेट के साथ जोड़ने पर हैंडलर पते की गणना की जा सकती है।
GCC का जंप टेबल DIR64 रिलोकेशन का एक ऐरे है। प्रत्येक रिलोकेशन एक केस स्टेटमेंट के पते को इंगित करता है। रिलोकेशन पहले से ट्रैक किए गए हैं इसलिए वे जंप टेबल प्रविष्टियाँ पहले से ही फिक्स्ड अप हैं। रनटाइम पर, वे रिलोकेशन प्रविष्टियाँ बेस एड्रेस द्वारा ऑफ़सेट की जाएंगी ताकि प्रत्येक प्रविष्टि को केस स्टेटमेंट के पते के लिए डेरेफ़रेंस किया जा सके।
कुछ अन्य प्रकार के अप्रत्यक्ष नियंत्रण प्रवाह हैं जिनका समर्थन करना होता है। उदाहरण के लिए, FuncInfo3 C++ अपवादों का उपयोग करने वाले CLANG बाइनरी पर, निरंतरता का पता रजिस्टर rax में लोड किया जाता है और फिर कॉलर को वापस किया जाता है। कॉलर निरंतरता के पते पर जंप करेगा।```asm
lea rax, [rip+X]
retn
भले ही lea जिस पते की ओर इशारा करता है वह कोड अनुभाग में हो, इसकी कोई गारंटी नहीं है कि वह वास्तव में कोड है। जंप टेबल और स्ट्रिंग्स कैश स्थानीयता के लिए बाइनरी के कोड अनुभागों में रखी जा सकती हैं। सही कोड पथों की खोज और डिसअसेंबली की जानी चाहिए ताकि उसके अंदर RVA संदर्भों को ट्रैक किया जा सके (साथ ही उसे अस्पष्ट करने में सक्षम होने के लिए) इसलिए यह महत्वपूर्ण है कि इन मामलों की पहचान की जा सके। ये 'जोखिम भरे' संदर्भ हैं।
जंप टेबल को गलत तरीके से डिसअसेंबली कतार में जोड़े जाने को ठीक करने के लिए, डिसअसेंबलर उन जोखिम भरे संदर्भों को कतार में जोड़ने से पहले पहले जांचता है कि यह एक जंप टेबल है या नहीं।
स्ट्रिंग्स को lea'd होने और गलत तरीके से डिसअसेंबली कतार में जोड़े जाने को ठीक करने के लिए, डिसअसेंबलर इसे अतिरिक्त सैनिटी जांचों के साथ एक बुनियादी ब्लॉक के रूप में डिसअसेंबल करने का प्रयास करता है (उदाहरण के लिए, यदि यह इन जोखिम भरे संदर्भों में से एक है तो इसमें एक टर्मिनेटर निर्देश होना चाहिए)। यदि इनमें से कोई भी सैनिटी जांच किसी जोखिम भरे संदर्भ ब्लॉक पर विफल होती है, तो पूरे ब्लॉक को अनदेखा कर दिया जाता है और डेटा मान लिया जाता है।
यदि कोई प्रतीक फ़ाइल प्रदान की जाती है, तो कोई डेटा प्रतीक जोखिम भरे संदर्भ के रूप में नहीं जोड़ा जाएगा।
### 2.2.3. Functions
कुछ अस्पष्टीकरण पासों को यह जानने की आवश्यकता होती है कि कौन से बुनियादी ब्लॉक किन फ़ंक्शनों से संबंधित हैं। इस कारण से, सभी बुनियादी ब्लॉकों को उनके स्वामित्व वाले संबंधित फ़ंक्शनों को सौंपा जाता है। प्रतीक फ़ाइलों को पार्स किया जाता है ताकि सभी फ़ंक्शन पतों को खोजा जा सके और उन्हें एक सूची में रखा जा सके।
प्रत्येक फ़ंक्शन के लिए, निम्नलिखित चरण किए जाते हैं:
- फ़ंक्शन के लिए प्रवेश बुनियादी ब्लॉक प्राप्त करें (फ़ंक्शन प्रारंभ पते पर बुनियादी ब्लॉक)।
- इस प्रवेश ब्लॉक को फ़ंक्शन को असाइन करें।
- बुनियादी ब्लॉक के सभी निकास खोजें (फॉलथ्रू शाखाएं, लक्ष्य शाखाएं)।
- प्रत्येक निकास बुनियादी ब्लॉक के लिए, इसे फ़ंक्शन को असाइन करें यदि यह किसी अन्य फ़ंक्शन का प्रवेश ब्लॉक नहीं है (बुनियादी ब्लॉक RVA != कोई फ़ंक्शन RVA)। ये अंतिम 2 चरण प्रत्येक खोजे गए बुनियादी ब्लॉक के लिए दोहराए जाते हैं।
- खोजे गए ब्लॉकों में कोई भी जंप टेबल पार्स की जाती है और उनके लक्ष्य बुनियादी ब्लॉक फ़ंक्शन को असाइन किए जाते हैं।
### 2.2.4. 'Noreturn' call handling
Noreturn फ़ंक्शन वापस नहीं लौटते। यदि कोई noreturn कॉल होता है, तो बुनियादी ब्लॉक का डिसअसेंबली जारी रहेगा क्योंकि कॉल बुनियादी ब्लॉकों को समाप्त नहीं करते। बाइनरी से कॉल के बाद निष्पादित होने की उम्मीद नहीं है, इसलिए कंपाइलर ने बुनियादी ब्लॉक को समाप्त करने वाला पर्याप्त कोड नहीं डाला है। पूर्वोक्त डिसअसेंबली जांच संभवतः इन मामलों को पकड़ लेगी और बुनियादी ब्लॉक को समाप्त कर देगी।```asm
sub_140006310 proc
sub rsp, 38h
mov rax, cs:__security_cookie
xor rax, rsp
mov [rsp+38h+var_8], rax
mov [rsp+38h+pExceptionObject], 2Ah
lea rdx, __TI1H
lea rcx, [rsp+38h+pExceptionObject]
call _CxxThrowException
db 0CCh
sub_140006310 endp
algn_14000633D:
align 20h
उदाहरण के लिए, इस noreturn _CxxThrowException कॉल में, कंपाइलर ने INT3 निर्देशों के रूप में पैडिंग डाली है, जिसे पैडिंग और समाप्त करने वाले निर्देश दोनों के रूप में पकड़ा जाएगा। noreturn कॉल के बाद UD2 निर्देश डाले जाने के कुछ अन्य मामले भी हैं, जिन्हें भी संभाला जाता है।
यह इसका पता लगाने का कोई अचूक तरीका नहीं है, जैसा कि CodeDefender टीम भी चर्चा करती है, लेकिन किसी भी डिस्सेम्बली को बहुत आगे बढ़ने से रोकने के लिए प्रतिउपाय मौजूद हैं। यदि किसी बेसिक ब्लॉक के अंदर कोई जंप टेबल एंट्री पाई जाती है, तो बेसिक ब्लॉक को विभाजित कर दिया जाता है ताकि जंप टेबल को प्राथमिकता दी जा सके। यह noreturn कॉल के बाद जंप टेबल एंट्री को निर्देशों के रूप में डिस्सेम्बल होने से रोकेगा। यदि अगले निर्देश मान्य कोड हैं, तो बेसिक ब्लॉक वैसे भी विभाजित हो जाएगा जब अगले पते के बेसिक ब्लॉक को डिस्सेम्बल/प्रोसेस किया जा रहा होगा।
ऑब्सफस्केटर अपने ऑब्सफस्केशन पास में स्टैक आवंटन का उपयोग करता है। यह इसे अपने द्वारा उपयोग किए जाने वाले रजिस्टरों के मान को सहेजने (जैसे push rax) और ऑब्सफस्केशन पास पूरा होने के बाद उन्हें पुनर्स्थापित करने की अनुमति देता है ताकि रजिस्टर खराब न हों। एक फ्रेम पॉइंटर एक रजिस्टर है जो स्टैक पर एक विशिष्ट स्थान को इंगित करता है।
सभी स्टैक आवंटन को किसी फंक्शन के अनवाइंड कोड द्वारा वर्णित किया जाना चाहिए (यदि कोई फ्रेम पॉइंटर मौजूद नहीं है), जो OS अपवाद हैंडलर को स्टैक को वापसी पते पर ट्रैक करने और एप्लिकेशन के अपवाद हैंडलर की खोज करने की अनुमति देता है। ये स्टैक आवंटन प्रोलॉग (फंक्शन की शुरुआत) में होने चाहिए, क्योंकि फंक्शन की शुरुआत से उनकी दूरी की एक सीमा होती है। यदि स्टैक आवंटन प्रोलॉग के बाहर किए जाते हैं, तो अनवाइंड कोड उनका वर्णन नहीं कर सकते हैं और OS अनवाइंड नहीं कर पाएगा, जिससे अपवाद समर्थन टूट जाएगा।
यदि कोई फ्रेम पॉइंटर उपयोग किया जाता है, तो OS को rsp रजिस्टर से स्टैक को ट्रैक करने की आवश्यकता नहीं होती है, और इसके बजाय वह फ्रेम पॉइंटर से ट्रैक कर सकता है। इसका मतलब है कि ऑब्सफस्केटर अनवाइंड कोड में इसका वर्णन किए बिना प्रोलॉग के बाहर स्टैक आवंटन कर सकता है।
रीराइटर हर रनटाइम फंक्शन में एक फ्रेम पॉइंटर रजिस्टर डालेगा जिसमें पहले से कोई नहीं है। यह OS को स्टैक को अनवाइंड करने की अनुमति देता है, भले ही ऑब्सफस्केटर ने प्रोलॉग के बाहर स्टैक आवंटन किया हो।
नीचे फंक्शन के प्रोलॉग और एपिलॉग में किए गए परिवर्तनों का एक उदाहरण दिया गया है:
मूल फंक्शन प्रोलॉग:```asm
DriverEntry proc
sub rsp, 38h
संशोधित फ़ंक्शन प्रस्तावना:```asm
DriverEntry proc
push rbp
push rbp
sub rsp, 38h
lea rbp, [rsp]
lea rbp, [rsp]
मूल फ़ंक्शन एपिलॉग:```asm
add rsp, 38h
ren
DriverEntry endp
संशोधित फ़ंक्शन उपसंहार:```asm
add rsp, 38h
pop rbp
pop rbp
retn
DriverEntry endp

चित्र 3. फ्रेम पॉइंटर सम्मिलन से पहले स्टैक लेआउट।
गैर-वाष्पशील रजिस्टर rbp का उपयोग रीराइटर द्वारा फ्रेम पॉइंटर के रूप में किया जाता है। गैर-वाष्पशील रजिस्टरों को संरक्षित किया जाना चाहिए, इसलिए rbp का मान प्रोलॉग में पुश किया जाता है और अनवाइंड कोड द्वारा वर्णित किया जाता है (ताकि OS अनवाइंडर rbp के मूल मान को पुनर्स्थापित कर सके)। rbp रजिस्टर को स्टैक को 16 बाइट्स पर पुनः संरेखित करने के लिए 2 बार पुश किया जाता है, दूसरा पुश पूरी तरह से पुनर्संरेखण उद्देश्यों के लिए होता है।
चूंकि rbp का मान प्रोलॉग में दो बार पुश किया जाता है, फ़ंक्शन के अंत में स्टैक पॉइंटर को उसके मूल मान पर वापस लाने के लिए इसे पॉप भी किया जाना चाहिए। ऐसा इसलिए है ताकि रिटर्न इंस्ट्रक्शन के लिए रिटर्न एड्रेस [rsp] पर हो।
इन पुश निर्देशों के लिए संबंधित अनवाइंड कोड डाले जाते हैं ताकि OS को पता चले कि प्रोलॉग में अतिरिक्त स्टैक आवंटन हैं (जिनसे फ्रेम पॉइंटर को अनवाइंड करना है)। यही कारण है कि एपिलॉग पर 2 पॉप होते हैं।
उन एग्ज़िट बेसिक ब्लॉक(ब्लॉकों)/एपिलॉग(एपिलॉगों) को खोजने के लिए, एक फ़ंक्शन में सभी बेसिक ब्लॉकों को 'ret' या ऐसे जंप के लिए खोजा जाता है जो वर्तमान फ़ंक्शन के बाहर जाता है। अप्रत्यक्ष जंप (जैसे jmp rcx) भी वर्तमान फ़ंक्शन के बाहर जाने के रूप में गिने जाते हैं, जंप टेबल को छोड़कर। सभी एग्ज़िट बेसिक ब्लॉकों को एक साथ समूहित करके, उनमें 2 पॉप निर्देश डाले जा सकते हैं ताकि यह सुनिश्चित हो सके कि फ़ंक्शन से बाहर निकलने पर प्रोलॉग में पुश के प्रभाव को पूर्ववत किया जाए।
प्रोलॉग के अंत में 'lea rbp, [rsp]' OS को स्टैक का एक ठोस स्थान बताने के लिए है जिसे वह rsp से अनवाइंड करने के बजाय अनवाइंड कर सकता है। इस फ्रेम पॉइंटर सेटिंग निर्देश के लिए संबंधित अनवाइंड जानकारी और कोड डाले जाते हैं।

चित्र 4. फ्रेम पॉइंटर सम्मिलन के बाद गलत स्टैक लेआउट।
दूसरा विचार स्टैक आर्गुमेंट्स का है, जो स्टैक पॉइंटर से पहले स्थित होते हैं। यदि किसी फ़ंक्शन में स्थानीय आवंटन के बाद स्टैक तक पहुँचा जाता है (रिटर्न एड्रेस के स्लॉट के बराबर या उसके बाद), तो उन संदर्भों को अपडेट किया जाना चाहिए। ऐसा इसलिए है क्योंकि पुश स्टैक को 16 बाइट्स तक समायोजित करते हैं, इसलिए इसके बाद के सभी संदर्भित डेटा को भी 16 बाइट्स स्थानांतरित करने की आवश्यकता है। उपरोक्त आरेख दिखाता है कि कैसे स्टैक संदर्भ गलत संरेखित हो जाते हैं और उन्हें ठीक करने की आवश्यकता है।
उदाहरण के लिए 'mov rax, [rsp+0x90]' में 0x10 (दशमलव: 16) जोड़ा जाएगा ताकि पुश निष्पादित होने के बाद भी यह उसी स्टैक स्लॉट तक पहुँच सके। ठीक किया गया निर्देश होगा: 'mov rax, [rsp+0xA0]'।
यदि कोई निर्देश स्थानीय स्टैक आवंटन से आगे पहुँचता है, तो उसे 16 से समायोजित किया जाएगा ताकि वह स्टैक पर किए गए पुश को छोड़ दे। कभी-कभी स्टैक पॉइंटर को विभिन्न रजिस्टरों में ले जाया जाता है और एक अलग रजिस्टर के माध्यम से एक्सेस किया जाता है, ऐसी स्थिति में उस रजिस्टर की निगरानी की जाती है और उसी तरह समायोजित किया जाता है जैसे स्टैक पॉइंटर के लिए किया जाता है।
एक अन्य मामला अपवादों के लिए कैच हैंडलर है, क्योंकि rdx में उन्हें EstablisherFrame का पता मिलता है (अपवाद के समय हमारे फ्रेम पॉइंटर रजिस्टर के मान के बराबर)। कैच हैंडलर rdx के माध्यम से अपवाद फ़ंक्शन के स्टैक तक पहुँचेंगे और इसलिए rdx को भी ट्रैक और समायोजित किया जाना चाहिए।
नीचे स्टैक संदर्भ समायोजनों के साथ ठीक किए जाने के बाद स्टैक लेआउट का आरेख है।

चित्र 5. फ्रेम पॉइंटर सम्मिलन के बाद ठीक किया गया स्टैक लेआउट।
अपवाद समर्थन के लिए ऑबफस्केटर को एक सिंबल फ़ाइल प्रदान करने की आवश्यकता होती है क्योंकि इसे बाइनरी के सिंबल के बारे में यथासंभव अधिक जानकारी की आवश्यकता होती है।
रीराइटर अपवाद हैंडलर जानकारी खोजने के लिए बाइनरी की अनवाइंड जानकारी को पार्स करता है। पाए गए किसी भी RVA को भी ट्रैक किया जाता है। समर्थित अपवाद हैंडलर जानकारी प्रकार हैं:
इसका प्रारूप निम्न तालिका प्रविष्टियों की एक सारणी है:```cpp
struct c_scope_table_entry_t
{
std::uint32_t begin_rva; // where exception-throwing range begins
std::uint32_t end_rva; // where exception-throwing range ends
std::uint32_t handler_rva; // the handler type/rva (normally 1)
std::uint32_t target_rva; // the catch handler rva
};
struct c_scope_table_t
{
std::uint32_t entry_count;
c_scope_table_entry_t table[1];
};
The begin/end RVA उस कोड की सीमा का वर्णन करते हैं जो एक अपवाद (exception) उत्पन्न कर सकता है। target RVA कैच हैंडलर का वर्णन करता है जो अपवाद होने पर उसे प्रोसेस करता है।
#### 2.3.2.2. FuncInfo3 and FuncInfo4
C++ अपवादों के लिए उपयोग किया जाता है, FH3 और FH4 के बीच मुख्य अंतर यह है कि FH4 मेमोरी बचाने के लिए एक संपीड़ित प्रारूप का उपयोग करता है। वे निम्नलिखित डिस्क्रिप्टर साझा करते हैं:
- Unwind map - उन C++ ऑब्जेक्ट्स की सूची जिन्हें नष्ट करने की आवश्यकता होती है, साथ ही फ्रेम से ऑब्जेक्ट का ऑफ़सेट।
- Try block map - कैच हैंडलर और उन प्रकारों की सूची जिन्हें प्रत्येक पकड़ सकता है (जैसे std::runtime_error)।
- IP2State map - वर्तमान इंस्ट्रक्शन पॉइंटर / फ़ंक्शन में ऑफ़सेट के आधार पर ऑब्जेक्ट्स की स्थिति का वर्णन करता है।
FH3 विशिष्टताएँ:
- Try block map में continuation address कोड में रखा जाता है और कैच हैंडलर द्वारा rax में लौटाया जाता है (जैसे lea rax, continuation_address)।
FH4 विशिष्टताएँ:
- मैप की जानकारी को स्थान बचाने के लिए संपीड़ित पूर्णांक प्रारूप में संग्रहीत करता है।
- Try block map में FH4 जानकारी संरचना में एन्कोडेड continuation address होता है।
C++ अपवादों के लिए केवल MSVC और CLANG/LLVM समर्थित हैं। C++ अपवादों के लिए GCC समर्थित नहीं है क्योंकि यह FH3/FH4 से भिन्न प्रारूप का उपयोग करता है।
### 2.3.3. RTTI and ThrowInfo पार्सिंग
रनटाइम टाइप इनफार्मेशन (RTTI) और थ्रो इन्फो ऐसी संरचनाएँ हैं जिनका उपयोग रनटाइम पर C++ प्रकारों का निरीक्षण करने के लिए किया जाता है, जिसमें अपवाद फेंकना शामिल है। इन संरचनाओं में कई RVA होते हैं और इसलिए स्थिरता उद्देश्यों के लिए उन्हें ट्रैक किया जाना चाहिए।
#### 2.3.3.1. RTTI
C++ अपवाद डिस्क्रिप्टर में 'try block' मैप में यह जानने के लिए प्रकार की जानकारी होती है कि वे फेंके गए प्रकार को पकड़ते हैं या नहीं। इस प्रकार की जानकारी को RTTI कहा जाता है, और यह प्रकार के बारे में अन्य चीजों का भी वर्णन करता है, जैसे:
- वर्चुअल फंक्शन टेबल।
- प्रकार का नाम।
- इनहेरिटिंग क्लासेस।
वर्चुअल फंक्शन के बिना क्लासेस के लिए, केवल एक टाइप डिस्क्रिप्टर उत्पन्न होता है:```cpp
struct type_descriptor_t
{
std::uint64_t vftable_address; // this is a DIR64 relocation
std::uint64_t unk;
char name[1];
};
यह डेटा सेक्शन में DIR64 रीलोकेशन को स्कैन करके पाया जाता है, जो मेंबर फील्ड 'vftable_address' पर होता है, और यह जांचा जाता है कि क्या यह असली virtual function table है।
वर्चुअल फंक्शन वाले क्लासेस के लिए, एक complete object locator और class hierarchy descriptor जनरेट किया जाता है। class hierarchy descriptor में बेस क्लासेस का एक array होता है।```cpp
struct complete_object_locator_t
{
std::uint32_t signature;
std::uint32_t offset;
std::uint32_t constructor_offset;
std::uint32_t type_rva;
std::uint32_t hierarchy_rva;
std::uint32_t self_rva;
};
struct hierarchy_descriptor_t
{
std::uint32_t signature;
std::uint32_t attributes;
std::uint32_t base_class_count;
std::uint32_t base_class_list_rva;
};
struct base_class_array_t
{
std::uint32_t class_rvas[1];
};
struct base_class_descriptor_t
{
std::uint32_t type_rva;
std::uint32_t element_count;
std::uint32_t member_displacement;
std::uint32_t unk;
std::uint32_t unk1;
std::uint32_t attributes;
std::uint32_t hierarchy_rva;
};
To find these, data sections are scanned for DIR64 relocations that point to a complete object locator. Checks are done on the target of the DIR64 relocation to ensure it points to a complete object locator (e.g. does self_rva point to the RVA of the base of the class, do the type descriptors and hierarchy descriptors parse properly).
#### 2.3.3.2. ThrowInfo
ThrowInfo is used to describe how to destroy the exception object once it is processed, as well as the thrown type. The inherited classes of the thrown exception type are described in the catchable type array (contains RTTI references) to ensure the exception handler can match it to the catch statements.
The ThrowInfo is scanned in data sections by checking the catchable type array's contents with the previously discovered RTTI information. All the RVAs of the catchable types and the ThrowInfo are added to the tracking list.```cpp
struct throw_info_t
{
std::uint32_t attributes;
std::uint32_t pmfn_unwind; // address of exception object destructor
std::uint32_t forward_compat;
std::uint32_t catchable_type_array;
};
struct catchable_type_array_t
{
std::uint32_t count;
std::uint32_t type_rvas[1];
};
struct catchable_type_t
{
std::uint32_t attributes;
std::uint32_t rva_type;
std::uint32_t mdisp;
std::uint32_t pdisp;
std::uint32_t vdisp;
std::uint32_t size_of_thrown_object;
std::uint32_t optional_copy_constructor_rva;
};
यह तकनीक x86-64 आर्किटेक्चर के निर्देशों को लेती है और उन्हें एक वर्चुअल CPU आर्किटेक्चर में अनुवादित करती है। इसका विश्लेषण करना बहुत कठिन होता है क्योंकि एक रिवर्स इंजीनियर को मूल निर्देशों का विश्लेषण करने से पहले वर्चुअल CPU आर्किटेक्चर को समझना होता है।
यह कार्यान्वयन वर्चुअल मशीन हैंडलर्स उत्पन्न करने के लिए एक सामान्य दृष्टिकोण का उपयोग करता है ताकि प्रत्येक x86-64 निर्देश के लिए हैंडलर्स को हार्डकोड किए बिना निर्देशों की एक विस्तृत श्रृंखला को अस्पष्ट किया जा सके।



चित्र 6. वर्चुअल मशीन आर्किटेक्चर।
वर्चुअलाइज़ किए जाने वाले निर्देशों के मूल अनुक्रम को वर्चुअल मशीन के एंट्री ब्लॉक के कॉल से बदल दिया जाता है।
वर्चुअल मशीन में प्रवेश करते समय, सभी सामान्य प्रयोजन रजिस्टर (rsp को छोड़कर) स्टैक पर पुश कर दिए जाते हैं। rflags रजिस्टर भी स्टैक पर पुश कर दिया जाता है। इन स्टैक स्लॉट्स का उपयोग वर्चुअल रजिस्टर के रूप में किया जाता है, प्रत्येक अपने मूल रजिस्टर के अनुरूप होता है (इसलिए rax के लिए स्लॉट rax के स्थान पर उपयोग किया जाएगा)। स्टैक में इन वर्चुअल रजिस्टरों का क्रम यादृच्छिक किया जाता है, इसलिए प्रत्येक वर्चुअल मशीन हैंडलर की रजिस्टर व्यवस्था बदल जाती है।
सामान्य प्रयोजन रजिस्टरों को वर्चुअल स्टैक लेआउट पर रखने के लिए पुश निर्देश भी यादृच्छिक किए जाते हैं, और यह 'sub rsp, 8; mov [rsp] reg' या 'push reg' हो सकता है। ऐसा वर्चुअल मशीन एंट्री के लिए सिग्नेचर बनाना कठिन बनाने के लिए किया जाता है।
हार्डवेयर रजिस्टर x86-64 आर्किटेक्चर के सामान्य प्रयोजन रजिस्टर हैं। अब जब हार्डवेयर रजिस्टरों को स्टैक पर उनकी वर्चुअल CPU स्थिति में सहेज लिया गया है, तो वे खराब होने के लिए स्वतंत्र हैं। वर्चुअल मशीन स्थिति वर्तमान में उपलब्ध हार्डवेयर रजिस्टरों की एक सूची रखती है जिनका उपयोग वर्चुअल मशीन स्टब्स द्वारा किया जा सकता है। एक बार स्टब पूरा हो जाने पर, उन हार्डवेयर रजिस्टरों को वापस सूची में जोड़ दिया जाता है ताकि उनका पुन: उपयोग किया जा सके।
अब एप्लिकेशन वर्चुअल मशीन में प्रवेश कर चुका है और लक्ष्य निर्देशों के हैंडलर्स को निष्पादन सौंपने का समय है। लक्ष्य निर्देश वे x86-64 निर्देश हैं जिन्हें इस CPU आर्किटेक्चर में वर्चुअलाइज़ किया जा रहा है।
पहले, लक्ष्य निर्देश के ऑपरेंड को स्टैक पर लोड करने की आवश्यकता होती है। ऑपरेंड के मान एक मुक्त हार्डवेयर रजिस्टर में लोड किए जाते हैं। फिर ऑपरेंड के मान अस्पष्ट किए जाते हैं और स्टैक पर पुश कर दिए जाते हैं। यह निर्देश हैंडलर के पिछले ब्लॉक (पिछला हैंडलर या वर्चुअल मशीन एंट्री ब्लॉक यदि यह पहला हैंडलर है) से किया जाता है।
ऑपरेंड के मानों पर लागू अस्पष्टीकरण इस प्रकार है:
तत्काल ऑपरेंड के लिए, यह अस्पष्टीकरण अस्पष्टीकरण समय पर किया जा सकता है क्योंकि मान ज्ञात होता है, इसलिए गणना रनटाइम पर नहीं की जाती है और इसलिए इसे उलटना कठिन होता है।
छिपे हुए ऑपरेंड जिन्हें विशिष्ट रजिस्टरों की आवश्यकता होती है (उदा. 'rep movsb' के लिए rsi और rdi) एक यादृच्छिक हार्डवेयर रजिस्टर के बजाय उन विशिष्ट रजिस्टरों में लोड किए जाते हैं।
निर्देश हैंडलर ब्लॉक में, ऑपरेंड को स्टैक से पॉप किया जाता है और डी-अस्पष्ट किया जाता है। ऑपरेंड के मूल मान प्राप्त करने के लिए उलटी संक्रिया निष्पादित की जाती है।
यदि मूल निर्देश rflags रजिस्टर से पढ़ता है, तो निर्देश को निष्पादित करने से पहले rflags को स्टैक संदर्भ से लोड किया जाता है।
यदि मूल निर्देश rflags रजिस्टर में लिखता है, तो निर्देश को निष्पादित करने से पहले rflags को स्टैक संदर्भ से लोड किया जाता है। मूल निर्देश निष्पादित होने के बाद, अद्यतन rflags को वापस स्टैक संदर्भ में लिखा जाता है।
यह सुनिश्चित करता है कि वर्चुअलाइज़ किए गए निर्देशों का फ़्लैग व्यवहार बिल्कुल मूल निर्देशों के समान हो।
निर्देश हैंडलर ब्लॉक के अंदर, मूल x86-64 निर्देश को डी-अस्पष्ट ऑपरेंड का उपयोग करने के लिए एन्कोड किया जाता है। एक बार मूल निर्देश निष्पादित हो जाने पर, परिणाम ऑपरेंड को अस्पष्ट किया जाता है और स्टैक पर पुश कर दिया जाता है।
यदि यह अंतिम निर्देश हैंडलर है, तो अगला मूल ब्लॉक वर्चुअल मशीन एग्ज़िट ब्लॉक होगा। यदि नहीं, तो अगला ब्लॉक अगले हैंडलर से होगा।
अगला मूल ब्लॉक अस्पष्ट परिणाम ऑपरेंड को स्टैक से पॉप करता है और उसी डी-अस्पष्टीकरण प्रक्रिया को लागू करता है। परिणाम मान उनके मूल गंतव्य पर लिखे जाते हैं। यह स्टैक संदर्भ पर एक वर्चुअल रजिस्टर या मूल निर्देश द्वारा वर्णित मेमोरी में एक विशिष्ट स्थान हो सकता है।
यह प्रक्रिया तब तक दोहराई जाती है जब तक मूल ब्लॉक में कोई निर्देश वर्चुअलाइज़ नहीं किया जा सकता (उदा. एक निर्देश जो rsp रजिस्टर का उपयोग करता है)।
फिर गैर-वर्चुअलाइज़ कोड पर वापस जाने के लिए वर्चुअल मशीन संदर्भ को अनलोड करने की आवश्यकता होती है। सभी वर्चुअल रजिस्टरों को स्टैक से उनके संबंधित सामान्य प्रयोजन रजिस्टरों में पॉप किया जाता है। संशोधित rflags रजिस्टर को भी स्टैक से पॉप करके पुनर्स्थापित किया जाता है। अब, वर्चुअल मशीन एग्ज़िट ब्लॉक कॉलर पर वापस लौटता है।
यदि कोई वर्चुअलाइज़ निर्देश अपवाद फेंकता है, तो OS को कॉलर्स में उपयुक्त अपवाद हैंडलर खोजने में सक्षम होने के लिए वर्चुअल मशीन संदर्भ से बाहर अनवाइंड करने में सक्षम होना चाहिए।
ऐसा होने देने के लिए, बाइनरी में अनवाइंड जानकारी जोड़ने की आवश्यकता है ताकि वर्चुअल मशीन फ़ंक्शन का स्टैक लेआउट OS को ज्ञात हो।
rbp में एक फ्रेम पॉइंटर लोड किया जाता है क्योंकि वर्चुअल मशीन हैंडलर प्रस्तावना के बाहर स्टैक आवंटन का उपयोग करते हैं। इसका मतलब है कि rbp को वर्चुअल मशीन हैंडलर द्वारा 'उपलब्ध' हार्डवेयर रजिस्टर के रूप में उपयोग करने की अनुमति नहीं है।
वर्चुअल मशीन के सभी हार्डवेयर रजिस्टर जो उपयोग किए जाते हैं, स्टैक संदर्भ पर पुश कर दिए जाते हैं, इसलिए उन पुश के लिए संबंधित अनवाइंड कोड डाले जाते हैं।
फिर वर्चुअल मशीन फ़ंक्शन के लिए रनटाइम फ़ंक्शन को अपवाद निर्देशिका में डाला जाता है। वर्चुअल मशीन अब अनवाइंड करने योग्य है।
यह तकनीक एक रिवर्स इंजीनियर को भ्रमित करने के लिए गलत डेटा प्रवाह वाले नकली मूल ब्लॉकों में शाखाएँ बनाती है।
अपारदर्शी प्रेडिकेट ऐसे कथन हैं जो केवल सत्य या असत्य का मूल्यांकन करते हैं।
मूल ब्लॉकों की नकल की जाती है और उन्हें एक अपारदर्शी प्रेडिकेट if स्टेटमेंट में लपेटा जाता है। ब्लॉकों में से एक का डेटा प्रवाह विकृत होगा ताकि वह समान लेकिन गलत हो।```cpp
if (opaque_statement_always_true)
{
… original basic block
}
else
{
… incorrect but similar basic block
}
एक रिवर्स इंजीनियर को और भी गुमराह करने के लिए, सभी निर्देश ऑपरेंड एकत्र किए जाते हैं और यादृच्छिक किए जाते हैं। प्रत्येक निर्देश को एकत्रित सूची से यादृच्छिक ऑपरेंड के साथ पुनः संकलित किया जाएगा। यह सुनिश्चित करता है कि डुप्लिकेट ब्लॉक का व्यवहार मूल से भिन्न हो।
ब्लॉकों का स्थान भी यादृच्छिक रूप से बदल दिया जाता है, जिससे मेमोरी में उनका भौतिक स्थान यह संकेत नहीं देगा कि सही शाखा कौन सी है।
शाखा चयन के लिए आवश्यक स्थिति भी यादृच्छिक रूप से चुनी जाती है। उदाहरण के लिए, एक पुनरावृत्ति में, सशर्त जंप की फॉलथ्रू शाखा सही ब्लॉक की ओर ले जाएगी। दूसरे पुनरावृत्ति में, सशर्त जंप की लक्ष्य शाखा सही ब्लॉक की ओर ले जाएगी। इससे सही शाखा खोजना कठिन हो जाता है।
अपारदर्शी विधेय अभिव्यक्ति भी बहुत महत्वपूर्ण है, क्योंकि यदि इसका मूल्यांकन करना आसान होता तो यह प्रभावी नहीं होती। इस कारण से, [फ़र्मा का अंतिम प्रमेय](https://en.wikipedia.org/wiki/Fermat's_Last_Theorem) को अपारदर्शी अभिव्यक्ति के रूप में चुना गया है। फ़र्मा का अंतिम प्रमेय "बताता है कि कोई भी तीन धनात्मक पूर्णांक a, b, और c, किसी भी पूर्णांक n के लिए जो 2 से बड़ा है, समीकरण a^n + b^n = c^n को संतुष्ट नहीं करते"(जहाँ ^ का अर्थ घात है)। यह अभिव्यक्ति दी गई शर्तों के तहत हमेशा गलत साबित हुई है। तीन संख्याओं a, b, c को चुना जाता है और उन्हें 3 से 7 तक यादृच्छिक रूप से चुनी गई घात तक बढ़ाया जाता है। अभिव्यक्ति को एक नवनिर्मित मूल ब्लॉक में निष्पादित किया जाता है और फिर सशर्त शाखा को सही ब्लॉक पर ले जाया जाता है।
यदि पैरामीटर a, b, c, और n ज्ञात होते, तो एक हमलावर अभिव्यक्ति को हल कर सकता था और सही शाखा का पता लगा सकता था। इसे बाधित करने के लिए, पैरामीटर रनटाइम पर निर्धारित मानों से चुने जाते हैं: स्टैक पॉइंटर (rsp) और इंस्ट्रक्शन पॉइंटर (ASLR का अर्थ है कि rip रनटाइम पर पुनः स्थानांतरित हो जाएगा)। उदाहरण के लिए:```cpp
if ((rsp^n) + (rip^n) != (c^n))
{
… incorrect but similar basic block
}
else
{
… original basic block
}
कंट्रोल फ़्लो वह निष्पादन पथ है जो एक प्रोग्राम लेता है (जैसे 'if स्टेटमेंट्स')। कंट्रोल फ़्लो में परिवर्तन तब होता है जब एक बेसिक ब्लॉक दूसरे पर जंप करता है। कंट्रोल फ़्लो में ये परिवर्तन एक एकल डिस्पैचर स्टब में समूहित किए जा सकते हैं और इसे समझने में कठिन बनाने के लिए फेरबदल किया जा सकता है। डिस्पैचर स्टब सीधे किए जाने के बजाय अगले बेसिक ब्लॉक में कंट्रोल फ़्लो बदलने के लिए जिम्मेदार होगा।

चित्र 7. कंट्रोल फ़्लो फ़्लैटनिंग।
एक फ़ंक्शन के सभी बेसिक ब्लॉक (प्रोलॉग को छोड़कर) एक सूची में समूहित किए जाते हैं, और उनकी सभी शाखाएँ (सशर्त, अशर्त) एकत्र की जाती हैं। प्रत्येक बेसिक ब्लॉक को एक अद्वितीय आईडी/पहचानकर्ता दिया जाता है। एक डिस्पैचर स्टब सभी संभावित शाखाओं को लेता है और सभी आईडी को केस के रूप में एक स्विच स्टेटमेंट बनाता है। केस स्टेटमेंट लक्ष्य बेसिक ब्लॉक पर जंप करते हैं। मूल बेसिक ब्लॉकों/मूल कंट्रोल फ़्लो पर सभी जंप को सही आईडी (लक्ष्य ब्लॉक की आईडी) के साथ स्विच स्टेटमेंट पर एक जंप से बदल दिया जाता है।
बेसिक ब्लॉकों का भौतिक लेआउट फेरबदल किया जाता है ताकि उनकी मेमोरी स्थिति मूल कंट्रोल फ़्लो के बारे में कोई संकेत न दे।
यह तकनीक किसी निर्देश में एन्कोडेड किसी भी संख्या (मेमोरी ऑपरेंड विस्थापन, तत्काल ऑपरेंड) को लेती है और रनटाइम पर उनकी गणना करके उनके वास्तविक मान को छिपाती है।
अस्पष्टीकरण समय पर, मूल संख्या के समान बिट चौड़ाई का एक यादृच्छिक संख्या मान उत्पन्न किया जाता है। इसे मूल संख्या में जोड़ा जाता है और एक अप्रयुक्त रजिस्टर में एक तत्काल ऑपरेंड के रूप में लोड किया जाता है।
रनटाइम पर, यादृच्छिक संख्या को रजिस्टर से घटाया जाता है, जिसके परिणामस्वरूप मूल संख्या प्राप्त होती है।
यदि R यादृच्छिक संख्या है और N मूल संख्या है, तो अभिव्यक्ति प्रभावी रूप से ((R+N) - R) है।
मेमोरी ऑपरेंड को पुनः एन्कोड करने के लिए, बेस ऑपरेंड में रजिस्टर सम्मिलन किया जाता है। यदि पहले से कोई बेस ऑपरेंड है, तो उसका मान शीर्ष पर जोड़ा जाता है।
स्टैक मेमोरी ऑपरेंड को प्रतिस्थापित करने के लिए, पुश के कारण होने वाले स्टैक विस्थापन को ध्यान में रखते हुए मान में एक विस्थापन जोड़ा जाता है। पुश का उपयोग rflags रजिस्टर के साथ-साथ अप्रयुक्त रजिस्टर को सहेजने के लिए किया जाता है।
यहाँ 'mov [rsp+24h], 0' में मेमोरी ऑपरेंड के प्रतिस्थापन का एक उदाहरण है:```asm
push r11
pushfq ; save flags for now
mov r11, rsp
add r11, 1D4D9F71h
sub r11, 1D4D9F3Dh
popfq ; restore flags, the original instruction will now execute and populate flags with the correct values
mov dword ptr [r11], 0 ; original instruction substituted (r11 == rsp+24h)
pop r11
यहाँ 'add rbx, 8' में तत्काल ऑपरेंड के प्रतिस्थापन का एक और उदाहरण है:```asm
push r10
pushfq
mov r10, 0FFFFFFFFE3F78112h
sub r10, 0FFFFFFFFE3F7810Ah
popfq
add rbx, r10
pop r10
यह तकनीक सामान्य अंकगणितीय अभिव्यक्तियाँ (जैसे x+y) लेती है और उन्हें अधिक जटिल अभिव्यक्तियों में बदल देती है जो समान परिणाम उत्पन्न करती हैं। यह समान रैखिक सर्वसमिकाओं के लिए अभिव्यक्तियों को प्रतिस्थापित करके ऐसा करता है।
उदाहरण के लिए, (x+y) की समान सर्वसमिका ((x & y) + (x | y)) है। जब ऑबफ़स्केटर (x+y) को संसाधित करता है, तो वह इसे उन सर्वसमिकाओं में से एक से प्रतिस्थापित करेगा जिन्हें रिवर्स इंजीनियर करना अधिक कठिन है।
निम्नलिखित अंकगणितीय निर्देशों के लिए सर्वसमिकाओं की एक सूची है: 'add', 'sub', 'and', 'or', 'xor'। इसका मतलब है कि इनमें से प्रत्येक निर्देश के लिए एक अधिक जटिल, यादृच्छिक प्रतिस्थापन चुना जाता है। सर्वसमिकाओं को सूची से यादृच्छिक रूप से चुना जाना सुनिश्चित करता है कि प्रत्येक ऑबफ़स्केशन आउटपुट अलग हो।
डीऑबफ़स्केट करना कठिन बनाने के लिए, इसे पुनरावर्ती रूप से लागू किया जाता है। इसके परिणामस्वरूप प्रत्येक प्रतिस्थापित सर्वसमिका को कई बार पुनः प्रतिस्थापित किया जाता है, जिससे जटिलता तेजी से बढ़ती है। नीचे इस तकनीक के 2 पासों के बाद (x+y) के परिवर्तन का एक उदाहरण दिया गया है:

चित्र 8. मिश्रित बूलियन अंकगणित।
विचार करने योग्य एक पहलू निर्देश का फ़्लैग गणना परिणाम है। मूल निर्देश में फ़्लैग रजिस्टर पर विशिष्ट फ़्लैग व्यवहार लागू होता होगा जो अभिव्यक्ति के विभिन्न संक्रियाओं/निर्देशों में विभाजित होने के कारण समान नहीं होगा। इसे हल करने के लिए, ऑबफ़स्केटर एक स्टब डालकर फ़्लैग व्यवहार का अनुकरण करता है जो सही फ़्लैग की गणना और लागू करता है।
'and', 'or', 'xor' के लिए फ़्लैग व्यवहार 'test' निर्देश के समान है; समान ऑपरेंड के साथ एक test निर्देश डालने से उन 3 प्रतिस्थापित निर्देशों के लिए फ़्लैग ठीक से अनुकरण किए जाएंगे।
'sub' के लिए, 'cmp' निर्देश का फ़्लैग व्यवहार समान है, और फ़्लैग गणना के लिए इसे समान ऑपरेंड के साथ डाला जा सकता है।
'add' के लिए, SF, ZF, PF की गणना 'test' द्वारा की जा सकती है, लेकिन CF, AF और OF की गणना मैन्युअल रूप से करनी होती है। फिर एक स्टब डाला जाता है जो add निर्देश के लिए CF, AF और OF की मैन्युअल रूप से गणना करता है।
ये फ़्लैग अनुकरण स्टब मूल निर्देश के बारे में संकेत देते हैं, इसलिए यह केवल तभी किया जाता है जब बिल्कुल आवश्यक हो। पूरे मूल ब्लॉक को उन निर्देशों के लिए स्कैन किया जाता है जो फ़्लैग पढ़ते हैं और फ़्लैग लिखते हैं। फ़्लैग अनुकरण जोड़ने की शर्तें इस प्रकार हैं:
यह सुनिश्चित करता है कि जब भी मूल ब्लॉक का अंत पहुँच जाता है या फ़्लैग पढ़े जाते हैं, तो वे फ़्लैग अनुकरण स्टब द्वारा अद्यतन रखे जाते हैं। यह शर्तों (जैसे if स्टेटमेंट) को गलत शाखाएँ लेने से रोकता है।
नीचे CMake का उपयोग करके प्रोजेक्ट बनाने के लिए उदाहरण कमांड दिए गए हैं। इन कमांडों का निष्पादन प्रोजेक्ट की रूट डायरेक्टरी से शुरू करें।```
cmake -B build
cmake --build build
विंडोज सिस्टम पर जिनमें Visual Studio स्थापित है, अंतिम कमांड को छोड़ा जा सकता है क्योंकि प्रोजेक्ट को जनरेटेड Visual Studio समाधान फ़ाइलों (.sln) के माध्यम से बनाया जा सकता है। Visual Studio समाधान फ़ाइलें 'build/' फ़ोल्डर में होंगी।
# 5. उपयोग
ऑबफस्केटर में कमांड लाइन आधारित कॉन्फ़िग सिस्टम है। कमांड लाइन आर्गुमेंट के माध्यम से निम्नलिखित कॉन्फ़िगर किए जा सकते हैं:
- कौन से ऑबफस्केशन पास का उपयोग करना है।
- इनपुट बाइनरी फ़ाइल का पथ।
- इनपुट सिंबल फ़ाइल का पथ (वैकल्पिक)।
- आउटपुट बाइनरी फ़ाइल का पथ (वैकल्पिक)।
नीचे कमांड लाइन आर्गुमेंट का एक सिंहावलोकन दिया गया है:
Usage: binprotect binary-path symbol-path [--out-binary-path VAR] [--control-flow-flattening VAR] [--virtual-machine VAR] [--opaque-predicates VAR] [--linear-substitution VAR] [--mixed-boolean-arithmetic VAR]
Positional arguments:
binary-path इनपुट बाइनरी का फ़ाइल पथ [आवश्यक]
symbol-path इनपुट बाइनरी के सिंबल का फ़ाइल पथ [वैकल्पिक]
--out, --out-path, --out-binary-path आउटपुट बाइनरी का वांछित फ़ाइल पथ
--cff, --control-flow-flattening कंट्रोल फ़्लो फ़्लैटनिंग पास सक्षम करें [डिफ़ॉल्ट: 1]
--vm, --virtual-machine वर्चुअल मशीन पास सक्षम करें [डिफ़ॉल्ट: 1]
--opa, --opaque, --opaque-predicate, --opaque-predicates अपारदर्शी प्रेडिकेट पास सक्षम करें [डिफ़ॉल्ट: 1]
--lin, --linear-substitution रैखिक प्रतिस्थापन पास सक्षम करें [डिफ़ॉल्ट: 1]
--mba, --mixed-boolean-arithmetic मिश्रित बूलियन अंकगणित पास की मात्रा निर्दिष्ट करें [डिफ़ॉल्ट: 2]
# 6. संक्षेपाक्षर
- bin2bin - बाइनरी से बाइनरी।
- RVA - सापेक्ष पता।
- MBA - मिश्रित बूलियन अंकगणित।
- [SEH - संरचित अपवाद हैंडलिंग](https://learn.microsoft.com/en-us/cpp/cpp/structured-exception-handling-c-cpp)।
- FH3 - FuncInfo3।
- FH4 - FuncInfo4।
- [MSVC - Microsoft Visual C++](https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B)।
- [LLVM - निम्न-स्तरीय वर्चुअल मशीन](https://en.wikipedia.org/wiki/LLVM)।
- [CLANG - LLVM के लिए C/C++ भाषा फ़्रंटएंड](https://clang.llvm.org/)।
- [GCC - GNU कंपाइलर संग्रह](https://en.wikipedia.org/wiki/GNU_Compiler_Collection)।
- SF - साइन फ़्लैग।
- ZF - शून्य फ़्लैग।
- PF - पैरिटी फ़्लैग।
- CF - कैरी फ़्लैग।
- AF - सहायक कैरी फ़्लैग।
- OF - ओवरफ़्लो फ़्लैग।
# 7. श्रेय
निम्नलिखित व्यक्तियों ने परियोजना के विकास के दौरान अमूल्य सलाह दी:
- Aita।
- Papstuc।
- Eriktion।
- IDontCode।
- Abdulla।
- Brit।
- Phage।