Computer Emergency Response Center

Stack əsaslı bufer daşması və uzaqdan kod icrası

BOF - proqramçının yazdığı proqram təminatında yaddaş daşmasının qarşısını almaq üçün lazımı yoxlamaları etmədiyi üçün baş verən bir xətadır. Lakin bu səhv olduqca kritikdir və pis məqsədlər üçün istifadəyə açıqdır. Bir çox növü olmasına baxmayaraq bu məqalədə stack overflow metodundan və bu boşluqdan faydalanaraq qarşı sistemə necə müdaxilə etmək olar bu haqda bəhs ediləcək.

Stack nədir?

Windows əməliyyat sistemində stack yaddaş bölgəsi proqram təminatlarının fəaliyyəti üçün olduqca kritik bölgədir. Proqram təminatına aid bir çox önəmli məlumatlar məhz bu yaddaş bölgədə saxlanılır. Stack data strukturu LIFO (Last in First Out) anlayışı ilə işləyir. "Son daxil olan məlumat ilk çıxır". Bunu daha yaxşı təsəvvür etmək üçün boşqab misalına baxaq. Yuyulan boşqablar ilk olaraq quruması üçün ayrı bir yerə yığılır daha sonra quruyan boşqab çəkməcəyə qoyulur. Burada yuyulan son boşqab quruduqdan sonra çəkməcəyə qoyulacaq ilk boşqabdır. Stackin işləmə prinsipidə eyni ilə belədir. Daxil olan ilk məlumat son, daxil olan son məlumat isə ilk çıxır.

Stack bölgəsində xüsusi ilə lokal dəyişənlər, funksiya geri dönüş adresləri və s. kimi məlumatlar saxlanılır. Bunu daha yaxşı izah etmək üçün pratik olaraq bir test keçirək və hazırladığımız kiçik proqram təminatının  stack bölgəsində hansı məlumatları saxladığına baxaq. Bunun üçün debugger alətindən istifadə ediləcəkdir. Kompilyatorun kod optimizasiya mexanizmi istifadə edilməyəcək.

DWORD B(DWORD P1, DWORD P2)
{
	DWORD dwNetice2 = 0;
	dwNetice2 = P1 + P2;

	return dwNetice2;
}

DWORD A(DWORD P1, DWORD P2)
{
	DWORD dwNetice1 = 0;
	dwNetice1 = B(P1, P2);

	return dwNetice1;
}

int main(void)
{
	A(1, 2);
}

main funksiyası:

Burada ilk olaraq push ebp, mov ebp, esp əməliyyatları ilə esp - yəni stack pointeri (stack bölgəsini) ebp registeri üzərindən idarə etmək üçün kiçik bir əməliyyat icra edilir. Daha sonra push 2 və push 1 instructionları ilə 1 və 2 rəqəmlərini stack bölgəsinə yazır. Bu rəqəmlər çağrılacaq A funksiyasının parametrləridir. Parametrlər yazıldıqdan sonra isə A funksiyası çağrılır. A funksiyası çağrılmazdan öncə stack bölgəsi:

Burada diqqət etməli olduğunuz məqam 0x008A1C9C dəyəridir. Bu dəyəri stack bölgəsinə yazan CALL instructionudur. main funksiyası A funksiyasını çağırdıqdan sonra A funksiyası özü işini görür və daha sonra yenidən main funksiyasına qayıtmalıdır. Hara qayıdacağını isə bu stack bölgəsinə call instr ilə yazılan bu dəyər təyin edir. ret instruction funksiyası öz işlərini bitirdikdən sonra stackdən geri dönəcəyi adresi alır və həmin adresə qayıdır. Bu adresi isə main funksiyasından, call əməliyyatından sonraki adresi göstərir.

Daha sonra B funksiyası çağrılır. Tərkib koduna baxsanız A funksiyasının öz növbəsində main funksiyasından gələn parametrləri  B funksiyasına toplama əməliyyatı üçün göndərir və gələn nəticəni dwNetice1 adlı lokal dəyişəndə saxlayır. Gəlin A funksiyasının bunu necə etdiyinə baxaq.

Burada ilk olaraq mov dword ptr ss:[ebp-4], 0 ilə dwNetice1 dəyişəni üçün stack bölgəsində boş yer ayırır və 0 dəyərini yazır. Bunun c dilində qarşılığı - dwNetice1 = 0. Daha sonra main funksiyasından gələn parametrləri (1 və 2) stack bölgəsindən götürür. Götürülən parametrlər push əməliyyatı ilə növbəti (B) funksiyasına göndərilmək üçün yenidən stack bölgəsində yazılır.

