Yeni Başlayanlar için Anti-Debugging, Bölüm 1: Program Debugger'ı Nasıl Fark Eder
Linux'ta anti-debugging'in temelleri: tek tracer kuralı, ptrace ile kendini izleme, /proc içinde TracerPid, ebeveyn kontrolü, zamanlama ölçümü ve 0xCC breakpoint taraması; artı analistlerin her kontrole verdiği yanıtlar.
Yeni Başlayanlar için Anti-Debugging, Bölüm 1: Program Debugger'ı Nasıl Fark Eder#
Anti-debugging, bir programın tek bir soruyu yanıtlamak için kullandığı numaralar bütünüdür: "beni çalışırken izleyen biri var mı?" Packer'lar, DRM sistemleri, bankacılık truva atları ve crackme'ler hep bu soruyu sorar. Tersine mühendisler ise tersini sorar: "fark edilmeden nasıl izlerim?"
Bu ilk bölüm, hemen her Linux örneğinde karşılaşacağınız beş kontrolü, her birinin neden işe yaradığını ve analistin verdiği yanıtları anlatır. İkinci bölüm, öz-bütünlük kontrolleri ve anti-VM numaralarına derinleşir.
Her şeyin üstüne kurulduğu tek kural#
Linux'ta hata ayıklama ptrace üzerinden döner. Çekirdek, süreç başına tam olarak bir tracer'a izin verir. Bir debugger zaten bağlıysa, ikinci bir ptrace attach başarısız olur. Başlangıç seviyesindeki hemen her anti-debug kontrolü, çekirdeğe şunu sormanın yaratıcı bir yoludur: "üstümde zaten bir tracer var mı?"
analist çalıştırır: gdb ./crackme | v +-------------------------------+ | süreç bir tracer altında başlar| +-------------------------------+ | v kontrol 1: PTRACE_TRACEME hata verdi mi? -> çık veya sapıt kontrol 2: TracerPid sıfırdan farklı mı? -> çık veya sapıt kontrol 3: ebeveyn gdb/strace mi? -> çık veya sapıt kontrol 4: bu adım çok mu uzun sürdü? -> çık veya sapıt kontrol 5: .text içinde 0xCC var mı? -> çık veya sapıt | v gerçek payload ancak tüm kontroller geçerse çalışır
Diyagramı aklınızda tutun; yazının geri kalanı her satırı tek tek iner.
Kontrol 1: önce kendini izlemek#
Bir süreç, PTRACE_TRACEME ile çekirdekten kendisini izlemesini isteyebilir. Bir debugger onu zaten izliyorsa, istek -1 ile başarısız olur. Bu tek dönüş değeri, kitaptaki en eski anti-debug kontrolüdür:
#include <stdio.h> #include <sys/ptrace.h> int main(void) { if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) { puts("debugger tespit edildi"); return 1; } puts("temiz calisiyor"); return 0; }
Bir yan etki saldırganın işini iki kez kolaylaştırır: bir süreç kendini izlemeye başladıktan sonra başka kimse bağlanamaz. gdb -p $(pidof crackme) "Operation not permitted" ile döner.
Analistin yanıtları, hafiften ağıra:
- Daha sonra attach olmak yerine binary'yi gdb içinde başlatın; izlenen bir çalıştırmanın içinden gelen
PTRACE_TRACEMEbaşarılı olur çünkü tracer slotu aynı slot'tur. - Kontrolü disassembly'de bulunca yama yapın; genelde tek bir koşullu jump'tır.
ptrace'i saran vePTRACE_TRACEMEiçin hiçbir şey yapmadan başarı döndüren bir shim'iLD_PRELOADile yükleyin.
Kontrol 2: kendi karneni okumak#
Çekirdek, tracer'ın PID'ini /proc/self/status içinde TracerPid alanı olarak yayınlar. Sıfır serbest demektir, başka her şey izleniyor demektir:
#include <stdio.h> #include <string.h> static int izleniyor_mu(void) { FILE *f = fopen("/proc/self/status", "r"); char line[256]; while (fgets(line, sizeof(line), f)) { if (strncmp(line, "TracerPid:", 10) == 0) { int pid = 0; sscanf(line + 10, "%d", &pid); fclose(f); return pid != 0; } } fclose(f); return 0; } int main(void) { if (izleniyor_mu()) { puts("guzel deneme"); return 1; } puts("temiz calisiyor"); return 0; }
Deneyin:
./tracerpid_check # "temiz calisiyor" yazar strace ./tracerpid_check # "guzel deneme" yazar, strace de bir tracer'dir
Bu kontrol strace, ltrace (o da ptrace kullanır) ve gdb'yi birden yakalar. Analistin yanıtları:
fopen/fgetsetrafınaLD_PRELOADile bir shim yazıpTracerPid:satırlarını sıfırla yeniden yazın.- gdb'de okumadan sonra break koyun, karşılaştırma sonucunu çevirin, devam edin.
- Dala yama yapın; yine disassembly'de tek bir koşullu jump'tır.
Kontrol 3: ebeveynin kim#
Shell'den başlatılan bir binary'nin ebeveyni shell'dir. gdb'den başlatılanın ebeveyni gdb'dir. /proc/self/stat okumak ebeveyn PID'ini verir, /proc/<ppid>/comm ise adını:
cat /proc/self/stat | awk '{print $4}' # ppid alani cat /proc/$(ps -o ppid= -p $$)/comm
Kontrol, ebeveynin adını bir kara listeyle karşılaştırır: gdb, strace, ltrace, r2, radare2. Daha dikkatli örnekler adı açık metinde saklamak yerine hash'ler, çünkü içinde "gdb" geçen bir strings çıktısı analiste hediyedir.
Analistin yanıtları sıkıcı ve etkilidir: debugger binary'sinin adını değiştirin, ya da hedefi double-fork yapan bir başlatıcı altında çalıştırarak yakın ebeveynin init veya temiz bir sarmalayıcı olmasını sağlayın.
Kontrol 4: zaman en gürültülü tanıktır#
Kodda adım adım ilerlemek, normal çalıştırmadan milyonlarca kat yavaştır. Kendi çalışma süresini ölçen bir program, debugger'ı iki zaman damgası arasındaki devasa boşluk olarak görür:
#include <stdio.h> #include <time.h> static long simdi_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * 1000000000L + ts.tv_nsec; } int main(void) { long a = simdi_ns(); volatile long x = 0; for (long i = 0; i < 1000; i++) x += i; long b = simdi_ns(); if (b - a > 1000000L) { /* basit bir dongu cok daha az surmeli */ puts("cok yavas, biri adimliyor"); return 1; } puts("temiz calisiyor"); return 0; }
x86'da örnekler çoğu zaman rdtsc komutunu kullanır; bu CPU çevrimi sayar ve duvar saati hakkında yalan söyleyerek kandırılamaz. Her iki durumda da analistin yanıtı, ölçülen noktada koşuyu hızlı tutmaktır: birinci zaman damgasından önce ve ikinciden sonra break koyun, eşiğe yama yapın, ya da zamanın kendisini durdurabilen bir emülatör kullanın.
Kontrol 5: kendi breakpoint'lerini avlamak#
Yazılım breakpoint'i bir register'daki bayrak değildir. gdb, breakpoint koyduğunuz komutun ilk baytının üstüne 0xCC (int3) yazar ve tuzak ateşlenince orijinal baytı geri koyar. Yani bir program, kendi kodunu okuyarak kendi breakpoint'lerini bulabilir:
#include <stdio.h> extern char _etext; /* linker sembolu: .text sonu */ extern char _stext; /* .text baslangici */ static int int3_say(void) { int n = 0; for (char *p = _stext; p < _etext; p++) if ((unsigned char)*p == 0xCC) n++; return n; } int main(void) { if (int3_say() > 0) { puts("kodumda breakpoint bulundu"); return 1; } puts("temiz calisiyor"); return 0; }
Gerçek örnekler her şeyi taramaz; küçük bir fonksiyonun checksum'ını alıp derleme zamanı sabitiyle karşılaştırır. Fikir aynı, gürültü daha az.
Analistin yanıtları:
- Donanım breakpoint'i kullanın (gdb'de
hbreak). DR0-DR7 debug register'larında yaşarlar ve kod baytlarına dokunmazlar, yani tarama hiçbir şey bulamaz. 0xCCile bayt karşılaştıran dar döngüyü fark edince taramaya yama yapın.
Sinyal oyunları#
İzlenen bir süreç int3'e çarptığında çekirdek SIGTRAP gönderir. Bir program kendi SIGTRAP işleyicisini kurup bilerek int3 çalıştırabilir; işleyici çalışırsa her şey yolundadır, ama bir debugger tuzağı önce yutarsa işleyici hiç çalışmaz ve program anlar. Daha yumuşak bir sürüm, programın kendisinin ürettiği bir sinyalin işbirlikçi bir çocuk süreç üzerinden waitpid semantiğiyle geri raporlanıp raporlanmadığını kontrol eder. Ayrıntılar değişir, ilke değişmez: programın kendi kontrolündeki bir kanalda beklenmedik sessizlik bir tespittir.
Analistler, gdb'ye sinyali geçirmesini söyleyerek yanıtlar: handle SIGTRAP nostop noprint pass.
Debug register'ları üzerine bir not#
Donanım breakpoint'leri DR register'larını kullanır ve bir süreç bunları userspace'ten doğrudan okuyamaz; ama debug register alanına karşı ptrace(PTRACE_PEEKUSER), izlenen bir sürecin çekirdekten kendi DR değerlerini sormasına izin verir. Sıfırdan farklı DR0-DR3, donanım breakpoint'i var demektir. Bazı packer'lar devam etmeden önce bunları PTRACE_POKEUSER ile temizler; bu hem kontrol hem sabotajı tek syscall'de birleştirir.
Analistin genel oyun planı#
Yukarıdaki her kontrol dört hamleden birine düşer:
- Kontrolden kaçın. Attach olmak yerine debugger içinde başlatın; yazılım breakpoint'i yerine donanım breakpoint'i kullanın.
- Kontrole yalan söyleyin.
ptrace,fopenveya zamanlama sonuçlarını sahteleyenLD_PRELOADshim'leri. - Kontrole yama yapın. Tek bir koşullu jump'ı çevirin; binary sormayı bırakır.
- Zemin gerçeği değiştirin. Analistin zamanı, register'ları ve belleği programın ayaklarının altında kontrol ettiği bir emülatör (QEMU, unicorn) altında çalıştırın.
Kontrol başına en ucuz hamleyi seçin. Yama kalıcıdır ama önce kontrolü bulmayı gerektirir; shim'ler hızlıdır ama sadece dinamik çağrıları etkiler.
Kontrolü disassembly'de okumak#
Beş kontrolü bilmek bir şeydir; strip edilmiş bir binary'de onları fark etmek asıl beceridir. Taranacak imzalar:
# syscall numaralari ve sihirli sabitler kontrolleri ele verir objdump -d ./crackme | grep -i "0xcc" objdump -d ./crackme | grep -i ptrace strings ./crackme | grep -i "tracerpid\|/proc/self\|gdb\|strace"
Hata ayıklamayla işi olmayan bir programdaki ptrace çağrısı bir bulgudur. Baytları 0xCC ile karşılaştıran dar bir döngü bir bulgudur. Argümanı /proc/self/status olan bir fopen çağrısı bir bulgudur. Hiçbir şeye yama yapmadan önce her noktayı notlarınıza işaretleyin, çünkü packer'lar aynı kontrolü çoğu zaman üç yere koyar ve payload'u ancak üçü de temiz rapor ederse ateşler.
Kontrolleri birleştirmek: dürüst maliyet modeli#
Tek kontrol bir hız tümseğidir. Farklı tarzlarda üç kontrol gerçek bir bütçedir:
- Statik bir
LD_PRELOADshim'i kütüphane seviyesindeki kontrolleri yanıtlar ama inline syscall'lere bir şey yapmaz. - Yama her şeyi yanıtlar ama nokta başına analiz süresi maliyeti vardır.
- Emülatör her şeyi aynı anda yanıtlar ama kurulum süresi maliyeti vardır ve kendisini araştıran programlarda kırılır.
Zararlı yazı yazarları bu matematiği bilir; gerçek örneklerin kategorileri karıştırmasının nedeni budur: bir ptrace kontrolü, bir zamanlama kontrolü, bir checksum. Tek numarayı değil, karışımı bekleyin.
2026 notu: kontroller hâlâ çalışıyor, çevre sertleşti#
Yukarıdaki beş kontrol 2026'da da birebir geçerli; değişen, çekirdeğin ve araçların bu kontrollerin etrafındaki varsayılanların sertleşmiş olması:
kullanıcı | v [ shell / systemd ] <-- ptrace_scope, Yama, LSM politikaları | v [ anti-debug ikilisi ] ----- ptrace /proc/self/status rdtsc 0xCC tarama | | | v | çekirdek: yalnızca izinli tracer | | | v +--- izlenen koşu: TracerPid 0-dan farklı, zamanlama sapması görünür
- Yama
ptrace_scope(/proc/sys/kernel/yama/ptrace_scope) dağıtım varsayılanlarında genellikle 1'dir: bir süreç yalnızca kendi çocuklarına veya açık rızayla tracer olarak bağlanabilir. Bu, hazırptracetabanlı logger'ları ve hazır anti-anti-debug olmadan rastgele attach'i kısar; analistin cevabı hâlâ aynıdır, süreci debugger içinde başlatmak daha da önem kazanır. - Seccomp ve user-namespace sınırları anti-debug örneklerinin
ptrace,process_vm_readvveperf_event_opengibi yakınlarını filtreler; inline syscall kullanan kontroller bu çağrıları saranLD_PRELOADshim'lerini atlatır. Kontrol başına maddi maliyet büyür. - eBPF ile izleme, analistin çekirdek seviyesinde sahip olduğu yeni yüzeydir. Hedef
ptracekanalını kendine bağlayarak ilk hunuyu tıkar; ama eBPF tabanlı bir gözlemcitracepoint:syscallsüzerinden aynı çağrıyı hedefin bilgisi dışında izleyebilir. Hedef eBPF programlarına saldırmaya kalkarsa,perf_event_paranoidve eBPF LSM devreye girer. - Zamanlama kontrolleri hâlâ gürültülüdür;
rdtscsanallaştırılmış ortamda yine çevrim sayar ama sanallaştırıcı zamanı sanal adrese bağlamıştır. Emülatör zaten zamanı dondurur; bu yüzden zamanlama kontrolü üç katmanlı savunmanın ne zaman gerektiğini anlatır, tek başına son nokta değildir.
Pratik sonuç: 2026'da ikili analizinin ilk yirmi dakikasında eBPF tracepoint'leri, ptrace imzası ve TracerPid okumalarını yan yana koyun. Üçü aynı noktaya işaret ediyorsa packer aynı kontrolü üç yere koymuştur; birine yama yapmak diğer ikisini tetikler.
Bir güven sınırı olarak düşünün#
Beş kontrolün ortak temasını adlandırmakta fayda var: hepsi, kullanıcı alanındaki programın çekirdekten veya yan kanallardan bir tutarlılık sözü istemesi. Tek tracer kuralı çekirdeğin sözüdür; TracerPid o sözün görünür hali; ebeveyn kontrolü ise çekirdeğin tutmadığı bir sözün kullanıcı tarafındaki tahmindir; zamanlama sınırı ise çekirdek dışı katmanların, sanallaştırıcının ve emülatörün dürüstlüğüne dayanır; 0xCC taraması ise debugger'ın kendisinin değiştirdiği bir yazılımı kendinden sorma girişimidir. Bu yüzden her kontrolün yanına durduğu katmanı yazın; aynı kontrol hem çekirdek sözüne hem debugger'ın iyi niyetine dayanıyorsa, saldırgan genellikle en zayıf katmandan gireceği için birini güçlendirmek diğerine dokunmaz.
Sınırı nerede çizmeli#
Kontrol ile yama arasındaki çizgi, kontrolün neye dayandığıdır. Çekirdek sözüne dayanan kontrolü çekirdek ayarıyla çözemezseniz, debugger konfigürasyonu veya küçük bir yama en ucuz yoldur; debugger davranışına dayanan kontrolü ise debugger'ı yeniden adlandırarak veya donanım breakpoint'ine geçerek atlatmak çoğu zaman yeter. 2026'da eBPF gözlemciliği bu iki uç arasında durur: hedeften bağımsız ama çekirdekte, yani iki tekniğin de tepe noktasına oturur. Hangi kontrolün hangi katmana tutunduğunu yazmadan yama yapmaya başlamayın; ucuz hamleyi doğru katmandan seçin.
| Kontrol | Dayandığı katman | En ucuz analist hamlesi |
|---|---|---|
PTRACE_TRACEME | Tek tracer kuralı (çekirdek) | Debugger içinde başlat veya shim |
TracerPid okuma | /proc yayını | okuma etrafına shim, sonra yama |
| Ebeveyn adı | Kullanıcı alanı tahmini | Binary'yi yeniden adlandır, double-fork başlatıcı |
| Zamanlama boşluğu | Sanallaştırıcı/emülatör ortamı | İki damga arasında hbreak + eşik yama |
0xCC tarama | Debugger davranışı | Donanım breakpoint (hbreak) |
SIGTRAP sessizliği | Çekirdek sinyal teslimi | handle SIGTRAP nostop noprint pass |
Bu tabloyu paketleyici için tersten okuyun: her satır, hangi analiz tekniğini yakalamak için tasarlandıysa o tekniğin maliyetini artırır. Katmanları karıştırmak, analistin tek bir shim veya tek bir yama ile bütün ağı birden dökmesini zorlaştırır. Gerçek örneklerde bu satırların çoğu aynı binary'de birlikte yaşar; her birinin tetiklenme eşiği ve geri raporlama kanalı farklıdır.
Lab alıştırmaları#
PTRACE_TRACEMEörneğini derleyin. Normal, gdb altında ve strace altında çalıştırın. Farkları not edin.TracerPidkontrolünü yazın ve strace altında çalışırken/proc/self/status'a elle bakın.- gdb'de zamanlama kontrolü binary'sinin içine breakpoint koyun, birkaç komut adımlayın ve kontrolün ateşlendiğini izleyin. Sonra önce ve sonra break koyup geçtiğini izleyin.
0xCCtarayıcısını derleyin, gdb altındamainiçine yazılım breakpoint'i koyun, çalıştırın ve bulunan baytları sayın.hbreakile tekrarlayın.ptraceiçinLD_PRELOADshim'ini yazın ve binary'ye dokunmadan 1. kontrolü alt edin.
Buradan geriye kalan#
Artık beş kontrolü isimlendirebilir, ilk üçünü besleyen tek tracer kuralını açıklayabilir ve kontrol başına bir analist hamlesi seçebilirsiniz. Bu kelime dağarcığı, bundan sonra okuyacağınız her anti-debug yazısı ve crackme anlatımı için giriş biletidir.
Lab bakımı#
Crackme'i her çalıştırmadan önce tanımanız gereken bir şey: paketleyiciler aynı kontrolü genellikle birkaç yere yayar ve devreye girmek için hepsinin temiz rapor üretmesini bekler. Tek bir atlatılan nokta, payload'ın çalışmama sebebi değildir; her kontrol noktasını ayrı işaretlenmiş bir yama olarak ele alın.
Katmanlı kontroller: self-checksum, kapılı şifre çözme, zamanlama, anti-VM#
Mekanik kontroller fark edilmenizi sağlar; katmanlı kontroller tek bir bypass'un yarım kalmasına neden olur. 2020 sonrası örneklerde dördü geri döner:
-
.textüzerinde self-checksum. İkili kendi kodunu FNV ile hash'ler ve sabit bir değerle karşılaştırır. Yamalanan bir bayt hash'i değiştirir. Analistin karşı önlemi: çalışan kopyayı tutarlı tutmak veya beklenen hash'i hesap edip karşılaştırma hedefini yamalayın. Hash anlık değere karşı karşılaştırılıyorsa, kontrol yerinejz/jnzeşitliğini ters çevirin. -
Kapılı şifre çözme. Bir bölüm, tüm kapılar geçilene kadar şifreli kalır: debugger yok, beklenen zamanlama farkı, doğru checksum, düzgün DMI. Payload bir anahtarı bu kontrollerden türetir ve ancak hepsi sağlanırsa
mprotect+decrypt eder. Bu, "fark ettin mi" sorusunu "tüm kapıları düzeltebildin mi"ye çevirir. Güvenilir lab karşı önlemi: binary'yi bir hipervizörde ya da CPU yamalarıyla VM'de çalıştırın veya her kapının dönüşünü Frida hook'uyla işaretleyin (Interceptor.attachcheck fonksiyonuna) ve0döndürün. -
CPU başına zamanlama döngüleri. İki veya daha fazla
rdtscçifti her işlemi sarar; her preempt debugger olarak sayılır. Örnekler ayrıcasched_getcpuve beklenen gecikme tablolarına bakar. Karşı önlem: analiz VM'nizde TSC'yi sanallaştırın veya döngülerin etrafındaptracedurma noktaları kullanın; böylece döngü fark görmez. -
Anti-VM sondaları.
/sys/class/dmi/id/içindeki DMI string'leri, CPUID hipervizör biti, MAC OUI, QEMU/VBOX servisleri. Karşı önlem:dmimodülünü değiştirerek sysfs/DMI'yi düzeltin, hipervizör bitleri kapalı KVM host-only kullanın veya VM profili üzerindencustom CPUIDseçin.
Her kontrolü bir pivot olarak düşünün. Başarılı bir kapıyı "temiz" olarak işaretleyen bir gdb tek satırı genellikle tek bir dal için yeterli olur; katmanlı binary yeniden kontrol eder. Lab yazısında her yama yerini ve her tetikleyen kareyi kaydedin.
İkinci bölüm ne anlatıyor#
İkinci bölüm tekil kontrollerin ötesine geçer: bütün bölümler üzerinde öz-checksum'lar, hiçbir kontrol ateşlenmezse kendini çözen kod, CPU başına ayarlanmış zamanlama döngüleri, /sys ve CPUID üzerinden anti-VM sondaları ve bunları tek tek kesen gdb betikleri. Buradaki beş kontrol mekanik geldiyse, bir dahaki sefere katmanlı olanlar satranç gibi gelecek. Kontroller tek başına değil, aynı ikili içindeki diğerleriyle birlikte okunmalıdır.
Ne düşünüyorsun?
Tepki bırakarak geri bildirim ver