به کار بردن تکنیک Disassembly Desynchronization در یک بدافزار موجب میشود تا نرم افزار های Disassembler نتوانند به درستی کد باینری آن بدافزار را disassemble کنند . یک روش ضد مهندسی معکوس ایستا (anti static reversing) است که غالبا بدافزار ها برای سخت و پیچیده کردن پروسه مهندسی معکوس ایستا به کار میبرند . دقت کنید همانطور که گفته شد این روش فقط مهندسی معکوس ایستا را مورد هدف قرار میدهد و تاثیری در مهندسی معکوس پویا ندارد .
Disassembly Desynchronization چگونه کار میکند ؟
تکه کد اسمبلی زیر را در نظر بگیرید :
xor eax , eax ; eax = 0
cmp eax , 10
jle jump_destination
here:
;
; some instructions here
;
jump_destination:
;
; some another instruction here
;
در خط سوم ، یک پرش شرطی (Conditional jump) صورت گرفته است . ما میدانیم در اسمبلی هر پرش شرطی دو شاخه دارد . اگر شرط آن برقرار باشد به شاخه اول و اگر شرط آن برقرار نباشد به شاخه دوم پرش میکند. در مثال بالا یکی از شاخه های پرش شرطی ،دقیقا دستور بعدی آن یعنی برچسب here است و یکی دیگر از شاخه های آن برچسب jump_destination است . اگر شرط آن برقرار باشد به jump_destination و اگر برقرار نباشد به here پرش میکند .
وقتی به یک Disassembler میگوییم تا خروجی کد بالا را برای ما Disassemble کند ، وقتی به پرش شرطی میرسد ،تلاش میکند تا هر دو شاخه مقصد آن را Disassemble کند. تکنیک Disassembly Desynchronization از همین نکته استفاده میکند . گاهی اوقات ما میتوانیم پرش های شرطی بگذاریم که به ظاهر انگار دو شاخه برای پرش دارند ولی در عمل ، همیشه به یک شاخه یکسان پرش میکنند. مثال بالا همینطور است . ما در خط دوم مقدار eax را با ۱۰ مقایسه کرده ایم اما نکته در خط قبل از آن است . ما قبل از آن مقدار eax را برابر صفر قرار داده ایم . بنابراین پرش خط ۳ همیشه به یکی از شاخه های آن یعنی jump_destination خواهد پرید چون مقدار eax همیشه کمتر از ۱۰ خواهد بود . بنابراین ما مطمئنیم که هیچوقت دستورات زیر برچسب here تا قبل از jump_destination اجرا نخواهد شد چون پرشی به آن جا صورت نمیگیرد . حالا که میدانیم هیچ دستوری در آن نقطه اجرا نمیشود پس چرا تعدادی بایت های هدفمند در آنجا قرار ندهیم تا disassembler را گمراه کنیم ؟ به هر حال که قرار نیست اجرا شوند پس هرچه میخواهیم میتوانیم قرار دهیم 🙂
پیاده سازی یک نمونه از Disassembly Desynchronization
فرض کنید یک برنامه ساده (در لینوکس) داریم که متن “Hacking World !” را روی صفحه چاپ میکند :
; a simple x86 (linux) program to print "Hacking World !" on the screen
global _start
section .data
msg_hello db "Hacking World !", 0xa
section .text
_start:
print:
mov eax , 4 ; write syscall
mov ebx , 1 ; stdout as output stream
mov ecx , msg_hello ; message to print
mov edx , 16 ; message length
int 0x80 ; execute the syscall
exit:
mov eax , 1 ; exit syscall
mov ebx , 0 ; exit code
int 0x80 ; execute the syscall
اجرای برنامه از برچسب start_ شروع میشود . کد های زیر برچسب print وظیفه ی چاپ متن روی صفحه و کد های زیر برچسب exit وظیفه ی خارج شدن از برنامه را دارند .
سورس بالا را در فایل source1.asm ذخیره میکنیم و با دستورات زیر آن را compile و اجرا میکنیم :
ظاهرا همه چیز خوب است . حالا بیایید خروجی کد بالا را با استفاده از objdump که یک disassembler است disassemble کنیم :
همانطور که میبینید objdump توانست به درستی و با صحت تمام ، کد مارا disassemble کند. یک نمونه از تکنیک disassembly desynchronization را در کدمان پیاده سازی میکنیم . برای اینکار کدمان را به شکل زیر تغییر میدهیم :
; a simple x86 (linux) program to print "Hacking World !" on the screen
global _start
section .data
msg_hello db "Hacking World !", 0xa
section .text
_start:
xor eax , eax
xor ebx , ebx
xor ecx , ecx
xor edx , edx
cmp eax , 10
jle print
db 0x0f ; desynchronizing the disassembly process
print:
add eax , 4 ; write syscall
add ebx , 1 ; stdout as output stream
add ecx , msg_hello ; message to print
add edx , 16 ; message length
int 0x80 ; execute the syscall
exit:
mov eax , 1 ; exit syscall
mov ebx , 0 ; exit code
int 0x80 ; execute the syscall
دستورات زیر برچسب start_ را دقت کنید . ابتدا هر ۴ رجیستر eax , ebx , ecx , edx را برابر صفر قرار داده ایم . سپس در خط ۱۵ یک پرش شرطی زده ایم با شرط اینکه eax کوچکتر از ۱۰ باشد . از آن جایی که مقدار eax برابر صفر است پس این پرش شرطی همیشه اجرا خواهد شد . در نتیجه کد های قبل از برچسب print و بعد از دستور پرش شرطی هیچگاه اجرا نخواهند شد . Disassembler این موضوع را نمیداند و سعی میکند حتی آن ها را هم disassemble کند . زیر برچسب print را دقت کنید . اولین دستور یک دستور add است (خط ۲۰)
add eax , 4
بایت کد های این دستور به گفته objdump به شکل زیر است :
804900e: 83 c0 04 add eax,0x4
از آنجایی که اولین بایت کد آن 0x83 است پس دنبال دستورات اسمبلی در معماری x86 میگردیم که بایت کد آن شامل مقدار 0x83 باشد . طبق تصویر زیر ، سه دستور JNB , JAE , JNC این ویژگی را دارند :
کاری که ما میکنیم این است که دقیقا قبل از برچسب print یک بایت 0x0f قرار میدهیم تا disassembler حین disassemble کردن شاخه ای از پرش شرطی که هیچگاه اجرا نخواهد شد ، به اشتباه این بایت 0x0f و 0x83 دستور add بعد از آن را به عنوان یک دستور معتبر اسمبلی disassemble کند :
jle print
db 0x0f ; desynchronizing the disassembly process
print:
add eax , 4 ; write syscall
این باعث میشود یک دستور به صورت اشتباه disassemble شود و از آنجایی که در معماری x86 سایز دستورات متغییر است ، پس این ناهماهنگی روی disassemble کردن دستورات بعدی نیز تاثیر میگذارد .
کد جدید را با دستورات زیر compile و اجرا میکنیم :
همانطور که میبینید بدونه هیچ مشکلی اجرا خواهد شد . دقت کنید یک بار بعد از آماده شدن فایل خروجی ، از دستور strip استفاده کرده ایم تا برچسب ها و اسم هایی که به صورت پیشفرض خود compiler در فایل خروجی قرار میدهد را حذف کنیم . این مورد بسیار ضروری است چون disassembler ها میتوانند با خواندن این برچسب ها علی رغم وجود تکنیک هایی مثل disassembly desynchronization نیز به درستی کد را disassemble کنند .
حالا فایل خروجی را به objdump میدهیم :
همانطور که میبینید objdump نتیجه ای غلط و نادرست به ما بر میگرداند .
این موضوع فقط مختص به objdump نیست . همانطور که در دو تصاویر زیر میبینید ، به ترتیب ghidra و radare2 نیز به اشتباه کد ما را disassemble میکنند :
بعضی از disassembler ها ، یک سری از الگوریتم هایی را به کار میگیرند تا پرش های شرطی که همیشه یکی از شاخه های آن ها اجرا خواهد شد را تشخیص بدهند . اگر با اینگونه disassembler هایی برخورد کنیم احتمالا کد ما را به درستی میتوانند disassemble کنند چون آن پرش به ظاهر شرطی ما را تشخیص میدهند و گمراه نمیشوند.
برای این موارد میتوانید از مقادیر روی حافظه stack برای صفر کردن رجیستر ها استفاده کنید . معمولا disassembler ها وقتی بحث کار با stack پیش میاید در حالت ایستا (static) ناتوان میشوند . برای مثال کد قبلی را دقت کنید . در اول کار چهار رجیستر eax , ebx , ecx , edx را توسط دستور xor صفر کرده ایم . این عمل به راحتی میتواند تشخیص داده شود . میتوانیم برای صفر کردن رجیستر ها از stack استفاده کنیم . برای مثال میتوانیم رجیستر eax را توسط stack به شکل زیر صفر کنیم :
mov eax , [esp]
dec_eax:
dec eax
jnz dec_eax
در کد بالا مقدار بالای حافظه stack را در eax ذخیره کرده ایم و سپس تا زمانی که eax برابر صفر نشده یکی از مقدار آن کم میکنیم .
بعد از اجرا شدن حلقه dec_eax ، مقدار eax برابر صفر خواهد بود و از آن به بعد هر مقداری که خواستیم درون آن قرار دهیم میتوانیم به راحتی توسط دستور add آن مقدار را به eax اضافه کنیم.
این آموزش متعلق به بخش توسعه بدافزار است
برای مشاهده تمام آموزش های توسعه بدافزار وبسایت مسترپایتون به بخش توسعه بدافزار مراجعه کنید