PE Header Analizi ile Uygulama Güvenliğini Değerlendirme: Çalıştırmadan Tehdit Tespiti

Bir dosya eline geldiğinde, “şunu bir açıp bakayım” dürtüsü çok güçlüdür. Ama güvenlik tarafında ben çoğu zaman tam tersini yaparım: Önce çalıştırmadan konuşmasını isterim.

Windows dünyasında bunun en pratik yolu, PE (Portable Executable) başlık yapısına bakmaktır. Özellikle Import Address Table (IAT) ve Export Address Table (EAT), bir uygulamanın hangi Windows API’lerine “dayandığını” ve dışarıya neler sunduğunu gösterir. Bu bilgiler, potansiyel kötü niyetli davranışları daha erken aşamada yakalamak için altın değerindedir.

Giriş

  • Bu yazının odağı: Bir uygulamayı çalıştırmadan PE başlığından “risk sinyalleri” yakalamak.
  • İncelediğimiz yerler: IAT (Import) ve EAT (Export).
  • Hedef: “Kesin hüküm” değil; triage (ön eleme) ve “derin analize değer mi?” sorusunu hızlandırmak.
Kısa güvenlik notu

Import/Export analizi tek başına “bu dosya kesin zararlı” dedirtmez. Ama şüpheli kombinasyonları erken fark etmeni sağlar. Bu yazı istismar üretmeyi anlatmaz; savunma/analiz perspektifinde kalır.

PE Dosya Formatına Hızlı Bakış

PE formatı, Windows işletim sisteminde çalıştırılabilir dosyaların (.exe, .dll, .sys) standart yapısıdır. Temel bileşenler şöyle:

  • DOS Header: Eski DOS uyumluluğu için.
  • PE Header: Dosya türü, hedef mimari, zaman damgası gibi bilgiler.
  • Section Headers: .text, .data, .rdata gibi bölüm tanımlamaları.
  • Import Directory (IAT): Kullanılan harici fonksiyonlar.
  • Export Directory (EAT): Dışa aktarılan fonksiyonlar.

Bu makalede en çok Import ve Export tablolarına odaklanacağız.

Neden Import/Export Tabloları Önemli?

Import tablosu, bir uygulamanın hangi DLL’lerden hangi fonksiyonları çağırdığını gösterir. Bazı API’ler tek başına masum olabilir; ama “kombinasyonlar” risk sinyali üretir.

Örnek birkaç yüksek sinyal:

  • CreateRemoteThread → Başka bir süreçte (process) kod çalıştırma senaryolarında görülebilir.
  • WriteProcessMemory → Bellek manipülasyonu.
  • VirtualAllocEx → Uzak process içinde bellek ayırma.
  • NtLoadDriver / ZwLoadDriver → Kernel seviyesinde driver yükleme teması.
Buradaki mantık

Bu fonksiyonlar “var” diye dosya kötü olmaz. Ama bir araya geldiklerinde, “bu dosya ne yapmaya çalışıyor olabilir?” sorusunu ciddi hale getirir.

Tehlikeli API Çağrılarına Örnekler

1) Process Injection İşaretleri

Bir yazılımın başka bir process’e müdahale etmesi için tipik olarak şu API kümesini görürsün:

Örnek “küme” (mantık göstermek için)
kernel32.dll
├── OpenProcess
├── VirtualAllocEx
├── WriteProcessMemory
├── CreateRemoteThread
└── VirtualProtectEx

Bu beşli aynı executable’da import edilmişse, “klasik” tekniklerle örtüşen bir yetenek seti var demektir. Bu noktada bağlam şart: EDR/anti-cheat/overlay gibi ürünler de benzer API’lere ihtiyaç duyabilir.

2) Güvenlik Açığı Olan Driver Kullanımı (BYOVD bağlamı)

Bazı eski ya da hatalı sürücüler (driver) bilinen zafiyetler içerebilir ve kötüye kullanılabilir. Eğer bir uygulama driver yükleme/servis kurma temasına giriyorsa dikkatli olmak gerekir.

Şu API’ler bu tarafta “sinyal” üretir:

Driver yükleme teması olan API’ler (sinyal amaçlı)
ntdll.dll
├── NtLoadDriver
├── NtUnloadDriver
└── ZwLoadDriver

advapi32.dll
└── CreateService (DRIVER türünde)

3) Anti-Debugging ve Anti-VM Teknikleri

Meşru yazılımlar bazen koruma/anti-tamper için bu temalara girer; ama malware dünyasında da sık görülür. Bu yüzden “tek başına değil, kümeyle” düşünmek gerekir.

Anti-debug sinyali üreten bazı API’ler
ntdll.dll
├── NtQueryInformationProcess
├── NtSetInformationThread
└── NtQuerySystemInformation

kernel32.dll
├── IsDebuggerPresent
├── CheckRemoteDebuggerPresent
└── OutputDebugString

4) Ağ İletişimi ve Veri Sızdırma İhtimali

Ağ API’leri her zaman şüpheli değildir; ama “beklenmeyen” bir uygulama tipinde görülürse soru işareti doğurur.

Ağ API’leri (beklenmedik yerde risk sinyali)
ws2_32.dll
├── WSAStartup
├── socket
├── connect
├── send
└── recv

wininet.dll
├── InternetOpen
├── InternetConnect
├── HttpSendRequest
└── InternetReadFile

Pratik Analiz: C++ ile PE Header Okuma

Aşağıdaki örnek, bir PE dosyasının import tablosunu okumayı gösteren basitleştirilmiş bir C++ örneğidir. (Üretim kalitesi bir parser için 32/64 ayrımı, bound import, delay import, robust RVA→FOA dönüşümü gibi ek kontroller gerekir.)

C++: Import tablosunu okuma (basitleştirilmiş)
#include <windows.h>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>

struct ImportedFunction {
    std::string dllName;
    std::string functionName;
};

std::vector<ImportedFunction> ParseImportTable(const char* filePath) {
    std::vector<ImportedFunction> imports;

    // Dosyayı aç
    std::ifstream file(filePath, std::ios::binary);
    if (!file.is_open()) {
        std::cerr << "Dosya acilamadi!" << std::endl;
        return imports;
    }

    // Dosyayı belleğe oku
    file.seekg(0, std::ios::end);
    size_t fileSize = static_cast<size_t>(file.tellg());
    file.seekg(0, std::ios::beg);

    std::vector<BYTE> buffer(fileSize);
    file.read(reinterpret_cast<char*>(buffer.data()), fileSize);
    file.close();

    // DOS header kontrolü
    PIMAGE_DOS_HEADER dosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(buffer.data());
    if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
        std::cerr << "Gecersiz DOS signature!" << std::endl;
        return imports;
    }

    // NT headers
    PIMAGE_NT_HEADERS ntHeaders = reinterpret_cast<PIMAGE_NT_HEADERS>(buffer.data() + dosHeader->e_lfanew);
    if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) {
        std::cerr << "Gecersiz PE signature!" << std::endl;
        return imports;
    }

    // Import Directory
    DWORD importDirRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    if (importDirRVA == 0) {
        std::cout << "Import tablosu bulunamadi." << std::endl;
        return imports;
    }

    // RVA'yı file offset'e çevir (basit yaklaşım)
    PIMAGE_SECTION_HEADER sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);
    DWORD importDirOffset = 0;

    for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
        if (importDirRVA >= sectionHeader[i].VirtualAddress &&
            importDirRVA < sectionHeader[i].VirtualAddress + sectionHeader[i].SizeOfRawData) {
            importDirOffset = importDirRVA - sectionHeader[i].VirtualAddress + sectionHeader[i].PointerToRawData;
            break;
        }
    }

    PIMAGE_IMPORT_DESCRIPTOR importDesc = reinterpret_cast<PIMAGE_IMPORT_DESCRIPTOR>(buffer.data() + importDirOffset);

    // Her bir DLL'i işle
    while (importDesc->Name != 0) {
        // DLL adını al
        DWORD nameOffset = 0;
        for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
            if (importDesc->Name >= sectionHeader[i].VirtualAddress &&
                importDesc->Name < sectionHeader[i].VirtualAddress + sectionHeader[i].SizeOfRawData) {
                nameOffset = importDesc->Name - sectionHeader[i].VirtualAddress + sectionHeader[i].PointerToRawData;
                break;
            }
        }

        char* dllName = reinterpret_cast<char*>(buffer.data() + nameOffset);

        // Import Name Table
        DWORD thunkOffset = 0;
        for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
            if (importDesc->OriginalFirstThunk >= sectionHeader[i].VirtualAddress &&
                importDesc->OriginalFirstThunk < sectionHeader[i].VirtualAddress + sectionHeader[i].SizeOfRawData) {
                thunkOffset = importDesc->OriginalFirstThunk - sectionHeader[i].VirtualAddress + sectionHeader[i].PointerToRawData;
                break;
            }
        }

        PIMAGE_THUNK_DATA thunk = reinterpret_cast<PIMAGE_THUNK_DATA>(buffer.data() + thunkOffset);

        // Her bir fonksiyonu işle
        while (thunk->u1.AddressOfData != 0) {
            if (!(thunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)) {
                DWORD nameRVA = static_cast<DWORD>(thunk->u1.AddressOfData);
                DWORD funcNameOffset = 0;

                for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
                    if (nameRVA >= sectionHeader[i].VirtualAddress &&
                        nameRVA < sectionHeader[i].VirtualAddress + sectionHeader[i].SizeOfRawData) {
                        funcNameOffset = nameRVA - sectionHeader[i].VirtualAddress + sectionHeader[i].PointerToRawData;
                        break;
                    }
                }

                PIMAGE_IMPORT_BY_NAME importByName = reinterpret_cast<PIMAGE_IMPORT_BY_NAME>(buffer.data() + funcNameOffset);

                ImportedFunction func;
                func.dllName = dllName;
                func.functionName = importByName->Name;
                imports.push_back(func);
            }
            thunk++;
        }

        importDesc++;
    }

    return imports;
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cout << "Kullanim: " << argv[0] << " <pe_dosyasi>" << std::endl;
        return 1;
    }

    auto imports = ParseImportTable(argv[1]);

    std::cout << "Import edilen fonksiyonlar:\\n" << std::endl;
    for (const auto& func : imports) {
        std::cout << func.dllName << " -> " << func.functionName << std::endl;
    }

    return 0;
}

