عند البحث عن "Unicorn Engine" على Habr ، فوجئت عندما اكتشفت أن هذه الأداة لم تظهر قط في المقالات. سأحاول ملء هذا الفراغ. لنبدأ بالأساسيات ، ونلقي نظرة على مثال لاستخدام المحاكي في الحياة الواقعية. لكي لا أعيد اختراع العجلة ، قررت ببساطة ترجمة هذا الدليل. قبل البدء ، سأقول أن كل تعليقاتي أو تعليقاتي ستبدو هكذا .
ما هو محرك يونيكورن؟
يكتب المطورون أنفسهم عن محرك يونيكورن محرك يونيكورن مثل هذا:
Unicorn هو محاكي معالج خفيف الوزن ومتعدد المنصات وبنية متعددة.
هذا ليس محاكيًا قياسيًا. لا يحاكي تشغيل البرنامج بأكمله أو نظام التشغيل بأكمله. لا يدعم أوامر النظام (مثل فتح ملف ، إخراج حرف إلى وحدة التحكم ، إلخ). سيكون عليك عمل ترميز الذاكرة وتحميل البيانات فيها بنفسك ، ثم تبدأ ببساطة التنفيذ من عنوان معين.
فكيف هي مفيدة؟
- عند تحليل الفيروسات ، يمكنك استدعاء وظائف مفردة دون إنشاء عملية ضارة.
- لحل CTF.
- للتشويش .
- مكوّن إضافي لـ gdb للتنبؤ بالحالة المستقبلية ، على سبيل المثال ، القفزات المستقبلية أو قيم التسجيل.
- مضاهاة رمز غني بالميزات.
ماذا تحتاج؟
- محرك Unicorn مثبت بربط Python.
- تفكيك
مثال
كمثال ، خذ مهمة مع hxp CTF 2017 تحت اسم فيبوناتشي . يمكن تنزيل الثنائي هنا .
عند بدء تشغيل البرنامج ، يبدأ في عرض علمنا في وحدة التحكم ، ولكن ببطء شديد. يعتبر كل بايت علم لاحق أبطأ وأبطأ.
The flag is: hxp{F
هذا يعني أنه من أجل الحصول على العلم في فترة زمنية معقولة ، نحتاج إلى تحسين تشغيل هذا التطبيق.
باستخدام IDA Pro ( أنا شخصياً استخدمت radare2 + Cutter ) قمنا بتجميع الشفرة في كود شبه كودي. على الرغم من حقيقة أنه لم يتم فك الشفرة بشكل صحيح ، لا يزال بإمكاننا الحصول على معلومات منها حول ما يحدث في الداخل.
كود فك الشفرة __int64 __fastcall main(__int64 a1, char **a2, char **a3) { void *v3;
unsigned int __fastcall fibonacci(int i, _DWORD *a2) { _DWORD *v2;
هنا هو رمز المجمع للوظائف الرئيسية ووظائف فيبوناتشي :
الرئيسية .text:0x4004E0 main proc near ; DATA XREF: start+1Do .text:0x4004E0 .text:0x4004E0 var_1C = dword ptr -1Ch .text:0x4004E0 .text:0x4004E0 push rbp .text:0x4004E1 push rbx .text:0x4004E2 xor esi, esi ; buf .text:0x4004E4 mov ebp, offset unk_4007E1 .text:0x4004E9 xor ebx, ebx .text:0x4004EB sub rsp, 18h .text:0x4004EF mov rdi, cs:stdout ; stream .text:0x4004F6 call _setbuf .text:0x4004FB mov edi, offset format ; "The flag is: " .text:0x400500 xor eax, eax .text:0x400502 call _printf .text:0x400507 mov r9d, 49h .text:0x40050D nop dword ptr [rax] .text:0x400510 .text:0x400510 loc_400510: ; CODE XREF: main+8Aj .text:0x400510 xor r8d, r8d .text:0x400513 jmp short loc_40051B .text:0x400513 ; --------------------------------------------------------------------------- .text:0x400515 align 8 .text:0x400518 .text:0x400518 loc_400518: ; CODE XREF: main+67j .text:0x400518 mov r9d, edi .text:0x40051B .text:0x40051B loc_40051B: ; CODE XREF: main+33j .text:0x40051B lea edi, [rbx+r8] .text:0x40051F lea rsi, [rsp+28h+var_1C] .text:0x400524 mov [rsp+28h+var_1C], 0 .text:0x40052C call fibonacci .text:0x400531 mov edi, [rsp+28h+var_1C] .text:0x400535 mov ecx, r8d .text:0x400538 add r8, 1 .text:0x40053C shl edi, cl .text:0x40053E mov eax, edi .text:0x400540 xor edi, r9d .text:0x400543 cmp r8, 8 .text:0x400547 jnz short loc_400518 .text:0x400549 add ebx, 8 .text:0x40054C cmp al, r9b .text:0x40054F mov rsi, cs:stdout ; fp .text:0x400556 jz short loc_400570 .text:0x400558 movsx edi, dil ; c .text:0x40055C add rbp, 1 .text:0x400560 call __IO_putc .text:0x400565 movzx r9d, byte ptr [rbp-1] .text:0x40056A jmp short loc_400510 .text:0x40056A ; --------------------------------------------------------------------------- .text:0x40056C align 10h .text:0x400570 .text:0x400570 loc_400570: ; CODE XREF: main+76j .text:0x400570 mov edi, 0Ah ; c .text:0x400575 call __IO_putc .text:0x40057A add rsp, 18h .text:0x40057E xor eax, eax .text:0x400580 pop rbx .text:0x400581 pop rbp .text:0x400582 retn .text:0x400582 main endp
فيبوناتشي .text:0x400670 fibonacci proc near ; CODE XREF: main+4Cp .text:0x400670 ; fibonacci+19p ... .text:0x400670 test edi, edi .text:0x400672 push r12 .text:0x400674 push rbp .text:0x400675 mov rbp, rsi .text:0x400678 push rbx .text:0x400679 jz short loc_4006F8 .text:0x40067B cmp edi, 1 .text:0x40067E mov ebx, edi .text:0x400680 jz loc_400710 .text:0x400686 lea edi, [rdi-2] .text:0x400689 call fibonacci .text:0x40068E lea edi, [rbx-1] .text:0x400691 mov r12d, eax .text:0x400694 mov rsi, rbp .text:0x400697 call fibonacci .text:0x40069C add eax, r12d .text:0x40069F mov edx, eax .text:0x4006A1 mov ebx, eax .text:0x4006A3 shr edx, 1 .text:0x4006A5 and edx, 55555555h .text:0x4006AB sub ebx, edx .text:0x4006AD mov ecx, ebx .text:0x4006AF mov edx, ebx .text:0x4006B1 shr ecx, 2 .text:0x4006B4 and ecx, 33333333h .text:0x4006BA mov esi, ecx .text:0x4006BC .text:0x4006BC loc_4006BC: ; CODE XREF: fibonacci+C2j .text:0x4006BC and edx, 33333333h .text:0x4006C2 lea ecx, [rsi+rdx] .text:0x4006C5 mov edx, ecx .text:0x4006C7 shr edx, 4 .text:0x4006CA add edx, ecx .text:0x4006CC mov esi, edx .text:0x4006CE and edx, 0F0F0F0Fh .text:0x4006D4 shr esi, 8 .text:0x4006D7 and esi, 0F0F0Fh .text:0x4006DD lea ecx, [rsi+rdx] .text:0x4006E0 mov edx, ecx .text:0x4006E2 shr edx, 10h .text:0x4006E5 add edx, ecx .text:0x4006E7 and edx, 1 .text:0x4006EA xor [rbp+0], edx .text:0x4006ED pop rbx .text:0x4006EE pop rbp .text:0x4006EF pop r12 .text:0x4006F1 retn .text:0x4006F1 ; --------------------------------------------------------------------------- .text:0x4006F2 align 8 .text:0x4006F8 .text:0x4006F8 loc_4006F8: ; CODE XREF: fibonacci+9j .text:0x4006F8 mov edx, 1 .text:0x4006FD xor [rbp+0], edx .text:0x400700 mov eax, 1 .text:0x400705 pop rbx .text:0x400706 pop rbp .text:0x400707 pop r12 .text:0x400709 retn .text:0x400709 ; --------------------------------------------------------------------------- .text:0x40070A align 10h .text:0x400710 .text:0x400710 loc_400710: ; CODE XREF: fibonacci+10j .text:0x400710 xor edi, edi .text:0x400712 call fibonacci .text:0x400717 mov edx, eax .text:0x400719 mov edi, eax .text:0x40071B shr edx, 1 .text:0x40071D and edx, 55555555h .text:0x400723 sub edi, edx .text:0x400725 mov esi, edi .text:0x400727 mov edx, edi .text:0x400729 shr esi, 2 .text:0x40072C and esi, 33333333h .text:0x400732 jmp short loc_4006BC .text:0x400732 fibonacci endp
في هذه المرحلة ، لدينا العديد من الفرص لحل هذه المشكلة. على سبيل المثال ، يمكننا استعادة الشفرة باستخدام إحدى لغات البرمجة وتطبيق التحسين هناك ، ولكن عملية استعادة الشفرة هي مهمة صعبة للغاية ، يمكننا خلالها ارتكاب الأخطاء. حسنًا ، مقارنة الرمز للعثور على الخطأ لا قيمة له بشكل عام. ولكن ، إذا استخدمنا محرك يونيكورن ، فيمكننا تخطي مرحلة إعادة بناء الكود وتجنب المشكلة الموضحة أعلاه. بالطبع ، يمكننا تجنب هذه المشاكل باستخدام فريدا أو كتابة نصوص لـ gdb ، لكن هذا ليس حول ذلك.
قبل بدء التحسين ، سنقوم بتشغيل المضاهاة في محرك Unicorn دون تغيير البرنامج. وفقط بعد الإطلاق الناجح ، دعنا ننتقل إلى التحسين.
الخطوة 1: دعوا المحاكاة الافتراضية تأتي
لنقم بإنشاء ملف fibonacci.py وحفظه بجوار الثنائي.
لنبدأ باستيراد المكتبات المطلوبة:
from unicorn import * from unicorn.x86_const import * import struct
يقوم السطر الأول بتحميل ثوابت يونيكورن الثنائية الأساسية. يقوم السطر الثاني بتحميل الثوابت لبنيتي x86 و x86_64.
بعد ذلك ، أضف بعض الوظائف الضرورية:
def read(name): with open(name) as f: return f.read() def u32(data): return struct.unpack("I", data)[0] def p32(num): return struct.pack("I", num)
أعلنّا هنا عن الوظائف التي سنحتاجها لاحقًا:
- تقرأ ببساطة إرجاع محتويات الملف ،
- يأخذ u32 سلسلة من 4 بايت في تشفير LE ويتحول إلى int ،
- يفعل p32 العكس - يأخذ رقمًا ويحوله إلى سلسلة من 4 بايت في تشفير LE.
ملاحظة: إذا قمت بتثبيت pwntools ، فأنت لست بحاجة إلى إنشاء هذه الوظائف ، ما عليك سوى استيرادها:
from pwn import *
وأخيرًا ، فلنبدأ في تهيئة فئة Unicorn Engine لفن العمارة x86_64:
mu = Uc (UC_ARCH_X86, UC_MODE_64)
هنا نسمي دالات Uc بالمعلمات التالية:
- المعلمة الأولى هي العمارة الرئيسية. تبدأ الثوابت بـ UC_ARCH_ ؛
- المعلمة الثانية هي مواصفات العمارة. تبدأ الثوابت بـ UC_MODE_ .
يمكنك العثور على جميع الثوابت في ورقة الغش .
كما كتبت أعلاه ، لاستخدام محرك Unicorn ، نحتاج إلى تهيئة الذاكرة الافتراضية يدويًا. في هذا المثال ، نحتاج إلى وضع الشفرة والمكدس في مكان ما في الذاكرة.
يبدأ العنوان الأساسي (addr الأساسي) للثنائي في 0x400000. دعونا نضع المكدس عند 0x0 ونخصص ذاكرة 1024 * 1024 لذلك. على الأرجح ، لا نحتاج إلى مساحة كبيرة ، لكنها لا تزال غير مؤلمة.
يمكننا ترميز الذاكرة عن طريق استدعاء طريقة mem_map .
أضف هذه الأسطر:
BASE = 0x400000 STACK_ADDR = 0x0 STACK_SIZE = 1024*1024 mu.mem_map(BASE, 1024*1024) mu.mem_map(STACK_ADDR, STACK_SIZE)
نحتاج الآن إلى تحميل الملف الثنائي في عنوانه الرئيسي بنفس الطريقة التي يقوم بها محمل الإقلاع. بعد ذلك ، نحتاج إلى تعيين RSP على نهاية المكدس.
mu.mem_write(BASE, read("./fibonacci")) mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 1)
يمكننا الآن بدء المضاهاة وتشغيل الكود ، لكننا بحاجة لمعرفة العنوان الذي نبدأ العمل به ومتى يجب أن يتوقف المحاكي.
خذ عنوان الأمر الأول من main () ، يمكننا بدء مضاهاة من 0x004004e0. سيتم اعتبار النهاية دعوة إلى putc ("\ n") ، والتي تقع في 0x00400575 ، بعد عرض العلم بالكامل.
.text:0x400570 mov edi, 0Ah ; c .text:0x400575 call __IO_putc
يمكننا البدء في محاكاة:
mu.emu_start(0x004004e0,0x00400575)
الآن قم بتشغيل البرنامج النصي:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py Traceback (most recent call last): File "solve.py", line 32, in <module> mu.emu_start(0x00000000004004E0, 0x0000000000400575) File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start raise UcError(status) unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
عفوًا ، حدث خطأ ما ، لكننا لا نعرف حتى ماذا. قبل الاتصال بـ mu.emu_start ، يمكننا إضافة:
def hook_code(mu, address, size, user_data): print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) mu.hook_add(UC_HOOK_CODE, hook_code)
يضيف هذا الرمز ربط. نعلن عن وظيفة hook_code الخاصة بنا ، والتي يتم استدعاؤها بواسطة المحاكي قبل كل أمر. يستغرق المعلمات التالية:
- نسختنا من Uc ،
- عنوان التعليمات
- تعليمات الحجم
- بيانات المستخدم (يمكننا تمرير هذه القيمة بحجة اختيارية إلى hook_add () ).
الآن ، إذا قمنا بتشغيل البرنامج النصي ، يجب أن نرى الإخراج التالي:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py >>> Tracing instruction at 0x4004e0, instruction size = 0x1 >>> Tracing instruction at 0x4004e1, instruction size = 0x1 >>> Tracing instruction at 0x4004e2, instruction size = 0x2 >>> Tracing instruction at 0x4004e4, instruction size = 0x5 >>> Tracing instruction at 0x4004e9, instruction size = 0x2 >>> Tracing instruction at 0x4004eb, instruction size = 0x4 >>> Tracing instruction at 0x4004ef, instruction size = 0x7 Traceback (most recent call last): File "solve.py", line 41, in <module> mu.emu_start(0x00000000004004E0, 0x0000000000400575) File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start raise UcError(status) unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
في العنوان الذي حدث فيه الخطأ ، يمكننا أن نفهم أنه لا يمكن للبرنامج النصي معالجة هذا الأمر:
.text:0x4004EF mov rdi, cs:stdout ; stream
تقرأ هذه التعليمات البيانات من العنوان 0x601038 (يمكنك رؤيتها في IDA Pro). هذا هو قسم .bss الذي لم نرمز إليه. سيكون الحل هو ببساطة تخطي جميع التعليمات الإشكالية إذا لم يؤثر ذلك على منطق البرنامج.
فيما يلي تعليمات إشكالية أخرى:
.text:0x4004F6 call _setbuf
لا يمكننا استدعاء أي وظائف باستخدام glibc ، حيث لا يوجد تحميل glibc في الذاكرة. على أي حال ، نحن لسنا بحاجة إلى هذا الأمر ، حتى نتمكن من تخطيه أيضًا.
إليك القائمة الكاملة للأوامر التي يجب تخطيها:
.text:0x4004EF mov rdi, cs:stdout ; stream .text:0x4004F6 call _setbuf .text:0x400502 call _printf .text:0x40054F mov rsi, cs:stdout ; fp
لتخطي الأوامر ، نحتاج إلى إعادة كتابة RIP مع التعليمات التالية:
mu.reg_write(UC_X86_REG_RIP, address+size)
الآن يجب أن يبدو hook_code مثل هذا:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data): print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) if address in instructions_skip_list: mu.reg_write(UC_X86_REG_RIP, address+size)
نحتاج أيضًا إلى القيام بشيء ما باستخدام الإرشادات التي تعرض العلم في وحدة تحكم البايت.
.text:0x400558 movsx edi, dil ; c .text:0x40055C add rbp, 1 .text:0x400560 call __IO_putc
يأخذ __IO_putc وحدات البايت للإخراج كوسيطة أولى (هذا هو سجل RDI ).
يمكننا قراءة البيانات مباشرة من السجل ، وإخراج البيانات إلى وحدة التحكم وتخطي هذه المجموعة من التعليمات. ويرد رمز_الخطأ المحدث أدناه:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data):
يمكننا أن نركض وسيعمل كل شيء ، ولكن ببطء.
الخطوة 2: زيادة السرعة!
دعونا نفكر في زيادة سرعة العمل. لماذا هذا البرنامج بطيء جدا؟
إذا نظرنا إلى الشفرة المترجمة ، فسوف نرى أن المكالمات الرئيسية () fibonacci () عدة مرات و fibonacci () هي دالة عودية. دعونا نلقي نظرة فاحصة على هذه الوظيفة ؛ فهي تأخذ وسيطتين وتعيدهما. يتم تمرير قيمة الإرجاع الأولى من خلال سجل RAX ، ويتم إرجاع الثانية من خلال الارتباط الذي تم تمريره من خلال الوسيطة الثانية للدالة. إذا نظرنا بشكل أعمق إلى العلاقة بين main () و fibonacci () ، فسوف نرى أن الوسيطة الثانية تأخذ فقط قيمتين محتملتين: 0 أو 1. إذا كنت لا تزال لا ترى هذا ، فقم بتشغيل gdb ووضع نقطة توقف في بداية الدالة فيبوناتشي () .
لتحسين تشغيل الخوارزمية ، يمكننا استخدام البرمجة الديناميكية لتذكر القيمة المرجعة للمعلمات الواردة. فكر بنفسك ، يمكن أن تأخذ الحجة الثانية قيمتين ممكنتين فقط ، لذلك كل ما علينا فعله هو التذكر $ inline $ 2 * MAX \ _OF \ _FIRST \ _ARGUMENT $ مضمنة $ بخار
بالنسبة لأولئك الذين لا يفهمونفيبوناتشي هي دالة تكرارية تحسب القيمة التالية كمجموع للقيمتين السابقتين. في كل خطوة تعمقها. في كل مرة تبدأ من جديد ، تسير بنفس الطريقة كما كانت من قبل ، بالإضافة إلى معنى جديد.
مثال:
لنفترض عمق = 6 ، ثم: 1 1 2 3 5 8 .
والآن عمق = 8 ، ثم: 1 1 2 3 5 8 13 21.
يمكننا فقط أن نتذكر أن الأعضاء الستة الأوائل هم 1 1 2 3 5 8 ، وعندما يطلبون منا أن نحسب أكثر مما نتذكر ، فإننا نأخذ ما نتذكره ونحسب فقط ما هو مفقود.
بمجرد أن يكون RIP في بداية فيبوناتشي () ، يمكننا الحصول على وسيطات الدالة. نحن نعلم أن الدالة ترجع نتيجة عندما تخرج من دالة. نظرًا لأنه لا يمكننا العمل مع معلمتين في وقت واحد ، فإننا بحاجة إلى تكديس لإرجاع المعلمات. عندما ندخل فيبوناتشي () ، نحتاج إلى وضع الحجج على المكدس ، ونلتقطها عند الخروج. لتخزين الأزواج المحسوبة ، يمكننا استخدام القاموس.
كيفية معالجة زوج من القيم؟
- في بداية الوظيفة ، يمكننا التحقق مما إذا كان هذا الزوج في النتائج التي نعرفها بالفعل:
- إذا كان هناك ، فيمكننا إرجاع هذا الزوج. نحتاج فقط إلى كتابة قيم الإرجاع في RAX وعلى عنوان الارتباط ، الموجود في الوسيطة الثانية. نقوم أيضًا بتعيين عنوان RIP للخروج من الوظيفة. لا يمكننا استخدام RET في فيبوناتشي () ، نظرًا لأن هذه المكالمات موصولة ، لذا سنأخذ بعض RET من main () ؛
- إذا لم تكن هذه القيم ، فإننا ببساطة نضيفها إلى المكدس.
- قبل الخروج من الوظيفة ، يمكننا حفظ الزوج الذي تم إرجاعه. نحن نعرف الحجج المدخلات ، حيث يمكننا قراءتها من مجموعتنا.
يتم تقديم هذا الرمز هنا. FIBONACCI_ENTRY = 0x00400670 FIBONACCI_END = [ 0x004006f1, 0x00400709] instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f]
حسنًا ، تمكنا أخيرًا من تحسين التطبيق باستخدام محرك Unicorn. عمل جيد!
ملاحظة
الآن قررت أن أعطيك القليل من الواجبات المنزلية.
هنا يمكنك العثور على ثلاث مهام أخرى ، لكل منها تلميح وحل كامل. يمكنك إلقاء نظرة خاطفة على ورقة الغش أثناء حل المشكلات.
واحدة من أكثر المشاكل المزعجة هي تذكر اسم الثابت المطلوب. من السهل التعامل مع هذا إذا كنت تستخدم إضافات Tab في IPython . عندما يكون لديك IPython مثبتًا ، يمكنك الكتابة من استيراد unicorn UC_ARCH_ اضغط Tab وستظهر لك جميع الثوابت التي تبدأ بنفس الطريقة.