C++ ile PE Header Okuma

Windows masaüstünde çalışan herhangi bir ygulmayı açmadan aklınza birkaç soru gelebilir: “Bu binary ASLR destekli mi?”, “CFG var mı?”, “x64 mü, kaç section var?” diye sorabilirsiniz.Benim cavbım "çalıştırmadan önce uygulama konuşsun." olacaktır PE Header tam da bunu yapar — dosyanın kendisi, Windows’a ne olduğunu anlatır.

Ne yapıyoruz?

  • Bir EXE/DLL dosyasını dosyadan açıp memory-map ediyoruz.
  • IMAGE_DOS_HEADERIMAGE_NT_HEADERS(32/64) → Section Table akışını doğruluyoruz.
  • Önemli alanları yazdırıyoruz: Machine, Sections, EntryPoint, ImageBase, Subsystem, DllCharacteristics.
  • ASLR/DEP/CFG/SEH açısından “iz” bırakan dizinleri de gösteriyoruz (Reloc, LoadConfig, Exception, Security).

Neden C++ ve neden “dosyadan” okumak?

Burada hedefim “debugger açmadan” veya “process içine girmeden” PE Header’ı görmek. Çünkü bir dosya eline geçtiğinde ilk adım çoğu zaman budur: Bu dosya ne? (x86/x64, DLL mi EXE mi, mitigations var mı, hangi tablolar var?)

C++ tarafında Win32 API ile dosyayı map edip PE yapılarını (winnt.h) kullanmak, hem doğru hem de hızlı bir yol. Üstelik “güncel Visual Studio” ile modern C++ (RAII, std::filesystem) kullanınca kod da temiz kalıyor.

Güvenlik notu

Bu uygulama dosyayı çalıştırmaz. Sadece bytes okur ve header parse eder. Yine de şüpheli dosyaları izole ortamda incelemek iyi bir alışkanlıktır.

Visual Studio’da proje kurulumu

  1. File → New → Project → “Console App” (C++) seç.
  2. Project Properties → C/C++ → LanguageC++ Language Standard: ISO C++20 (veya daha güncel).
  3. Yeni bir dosya oluştur: pe_viewer.cpp
  4. Derle: x64 seçili olsa iyi (ama bu kod hem x86 hem x64 PE’yi okuyabilir).

PE Header okuyucu uygulama (C++20)

pe_viewer.cpp (C++20 / Win32, açıklamalı)
#define NOMINMAX
#include <Windows.h>

#include <cstdint>
#include <filesystem>
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
#include <stdexcept>
#include <cstddef>   // std::byte
#include <cstring>   // memcpy

// ------------------------------
// Küçük bir RAII HANDLE sarmalayıcı
// ------------------------------
class unique_handle {
public:
    unique_handle() noexcept = default;
    explicit unique_handle(HANDLE h) noexcept : h_(h) {}
    ~unique_handle() { reset(); }

    unique_handle(const unique_handle&) = delete;
    unique_handle& operator=(const unique_handle&) = delete;

    unique_handle(unique_handle&& other) noexcept : h_(other.h_) { other.h_ = nullptr; }
    unique_handle& operator=(unique_handle&& other) noexcept {
        if (this != &other) {
            reset();
            h_ = other.h_;
            other.h_ = nullptr;
        }
        return *this;
    }

    HANDLE get() const noexcept { return h_; }
    explicit operator bool() const noexcept { return h_ && h_ != INVALID_HANDLE_VALUE; }

    void reset(HANDLE h = nullptr) noexcept {
        if (h_ && h_ != INVALID_HANDLE_VALUE) CloseHandle(h_);
        h_ = h;
    }

private:
    HANDLE h_ = nullptr;
};

// ------------------------------
// Hata mesajını okunur yapmak için
// ------------------------------
std::wstring win32_error_message(DWORD err) {
    LPWSTR buf = nullptr;
    const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;

    const DWORD len = FormatMessageW(flags, nullptr, err, 0, (LPWSTR)&buf, 0, nullptr);
    std::wstring msg = (len && buf) ? std::wstring(buf, buf + len) : L"(no message)";
    if (buf) LocalFree(buf);
    return msg;
}