Güvenlik Analizi: Tehlikeli Kombinasyonları Tespit Etme

Import listesini çıkardıktan sonra, asıl değer genelde “pattern” yakalamaktır. Aşağıdaki örnekler, şüpheli kombinasyonları kaba bir şekilde işaretlemek için kullanılabilir.

C++: Şüpheli kombinasyonları işaretleme (örnek)
bool IsProcessInjectionSuspicious(const std::vector<ImportedFunction>& imports) {
    bool hasOpenProcess = false;
    bool hasVirtualAllocEx = false;
    bool hasWriteProcessMemory = false;
    bool hasCreateRemoteThread = false;

    for (const auto& func : imports) {
        if (func.functionName == "OpenProcess") hasOpenProcess = true;
        if (func.functionName == "VirtualAllocEx") hasVirtualAllocEx = true;
        if (func.functionName == "WriteProcessMemory") hasWriteProcessMemory = true;
        if (func.functionName == "CreateRemoteThread") hasCreateRemoteThread = true;
    }

    // Klasik process injection pattern
    return hasOpenProcess && hasVirtualAllocEx &&
           hasWriteProcessMemory && hasCreateRemoteThread;
}

bool HasDriverLoadingCapability(const std::vector<ImportedFunction>& imports) {
    for (const auto& func : imports) {
        if (func.functionName == "NtLoadDriver" ||
            func.functionName == "ZwLoadDriver") {
            return true;
        }

        if (func.dllName == "advapi32.dll" && (func.functionName == "CreateServiceA" || func.functionName == "CreateServiceW")) {
            return true;
        }
    }
    return false;
}

