Подписывайтесь на наш Telegram и не пропускайте важные новости! Перейти

Исходник [Сурс] Apex Legends PE Dumper — Дамп памяти через DMA (PCILeech)

Sloppy
Начинающий
Начинающий
Статус
Оффлайн
Регистрация
13 Фев 2026
Сообщения
347
Реакции
7
Народ, кто ковыряет Apex через DMA, держите годную базу под допил. Это порт оригинального дампера от killogram, но переписанный под LeechCore / VMM. Главный плюс — никакого kernel-мусора на целевой машине, всё летит через FPGA.

Что умеет этот билд:
  1. Дамп PE-памяти напрямую через железо.
  2. Автоматический фикс IAT (Import Address Table) — читает зарезолвленные адреса прямо из памяти.
  3. Корректная обработка релокаций и заголовков секций.
  4. Расчет энтропии для детекта упакованных или зашифрованных секций.
  5. Приведение маппинга файла к виду Disk Layout = Memory Layout для корректного анализа в IDA/Ghidra.

Техническое мясо
В коде реализован правильный подход к дампу: PointerToRawData приравнивается к VirtualAddress. Это заставляет анализаторы видеть структуру так, как она лежит в памяти. Для фикса импортов используется проход по дескрипторам с проверкой границ, чтобы дамп не падал на битых страницах.

Код:
Expand Collapse Copy
#define _CRT_SECURE_NO_WARNINGS
 
 
#include <Windows.h>
#include <process.h>
#include <TlHelp32.h>
#include <inttypes.h>
#include <iostream>
#include <stdexcept>
#include <algorithm>
#include <chrono>
#include <sstream>
#include <memory>
#include <string_view>
#include <cstdint>
#include <string>
#include <cmath>
#include <thread>
#include <cassert>
#include <xstring>
#include <dwmapi.h>
#include <vector>
#include <map>
#include <array>
#include <fstream>
#include <direct.h>
#include <set>
#include <stack>
#include <unordered_set>
#include <wininet.h>
#include <random>
#include <Psapi.h>
#include <urlmon.h>      // URLDownloadToFile
 
// DMA Memory class (LeechCore/VMMDLL based)
#include "include/DMALibrary/Memory/Memory.h"
 
#pragma comment(lib, "wininet.lib")
 
// Safe memory reader
bool ReadSafe(uint64_t address, void* buffer, size_t size) {
    uint8_t* out = reinterpret_cast<uint8_t*>(buffer);
    for (size_t offset = 0; offset < size; offset += 0x1000) {
        size_t chunk = (size - offset < 0x1000) ? (size - offset) : 0x1000;
        if (!mem.Read(address + offset, out + offset, chunk)) {
            // Fill unreadable pages with nulls to maintain alignment
            memset(out + offset, 0, chunk);
        }
    }
    return true;
}
 
// Entropy calculation
double CalculateEntropy(const uint8_t* data, size_t size) {
    if (!size) return 0.0;
    int freq[256]{};
 
    for (size_t i = 0; i < size; i++)
        freq[data[i]]++;
 
    double entropy = 0.0;
    for (int i = 0; i < 256; i++) {
        if (freq[i]) {
            double p = static_cast<double>(freq[i]) / size;
            entropy -= p * log2(p);
        }
    }
    return entropy;
}
 
// Relocations
bool FixRelocations(std::vector<uint8_t>& image, IMAGE_NT_HEADERS64& nt, uint64_t runtimeBase) {
    auto& dir = nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    if (!dir.VirtualAddress || !dir.Size)
        return true;
 
    uint64_t delta = runtimeBase - nt.OptionalHeader.ImageBase;
    uint8_t* cur = image.data() + dir.VirtualAddress;
    uint8_t* end = cur + dir.Size;
 
    while (cur < end) {
        auto* block = reinterpret_cast<IMAGE_BASE_RELOCATION*>(cur);
        cur += sizeof(*block);
 
        size_t count = (block->SizeOfBlock - sizeof(*block)) / sizeof(WORD);
        WORD* entries = reinterpret_cast<WORD*>(cur);
 
        for (size_t i = 0; i < count; i++) {
            WORD type = entries[i] >> 12;
            WORD off = entries[i] & 0xFFF;
 
            if (type == IMAGE_REL_BASED_DIR64) {
                uint64_t* patch = reinterpret_cast<uint64_t*>(image.data() + block->VirtualAddress + off);
                *patch += delta;
            }
        }
        cur += count * sizeof(WORD);
    }
 
    nt.OptionalHeader.ImageBase = runtimeBase;
    return true;
}
 