// ------------------------------
// PE yardımcıları (string'e çevirme)
// ------------------------------
std::wstring machine_to_string(WORD machine) {
    switch (machine) {
    case IMAGE_FILE_MACHINE_I386:  return L"x86 (I386)";
    case IMAGE_FILE_MACHINE_AMD64: return L"x64 (AMD64)";
    case IMAGE_FILE_MACHINE_ARM64: return L"ARM64";
    default: return L"Unknown";
    }
}

std::wstring subsystem_to_string(WORD ss) {
    switch (ss) {
    case IMAGE_SUBSYSTEM_WINDOWS_GUI:     return L"Windows GUI";
    case IMAGE_SUBSYSTEM_WINDOWS_CUI:     return L"Console";
    case IMAGE_SUBSYSTEM_NATIVE:         return L"Native";
    case IMAGE_SUBSYSTEM_EFI_APPLICATION:return L"EFI Application";
    default: return L"Unknown";
    }
}

bool has_flag(WORD v, WORD flag) { return (v & flag) == flag; }
bool has_flag(DWORD v, DWORD flag) { return (v & flag) == flag; }

// Data Directory isimleri (bazılarını özellikle göstereceğiz)
struct dir_info {
    int index;
    const wchar_t* name;
};

static const dir_info kDirs[] = {
    { IMAGE_DIRECTORY_ENTRY_IMPORT,      L"IMPORT" },
    { IMAGE_DIRECTORY_ENTRY_BASERELOC,   L"BASE RELOC (Reloc)" },
    { IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG, L"LOAD CONFIG" },
    { IMAGE_DIRECTORY_ENTRY_EXCEPTION,   L"EXCEPTION (Unwind)" },
    { IMAGE_DIRECTORY_ENTRY_SECURITY,    L"SECURITY (Authenticode)" },
};

// ------------------------------
// Dosyayı memory-map edip bytes döndürmek
// ------------------------------
struct mapped_file {
    unique_handle file;
    unique_handle mapping;
    const std::byte* data = nullptr;
    size_t size = 0;

    mapped_file() = default;
    ~mapped_file() { reset_view(); }

    // copy yasak
    mapped_file(const mapped_file&) = delete;
    mapped_file& operator=(const mapped_file&) = delete;

    // move serbest (manuel, çünkü data pointer'ı "sahiplik" taşıyor)
    mapped_file(mapped_file&& other) noexcept
        : file(std::move(other.file))
        , mapping(std::move(other.mapping))
        , data(other.data)
        , size(other.size)
    {
        other.data = nullptr;
        other.size = 0;
    }

    mapped_file& operator=(mapped_file&& other) noexcept {
        if (this != &other) {
            reset_view(); // önce mevcut view'i kapat

            mapping = std::move(other.mapping);
            file    = std::move(other.file);

            data = other.data;
            size = other.size;

            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }

private:
    void reset_view() noexcept {
        if (data) {
            UnmapViewOfFile(data);
            data = nullptr;
        }
        size = 0;
    }
};

mapped_file map_whole_file(const std::filesystem::path& p) {
    mapped_file mf;

    mf.file.reset(CreateFileW(
        p.c_str(),
        GENERIC_READ,
        FILE_SHARE_READ,
        nullptr,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        nullptr));

    if (!mf.file) {
        throw std::runtime_error("CreateFileW failed");
    }

    LARGE_INTEGER li{};
    if (!GetFileSizeEx(mf.file.get(), &li) || li.QuadPart <= 0) {
        throw std::runtime_error("GetFileSizeEx failed");
    }
    mf.size = static_cast<size_t>(li.QuadPart);

    mf.mapping.reset(CreateFileMappingW(mf.file.get(), nullptr, PAGE_READONLY, 0, 0, nullptr));
    if (!mf.mapping) {
        throw std::runtime_error("CreateFileMappingW failed");
    }

    void* view = MapViewOfFile(mf.mapping.get(), FILE_MAP_READ, 0, 0, 0);
    if (!view) {
        throw std::runtime_error("MapViewOfFile failed");
    }

    mf.data = reinterpret_cast<const std::byte*>(view);
    return mf;
}

// ------------------------------
// Güvenli sınır kontrolü
// ------------------------------
template<typename T>
const T* view_as(const std::byte* base, size_t size, size_t offset) {
    if (offset > size || (size - offset) < sizeof(T)) return nullptr;
    return reinterpret_cast<const T*>(base + offset);
}

void print_common_dirs(const IMAGE_DATA_DIRECTORY* dirs, size_t count) {
    std::wcout << L"\n[Data Directories]\n";
    for (const auto& d : kDirs) {
        if (d.index >= 0 && static_cast<size_t>(d.index) < count) {
            const auto& dd = dirs[d.index];
            std::wcout << L" - " << d.name
                       << L": RVA=0x" << std::hex << dd.VirtualAddress
                       << L"  Size=0x" << dd.Size << std::dec << L"\n";
        }
    }
}

void dump_pe(const std::filesystem::path& path) {
    auto mf = map_whole_file(path);

    // 1) DOS Header
    const auto* dos = view_as<IMAGE_DOS_HEADER>(mf.data, mf.size, 0);
    if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE) {
        throw std::runtime_error("Not a valid MZ / DOS header");
    }

