آموزش تکنیک Disassembly Desynchronization

به کار بردن تکنیک 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 و اجرا میکنیم :

compiling source1.asm

ظاهرا همه چیز خوب است . حالا بیایید خروجی کد بالا را با استفاده از 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 اضافه کنیم.

مارا دنبال کنید

پست های مرتبط

مطالعه این پست ها رو از دست ندین!
KeyLogger کیلاگر

ساخت KeyLogger با استفاده از GetAsyncKeyState

آنچه در این پست میخوانید مارا دنبال کنید یکی از روش های مرسوم و اولیه برای پیاده سازی KeyLogger ها…

بیشتر بخوانید

جاسازی Shellcode در فایل اجرایی exe

در اینجا میخواهیم ببینیم چطور میتوان یک Shellcode را در یک فایل اجرایی ویندوزی جاسازی کرد به طوری که با اجرای فایل اجرایی آلوده ، ابتدا Shellcode به صورت مخفیانه اجرا شده و سپس محتوای عادی فایل اجرایی شروع به اجرا شدن میکنند طوری که همه چیز عادی به نظر برسد .

بیشتر بخوانید

پیاده سازی APC Injection در C

APC Injection یکی دیگر از روش های تزریق و اجرای کد در پروسه های دیگر است . در این پست به بررسی اینکه APC در ویندوز چیست و چگونه بدافزار ها از آن برای تزریق کد استفاده میکنند میپردازیم . 

بیشتر بخوانید

نظرات

سوالات و نظراتتون رو با ما به اشتراک بذارید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *