
آلة افتراضية لأندرويد ومُزِيل التعتيم
يقوم Simplify بتنفيذ التطبيق افتراضيًا لفهم سلوكه، ثم يحاول تحسين الكود بحيث يتصرف بنفس الطريقة ولكنه يكون أسهل للفهم البشري. كل نوع من أنواع التحسين بسيط وعام، لذا لا يهم نوع التعتيم المستخدم.
الكود على اليسار هو فك ترجمة لتطبيق معتم، والكود على اليمين تم إلغاء تعقيده.
يتكون المشروع من ثلاثة أجزاء: smalivm، simplify، وتطبيق العرض التوضيحي.
if أو switch بقيمة غير معروفة يؤدي إلى اتخاذ كلا الفرعين.usage: java -jar simplify.jar <input> [options]
deobfuscates a dalvik executable
-et,--exclude-types <pattern> استبعاد الفئات والطرق التي تتضمن تعبيرًا منتظمًا (REGEX)، مثال: "com/android"، يتم تطبيقه بعد include-types
-h,--help عرض هذه الرسالة
-ie,--ignore-errors تجاهل الأخطاء أثناء تنفيذ وتحسين الدوال. قد يؤدي هذا إلى سلوك غير متوقع.
--include-support محاولة تنفيذ وتحسين الفئات في حزم مكتبة دعم أندرويد، الافتراضي: false
-it,--include-types <pattern> قصر التنفيذ على الفئات والطرق التي تتضمن تعبيرًا منتظمًا (REGEX)، مثال: ";->targetMethod\("
--max-address-visits <N> التخلي عن تنفيذ دالة بعد زيارة نفس العنوان N مرة، يحد من الحلقات، الافتراضي: 10000
--max-call-depth <N> عدم استدعاء دوال بعد الوصول إلى عمق استدعاء N، يحد من التكرار وسلاسل الدوال الطويلة، الافتراضي: 50
--max-execution-time <N> التخلي عن تنفيذ دالة بعد N ثانية، الافتراضي: 300
--max-method-visits <N> التخلي عن تنفيذ دالة بعد تنفيذ N تعليمة في تلك الدالة، الافتراضي: 1000000
--max-passes <N> عدم تشغيل المحسنات على دالة أكثر من N مرة، الافتراضي: 100
-o,--output <file> إخراج الإدخال المبسط إلى FILE
--output-api-level <LEVEL> تعيين توافق DEX API للمخرجات إلى LEVEL، الافتراضي: 15
-q,--quiet كن هادئًا
--remove-weak إزالة الكود حتى في حالة وجود آثار جانبية ضعيفة، الافتراضي: true
-v,--verbose <LEVEL> تعيين مستوى التفاصيل إلى LEVEL، الافتراضي: 0
يتطلب البناء تثبيت Java Development Kit 8 (JDK).
نظرًا لأن هذا المشروع يحتوي على وحدات فرعية لأطر عمل أندرويد، قم إما بالاستنساخ باستخدام --recursive:
git clone --recursive https://github.com/CalebFenton/simplify.git
أو قم بتحديث الوحدات الفرعية في أي وقت باستخدام:
git submodule update --init --recursive
ثم، لبناء jar واحد يحتوي على جميع التبعيات:
./gradlew fatjar
سيكون ملف Simplify jar في simplify/build/libs/. يمكنك اختبار عمله عن طريق تبسيط تطبيق المثال المعتم المقدم. إليك كيفية تشغيله (قد تحتاج إلى تغيير simplify.jar):
java -jar simplify/build/libs/simplify.jar -it "org/cf/obfuscated" -et "MainActivity" simplify/obfuscated-app.apk
لفهم ما يتم إلغاء تعقيده، اطلع على README تطبيق Obfuscated App.
إذا فشل Simplify، جرب هذه التوصيات بالترتيب:
-it.--max-address-visits و --max-call-depth و --max-method-visits.-v أو -v 2 وأبلغ عن المشكلة مع السجلات وقيمة هاش (hash) لـ DEX أو APK.إذا كنت تبني على Windows وفشل البناء بخطأ مشابه لـ:
Could not find tools.jar. Please check that C:\Program Files\Java\jre1.8.0_151 contains a valid JDK installation.
هذا يعني أن Gradle غير قادر على العثور على مسار JDK صحيح. تأكد من تثبيت JDK، واضبط متغير البيئة JAVA_HOME على مسار JDK الخاص بك، وتأكد من إغلاق وإعادة فتح موجه الأوامر الذي تستخدمه للبناء.
لا تكن خجولًا. أعتقد أن التنفيذ الافتراضي وإلغاء التعقيد مشكلتان رائعتان. أي شخص مهتم فهو رائع تلقائيًا، والمساهمات مرحب بها، حتى لو كانت مجرد تصحيح خطأ إملائي. لا تتردد في طرح الأسئلة في القضايا (issues) وتقديم طلبات السحب (pull requests).
يرجى تضمين رابط لـ APK أو DEX والأمر الكامل الذي تستخدمه. هذا يسهل كثيرًا إعادة إنتاج (وبالتالي إصلاح) مشكلتك.
إذا لم تتمكن من مشاركة العينة، فالرجاء تضمين هاش الملف (SHA1، SHA256، إلخ).
إذا وضعت عملية (op) قيمة من نوع يمكن تحويلها إلى ثابت مثل سلسلة نصية أو رقم أو قيمة منطقية، فسيستبدل هذا التحسين تلك العملية بالثابت. على سبيل المثال:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
# Decrypts to: "Tell me of your homeworld, Usul."
move-result v0
في هذا المثال، يتم فك تشفير سلسلة مشفرة ووضعها في v0. نظرًا لأن السلاسل النصية "قابلة للتحويل إلى ثوابت"، يمكن استبدال move-result v0 بـ const-string:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
const-string v0, "Tell me of your homeworld, Usul."
الكود ميت إذا كانت إزالته لا يمكن أن تغير سلوك التطبيق. الحالة الأكثر وضوحًا هي إذا كان الكود غير قابل للوصول، مثل if (false) { // dead }). إذا كان الكود قابلًا للوصول، فقد يُعتبر ميتًا إذا لم يؤثر على أي حالة خارج الدالة، أي ليس له تأثير جانبي. على سبيل المثال، قد لا يؤثر الكود على القيمة المُعادة للدالة، أو يغير أي متغيرات فئة، أو ينفذ أي إدخال/إخراج. هذا أمر صعب التحديد في التحليل الثابت. لحسن الحظ، لا يحتاج smalivm إلى أن يكون ذكيًا. فهو فقط ينفذ كل شيء بغباء ويفترض وجود آثار جانبية إذا لم يكن متأكدًا. ضع في اعتبارك المثال من نشر الثوابت:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
const-string v0, "Tell me of your homeworld, Usul."
في هذا الكود، لم يعد invoke-static يؤثر على القيمة المُعادة للدالة ولنفترض أنه لا يفعل أي شيء غريب مثل كتابة بايتات إلى نظام الملفات أو مأخذ شبكة، لذلك ليس له آثار جانبية. يمكن إزالته ببساطة.
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
const-string v0, "Tell me of your homeworld, Usul."
أخيرًا، يقوم أول const-string بتعيين قيمة لسجل، لكن هذه القيمة لا تُستخدم أبدًا، أي أن التعيين ميت. يمكن إزالته أيضًا.
const-string v0, "Tell me of your homeworld, Usul."
"
مرحبًا!
### إزالة الانعكاس
أحد التحديات الرئيسية في التحليل الثابت لجافا هو الانعكاس (reflection). من المستحيل معرفة الوسائط لدوال الانعكاس دون إجراء تحليل دقيق لتدفق البيانات. هناك طرق ذكية وبارعة للقيام بذلك، لكن smalivm يفعلها بمجرد تنفيذ الكود. عندما يجد استدعاء دالة منعكسة مثل:
```smali
invoke-virtual {v0, v1, v2}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
يمكنه معرفة قيم v0 و v1 و v2. إذا كان متأكدًا من القيم، يمكنه استبدال استدعاء Method.invoke() باستدعاء دالة فعلي غير منعكس. ينطبق الأمر نفسه على عمليات البحث عن الحقول والفئات المنعكسة.
لكل ما لا يندرج بشكل نظيف في فئة معينة، هناك تحسينات الثقب. يتضمن ذلك إزالة عمليات check-cast غير المفيدة، واستبدال استدعاءات Ljava/lang/String;-><init> بـ const-string، وهكذا.
.method public static test1()I
.locals 2
new-instance v0, Ljava/lang/Integer;
const/4 v1, 0x1
invoke-direct {v0, v1}, Ljava/lang/Integer;-><init>(I)V
invoke-virtual {v0}, Ljava/lang/Integer;->intValue()I
move-result v0
return v0
.end method
كل ما يفعله هذا هو v0 = 1.
.method public static test1()I
.locals 2
new-instance v0, Ljava/lang/Integer;
const/4 v1, 0x1
invoke-direct {v0, v1}, Ljava/lang/Integer;-><init>(I)V
invoke-virtual {v0}, Ljava/lang/Integer;->intValue()I
const/4 v0, 0x1
return v0
.end method
تم استبدال move-result v0 بـ const/4 v0, 0x1. هذا لأنه لا يوجد سوى قيمة إرجاع واحدة محتملة لـ intValue()I ونوع الإرجاع يمكن جعله ثابتًا. الوسائط v0 و v1 لا لبس فيها ولا تتغير. بمعنى آخر، هناك إجماع على القيم لكل مسار تنفيذ ممكن عند intValue()I. أنواع أخرى من القيم التي يمكن تحويلها إلى ثوابت:
const/4، const/16، إلخ.const-stringconst-class.method public static test1()I
.locals 2
const/4 v0, 0x1
return v0
.end method
نظرًا لأن الكود أعلاه const/4 v0, 0x1 لا يؤثر على الحالة خارج الدالة (لا آثار جانبية)، يمكن إزالته دون تغيير السلوك. إذا كان هناك استدعاء دالة يكتب شيئًا إلى نظام الملفات أو الشبكة، فلا يمكن إزالته لأنه يؤثر على الحالة خارج الدالة. أو إذا كانت test()I تأخذ وسيطة قابلة للتغيير، مثل LinkedList، فلا يمكن اعتبار أي تعليمات تصل إليها ميتة.
أمثلة أخرى على الكود الميت:
if (false) { dead_code(); }هذه الأداة متاحة بموجب ترخيص مزدوج: ترخيص تجاري مناسب للمشاريع مغلقة المصدر وترخيص GPL يمكن استخدامه في البرامج مفتوحة المصدر.
اعتمادًا على احتياجاتك، يجب عليك اختيار أحدهما واتباع سياساته. تفاصيل السياسات والاتفاقيات لكل نوع ترخيص متاحة في ملفي LICENSE.COMMERCIAL و LICENSE.GPL.