    // 2) NT Headers başlangıcı (e_lfanew)
    const auto nt_off = static_cast<size_t>(dos->e_lfanew);

    const auto* sig = view_as<DWORD>(mf.data, mf.size, nt_off);
    if (!sig || *sig != IMAGE_NT_SIGNATURE) {
        throw std::runtime_error("Missing PE\\0\\0 signature");
    }

    // Signature + FileHeader + OptionalHeader
    const auto* fileHeader = view_as<IMAGE_FILE_HEADER>(mf.data, mf.size, nt_off + sizeof(DWORD));
    if (!fileHeader) throw std::runtime_error("Invalid FILE_HEADER");

    const auto opt_off = nt_off + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER);
    const auto* magic = view_as<WORD>(mf.data, mf.size, opt_off);
    if (!magic) throw std::runtime_error("Invalid OptionalHeader");

    std::wcout << L"\n=== " << path.filename().c_str() << L" ===\n";
    std::wcout << L"[COFF]\n";
    std::wcout << L" Machine: " << machine_to_string(fileHeader->Machine)
               << L" (0x" << std::hex << fileHeader->Machine << std::dec << L")\n";
    std::wcout << L" Sections: " << fileHeader->NumberOfSections << L"\n";
    std::wcout << L" TimeDateStamp: 0x" << std::hex << fileHeader->TimeDateStamp << std::dec << L"\n";

    // 3) Optional Header (32/64)
    if (*magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
        const auto* nt = view_as<IMAGE_NT_HEADERS64>(mf.data, mf.size, nt_off);
        if (!nt) throw std::runtime_error("Invalid NT_HEADERS64");

        const auto& opt = nt->OptionalHeader;

        std::wcout << L"\n[OptionalHeader64]\n";
        std::wcout << L" EntryPoint RVA: 0x" << std::hex << opt.AddressOfEntryPoint << std::dec << L"\n";
        std::wcout << L" ImageBase: 0x" << std::hex << opt.ImageBase << std::dec << L"\n";
        std::wcout << L" Subsystem: " << subsystem_to_string(opt.Subsystem) << L"\n";

        const WORD dc = opt.DllCharacteristics;
        std::wcout << L"\n[DllCharacteristics]\n";
        std::wcout << L" DYNAMIC_BASE (ASLR): " << (has_flag(dc, IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) ? L"Yes" : L"No") << L"\n";
        std::wcout << L" NX_COMPAT (DEP/NX): " << (has_flag(dc, IMAGE_DLLCHARACTERISTICS_NX_COMPAT) ? L"Yes" : L"No") << L"\n";
        std::wcout << L" GUARD_CF (CFG): " << (has_flag(dc, IMAGE_DLLCHARACTERISTICS_GUARD_CF) ? L"Yes" : L"No") << L"\n";
        std::wcout << L" HIGH_ENTROPY_VA: " << (has_flag(dc, IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA) ? L"Yes" : L"No") << L"\n";

        print_common_dirs(opt.DataDirectory, opt.NumberOfRvaAndSizes);

        // 4) Section headers
        const auto sec_off = opt_off + fileHeader->SizeOfOptionalHeader;
        const auto* sec0 = view_as<IMAGE_SECTION_HEADER>(mf.data, mf.size, sec_off);
        if (!sec0) throw std::runtime_error("Invalid section table");

        std::wcout << L"\n[Sections]\n";
        for (WORD i = 0; i < fileHeader->NumberOfSections; ++i) {
            const auto* sh = view_as<IMAGE_SECTION_HEADER>(mf.data, mf.size, sec_off + i * sizeof(IMAGE_SECTION_HEADER));
            if (!sh) break;

            char name[9]{};
            std::memcpy(name, sh->Name, 8);

            std::wcout << L" - " << name
                       << L"  RVA=0x" << std::hex << sh->VirtualAddress
                       << L"  VSz=0x" << sh->Misc.VirtualSize
                       << L"  RawPtr=0x" << sh->PointerToRawData
                       << L"  RawSz=0x" << sh->SizeOfRawData
                       << std::dec << L"\n";
        }
    }
    else if (*magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
        const auto* nt = view_as<IMAGE_NT_HEADERS32>(mf.data, mf.size, nt_off);
        if (!nt) throw std::runtime_error("Invalid NT_HEADERS32");

        const auto& opt = nt->OptionalHeader;

        std::wcout << L"\n[OptionalHeader32]\n";
        std::wcout << L" EntryPoint RVA: 0x" << std::hex << opt.AddressOfEntryPoint << std::dec << L"\n";
        std::wcout << L" ImageBase: 0x" << std::hex << opt.ImageBase << std::dec << L"\n";
        std::wcout << L" Subsystem: " << subsystem_to_string(opt.Subsystem) << L"\n";

        const WORD dc = opt.DllCharacteristics;
        std::wcout << L"\n[DllCharacteristics]\n";
        std::wcout << L" DYNAMIC_BASE (ASLR): " << (has_flag(dc, IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) ? L"Yes" : L"No") << L"\n";
        std::wcout << L" NX_COMPAT (DEP/NX): " << (has_flag(dc, IMAGE_DLLCHARACTERISTICS_NX_COMPAT) ? L"Yes" : L"No") << L"\n";
        std::wcout << L" GUARD_CF (CFG): " << (has_flag(dc, IMAGE_DLLCHARACTERISTICS_GUARD_CF) ? L"Yes" : L"No") << L"\n";

        print_common_dirs(opt.DataDirectory, opt.NumberOfRvaAndSizes);

        const auto sec_off = opt_off + fileHeader->SizeOfOptionalHeader;

        std::wcout << L"\n[Sections]\n";
        for (WORD i = 0; i < fileHeader->NumberOfSections; ++i) {
            const auto* sh = view_as<IMAGE_SECTION_HEADER>(mf.data, mf.size, sec_off + i * sizeof(IMAGE_SECTION_HEADER));
            if (!sh) break;

            char name[9]{};
            std::memcpy(name, sh->Name, 8);

            std::wcout << L" - " << name
                       << L"  RVA=0x" << std::hex << sh->VirtualAddress
                       << L"  VSz=0x" << sh->Misc.VirtualSize
                       << L"  RawPtr=0x" << sh->PointerToRawData
                       << L"  RawSz=0x" << sh->SizeOfRawData
                       << std::dec << L"\n";
        }
    }
    else {
        throw std::runtime_error("Unknown OptionalHeader magic");
    }
}