// Fix Import Address Table (IAT) by copying original memory addresses
bool FixIAT(uint64_t base, std::vector<uint8_t>& image, IMAGE_NT_HEADERS64* nt) {
    auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    if (!dir.VirtualAddress || !dir.Size) return true;
 
    // Bug #3 fix: validate import directory is within image bounds
    if (static_cast<uint64_t>(dir.VirtualAddress) + dir.Size > image.size()) return false;
 
    auto* imports = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(image.data() + dir.VirtualAddress);
    auto* importsEnd = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(
        image.data() + dir.VirtualAddress + dir.Size);
 
    // Bug #3 fix: bound the descriptor walk and require both Name and FirstThunk to be non-zero
    for (; imports + 1 <= importsEnd && imports->Name && imports->FirstThunk; imports++) {
        uint64_t firstThunkRVA = imports->FirstThunk;
 
        // Bug #3 fix: firstThunkRVA must be inside the image
        if (firstThunkRVA >= image.size()) continue;
 
        for (int i = 0; ; i++) {
            uint64_t offset = static_cast<uint64_t>(i) * sizeof(uint64_t);
 
            // Bug #3 fix: bounds-check every write before touching the buffer
            if (firstThunkRVA + offset + sizeof(uint64_t) > image.size()) break;
 
            uint64_t funcAddr = 0;
            // Read the actual resolved address from the live process memory
            if (!mem.Read(base + firstThunkRVA + offset, &funcAddr, sizeof(funcAddr)))
                break;
 
            if (funcAddr == 0) break;
 
            // Patch the dump's memory with the live resolved address
            *reinterpret_cast<uint64_t*>(image.data() + firstThunkRVA + offset) = funcAddr;
        }
    }
    return true;
}
 
// Dump module
bool DumpModule(uint64_t base, const std::string& outName) {
    IMAGE_DOS_HEADER dos{};
    if (!mem.Read((uintptr_t)base, &dos, sizeof(dos)) || dos.e_magic != IMAGE_DOS_SIGNATURE)
        return false;
 
    IMAGE_NT_HEADERS64 nt{};
    if (!mem.Read((uintptr_t)(base + dos.e_lfanew), &nt, sizeof(nt)))
        return false;
 
    // 1. Prepare Buffer (SizeOfImage is the size in memory)
    size_t imageSize = nt.OptionalHeader.SizeOfImage;
    std::vector<uint8_t> image(imageSize, 0);
 
    // 2. Read Headers
    ReadSafe(base, image.data(), nt.OptionalHeader.SizeOfHeaders);
 
    // 3. Read Sections
    uint64_t sectionHeaderAddr = base + dos.e_lfanew + sizeof(IMAGE_NT_HEADERS64);
    std::vector<IMAGE_SECTION_HEADER> sections(nt.FileHeader.NumberOfSections);
    mem.Read((uintptr_t)sectionHeaderAddr, sections.data(), sections.size() * sizeof(IMAGE_SECTION_HEADER));
 
    for (auto& s : sections) {
        uint64_t sectionVA = base + s.VirtualAddress;
        uint32_t sectionSize = s.Misc.VirtualSize;
 
        // Fix #1: Safe section name print (IMAGE_SIZEOF_SHORT_NAME=8, may not be null-terminated)
        char secName[IMAGE_SIZEOF_SHORT_NAME + 1]{};
        memcpy(secName, s.Name, IMAGE_SIZEOF_SHORT_NAME);
        std::cout << "[SEC] " << secName << " | VA: 0x" << std::hex << s.VirtualAddress << " | Size: 0x" << sectionSize;
 
        ReadSafe(sectionVA, image.data() + s.VirtualAddress, sectionSize);
 
        // Check for encryption/compression
        double entropy = CalculateEntropy(image.data() + s.VirtualAddress, sectionSize);
        std::cout << " | Entropy: " << std::dec << entropy << (entropy > 7.4 ? " [ENCRYPTED]" : "") << std::endl;
 
        // CRITICAL: Set PointerToRawData = VirtualAddress and SizeOfRawData = VirtualSize
        // This makes the file "Disk Layout" identical to "Memory Layout"
        auto* headerInDump = reinterpret_cast<IMAGE_SECTION_HEADER*>(image.data() + (sectionHeaderAddr - base) + (&s - &sections[0]) * sizeof(IMAGE_SECTION_HEADER));
        headerInDump->PointerToRawData = s.VirtualAddress;
        headerInDump->SizeOfRawData = s.Misc.VirtualSize;
    }
 
    // 4. Fix IAT (Optional but recommended for analysis)
    FixIAT(base, image, &nt);
 
    // 5. Bug #1 fix: tell analysis tools the dump is based at runtime address.
    //    The in-memory absolute pointers already reflect `base`, so instead of
    //    un-relocating we just update ImageBase. No need to touch .reloc.
    nt.OptionalHeader.ImageBase = base;
 
    //    Because PointerToRawData == VirtualAddress (set in the section loop),
    //    file alignment must equal section alignment, otherwise the layout is
    //    invalid per PE spec and strict loaders/parsers will reject it.
    nt.OptionalHeader.FileAlignment = nt.OptionalHeader.SectionAlignment;
 
    //    Bound imports cache addresses computed at link time and are stale
    //    after dumping — clear the directory so loaders don't trust it.
    nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress = 0;
    nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].Size = 0;
 
    // 6. Bug #2 fix: write the modified NT headers back into the image buffer.
    //    All edits above were made on a stack copy; without this memcpy they
    //    would never reach the output file.
    if (dos.e_lfanew + sizeof(nt) <= image.size())
        memcpy(image.data() + dos.e_lfanew, &nt, sizeof(nt));
 
    // 7. Write to File
    std::ofstream f(outName, std::ios::binary);
    if (f.is_open()) {
        f.write(reinterpret_cast<char*>(image.data()), image.size());
        f.close();
        return true;
    }
    return false;
}
 
