İçeriğe geç
Sedat Özdemir
Yazılar

assembly

`mov eax, 1`’den Ötesi: Bir Binary’nin Zihnine Sızmak ve Kayıp Mantığı Aramak

Kodun bittiği yerde bizim işimiz başlar. Bir binary'nin içine girip geliştiricinin bile unuttuğu o gizli kapıyı bulmaya hazır mısın?

Sedat Özdemir
· 6 dk okuma

0x55 0x48 0x89 0xE5 – Bu sadece bir byte dizisi değil, x86_64 mimarisinde bir fonksiyonun "Ben buradayım, başlıyorum" dediği o kutsal anın, yani stack frame oluşturmanın imzasıdır. Kaynak kodu elinde olmayan bir binary ile karşı karşıya kaldığında, o meşhur push rbp; mov rbp, rsp komutlarını gördüğünde aslında bir labirente ilk adımını atmış oluyorsun.

Selam dostum, bugün biraz "vibe" değiştirelim ve o çok sevdiğimiz ama bazen saç baş yolduran Reverse Engineering (Tersine Mühendislik) dünyasına dalarak ellerimizi biraz kirletelim. Red Team tarafında işler her zaman bir exploit bulup whoami yazmakla bitmiyor. Bazen karşımıza öyle bir özel protokol, öyle bir karmaşık lisans kontrolü veya öyle bir obfuscated (karartılmış) malware çıkıyor ki, "Bunun içinde ne dönüyor?" diye sormaktan başka çaremiz kalmıyor.

Gözlerini Kapat ve Matrisi Gör

Bir C programını derlediğinde (compile ettiğinde), o güzelim okunabilir if-else blokların, switch-case yapıların ve o süslü fonksiyon isimlerin birer makine koduna dönüşür. Linker işini bitirdiğinde elimizde artık işlemcinin anlayacağı ama bizim (normal şartlarda) anlayamayacağımız bir binary kalır. Tersine mühendislik dediğimiz olay, bu süreci geri sarmaya çalışmaktır. Ama bu bir "Geri Dönüşüm Kutusu"ndan dosya kurtarmak gibi değildir; bu, kıyma makinesinden geçmiş bir eti tekrar inek haline getirmeye çalışmaya benzer. İmkansız mı? Hayır. Sadece biraz sabır ve doğru araç gereç istiyor.

Genelde iki temel yolumuz var: Static Analysis ve Dynamic Analysis.

Statik analizde koda dokunmayız, sadece bakarız. Sanki bir tabloyu inceler gibi. IDA Pro, Ghidra veya Binary Ninja gibi araçları açar, kontrol akış grafiklerine (Control Flow Graph - CFG) bakarız. Dinamik analizde ise o kodu çalıştırırız. x64dbg veya GDB (veya o efsanevi GEF eklentisi) ile kodun damarlarına girer, register'lardaki değişimleri anlık izleriz.

Basit Bir Senaryo: Kapıyı Kırmak mı, Maymuncuk mu?

Diyelim ki elimizde şöyle bir C kodu var ve biz bunun kaynak koduna sahip değiliz, sadece derlenmiş hali elimizde:

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

int check_password(char *input) {
    if (strcmp(input, "P4yt3n_R3d_T34m_2024") == 0) {
        return 1;
    }
    return 0;
}

int main() {
    char password[64];
    printf("Sifreyi gir kanka: ");
    scanf("%63s", password);

    if (check_password(password)) {
        printf("Sistem acildi, iceridesin!\n");
    } else {
        printf("Yanlis kapi, baska kapiya.\n");
    }
    return 0;
}

Bu kodu derleyip (gcc crackme.c -o crackme) birine verdiğinde, o kişi strings komutuyla bile şifreyi görebilir. Ama gerçek hayat bu kadar insaflı değil. Şifreler hash'lenir, XOR'lanır veya garip algoritmalardan geçer.

Şimdi bu programın Assembly (x64) tarafında nasıl göründüğüne bakalım. check_password fonksiyonu muhtemelen şöyle bir şeye dönüşecek:

0000000000001149 <check_password>:
    1149:   55                      push   rbp
    114a:   48 89 e5                mov    rbp,rsp
    114d:   48 83 ec 10             sub    rsp,0x10
    1151:   48 89 7d f8             mov    QWORD PTR [rbp-0x8],rdi
    1155:   48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
    1159:   48 8d 35 a4 0e 00 00    lea    rsi,[rip+0xea4]        # "P4yt3n_R3d_T34m_2024"
    1160:   48 89 c7                mov    rdi,rax
    1163:   e8 e8 fe ff ff          call   1050 <strcmp@plt>
    1168:   85 c0                   test   eax,eax
    116a:   75 07                   jne    1173 <check_password+0x2a>
    116c:   b8 01 00 00 00          mov    eax,0x1
    1171:   eb 05                   jmp    1178 <check_password+0x2f>
    1173:   b8 00 00 00 00          mov    eax,0x0
    1178:   c9                      leave
    1179:   c3                      ret

Buradaki kritik nokta 116a adresindeki jne (Jump if Not Equal) komutudur. Eğer strcmp sonucu sıfır değilse (yani şifre yanlışsa), program başarıyı temsil eden mov eax, 0x1 kısmını atlayıp direkt sıfıra gidiyor. Bir reverse engineer ne yapar? O jne (opcode: 75) komutunu je (opcode: 74) yapar veya daha kötüsü, orayı nop (No Operation - 90 90) ile doldurur. İşte "patching" dediğimiz o zevkli an budur. Yanlış şifre girsen bile sistem seni içeri alır.