int wmain(int argc, wchar_t** argv) {
    try {
        if (argc < 2) {
            std::wcout << L"Usage: pe_viewer.exe <path-to-exe-or-dll>\n";
            std::wcout << L"Example: pe_viewer.exe C:\\\\Windows\\\\System32\\\\notepad.exe\n";
            return 0;
        }

        std::filesystem::path p = argv[1];
        dump_pe(p);
        return 0;
    }
    catch (const std::exception& ex) {
        const DWORD e = GetLastError();
        std::wcerr << L"Error: " << ex.what() << L"\n";
        if (e) std::wcerr << L"Win32: " << e << L" - " << win32_error_message(e) << L"\n";
        return 1;
    }
}

Örnek çıktı

Uygulamayı şöyle çalıştırırsın:

Komut satırı
pe_viewer.exe C:\Windows\System32\notepad.exe

Ve konsolda şuna benzer bir çıktı görürsün (değerler dosyaya göre değişir):

Konsol çıktısı (örnek)
=== notepad.exe ===
[COFF]
 Machine: x64 (AMD64) (0x8664)
 Sections: 6
 TimeDateStamp: 0x65A1....

[OptionalHeader64]
 EntryPoint RVA: 0x12340
 ImageBase: 0x140000000
 Subsystem: Windows GUI

