
VMProtect सॉफ्टवेयर सुरक्षा के साथ खेलना। प्रतीकात्मक निष्पादन और LLVM का उपयोग करके शुद्ध फलनों का स्वचालित डीओबफस्केशन।
एक प्रायोगिक गतिशील दृष्टिकोण जो VMProtect 3.x द्वारा संरक्षित शुद्ध फ़ंक्शनों को डीवर्चुअलाइज़ करता है
मैं VMProtect द्वारा संरक्षित शुद्ध फ़ंक्शनों को डीवर्चुअलाइज़ करने के एक गतिशील दृष्टिकोण के बारे में कुछ नोट्स साझा कर रहा हूँ। यह दृष्टिकोण बहुत अच्छे परिणाम दिखाता है यदि वर्चुअलाइज़्ड फ़ंक्शन में केवल एक बेसिक ब्लॉक हो (उसके आकार की परवाह किए बिना)। यह एक सामान्य परिदृश्य है जब बाइनरी अंकगणितीय संक्रियाओं को सुरक्षित करती हैं। हालाँकि, यह दृष्टिकोण थोड़ा अधिक प्रायोगिक है जब लक्ष्य फ़ंक्शन में एक से अधिक बेसिक ब्लॉक हों। फिर भी, हम उन नमूनों से बाइनरी कोड को डीवर्चुअलाइज़ और पुनर्निर्मित करने में सफल हुए जिनमें 2 बेसिक ब्लॉक हैं, जो बताता है कि छोटे फ़ंक्शनों को गतिशील रूप से पूरी तरह से डीवर्चुअलाइज़ करना संभव है।
VMProtect एक सॉफ़्टवेयर सुरक्षा है जो कोड को गैर-मानक आर्किटेक्चर वाली वर्चुअल मशीन के माध्यम से चलाकर सुरक्षित करता है। यह सुरक्षा asm प्रेमियों के लिए एक बेहतरीन खेल का मैदान है [0, 1, 2, 3, 4, 5, 6, 11]। इसके अलावा, पहले से ही कई उपकरण मौजूद हैं जो इस सुरक्षा पर हमला करते हैं [7, 8, 9, 12, 13]। 2016 में हमने Tigress सॉफ़्टवेयर सुरक्षा समाधान पर एक नज़र डाली और प्रतीकात्मक निष्पादन और LLVM का उपयोग करके इसके वर्चुअलाइज़ेशन को पराजित करने में सफल रहे। यह दृष्टिकोण DIMVA 2018 [10] में प्रस्तुत किया गया था और मैं इसे VMProtect पर परखना चाहता था। ध्यान दें कि ऐसा कोई जादुई समाधान नहीं है जो हर बाइनरी पर काम करता हो, लक्ष्य और आपके लक्ष्यों के आधार पर हमेशा समझौते होते हैं। यह विनम्र योगदान VMProtect द्वारा वर्चुअलाइज़ किए गए शुद्ध फ़ंक्शनों के विरुद्ध एक गतिशील हमले का उदाहरण प्रदान करने का लक्ष्य रखता है। गतिशील हमले का मुख्य लाभ यह है कि यह डिज़ाइन द्वारा VMProtect की कुछ स्थैतिक सुरक्षाओं को पराजित करता है, जैसे स्व-संशोधित कोड, कुंजी और ऑपरेंड एन्क्रिप्शन आदि।
हम एक शुद्ध फ़ंक्शन को ऐसा फ़ंक्शन मानते हैं जिसमें पथों की एक सीमित संख्या होती है और जिसके कोई साइड इफेक्ट नहीं होते। इसमें कई इनपुट हो सकते हैं लेकिन केवल एक आउटपुट। नीचे एक शुद्ध फ़ंक्शन का उदाहरण दिया गया है:```cpp int secret(int x, int y) { int r = x ^ y; return r; }
# दृष्टिकोण
हम इस मुख्य अंतर्ज्ञान पर भरोसा करते हैं कि एक अस्पष्ट ट्रेस T' (अस्पष्ट कोड P' से) मूल कोड P (मूल कोड में T' के अनुरूप ट्रेस T) और वर्चुअल मशीन VM के निर्देशों को इस प्रकार जोड़ता है कि T' = T + VM(T)। यदि हम निर्देशों के इन दो उप-अनुक्रमों T और VM(T) के बीच अंतर करने में सक्षम हैं, तो हम एक ट्रेस T' से मूल प्रोग्राम P के एक पथ का पुनर्निर्माण करने में सक्षम हैं। इस ऑपरेशन को वर्चुअलाइज्ड प्रोग्राम के सभी पथों को कवर करने के लिए दोहराकर, हम मूल प्रोग्राम P का पुनर्निर्माण करने में सक्षम होंगे। हमारे व्यावहारिक उदाहरण में, मूल कोड में निष्पादन योग्य पथों की एक सीमित संख्या है, जो बौद्धिक संपदा संरक्षण से जुड़ी कई स्थितियों में होता है। ऐसा करने के लिए, हम निम्नलिखित चरणों के साथ आगे बढ़ते हैं:
1. वर्चुअलाइज्ड फ़ंक्शन और उसके तर्कों की पहचान करें
2. लक्ष्य का VMProtect ट्रेस उत्पन्न करें
3. VMP ट्रेस को फिर से चलाएँ और इनपुट और आउटपुट के बीच संबंध प्राप्त करने के लिए प्रतीकात्मक अभिव्यक्तियाँ बनाएँ
4. VM से निर्देशों को जितना संभव हो उतना कम करने के लिए प्रतीकात्मक अभिव्यक्तियों पर अनुकूलन लागू करें
5. लक्ष्य का एक नया असुरक्षित संस्करण बनाने के लिए अपने प्रतीकात्मक प्रतिनिधित्व को LLVM-IR पर उठाएँ
## उदाहरण 1: एक सरल बिटवाइज़ ऑपरेशन
आइए पहले उदाहरण के रूप में निम्नलिखित फ़ंक्शन लें: यह दो इनपुट लेता है और `x ^ y` लौटाता है जो VMProtect द्वारा संरक्षित है।```cpp
int secret(int x, int y) {
VMProtectBegin("secret");
int r = x ^ y;
VMProtectEnd();
return r;
}
हम यह पहचान कर शुरू करते हैं कि फ़ंक्शन VMProtect का उपयोग कहाँ कर रहे हैं और उनके पास कितने आर्गुमेंट हैं। हमारे उदाहरण के लिए हमारे पास नीचे कुछ ऐसा हो सकता है:
कोड पढ़कर ही हम जानते हैं कि फ़ंक्शन पते 0x4011c0 से शुरू होता है, इसमें दो 32-बिट आर्गुमेंट (edi और esi) हैं और 0x4011ef पर रिटर्न करता है। हमें बस इतना ही रिवर्स-इंजीनियरिंग चाहिए। अगले भाग स्वचालित होंगे। अब हमें इस वर्चुअलाइज़्ड फ़ंक्शन की एक ट्रेस एक्ज़ीक्यूशन उत्पन्न करनी होगी। ऐसा करने के लिए हम एक Pintool का उपयोग करते हैं। इसे केवल एक start और एक end पते की आवश्यकता है (हमारे उदाहरण के लिए, 0x4011c0 और 0x4011ef) जो इंस्ट्रूमेंटेशन की सीमा को दर्शाता है। ध्यान दें कि किसी भी प्रकार का DBI या एमुलेटर यह काम कर सकता है।```
$ ./pin/pin -t ./pin/source/tools/VMP_Trace/obj-intel64/VMP_Trace.so -start 4198848 -end 4198895 -- ./vmp_binaries/binaries/sample2.vmp.bin 1 2 &> ./vmp_traces/sample2.vmp.trace
आप परिणाम [यहाँ](https://github.com/jonathansalwan/vmprotect-devirtualization/blob/main/vmp_traces/sample2.vmp.trace) देख सकते हैं। ट्रेस प्रारूप तीन प्रकार के ऑपरेशन का उपयोग करता है: `mr`, `r` और `i`। `mr` निर्देश `i` द्वारा किया गया एक मेमोरी रीड एक्सेस है, और `r` CPU रजिस्टर हैं। उदाहरण के लिए:```
mr:0x7ffda459d718:8:0x227db4f8
r:0x40200a:0x0:0x7ffda459f571:0x2:0x40200a:0x0:0x0:0x7ffda459d688:0x0:0x0:0x7feee9b80ac0:0x7feee9b8000f:0xad1c3e:0x0:0x0:0x0
i:0x89173e:8:488BB42490000000
हमारे पास एक मेमोरी रीड है जो 0x7ffda459d718 पते से 8 बाइट्स का स्थिरांक 0x227db4f8 लोड करता है।
निर्देश 0x89173e पते पर निष्पादित होता है और इसका 8-बाइट लंबा ऑपकोड 488BB42490000000 है, जो mov rsi, qword ptr [rsp + 0x90] है।
निष्पादन से पहले रजिस्टर स्थिति निम्नलिखित है:```python
(1) RAX = 0x40200a (9) R8 = 0
(2) RBX = 0 (10) R9 = 0
(3) RCX = 0x7ffda459f571 (11) R10 = 0x7feee9b80ac0
(4) RDX = 0x2 (12) R11 = 0x7feee9b8000f
(5) RDI = 0x40200a (13) R12 = 0xad1c3e
(6) RSI = 0 (14) R13 = 0
(7) RBP = 0 (15) R14 = 0
(8) RSP = 0x7ffda459d688 (16) R15 = 0
एक बार VMP ट्रेस उत्पन्न हो जाने के बाद, हम इसे [attack_vmp.py](https://github.com/jonathansalwan/vmprotect-devirtualization/blob/main/attack_vmp.py) स्क्रिप्ट का उपयोग करके रीप्ले करते हैं। यह स्क्रिप्ट ट्रेस का पथ प्रेडिकेट बनाने के लिए [Triton](https://github.com/jonathansalwan/Triton) का उपयोग करती है। ध्यान दें कि सभी अभिव्यक्तियाँ जिनमें प्रतीकात्मक चर (फ़ंक्शन के इनपुट) शामिल हैं, उन्हें प्रतीकात्मक रखा जाता है जबकि सभी असंबंधित इनपुट अभिव्यक्तियाँ कंक्रीटीकृत की जाती हैं। दूसरे शब्दों में, हमारी प्रतीकात्मक अभिव्यक्तियों में वर्चुअल मशीन (मशीनरी स्वयं उपयोगकर्ता पर निर्भर नहीं करती) से संबंधित कोई संक्रिया नहीं होती, बल्कि केवल मूल प्रोग्राम से संबंधित संक्रियाएँ होती हैं।
उदाहरण के लिए, नीचे एक कंक्रीटीकरण का उदाहरण है। बाईं ओर हमारे पास एक AST है जिसमें उप-अभिव्यक्तियाँ हैं जिनमें प्रतीकात्मक चर शामिल नहीं है (`1 + 2` और `6 ^ 3`)। इसलिए इन शाखाओं को कंक्रीटीकृत किया जाता है और स्थिरांक `3` और `5` द्वारा प्रतिस्थापित किया जाता है, जो दाईं ओर AST की ओर ले जाता है। **इस प्रकार हम कोड को डीवर्चुअलाइज़ करते हैं।**
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/8096/857ed50cfe9cb2347f816ece1d8dc4c13174c971dcb65ebc5621be14269a5ca8.png">
</p>