
إزالة التعتيم عبر التحسين باستخدام LLVM IR وتحليل لغة التجميع.
Mergen هي أداة مصممة لتحويل كود التجميع (Assembly) إلى التمثيل الوسيط لـ LLVM (IR). هذه الأداة مصممة من أجل:
لبناء المشروع وتشغيله، ألقِ نظرة على docs/BUILDING.md.
يجب أن يُبقي عمل إعادة الكتابة بوابة انحدار خط الأساس خضراء. تقوم البوابة ببناء عينات PE مركزة، وتشغيل lifter، والتحقق من مخرجات IR المرفوعة.
scripts\\rewrite\\run.cmdنقوم بالتنفيذ الرمزي (أو الرفع الرمزي) للهدف، والفكرة هنا ليست رفع التعليمات الفردية، بل رفع دالة كاملة. لا نتوقع أن تتصرف تعليمة واحدة أو كتلة أساسية واحدة بنفس الطريقة في كل مرة، بل نتعامل معها كما لو أنها يمكن أن تكون ولأغراض مختلفة في كل مرة. نحاول إبقاء الـ IR المُولَّد بسيطًا وقابلًا للتحسين قدر الإمكان. لدينا أيضًا احتياجات مختلفة عن المترجم المعتاد. نستخدم التحليل لتقييم تدفق التحكم. لا يمكننا الاعتماد على LLVM في كل تحليلاتنا، لأنها أُنشئت لأهداف مختلفة وقد تكون غير مثالية لحالة استخدامنا.

هذا هو المثال العملي لتوضيح كيفية تعامل Mergen مع البرامج المُخْتَلَقة (المُعتمد على الأجهزة الافتراضية).
هذا هو برنامجنا الهدف
struct test {
int a;
int b;
int c;
};
int maths(test a, int b, int c) {
return a.a + b - c;
}


إعدادات VMProtect، كل شيء مغلق، نقوم باختزال (virtualize) الدالة على الإعداد الفائق. (الإصدارات المختبرة 3.4.0-3.6.0 3.8.1)


هنا، نشغّل mergen. الوسيطة الأولى هي اسم الملف والوسيطة الثانية هي عنوان الدالة. انظر كم هو بسيط التشغيل. ويمكننا تجميع المخرجات لاستكشافها باستخدام مفككنا المفضل.

; ModuleID = 'my_lifting_module'
source_filename = "my_lifting_module"
; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: read)
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %0, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr nocapture readonly %memory) local_unnamed_addr #0 {
entry:
%stackmemory = alloca i128, i128 13758960, align 8
%1 = trunc i64 %r8 to i32
%2 = trunc i64 %rdx to i32
%GEPLoadxd-5369456437- = getelementptr i8, ptr %memory, i64 %rcx
%3 = load i32, ptr %GEPLoadxd-5369456437-, align 4
%adc-temp-5370242400- = sub i32 %2, %1
%realnot-5369532059- = add i32 %adc-temp-5370242400-, %3
%stackmemory10243.sroa.55.1375304.insert.ext10255 = zext i32 %realnot-5369532059- to i64
ret i64 %stackmemory10243.sroa.55.1375304.insert.ext10255
}
attributes #0 = { mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: read) }
بعد التجميع:


الآن قد تلاحظ أن السجلات غير مضبوطة قليلاً. هذا لأننا لا نتبع اصطلاحات الاستدعاء، فلو اتبعنا اصطلاحات الاستدعاء، لكان توقيع الدالة بهذا الشكل:
define i64 @main(i64 %rcx, i64 %rdx, i64 %rdx, i64 %r8, i64 %r9 ...)
لذلك، نقوم فقط بتعديل توقيع الدالة ليبدو طبيعيًا. إذا كانت لديك المزيد من الأسئلة حول هذا الجزء، أقترح عليك البحث في اصطلاحات الاستدعاء وABI.
لنفترض أن لدينا هذا الكود. ستأخذ الأجهزة الافتراضية (VM's) الكود التالي وتحوّله إلى قفزة غير مباشرة، وهذا غير مريح بعض الشيء للمهندس العكسي.
int maths(int a, int b, int c) {
if (a > b)
return a + b + c;
else
return a - b - c;
}
next_handler = xxx;
if ( a-b > 0 )
next_handler = yyy;
jump next_handler;
نحاول دائمًا تحليل القيم وتتبعها. وهذا يتيح لنا فهم تدفق التحكم. للفروع الشبيهة بجدول القفز
سيكون الناتج المُحسَّن بسيطًا:
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %rsp, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr nocapture readnone %TEB, ptr nocapture readnone %memory) local_unnamed_addr #0 {
fake_ret:
%0 = lshr i64 %rcx, 62
%common.ret.op = and i64 %0, 2
ret i64 %common.ret.op
}
الناتج غير المُحسَّن. (تمت إزالة الكود الميت (DCE) من أجل سهولة القراءة)
source_filename = "my_lifting_module"
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %rsp, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr %TEB, ptr %memory) {
%lsb = and i64 %rcx, 255
%pf1 = mul i64 %lsb, 72340172838076673
%pf2 = and i64 %pf1, -9205322385119247871
%pf3 = urem i64 %pf2, 511
%pf4 = and i64 %pf3, 1
%pf5 = icmp eq i64 0, %pf4
%0 = zext i1 %pf5 to i64
%createrflag2 = shl i64 %0, 2
%creatingrflag = or i64 2, %createrflag2
%zeroflag = icmp eq i64 %rcx, 0
%1 = zext i1 %zeroflag to i64
%createrflag21 = shl i64 %1, 6
%creatingrflag2 = or i64 %creatingrflag, %createrflag21
%signflag = icmp slt i64 %rcx, 0
%2 = zext i1 %signflag to i64
%createrflag23 = shl i64 %2, 7
%creatingrflag4 = or i64 %creatingrflag2, %createrflag23
%GEPSTORE-5368713221- = getelementptr i8, ptr %memory, i64 1376032
store i64 %creatingrflag4, ptr %GEPSTORE-5368713221-, align 4
%realand-5368713229- = and i64 %creatingrflag4, 128
%shr-lshr-5368713233- = lshr i64 %realand-5368713229-, 7
%3 = mul i64 %shr-lshr-5368713233-, 4
%bvalue_indexvalue = add i64 5368713249, %3
%4 = icmp eq i64 %bvalue_indexvalue, 5368713253
%lolb- = select i1 %4, i64 5368713264, i64 5368713257
%GEPSTORE-5368713248- = getelementptr i8, ptr %memory, i64 1376032
store i64 %lolb-, ptr %GEPSTORE-5368713248-, align 4
br i1 %4, label %real_ret, label %real_ret41
real_ret: ; preds = %fake_ret
%inc-5368713273- = add i64 %shr-lshr-5368713233-, 1
ret i64 %inc-5368713273-
real_ret41: ; preds = %fake_ret
ret i64 %shr-lshr-5368713233-
}
لاحظ هذا الجزء:
%realand-5368713229- = and i64 %creatingrflag4, 128
%shr-lshr-5368713233- = lshr i64 %realand-5368713229-, 7
نحصل على الأعلام، ثم نحصل على البتة السابعة وهي علامة الإشارة (Sign Flag)، ثم نستخدم علامة الإشارة لحساب عنوان. من خلال التحليل، نحدد أن العنوان يمكن أن يكون واحدًا من قيمتين، 5368713257 أو 5368713264، ثم نحول ذلك إلى مقارنة. إذا كان العنوان 5368713257، نسلك فرعًا، وإذا كان الآخر، نسلك فرعًا آخر. عند القيام بذلك، من المهم أيضًا تعليم الشرط بالقيمة المناسبة لأنه لاحقًا قد نحتاج إلى حساب قفزة أخرى بنفس القيمة تمامًا.
على الرغم من أننا نحل القفزات غير المباشرة، إلا أن القفزات التي تحتوي على أكثر من موقعين محتملين غير مدعومة. وذلك لأن التحليل الخاص بها غير منفَّذ بعد. هذا يتيح لنا حل الفروع بنمط VM، لكننا نواجه مشكلة مع جداول القفز الواقعية.
برنامجنا الهدف:

إعدادات Themida (نهتم فقط بالأجهزة الافتراضية حاليًا):



بعد الاختزال (vm):

تشغيل Mergen:

الكود الناتج: اضغط هنا
إذًا، لماذا ليست نتيجتنا ناجحة بقدر رفع ثنائي محمي بواسطة vmp؟
يكتب Themida بنشاط في قسم .themida. وعلى عكس المكدس، لا يمكننا تجاهل هذه الكتابات، لأن هذه القيم قد تُقرأ بواسطة أشياء أخرى لاحقًا.
لكن لدينا حل مؤقت لذلك. قم بإزالة جميع عمليات التخزين في قسم .themida. وبما أن برنامجنا لا يكتب في الذاكرة، لقد قمت فقط بتعليق كل عمليات التخزين. الآن تبقى لدينا هذا:
source_filename = "my_lifting_module"
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %rsp, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr writeonly %memory) local_unnamed_addr #0 {
%trunc = trunc i64 %r8 to i32
%trunc1 = trunc i64 %rdx to i32
%trunc2 = trunc i64 %rcx to i32
%realadd-5369771371- = add i32 %trunc1, %trunc2
%realadd-5369582686- = add i32 %realadd-5369771371-, %trunc
%trunc457139 = zext i32 %realadd-5369582686- to i64
ret i64 %trunc457139
}
attributes #0 = { mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: write) }
انضم إلى خادم Discord الخاص بـ Mergen لتبادل الأفكار أو مجرد الدردشة بشكل عام.