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ş
- PE Dosya Formatına Hızlı Bakış
- Neden Import/Export Tabloları Önemli?
- Tehlikeli API Çağrılarına Örnekler
- Pratik Analiz: C++ ile PE Header Okuma
- Güvenlik Analizi: Tehlikeli Kombinasyonları Tespit Etme
- Gerçek Dünya Örneği: Vulnerable Driver (BYOVD) Tespiti
- Analiz Araçları
- Risk Değerlendirme Matrisi
- Yanlış Pozitiflerden Kaçınma
- Sonuç ve Öneriler
- Gelişmiş Konular
- Kaynaklar
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.
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,.rdatagibi 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ı.
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:
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:
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.
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.
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.)
#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.
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;
}
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.
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ı)
#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:
#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
- Microsoft — PE Format Dokümantasyonu
- Malware Analyst’s Cookbook (kitap)
- Practical Malware Analysis (kitap)
- The Art of Memory Forensics (kitap)