
اللعب مع حماية برنامج VMProtect. إزالة التعتيم التلقائي للدوال النقية باستخدام التنفيذ الرمزي و LLVM.
نهج ديناميكي تجريبي لإلغاء الظاهرية عن الوظائف البحتة المحمية بـ VMProtect 3.x
أشارك بعض الملاحظات حول نهج ديناميكي لإلغاء الظاهرية عن الوظائف البحتة المحمية بواسطة VMProtect. أظهر هذا النهج نتائج جيدة جدًا إذا كانت الوظيفة الظاهرية تحتوي على كتلة أساسية واحدة فقط (بغض النظر عن حجمها). هذا سيناريو شائع عندما تحمي الملفات الثنائية العمليات الحسابية. ومع ذلك، هذا النهج أكثر تجريبية بعض الشيء عندما تحتوي الوظيفة المستهدفة على أكثر من كتلة أساسية واحدة. مع ذلك، تمكنا من إلغاء الظاهرية وإعادة بناء الكود الثنائي من عينات تحتوي على كتلتين أساسيتين مما يشير إلى أنه من الممكن إلغاء الظاهرية بالكامل للوظائف الصغيرة ديناميكيًا.
VMProtect هي أداة حماية برمجيات تحمي الكود عن طريق تشغيله عبر آلة افتراضية ذات بنية غير قياسية. هذه الحماية هي ساحة لعب رائعة لعشاق لغة التجميع [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)، فسنتمكن عندئذٍ من إعادة بناء مسار واحد من
البرنامج الأصلي P من أثر T'. وبتكرار هذه العملية لتغطية جميع مسارات البرنامج
الافتراضي، سنكون قادرين على إعادة بناء البرنامج الأصلي 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` هي سجلات وحدة المعالجة المركزية. على سبيل المثال:```
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
لدينا قراءة ذاكرة تقوم بتحميل ثابت 8 بايت 0x227db4f8 من العنوان 0x7ffda459d718.
يتم تنفيذ التعليمات على العنوان 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>
**ملاحظة حول القطع العكسي على مستوى الصيغة**: كما هو شائع في التنفيذ الرمزي، يتم أولاً حساب التمثيل الرمزي بطريقة تقدمية على طول المسار، ثم تتم إزالة جميع العمليات المنطقية والتعريفات التي لا تؤثر على النتيجة النهائية ولا على المسار المتبع من التعبير الرمزي (قطع الصيغة، المعروف أيضًا بتقليم الصيغة). يتبين أن هذا يؤدي على الصيغة ما يعادل تحليل قطع عكسي للكود من مخرجات البرنامج. وبالتالي، عند عودة دالة `secret`، يكون لدينا تعبير للعلاقة بين المدخلات والمخرجات بدون تعليمات VMProtect.
يأخذ السكريبت `./attack_vmp.py` كمعاملات ملف الأثر وحجم المتغيرات الرمزية. تذكر، كانت `edi` و `esi`، لذا فإن طولهما 4 بايت. نتيجة السكريبت هي التالية:```
$ ./attack_vmp.py --trace1 ./vmp_traces/sample2.vmp.trace --symsize 4
[+] Replaying the VMP trace
[+] Symbolize inputs
[+] Instruction executed: 12462
[+] Emulation done
[+] Return value: 0x3
[+] Devirt expr: (bvor (bvnot (bvor (bvnot (bvnot x)) (bvnot y))) (bvnot (bvor (bvnot x) (bvnot (bvand (bvnot y) (bvnot y))))))
[+] Synth expr: (bvxor x y)
[+] LLVM IR ==============================
; ModuleID = 'tritonModule'
source_filename = "tritonModule"
define i32 @__triton(i32 %SymVar_0, i32 %SymVar_1) {
entry:
%0 = xor i32 %SymVar_0, %SymVar_1
ret i32 %0
}