Debugger İçinde Kaybolmak

Debugger kullanırken en sadık dostun register'lardır. RAX, RBX, RCX, RDX... Bunlar senin çalışma masan üzerindeki küçük kutular gibidir.

Özellikle RAX, fonksiyonların dönüş değerini tutar. Bir fonksiyonun başarısız olduğunu mu gördün? Hemen o fonksiyonun bittiği yere bir breakpoint koy, tam ret komutundan önce RAX değerini 1 yap ve akışın nasıl değiştiğini izle.

Bir malware analiz ettiğini hayal et. Malware genelde IsDebuggerPresent gibi API'leri çağırarak izlenip izlenmediğini kontrol eder. Eğer bu fonksiyon 1 dönerse, malware kendini kapatır veya saçma sapan işler yapmaya başlar. Biz ne yapıyoruz? Fonksiyonun tam çıkışında register'ı manipüle edip "Hayır abi, debug falan edilmiyorsun, rahat ol" diyoruz.

Stack: Kim Nereye Gidiyor?

Stack (yığın) yapısını anlamadan reverse engineering yapmak, gözü kapalı araba sürmeye benzer. Fonksiyonlar çağrıldığında, dönüş adresleri (return address) stack'e basılır. Eğer bir buffer overflow açığı arıyorsan, gözün hep o stack'in sınırlarında olmalı.

Bir terminal çıktısı üzerinden düşünelim:

(gdb) info frame
Stack level 0, frame at 0x7fffffffe0a0:
 rip = 0x555555555168 in check_password; saved rip = 0x5555555551bd
 source language c.
 Arglist at 0x7fffffffe090, args: input=0x7fffffffe310 "deneme123"
 Locals at 0x7fffffffe090, Previous frame's sp is 0x7fffffffe0a0
 Saved registers:
  rbp at 0x7fffffffe090, rip at 0x7fffffffe098

Buradaki saved rip değeri, fonksiyon bittiğinde programın nereye döneceğini söyler. Eğer biz input alanına gereğinden fazla veri gönderip bu saved rip adresini kendi istediğimiz bir adresle (mesela bir shellcode'un başlangıcıyla) değiştirebilirsek, oyun biter.

Obfuscation ve Anti-Reversing Belası

Tabii ki geliştiriciler de armut toplamıyor. Kodun analiz edilmesini zorlaştırmak için binbir takla atıyorlar.

  1. Instruction Substitution: add eax, 5 yazmak yerine sub eax, -5 yazmak gibi basit şeylerden tut da, çok daha karmaşık matematiksel denklemlere kadar gider.
  2. Control Flow Flattening: Programın akışını devasa bir switch-case yapısına sokarlar. Kodun nerede başlayıp nerede bittiğini anlamak için saatlerini harcarsın.
  3. Junk Code: Hiçbir işe yaramayan binlerce satır assembly kodu araya serpiştirilir.
  4. Packing: Binary'yi sıkıştırıp şifrelerler. Program çalıştığında önce kendi kendini bellekte çözer (unpack), sonra asıl koda zıplar (Original Entry Point - OEP).

Eğer bir UPX packlenmiş dosya görürsen şanslısın, upx -d der geçersin. Ama özel bir packer varsa, bellekte o OEP'i bulup dump etmen gerekir. Bu da işin "siyah kuşak" kısımlarından biridir.

Pratik Bir İpucu: IDA'da Kaybolmamak İçin

IDA Pro'yu ilk açtığında o karmaşık grafik seni korkutabilir. Benim yöntemim şudur: "Aşağıdan yukarıya git."

Kritik bir hata mesajı mı gördün? "Invalid License" veya "Access Denied" gibi. Hemen o string'i (Shift + F12) ara. O string'in nerede kullanıldığını bul (Cross-Reference - Xref). Oraya kimin zıpladığını (Jump) takip et. Karşına çıkan o test/cmp ve jz/jnz ikilisi muhtemelen senin "karar noktan" dır. Orayı anladığında, kaleyi fethetmiş sayılırsın.

Son Sözü Kim Söyler?

Reverse engineering bir araç değil, bir düşünce biçimidir. "Bu nasıl çalışıyor?" sorusunun en saf cevabıdır. Payten'de Red Team operasyonlarında bazen bir güvenlik cihazının (WAF, EDR vs.) internal mantığını anlamak için bu yola başvuruyoruz. Kodun derinliklerine indiğinde, aslında her şeyin birer mantık kapısından ibaret olduğunu görüyorsun.

Assembly öğrenmekten korkma; o, bilgisayarın ana dilidir. İngilizceyi nasıl öğrendiysen, mov, push, pop, xor komutlarını da öyle öğrenebilirsin. Bir süre sonra o byte'lar sana bir hikaye anlatmaya başlayacak.

Hadi şimdi kapat o IDE'yi, aç bir objdump veya Ghidra, rastgele bir binary'yi içeri at ve içine bir bak bakalım. Geliştirici orada senin için ne gibi sürprizler bırakmış?

Unutma; kaynak kodu olmayan her program, aslında çözülmeyi bekleyen bir bilmecedir. Ve biz Red Team olarak, bilmeceleri severiz.

Daldığın derinliklerde boğulmaman dileğiyle. Bir sonraki teknik macerada görüşürüz.

İlgili yazılar