[DllCharacteristics]
 DYNAMIC_BASE (ASLR): Yes
 NX_COMPAT (DEP/NX): Yes
 GUARD_CF (CFG): Yes
 HIGH_ENTROPY_VA: Yes

[Data Directories]
 - IMPORT: RVA=0x....  Size=0x....
 - BASE RELOC (Reloc): RVA=0x....  Size=0x....
 - LOAD CONFIG: RVA=0x....  Size=0x....
 - EXCEPTION (Unwind): RVA=0x....  Size=0x....
 - SECURITY (Authenticode): RVA=0x....  Size=0x....

[Sections]
 - .text  RVA=0x1000  VSz=0x....  RawPtr=0x....  RawSz=0x....
 - .rdata RVA=0x....  ...

ASLR / DEP / CFG / SEH açısından “PE’de neyi görmüş oluyorum?”

Kavram Bu programda nereden görünüyor? Ne yorumlarım?
ASLR DllCharacteristicsDYNAMIC_BASE + (pratikte) BASE RELOC dizini Adres tahminini zorlaştırır; relocation verisi yoksa etki düşebilir.
DEP / NX DllCharacteristicsNX_COMPAT Veri sayfalarının çalıştırılmasını engellemeye yönelik uyumluluk işareti.
CFG DllCharacteristicsGUARD_CF + LOAD CONFIG dizini Dolaylı çağrılarda hedef doğrulama; kontrol akışını sıkılaştırır.
SEH Özellikle x64’te EXCEPTION (unwind) dizini (tablo-temelli exception) Crash/exception yönetiminin altyapı izleri; analizde çok işe yarar.
Kısa ama önemli

PE Header “tek başına güvenlik garantisi” vermez. Ama mitigations tarafında izleri gösterir. Benim için bu, bir binary’yi tanımanın en hızlı adımı.

Alternatif: DUMPBIN ile hızlı kontrol

Kod yazmadan hızlı bakmak istersen, Visual Studio Developer Command Prompt’ta: dumpbin /HEADERS dosya.exe komutu da section header’ları ve temel header alanlarını döker.

SSS

1) Bu uygulama “process içindeki” PE’yi mi okuyor, disk dosyasını mı?

Diskteki dosyayı okuyor. Process içi inceleme için farklı bir yaklaşım (OpenProcess + ReadProcessMemory) gerekir.

2) Security Directory (Authenticode) neden “RVA gibi” davranmıyor?

Authenticode imza verisi PE’nin “image mapping” parçası gibi belleğe map edilmez; dosyada “attribute certificate table” olarak tutulur. Bu yüzden yorumlarken “dosya ofseti” mantığı önem kazanır.

3) CFG var görünüyor ama gerçekten çalışıyor mu?

GUARD_CF ve Load Config izleri iyi bir sinyal. “Gerçek etkinlik” için build ayarları, OS politikaları ve runtime detayları da etkiler. Ama ilk teşhis için PE izleri çok değerlidir.


Kaynaklar / İleri Okuma


İlginizi çekebilecek diğer konular

Yorum Bırakın

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