bool HasAntiDebuggingAPIs(const std::vector<ImportedFunction>& imports) {
    int antiDebugCount = 0;

    for (const auto& func : imports) {
        if (func.functionName == "IsDebuggerPresent" ||
            func.functionName == "CheckRemoteDebuggerPresent" ||
            func.functionName == "NtQueryInformationProcess" ||
            func.functionName == "NtSetInformationThread") {
            antiDebugCount++;
        }
    }

    // 2 veya daha fazla anti-debug API kullanımı şüpheli
    return antiDebugCount >= 2;
}
Gerçek hayatta kritik olan kısım

Bu fonksiyonlar “var” diye dosyaya mühür vurulmaz. Ama birden fazla şüpheli küme aynı dosyada birleşiyorsa, ben dosyayı “derin analiz” sırasına taşırım. Çünkü risk genelde “tek API” değil, “akış”tır.


Gerçek Dünya Örneği: Vulnerable Driver (BYOVD) Tespiti

Bazı tehditler, BYOVD (Bring Your Own Vulnerable Driver) yaklaşımıyla kernel seviyesinde ayrıcalık kazanmaya çalışır. Bu yüzden “driver teması” ve “bilinen zafiyetli driver isimleri” triage’da değerli bir sinyaldir.

Bu bölüm savunma amaçlıdır

Buradaki amaç “nasıl yapılır” anlatmak değil; bilinen risk sınıfını tanıyıp erken uyarı üretmektir. Eğer bir uygulama driver yükleme kabiliyeti taşıyor ve paketinde şüpheli bir .sys barındırıyorsa, bu ciddi bir kırmızı bayrak olabilir.

Bilinen örnek isimler (farkındalık amaçlı):

  • RTCore64.sys (MSI Afterburner ekosisteminde görülen sürümler; CVE-2019-16098 bağlamı sık anılır)
  • cpuz141_x64.sys (CPU-Z tarafında eski sürümler; CVE-2017-15303 bağlamı)
  • AsUpIO64.sys / AsIO.sys (bazı ASUS bileşenleriyle ilişkilendirilen sürümler)
  • DBUtil_2_3.sys (Dell ekosisteminde CVE-2021-21551 bağlamı)
  • PROCEXP.sys (eski bazı paketlerde adı geçen sürücüler)
  • gdrv.sys (bazı ekosistemlerde görülen sürücü adı)
C++: Dosya içinde sürücü adı izi arama (çok basitleştirilmiş)
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

std::vector<std::string> knownVulnerableDrivers = {
    "RTCore64.sys",
    "cpuz141_x64.sys",
    "AsUpIO64.sys",
    "PROCEXP.sys",
    "DBUtil_2_3.sys",
    "AsIO.sys",
    "gdrv.sys"
};

bool CheckForEmbeddedVulnerableDriver(const char* filePath) {
    // Not: Gerçek "embedded driver" tespiti için .rsrc ve paket yapısını analiz etmek gerekir.
    // Bu örnek sadece dosya içinde driver ismi string'i geçiyor mu diye kaba bir sinyal üretir.

    std::ifstream file(filePath, std::ios::binary);
    if (!file.is_open()) return false;

    std::string content((std::istreambuf_iterator<char>(file)),
                        std::istreambuf_iterator<char>());

    for (const auto& driver : knownVulnerableDrivers) {
        if (content.find(driver) != std::string::npos) {
            std::cout << "UYARI: Bilinen riskli driver ismi tespit edildi: "
                      << driver << std::endl;
            return true;
        }
    }
    return false;
}

Analiz Araçları

Ücretsiz Araçlar

  • PE-bear — Görsel PE analizi
  • CFF Explorer — Detaylı PE görüntüleme/düzenleme
  • PEview — Basit PE görüntüleme
  • Dependencies — Import/Export bağımlılık analizi
  • PEiD — Packer/compiler tespiti (eski ama hâlâ referans olarak anılır)

Profesyonel / İleri Seviye

  • IDA Pro — Ters mühendislik ve statik analiz
  • Ghidra — Ücretsiz, güçlü bir alternatif
  • x64dbg / OllyDbg — Dinamik analiz ve debugging
  • PE Studio — Otomatik güvenlik değerlendirmesi

Risk Değerlendirme Matrisi

Pratikte ben “tek sinyal” yerine, sinyalleri bir skora dönüştürmeyi seviyorum. Aşağıdaki tablo basit bir yaklaşım:

Özellik Risk Puanı
Process injection API’leri (4/4 var)+50
Driver yükleme yetenekleri+40
Anti-debugging API’leri (2+ adet)+30
Beklenmedik ağ iletişimi+25
Kod şifreleme/obfuscation sinyali+20
Keylogger teması (ör. GetAsyncKeyState vb.)+35
Güvenlik yazılımı manipülasyonu teması+45
Bilinen vulnerable driver embedding sinyali+60

Toplam Puan Yorumlama:

  • 0–30: Düşük risk
  • 31–70: Orta risk (detaylı analiz önerilir)
  • 71–100: Yüksek risk (şüpheli)
  • 100+: Kritik risk (muhtemelen kötü niyetli)

Yanlış Pozitiflerden Kaçınma

Meşru yazılımlar da bu API’leri kullanabilir. Örneğin:

  • Antivirüs/EDR yazılımları süreç/bellek API’lerine ihtiyaç duyabilir.
  • Oyun anti-cheat sistemleri anti-debugging ve kernel driver kullanabilir.
  • Sistem yönetim araçları driver yükleme yetkisine ihtiyaç duyabilir.
  • Debugger’lar doğal olarak debugging API’lerini kullanır.

Bu yüzden bağlamı ve dijital imzayı da kontrol etmek gerekir. Aşağıdaki örnek, bir dosyanın Authenticode imzasını kaba şekilde doğrulamak için kullanılan WinVerifyTrust akışına örnektir:

C++: Dijital imza kontrolü (WinVerifyTrust - örnek)
#include <windows.h>
#include <wintrust.h>
#include <softpub.h>

#pragma comment(lib, "wintrust.lib")

bool IsDigitallySigned(const wchar_t* filePath) {
    WINTRUST_FILE_INFO fileInfo{};
    fileInfo.cbStruct = sizeof(fileInfo);
    fileInfo.pcwszFilePath = filePath;

    WINTRUST_DATA data{};
    data.cbStruct = sizeof(data);
    data.dwUIChoice = WTD_UI_NONE;
    data.fdwRevocationChecks = WTD_REVOKE_NONE;
    data.dwUnionChoice = WTD_CHOICE_FILE;
    data.pFile = &fileInfo;
    data.dwStateAction = WTD_STATEACTION_VERIFY;

    GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
    LONG status = WinVerifyTrust(nullptr, &policyGUID, &data);

    // State'i kapatmayı unutma
    data.dwStateAction = WTD_STATEACTION_CLOSE;
    WinVerifyTrust(nullptr, &policyGUID, &data);

    return (status == ERROR_SUCCESS);
}

Sonuç ve Öneriler

PE Header analizi, bir uygulamanın güvenilirliğini değerlendirmede güçlü bir ilk adımdır; ama tek başına yeterli değildir. Benim yaklaşımım şu: “Sinyali yakala, sonra kanıtla.”

Kapsamlı analiz için önerilen kombinasyon

  • Statik analiz: PE Header, strings, entropy analizi, import/export pattern’ları
  • Dinamik analiz: İzole ortamda/sandbox’ta çalıştırma, davranış izleme
  • Network analizi: Trafik inceleme (beklenmeyen bağlantılar, DNS, HTTP pattern’ları)
  • Memory analizi: Gerekirse RAM dump ve bellek artefaktları

Gelişmiş Konular

Temeli oturttuktan sonra, bu başlıklar analiz kalitesini ciddi artırır:

  • Packer Detection: UPX, Themida, VMProtect gibi packer’ları tespit etme
  • Code Cave Analysis: Beklenmedik kod bölgelerini bulma
  • Section Analysis: Garip permission kombinasyonları (ör. RWX section)
  • Entropy Analysis: Şifrelenmiş/sıkıştırılmış bölgeleri tespit etme
  • TLS Callbacks: Erken çalışan anti-debugging/başlatma kodları

Kaynaklar


İlginizi çekebilecek diğer konular

Yorum Bırakın

E-posta adresiniz yayınlanmayacaktır. Zorunlu alanlar * ile işaretlenmiştir