mov  eax,dword ptr ss:[ebp+C]
push eax
mov  ecx,dword ptr ss:[ebp+8]
push ecx
call  

Əməliyyatdan öncə stack bölgəsi

Əməliyyatdan sonra stack bölgəsi

Stack Frame

Yuxarıdakı şəkilə diqqət yetirsəniz debuggerin qara xətlər ilə stack bölgəsini ayırdığını görə bilərsiniz. Bunun səbəbi stack frame anlayışıdır. Hər bir prosedurun (funksiyanın) öz stack frame-i olur. Yəni main funksiyasının öz , A funksiyasının öz stack frame-i olur. Funksiyaya aid lokal dəyişənlər geri dönüş adresləri məhz bu frame içərisində saxlanılır. Stack frame funksiya çağrıldığı zaman cari ESP-də yaradılır. Stack haqqında ümumi məlumatlandıqdan sonra keçirik explotasiya qisminə.

Stack Bufferinin explotasiya edilməsi

Stack Bufferinin explotasiya edilməsində məqsəd funksiya geri donüş dəyərinə müdaxilə edərək EIP hücüm edən tərəfinən istədiyi kodlara (bölgəyə) yönləndirməkdir. Bu əksər hallarda elə məhz stack bölgəsinin özü olur. ESP - nin korlanması ilə stack frame içərisində olan  funksiya geri dönüş dəyəri lazımı dəyər ilə dəyişdirilir. Bundan sonra idarə etmə tamami ilə explotasiya edən tərəfin əlinə keçir və shell kod icra edilir. Bunu daha yaxşı anlamaq üçün praktiki olaraq stack overflow boşluğunu explotasiya edərək boşluğun necə istismar olunduğuna baxaq. Test sistemi olaraq Windows 7 x86 English, explotasiya üçün isə stack overflow boşluğu daşıyan Stephen Bradshaw tərəfindən hazırlanan vulnserver applikasiyası hədəf olaraq istifadə ediləcək. Bu applikasiya məhz bu tip əməliyyatları test etmək üçün hazırlanmışdır. Daha ətraflı: https://thegreycorner.com/vulnserver.html
Vulnserveri normal bir server applikasiya kimi düşünə bilərsiniz. Server sadəcə olaraq qarşıdan gələn məlumatları emal etmək üçün proqramlaşdırılıb. Burada stack overflow boşluğunu aktivləşdirmək üçün TRUN əmrindən istifadə edəcəyik. Tərkib kodlarına baxsaq boşluğun strcpy funksiyasında olduğunu görə bilərik. Strcpy funksiyası mətn kopyalama əməliyyatları zamanı istifadə edilir. Lakin funksiyanın daxil olan məlumat həcmini yoxlamadığı boşluq yaranır.

