Hiç assembly görmediysen de buradan başlayabilirsin. Kavramlardan gerçek bir fonksiyonu satır satır çözmeye kadar — her adımı tlk-hex üzerinde deneyerek.
Disassembler, assembly, register, bellek — neyle uğraşıyoruz?
PE/ELF başlıkları, bölümler, import, giriş noktası.
Register'lar, sık komutlar, çağrı kuralı, yığın.
Bir fonksiyonu tlk-hex ile baştan sona çözelim.
if / döngü / switch'i grafikten okuma.
Nerede, neyle çalışılır; meşru alıştırma kaynakları.
Her şeyi tek gerçek analiz akışında birleştir: string → xref → karar → doğrula.
Anahtarı bulmak yerine davranışı değiştir: bir dalı çevir, kontrolü atla.
Kod gizliyse: entropi, paketleyici imzaları, imphash ve seyrek import.
Bir program yazarsın, derleyici onu makine koduna çevirir: işlemcinin doğrudan çalıştırdığı baytlar. Kaynak kod çoğu zaman elimizde olmaz — elimizde sadece o baytlar vardır. Tersine mühendislik, bu baytlardan geriye doğru giderek programın ne yaptığını anlamaktır.
İşlemci baytları okur ve komut olarak çalıştırır. Örneğin 48 83 EC 28 baytları
aslında "yığından 0x28 bayt yer aç" demektir. Bir disassembler bu baytları insanın okuyabileceği
assembly'ye çevirir:
48 83 EC 28 sub rsp, 28h ; yığında 40 bayt yer aç
tlk-hex bütün dosyayı bu şekilde çözer, sonra fonksiyonları, çağrıları ve veriyi birbirine bağlar.
rax, rbx, rcx… diye adlandırılır.0x140001008 gibi onaltılık yazılır.mov, add, call…call ile çağrılır, ret ile döner.00–FF
aralığı tam iki hex hanesine sığar. Bu yüzden adresler ve değerler hep hex yazılır. tlk-hex'te H ile bir
sayıyı ondalığa çevirebilirsin.Bir .exe yalnızca koddan ibaret değildir. Windows'un onu belleğe doğru yükleyebilmesi için
bir düzeni vardır: PE (Portable Executable) formatı. Linux tarafında bunun karşılığı ELF'tir.
Dosya, işlevine göre bölümlere ayrılır. tlk-hex'te Shift+F7 ile hepsini görürsün:
| Bölüm | İçinde ne var |
|---|---|
| .text | Çalıştırılabilir kod. Fonksiyonlar burada. |
| .rdata | Salt-okunur veri: string'ler, import tablosu, sabitler. |
| .data | Yazılabilir global değişkenler. |
| .rsrc | Kaynaklar: ikonlar, sürüm bilgisi, diyaloglar. |
| .pdata | x64 istisna/çözülme bilgisi — fonksiyon sınırlarını bulmaya yarar. |
CreateFileW, send…). Ne yaptığına dair en güçlü ipucu.0x140000000).Korkutucu görünür ama birkaç düzine komut işlerin %90'ını kaplar. Önce register'lar:
| 64-bit | 32-bit | Tipik kullanım |
|---|---|---|
| rax | eax | Dönüş değeri; genel hesap |
| rcx rdx r8 r9 | ecx edx | Fonksiyona geçen ilk 4 argüman (x64) |
| rbx rsi rdi | ebx esi edi | Korunan genel değişkenler |
| rsp | esp | Yığın işaretçisi (stack pointer) |
| rbp | ebp | Yığın çerçevesi tabanı |
| rip | eip | Sıradaki komutun adresi |
Aynı register'ın küçük parçaları da kullanılır: rax → eax (32) →
ax (16) → al (8 bit).
| Komut | Anlamı |
|---|---|
| mov a, b | a = b — değer kopyala |
| lea a, [b] | a = b'nin adresi — adres hesapla (hep bellek okumaz) |
| add / sub | Topla / çıkar |
| xor a, a | Register'ı sıfırla (kendisiyle XOR = 0) |
| cmp a, b | Karşılaştır (a - b) ve bayrakları ayarla |
| test a, a | a sıfır mı diye bakar (çok yaygın) |
| jmp | Koşulsuz atla |
| jz / je | Eşitse / sıfırsa atla · jnz/jne: değilse |
| call | Fonksiyon çağır |
| ret | Fonksiyondan dön |
| push / pop | Yığına koy / al |
Bir fonksiyon çağrılırken argümanlar sırayla rcx, rdx, r8, r9 register'larına konur;
fazlası yığına gider. Dönüş değeri rax'tedir. Yani şunu gördüğünde:
mov rcx, 1 lea rdx, aMerhaba ; "merhaba" call MesajYaz ; MesajYaz(1, "merhaba") mov ebx, eax ; dönüş değerini sakla
…aslında MesajYaz(1, "merhaba") çağrısını okuyorsun demektir.
Yerel değişkenler yığında tutulur. tlk-hex bunları var_18 (yerel) ve arg_8
(argüman) gibi otomatik adlandırır; [rsp+88h+var_18] yazısı "bu fonksiyonun bir yerel değişkeni" demektir.
Üzerine gel, N ile anlamlı bir isim verebilirsin.
Artık gerçek bir fonksiyon okuyalım. Ana sayfadaki örneği alalım ve satır satır çözelim:
140001008 ParseConfigFile proc near 140001008 mov r11, rsp 14000100B sub rsp, 88h ; yığında yer aç 140001012 mov rax, cs:__security_cookie ; yığın koruması 140001019 xor rax, rsp 14000101C mov [rsp+88h+var_18], rax 140001053 lea rcx, aConfigLoaded ; "config loaded: %s" 14000105A mov edx, esi 140001073 call LogMessage ; LogMessage("config loaded: %s", esi) 140001080 call cs:__security_check_cookie 140001085 add rsp, 88h ; yeri geri ver 14000108C retn
Ne okuduk?
sub rsp + security_cookie): Her derleyici fonksiyonunun standart açılışı. Yığın taşmasına karşı koruma. Mantığı burada değil.lea rcx, aConfigLoaded bir metnin adresini alıyor, call LogMessage onu yazdırıyor. Demek ki bu fonksiyon bir şeyi logluyor.security_check + add rsp + retn): Standart kapanış.Yani gürültüyü eleyince fonksiyonun özü tek satır: "config loaded" mesajını logla. Gerçek analizin yarısı, bu standart kalıpları tanıyıp önemli olana odaklanmaktır.
call hedefine çift tıkla,
o fonksiyonun içine girersin.socket/connect görürsen ağ var; CryptEncrypt görürsen şifreleme var.Space ile grafiğe geç. Bloklar arası oklar programın mantığıdır. Birkaç kalıbı tanı:
Bir cmp + koşullu atlama, bir dallanmadır. Grafikte iki ok çıkar:
yeşil = koşul doğru, kırmızı = yanlış.
cmp eax, 0 jz loc_hata ; eax == 0 ise hataya atla ; ... buraya düşerse eax != 0 (başarı yolu)
C diliyle: if (eax == 0) goto hata;
Grafikte yukarı doğru dönen bir ok gördüğünde orası bir döngüdür: blok, kendinden önceki bir bloğa geri atlıyordur.
Genelde bir sayaç (inc/dec) ve bir cmp ile biter.
Bir değere göre çok yöne dallanma. tlk-hex atlama tablolarını otomatik çözer ve
jumptable ... case 3 gibi yorumlar ekler — hangi değerin nereye gittiğini doğrudan okursun.
if/goto'lu C benzeri bir özete çevirir. Kesin değildir ama mantığı hızlı kavratır.Okumak öğretmez; çözmek öğretir. Başlamak için güvenli ve meşru yollar:
Buraya kadar parçaları ayrı ayrı gördük: string'ler, import'lar, xref'ler, assembly, graph ve pseudocode. Şimdi hepsini tek bir gerçek analiz akışında birleştireceğiz. Acele etme — bu modülün amacı hız değil, bir binary'ye nasıl soru sorulacağını kavramak.
Elimizde kaynak kod yokmuş gibi davranacağız ve şu sorulara yalnızca dosyanın kendisinden cevap arayacağız:
Önce analiz edeceğimiz küçük programı yazıyoruz. Bir sample.c dosyası aç:
#include <stdio.h> #include <string.h> static int classify(const char *mode) { if (strcmp(mode, "debug") == 0) return 2; if (strcmp(mode, "safe") == 0) return 1; return 0; } int main(int argc, char **argv) { const char *mode = argc > 1 ? argv[1] : "safe"; int result = classify(mode); printf("mode=%s result=%d\n", mode, result); return result; }
Programın ne yaptığını şimdilik kafandan sil. Kaynağı yalnızca hedefi üretmek için kullanıyoruz; EXE hazır olduktan sonra ona sadece binary tarafından bakacağız. Derlemek için (hangisi eldeyse):
cl /O2 /Fe:sample.exe sample.c
gcc -O2 -o sample.exe sample.c
Artık elimizde sample.exe var. Bundan sonra kaynağı kapat.
sample.exe'i tlk-hex penceresine sürükle. Otomatik analiz başlar ve bölümleri,
fonksiyonları, çağrıları, string'leri, xref'leri ve import'ları çıkarır.
Analiz bitince hemen assembly okumaya dalma. Önce dosyanın genel havasını al. Tersine mühendislikte ilk hedef ayrıntı değil, bağlamdır — nerede olduğunu bilmeden tek tek komut okumak seni yorar.
Shift+F12 ile Strings'i aç. Şuna benzer metinler ararsın:
debug safe mode=%s result=%d
Bu üç satır, daha tek bir assembly komutu okumadan bize çok şey anlatıyor. debug ve
safe büyük ihtimalle kullanıcı girdisiyle karşılaştırılan değerler;
mode=%s result=%d ise programın sonunda bir sonuç yazdırdığını düşündürüyor.
debug string'ini seç ve X'e bas. Bu, string'in nerede kullanıldığını
(cross-reference) gösterir. Bir kod referansına git. Aynısını safe için de yap.
İki string'in referanslarının aynı fonksiyona ya da birbirine çok yakın kod alanlarına gittiğini görürsün. Bu güçlü bir ipucu:
"debug" ve
"safe" ile karşılaştırıyor.Şimdi programın dışarıdan kullandığı fonksiyonlara bak. Derleyiciye göre isimler biraz değişse de şunları görürsün:
| Import | Ne yapar |
|---|---|
| strcmp | İki C string'ini karşılaştırır (eşitse 0 döner) |
| printf | Biçimlendirilmiş metni ekrana yazar |
Artık üç bağımsız kanıt birikti: "debug", "safe" ve
strcmp. Üçü birlikte güçlü bir ihtimale işaret ediyor: program bir string'i
"debug" ve "safe" ile karşılaştırıyor. Yine de assembly'de görmeden emin olmayacağız.
debug xref'inden ulaştığın fonksiyonun başına git. Otomatik adı
sub_140001000 gibi olabilir — gerçek ismini henüz bilmiyoruz. İçeride şuna benzer bir
kalıp görürsün (tam komutlar derleyiciye göre değişir, önemli olan kalıbı okumak):
lea rdx, aDebug ; ikinci argüman: "debug" mov rcx, rbx ; ilk argüman: gelen girdi call strcmp ; strcmp(girdi, "debug") test eax, eax ; dönüş 0 mı? (eşit mi?) jz loc_debug ; eşitse debug bloğuna atla
İlk iki satır strcmp'nin argümanlarını hazırlar. strcmp iki string
aynıysa 0 döndürür; test eax, eax sonucun sıfır olup olmadığına bakar ve
jz (sıfırsa atla) ilgili bloğa gider. Yani buradaki mantık net:
if (strcmp(input, "debug") == 0)
Ne yaptığını artık yaklaşık biliyoruz. Fonksiyonun üstünde N'ye basıp
sub_140001000 yerine classify_mode yaz.
İsimlendirme lüks değil, yöntemin kalbidir. Başta ekranda onlarca
sub_140001000 görürsün; bunları classify_mode,
print_result, load_config gibi adlara çevirdikçe dosya her geçişte
biraz daha okunur hale gelir.
Fonksiyonun içindeyken Space'e bas; metin yerine akış grafiğini görürsün. Bu örnekte kabaca şöyle bir karar ağacı bekliyoruz:
girdi
│
▼
girdi == "debug"?
┌──────┴──────┐
evet hayır
│ │
▼ ▼
return 2 girdi == "safe"?
┌─────┴─────┐
evet hayır
│ │
▼ ▼
return 1 return 0Assembly'de karmaşık görünen şey, grafikte bir bakışta anlaşılır. İlk karşılaştırmanın hayır (false)
yolunu takip edince, bir süre sonra "safe" string'inin kullanıldığı ikinci bir
karşılaştırma görürsün — mantığı birebir aynı:
lea rdx, aSafe mov rcx, rbx call strcmp test eax, eax jz loc_safe ; if (strcmp(input, "safe") == 0)
Şimdi her yolun sonunda hangi değerin döndüğüne bak. x64 Windows'ta tamsayı dönüş değeri
eax register'ından döner:
mov eax, 2 ret ; return 2; (debug yolu) mov eax, 1 ret ; return 1; (safe yolu) xor eax, eax ret ; return 0; (xor = sıfırla; varsayılan yol)
xor eax, eax register'ı sıfırlar — yani return 0; demek. Bu küçük
kalıbı tanımak çok işine yarayacak, her yerde görürsün.
Topladığımız kanıtları birleştirince binary bize şunu söylüyor:
int classify_mode(const char *input) { if (strcmp(input, "debug") == 0) return 2; if (strcmp(input, "safe") == 0) return 1; return 0; }
Kaynak elimizde olmadan fonksiyonun davranışını çıkardık — tersine mühendisliğin asıl amacı da bu: kaynağı birebir geri almak değil, davranışı yeterince doğru anlamak.
Şimdi F5'e bas; tlk-hex aynı fonksiyonu C benzeri pseudocode'a çevirir ve senin elle çıkardığın mantığa çok yakın bir şey gösterir. Hızlıdır ama bir uyarı:
classify_mode'un üstünde yine X'e bas — bu sefer fonksiyonun nereden
çağrıldığını görürsün. Muhtemelen main'e ya da ona yakın bir yere çıkarsın:
mov rcx, rbx ; girdi call classify_mode mov esi, eax ; dönüş değerini sakla (sonra kullanılacak)
Sonra tekrar Strings'e git, mode=%s result=%d string'ini X ile takip et;
yakınında bir printf çağrısı görürsün. Böylece programın bütün zincirini çıkardık:
komut satırı girdisi → classify_mode → 0 / 1 / 2 → printf → çıkış
Bulduklarını kalıcı hale getir:
sub_140001000 → classify_mode,
aDebug → mode_debug, aSafe → mode_safe…Tersine mühendislik yalnızca binary okumak değil; bulduğunu düzenli biçimde kaydetme sürecidir. İyi isimlenmiş bir veritabanı, birkaç saat sonra ilk açtığın halinden tamamen farklı — çok daha okunur — görünür.
İyi bir çalışmanın sonunda kısa bir sonuç yazabilmelisin. Bu örnek için:
sample.exe komut satırından bir "mode" değeri alıyor. mode == "debug" → sonuç = 2 mode == "safe" → sonuç = 1 diğer tüm değerler → sonuç = 0 Sonuç, "mode=%s result=%d" formatıyla printf ile ekrana yazdırılıyor.
Kaynak koduna bakmadan programın temel davranışını çıkardık. İşte bütün mesele buydu.
Bu döngü neredeyse her dosyada işe yarar — ezberlemene gerek yok, birkaç kez yapınca kendiliğinden gelir:
1. Dosyayı aç, otomatik analizi bekle 2. Sections / imports / strings'e bak (bağlam) 3. İlginç bir string veya import seç 4. Xref (X) ile koda git 5. Fonksiyonun graph'ını incele (Space) 6. Call hedeflerini takip et 7. Koşulları ve return değerlerini çıkar 8. F5 pseudocode ile hızlı kontrol 9. Assembly ile doğrula 10. Fonksiyonlara isim ver (N) 11. Yorum ekle (;) 12. Kaydet (Ctrl+W) 13. Sonucu birkaç cümleyle yaz
String'ler en güçlü ipuçlarıdır. Şunları görürsen dur ve string → X → fonksiyon
zincirini izle:
error failed password token config login connect http registry file debug
Import'lar programın hangi yetenekleri kullandığını ele verir:
| Import grubu | İşaret ettiği şey |
|---|---|
| CreateFileW · ReadFile · WriteFile | Dosya işlemleri |
| RegOpenKeyExW · RegSetValueExW | Registry kullanımı |
| connect · send · recv | Ağ haberleşmesi |
| CryptEncrypt · CryptDecrypt | Şifreleme |
tlk-hex şu an ağırlıklı olarak statik analiz yapar — programı çalıştırmadan inceler. Bu yüzden assembly analizi, fonksiyon keşfi, xref, string/import analizi, graph, pseudocode, PDB sembolleri, hex inceleme ve yama hazırlama rahatça yapılır. Ama bazı şeyler saf statikle her zaman tam görülemez:
runtime'da hesaplanan değerler bellekte açılan/çözülen şifreli kod self-modifying (kendini değiştiren) kod runtime unpacking (çalışırken açılan paketleyiciler)
Böyle durumlarda bir debugger gibi dinamik araçlar devreye girer. Yine de işin büyük kısmı statik başlar; ne arayacağını statik analizle bulup gerekirse dinamiğe geçersin.
Örnek programı küçük bir değişiklikle yeniden derle (kaynağa tekrar bakmadan analiz etmeye çalış):
if (strcmp(mode, "admin") == 0) return 3;
Yeni EXE'yi tlk-hex'te aç ve şunları yalnızca binary üzerinden bul:
Bunların hepsini binary'den bulabiliyorsan ilk gerçek tersine mühendislik akışını tamamladın.
Bazen doğru parolayı/serial'i bulmak istemezsin — programın davranışını değiştirmek istersin. Örneğin bir kontrolü tersine çevirip "her zaman geçer" hale getirmek. Buna yama (patching) denir: diskteki baytları düzenlersin. tlk-hex bunu güvenli yapar — orijinal dosyaya dokunmaz, değişiklikleri ayrı tutar.
Çoğu kontrol bir cmp/test + koşullu atlama ile biter.
Tipik bir "başarısızsa reddet" kalıbı:
cmp eax, 0DEADBEEFh jnz loc_denied ; eşit DEĞİLSE reddet bloğuna atla ; ... buraya düşerse "granted" (başarılı) yol
Burada akışı değiştirmenin birkaç yolu var — hepsi tek bir baytı değiştirmekle olur:
| İstediğin | Nasıl | Bayt |
|---|---|---|
| Koşulu tersine çevir | jnz → jz (ya da tam tersi) | 75 ↔ 74 |
| Atlamayı hiç yapma | Koşullu atlamayı NOP'la | 90 90 |
| Her zaman atla | Koşulluyu koşulsuz yap: jnz → jmp | 75 → EB |
74 = jz/je, 75 = jnz/jne,
90 = nop, EB = kısa jmp. Bu dört baytı
ezberlemek işini çok hızlandırır.jnz loc_denied).75'i 74 yap (ya da ne istiyorsan). F2 / Esc ile çık.jz olarak görünür — mantık tersine döndü.Yama şimdilik sadece veritabanında. Kalıcı bir dosya üretmek için:
Pratik crackme'lerden ikisi tam bu modül için:
0DEADBEEFh). cmp'nin
ardındaki dalı bulup yamala → "Access granted".İkisini de crackmes sürümünden indir. Diğer seviyeler ve çözüm ipuçları Modül 07'deki akışla birleşir.
Dosyayı açtın; disassembly çok küçük, import tablosunda beş girdi var ve stringler anlamsız mı? Büyük ihtimalle paketlenmiş ya da korumalı bir dosyaya bakıyorsun: gerçek kod sıkıştırılmış/şifrelenmiş ve ancak çalışma anında bellekte açılıyor. Statik olarak okumadan önce bunu fark etmen gerekir — ve tlk-hex Bulgular panelinde sana dört hızlı işaret verir.
Entropi, rastgeleliği 0–8 ölçeğinde (bayt başına bit) ölçer. Normal kod 6–6.8 civarındadır.
Sıkıştırılmış veya şifreli veri 7.5–8'e doğru çıkar, çünkü paketli baytlar gürültü gibi görünür. tlk-hex
Segments görünümünde her bölüm için entropi değeri gösterir ve kaynak olmayan bir bölüm
7.5 üstüne çıkınca bulgu üretir.
| Entropi | Genelde şu demektir |
|---|---|
| < 6.0 | Metin, tablolar, sıkıştırılmamış veri |
| 6.0 – 7.0 | Sıradan makine kodu |
| > 7.5 | Sıkıştırılmış / şifreli — muhtemelen paketli |
.rsrc normaldir (PNG ya da zip tutabilir).
Asıl uyarı, kod bölümündeki yüksek entropidir.Birçok paketleyici parmak izini bölüm adlarında bırakır. tlk-hex gömülü bir listeyle eşleştirir ve aracı senin için adlandırır:
| Bölüm | Paketleyici |
|---|---|
UPX0 / UPX1 | UPX (ücretsiz, kolay açılır) |
.aspack / .adata | ASPack |
.vmp0 / .vmp1 | VMProtect (sanallaştırma) |
.themida / winlice | Themida / WinLicense |
.petite, .mpress, .fsg | Petite / MPRESS / FSG |
UPX en kolay olanı: upx -d ornek.exe onu açar, sonucu normal şekilde analiz edersin.
VMProtect/Themida ise bambaşka bir lig — kodu sanallaştırır ve yalnızca-statik çalışmanın kapsamı dışındadır.
Gerçek bir arayüz programı onlarca–yüzlerce fonksiyon import eder. DLL olmayan bir dosya yalnızca birkaç
tane import ediyorsa ve ayrıca LoadLibrary + GetProcAddress görüyorsan,
program gerçek API'sini import tablosundan gizlemek için çalışma anında çözüyordur. tlk-hex hem seyrek import
tablosunu hem de dinamik çözüm desenini işaretler.
imphash, import tablosu üzerinde alınan bir MD5'tir (her dll.fonksiyon, sırasıyla).
Aynı kaynaktan/araçtan derlenen iki örnek, baytları farklı olsa bile genelde aynı imphash'i paylaşır — yani bir
zararlı yazılım ailesini kümelemek ya da kardeş örnekleri bulmak için ucuz bir yoldur. tlk-hex bunu hesaplar
ve Bulgular'da gösterir; ilgili örnekleri bulmak için bir tehdit-istihbaratı aramasına yapıştırabilirsin.
Paketli bir dosya bile biraz sızdırır. Açmadan önce şunlara göz at:
VirtualAlloc + VirtualProtect
klasik "bellek ayır, içine aç, çalıştırılabilir yap" üçlüsüdür.jmp ile
biter (orijinal giriş noktasına / OEP'ye "tail jump").VirtualProtect(lpAddress, dwSize, flNewProtect, lpflOldProtect)) ki açma stub'ı net okunsun.Sahibi olduğun zararsız bir programı al, bir kopyasını UPX ile paketle ve ikisini de tlk-hex'te aç:
upx -d çalıştır, yeniden aç ve import'ların ile stringlerin geri geldiğini gör.mov, call, jmp…rax, rcx…C3 = ret gibi.sub_X gerçek adına döner.