void SetColor(WORD color) {
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color);
}
 
// ==================== Main ====================
int main() {
    SetConsoleTitleA("PE Memory Dumper (DMA)");
 
    const WORD COLOR_SUCCESS = FOREGROUND_GREEN | FOREGROUND_INTENSITY;
    const WORD COLOR_ERROR = FOREGROUND_RED | FOREGROUND_INTENSITY;
    const WORD COLOR_INFO = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY; // cyan
 
    // Initialize DMA and connect to process
    SetColor(COLOR_INFO);
    std::cout << "[*] Initializing DMA connection...\n";
    if (!mem.Init("r5apex_dx12.exe", true, false)) {
        SetColor(COLOR_ERROR);
        std::cerr << "[-] Failed to initialize DMA or find process. Is the DMA connected and game running?\n";
        system("pause"); return 1;
    }
    SetColor(COLOR_SUCCESS);
    std::cout << "[+] DMA connected! Process found!\n";
    std::cout << "[+] PID: " << std::dec << Memory::current_process.PID << "\n";
 
    // Module base (already obtained during Init)
    uint64_t moduleBase = Memory::current_process.base_address;
    uint64_t moduleSize = Memory::current_process.base_size;
 
    if (!moduleBase) {
        SetColor(COLOR_ERROR);
        std::cerr << "[-] Failed to find module base!\n";
        system("pause"); return 1;
    }
 
    SetColor(COLOR_SUCCESS);
    std::cout << "[+] Module base: 0x" << std::hex << moduleBase
        << "  Size: 0x" << moduleSize << std::dec << "\n";
 
    // Dump module
    SetColor(COLOR_INFO);
    std::cout << "[*] Dumping module...\n";
    if (!DumpModule(moduleBase, "game_dumped.exe")) {
        SetColor(COLOR_ERROR); std::cerr << "[-] Dump failed!\n"; system("pause"); return 1;
    }
    SetColor(COLOR_SUCCESS);
    std::cout << "[+] Dump completed successfully!\n";
 
    SetColor(COLOR_SUCCESS);
    std::cout << "[*] Press Enter to exit...\n";
    std::cin.get();
 
    SetColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
    return 0;
}

GitHub: ApexDumperPEMemory-DMA

Как правильно заюзать:
  1. Подключаем DMA, залетаем в Apex (r5apex_dx12.exe).
  2. Запускаем дампер от админа на хост-машине.
  3. Топаем на стрельбище (Fire Range): бегаем, стреляем, лутаем всё подряд. Это нужно, чтобы дампер зацепил динамические оффсеты.
  4. Для верности играем один обычный матч.
  5. Забираем game_dumped.exe и закидываем в анализатор.

Железо: Любая плата с поддержкой PCILeech.
Либы: В папке с экзешником должны лежать vmm.dll, leechcore.dll и FTD3XX.dll.
Косяки: На данный момент криво читаются GlobalVars и m_weaponNameIndex. Если кто пофиксит — делитесь в треде.

Сурс чистый, под реверс и обновление своих офсетов залетает идеально. EAC на такое железо не ругается, если прошивка не в паблике.

Кто уже пробовал стягивать оффсеты этим билдом на новой обнове?
 
Назад
Сверху Снизу