else if (strncmp(RecvBuf, "TRUN ", 5) == 0) {
     char *TrunBuf = malloc(3000);
     memset(TrunBuf, 0, 3000);
     for (i = 5; i < RecvBufLen; i++) {
          if ((char)RecvBuf[i] == '.') {
              strncpy(TrunBuf, RecvBuf, 3000);                
              Function3(TrunBuf);
              break;

Burada server socket üzərindən gələn məlumatın TRUN . ilə başladığını təyin etdikdən sonra buradan 3000 baytlıq məlumatı götürərək Function3 funksiyasına parametr olaraq göndərir. Function3 isə boşluğun yarandığı funksiyadır.

void Function3(char *Input) {
    char Buffer2S[2000];    
    strcpy(Buffer2S, Input);
}

Diqqət etsəniz funksiya stack framində lokal dəyişən Buffer2S üçün 2000 baytlıq yer ayrılır. Lakin funksiya-ya parametr olaraq göndərilən dəyər ayrılan həcmdən daha çoxdur. Belə olan halda buffer (stack) daşması meydana gəlir. İlk olaraq vulnserver 1892 (lokal port) serveri qaldırırıq və python ilə serverə məlumat göndəririk.

Əmri göndərikdən sonra proqram normal fəaliyətinə davam edir. Daha çox məlumat göndərərək test edirik. Bu dəfə əmr + 3000 ədəd A xarakteri göndəriririk. Windows Error Report vulnserver də baş verən exception haqqında məlumat göstərir. Burada diqqət edilməli məqam Exception Offsetdir. Exception baş verdiyi adres 0x41414141 adresidir. Lakin bu normal adres deyil. Xatırlayırsınızsa biz vulnserver-ə “A” xarakterini göndərmişdik. Bu xarakterin hexa qarşılığı isə 0x41 (A)-dir. Deməli EIP registeri korlanaraq bizim göndərdiyimiz A xarakteri ilə əvəzlənib. Bunun səbəbi isə stack framedə saxlanan öncəki funksiyanın geri dönüş adresidir.

Göndərdiyimiz 3000 baytlıq məlumat stack bölgəsini korlayaraq EIP-nin əvəzlənməsinə səbəb oldu. Həmçinin stack yaddaşıda eyni şəkildə. Vulnserver-i debug edərək daha ətraflı məlumat görə bilərik.

Gördüyünüz kimi stack tamami ilə korlanıb və bu birbaşa EIP registerinə təsir edib. Bundan sonra EIP -ni düzgün şəkildə yönləndirərək shell kodumuzun icra edilməsini təmin etməliyik. Lakin burada kiçik bir problem var. Göndərdiyimiz A xarakterlərindən hanslarının (4 bayt – 32) EIP-yə yazıldığını öyrənməliyik.

Bunun üçün metasploit alətindən istifadə edə bilərik. Metasploit alətləri arasında məhz bu məqsəd üçün yaradılmış kiçik alətlər var ( pattern_createpattern_offset). İlk olaraq pattern_create ilə 3000 baytlıq pattern yaradırıq. 3000 baytlıq bu pattern müxtəlif xarakterlərdən ibarətdir. Əsas məqsəd EIP registeri bizim göndərdiyimiz patternlər ilə dəyişdiyi zaman EIP-nin dəyərinin (pattern) hansı offsetdən sonra yazıldığını aşkarlamaqdır.

Patternləri socket üzərindən göndəririk.

EIP = 0x396F4338. Bundan sonra pattern_offset ilə EIP registerinə yazılan dəyərin hansı offsetdən etibarən yazıldığını öyrənirik.

2006-cı xarakterdən sonra gələn 4 baytlıq dəyər (32 bit address) EIP registerinə yazılır. Bunu test etmək üçün 2006 baytdan sonra ‘Z’ xarakteri göndəririk.

Bəli gördüyünüz kimi EIP registeri 5A5A5A5A (‘ZZZZ’) dəyərlərini aldı. EIP registerini idarə edə bildiyimizə görə bundan sonra qalır faydalı yükümüz. Faydalı yükü qarşı tərəfə ötürmək üçün stack bölgəsindən istifadə edilir. 2006 + 4(eip) məlumatdan sonra göndərdiyimiz məlumat artıq shell kod hissəsidir. Bunu sınaqdan keçirmək üçün 2006 + 4 dən sonra 10 ədəd ‘D’ xarakterini göndərib stack bölgəsinə baxırıq.

Burada da ist'diyimiz nəticəni əldə etdikdən sonra geriyə qalır pointeri stack bölgəsinə yönləndirmək. Bunun üçün isə proqram yaddaşında JMP ESP instructionu axtarmaq lazımdır. Yaddaşda bu instruction taparaq eip registerini bu instructionun olduğu adres ilə əvəzləyərək CPU-nun stack bölgəsində olan kodları icra etməsini təmin edirik və shell kodumuzicra olunur.

Əlbətdə məsələ bu qədər asan deyil. Xüsusi ilə əməliyyat sistemlərinin və kompilyatorların gətirdiyi bəzi təhlükəsizlik mexanizmləri (DEP, ASLR, SEH, Stack Cookie etc) işləri bir xeyli çətinləşdirir. Lakin zamanla bu mexanizmlərində yayınma metodları araşdırılaraq hazırlanır.

 

 

İstinadlar

[1] https://docs.microsoft.com/enus/windows/win32/memory/data-execution-prevention
[2] https://msrc-blog.microsoft.com/2010/12/08/on-theeffectiveness-of-dep-and-aslr
[3] https://en.wikipedia.org/wiki/Stack_buffer_overflow
[4] https://thegreycorner.com/vulnserver.html

Press ESC to close