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

Исходник [Сурс] Rust Material Dumper — Дамп ресурсов через инжект

Sloppy
Начинающий
Начинающий
Статус
Оффлайн
Регистрация
13 Фев 2026
Сообщения
347
Реакции
7
Народ, попал в руки простенький, но рабочий дампер для Раста. Ничего лишнего — чистый C#, который делает свою работу через стандартный мануальный инжект. Кто копается в материалах игры или пытается вытянуть структуру ассетов для своих нужд — вещь вполне юзабельная для быстрого сбора инфы.

Функционал:
  1. Показывает прогресс дампинга в консоли.
  2. Поддерживает кастомные имена файлов, что удобно для автоматизации скриптами.
  3. Работает через стандартный процесс инжекта (можно юзать Process Hacker или подставить свой загрузчик).

Сам инжектор реализован через стандартные WinAPI:
Код:
Expand Collapse Copy
OpenProcess
Код:
Expand Collapse Copy
VirtualAllocEx
и
Код:
Expand Collapse Copy
CreateRemoteThread
В сурсе есть логика вычисления RVA через
Код:
Expand Collapse Copy
LoadLibraryExA
без резолва зависимостей, что позволяет цепляться к процессу RustClient.exe без лишних головняков.

Код:
Expand Collapse Copy
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
 
namespace RustDumperNET
{
    static class Injector
    {
        const uint PROCESS_ALL_ACCESS = 0x001F_0FFF;
        const uint MEM_COMMIT = 0x0000_1000;
        const uint MEM_RESERVE = 0x0000_2000;
        const uint MEM_RELEASE = 0x0000_8000;
        const uint PAGE_READWRITE = 0x04;
        const uint INFINITE = 0xFFFF_FFFF;
 
        const uint DONT_RESOLVE_DLL_REFERENCES = 0x0000_0001;
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr OpenProcess(uint dwAccess, bool bInherit, int dwPid);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddr,
            IntPtr dwSize, uint flType, uint flProtect);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddr,
            IntPtr dwSize, uint dwFreeType);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddr,
            byte[] lpBuffer, IntPtr nSize, out IntPtr lpWritten);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpAttr,
            IntPtr dwStackSize, IntPtr lpStartAddr, IntPtr lpParam,
            uint dwFlags, out uint lpThreadId);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMs);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool CloseHandle(IntPtr hObject);
 
        [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
        static extern IntPtr LoadLibraryA(string lpFileName);
 
        [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
        static extern IntPtr LoadLibraryExA(string lpFileName, IntPtr hFile, uint dwFlags);
 
        [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
        static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool FreeLibrary(IntPtr hModule);
 
        public static void InjectAndRun(Process target, string dllPath,
            string outputPath, bool waitForCompletion = true)
        {
            string fullDllPath = Path.GetFullPath(dllPath);
            if (!File.Exists(fullDllPath))
                throw new FileNotFoundException($"DLL not found: {fullDllPath}");
 
            IntPtr hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, target.Id);
            if (hProcess == IntPtr.Zero)
                throw new InvalidOperationException(
                    $"OpenProcess failed (error {Marshal.GetLastWin32Error()})");
            try
            {
                InjectDll(hProcess, fullDllPath);
 
                IntPtr remoteStartDump = ResolveRemoteExport(target, fullDllPath, "StartDump");
 
                byte[] pathBytes = Encoding.UTF8.GetBytes(outputPath + '\0');
                IntPtr remotePath = VirtualAllocEx(hProcess, IntPtr.Zero,
                    (IntPtr)pathBytes.Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
                if (remotePath == IntPtr.Zero)
                    throw new InvalidOperationException(
                        $"VirtualAllocEx (path) failed (error {Marshal.GetLastWin32Error()})");
 
                if (!WriteProcessMemory(hProcess, remotePath, pathBytes,
                        (IntPtr)pathBytes.Length, out _))
                    throw new InvalidOperationException(
                        $"WriteProcessMemory (path) failed (error {Marshal.GetLastWin32Error()})");
 
                IntPtr hDumpThread = CreateRemoteThread(hProcess, IntPtr.Zero, IntPtr.Zero,
                    remoteStartDump, remotePath, 0, out _);
                if (hDumpThread == IntPtr.Zero)
                    throw new InvalidOperationException(
                        $"CreateRemoteThread (StartDump) failed (error {Marshal.GetLastWin32Error()})");
 
                if (waitForCompletion)
                    WaitForSingleObject(hDumpThread, INFINITE);
 
                CloseHandle(hDumpThread);
            }
            finally
            {
                CloseHandle(hProcess);
            }
        }
 
        // -------------------------------------------------------------------------
 
        static void InjectDll(IntPtr hProcess, string fullDllPath)
        {
            byte[] pathBytes = Encoding.ASCII.GetBytes(fullDllPath + '\0');
 
            IntPtr remoteDllPath = VirtualAllocEx(hProcess, IntPtr.Zero,
                (IntPtr)pathBytes.Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
            if (remoteDllPath == IntPtr.Zero)
                throw new InvalidOperationException(
                    $"VirtualAllocEx (DLL path) failed (error {Marshal.GetLastWin32Error()})");
            try
            {
                if (!WriteProcessMemory(hProcess, remoteDllPath, pathBytes,
                        (IntPtr)pathBytes.Length, out _))
                    throw new InvalidOperationException(
                        $"WriteProcessMemory (DLL path) failed (error {Marshal.GetLastWin32Error()})");
 
                IntPtr hKernel32 = LoadLibraryA("kernel32.dll");
                IntPtr loadLibraryA = GetProcAddress(hKernel32, "LoadLibraryA");
                FreeLibrary(hKernel32);
 
                IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, IntPtr.Zero,
                    loadLibraryA, remoteDllPath, 0, out _);
                if (hThread == IntPtr.Zero)
                    throw new InvalidOperationException(
                        $"CreateRemoteThread (LoadLibraryA) failed (error {Marshal.GetLastWin32Error()})");
 
                WaitForSingleObject(hThread, INFINITE);
                CloseHandle(hThread);
            }
            finally
            {
                VirtualFreeEx(hProcess, remoteDllPath, IntPtr.Zero, MEM_RELEASE);
            }
        }
 
        static IntPtr ResolveRemoteExport(Process target, string fullDllPath, string exportName)
        {          
            IntPtr hLocal = LoadLibraryExA(fullDllPath, IntPtr.Zero, DONT_RESOLVE_DLL_REFERENCES);
            if (hLocal == IntPtr.Zero)
                throw new InvalidOperationException(
                    $"Local LoadLibraryEx failed (error {Marshal.GetLastWin32Error()})");
 
            IntPtr localExport = GetProcAddress(hLocal, exportName);
            if (localExport == IntPtr.Zero)
            {
                FreeLibrary(hLocal);
                throw new InvalidOperationException(
                    $"Export '{exportName}' not found in {Path.GetFileName(fullDllPath)}");
            }
 
            long exportRva = localExport.ToInt64() - hLocal.ToInt64();
            FreeLibrary(hLocal);
 
            IntPtr remoteBase = FindRemoteModuleBase(target, fullDllPath);
            return new IntPtr(remoteBase.ToInt64() + exportRva);
        }
 
        static IntPtr FindRemoteModuleBase(Process target, string fullDllPath,
            int retries = 15, int delayMs = 200)
        {
            string dllName = Path.GetFileName(fullDllPath);
            for (int i = 0; i < retries; i++)
            {
                try
                {
                    target.Refresh();
                    foreach (ProcessModule mod in target.Modules)
                    {
                        if (string.Equals(Path.GetFileName(mod.FileName), dllName,
                                StringComparison.OrdinalIgnoreCase))
                            return mod.BaseAddress;
                    }
                }
                catch (Exception) { }
 
                Thread.Sleep(delayMs);
            }
            throw new InvalidOperationException(
                $"Module '{dllName}' not found in target process after {retries} retries.");
        }
    }
}

Нюансы:
Поскольку это чистый C# Dumper, не забывайте, что инжект в игру с EAC — это всегда лотерея. Если юзаете на мейне — отлетите быстрее, чем прогрузится карта. Для легитной игры не подходит, используйте только на локалке или чистом клиенте.

Кто уже пробовал прокидывать этот дамп под актуальный билд, отпишитесь по результатам в треде?
 
Народ, попал в руки простенький, но рабочий дампер для Раста. Ничего лишнего — чистый C#, который делает свою работу через стандартный мануальный инжект. Кто копается в материалах игры или пытается вытянуть структуру ассетов для своих нужд — вещь вполне юзабельная для быстрого сбора инфы.

Функционал:
  1. Показывает прогресс дампинга в консоли.
  2. Поддерживает кастомные имена файлов, что удобно для автоматизации скриптами.
  3. Работает через стандартный процесс инжекта (можно юзать Process Hacker или подставить свой загрузчик).

Сам инжектор реализован через стандартные WinAPI:
Код:
Expand Collapse Copy
OpenProcess
Код:
Expand Collapse Copy
VirtualAllocEx
и
Код:
Expand Collapse Copy
CreateRemoteThread
В сурсе есть логика вычисления RVA через
Код:
Expand Collapse Copy
LoadLibraryExA
без резолва зависимостей, что позволяет цепляться к процессу RustClient.exe без лишних головняков.

Код:
Expand Collapse Copy
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
 
namespace RustDumperNET
{
    static class Injector
    {
        const uint PROCESS_ALL_ACCESS = 0x001F_0FFF;
        const uint MEM_COMMIT = 0x0000_1000;
        const uint MEM_RESERVE = 0x0000_2000;
        const uint MEM_RELEASE = 0x0000_8000;
        const uint PAGE_READWRITE = 0x04;
        const uint INFINITE = 0xFFFF_FFFF;
 
        const uint DONT_RESOLVE_DLL_REFERENCES = 0x0000_0001;
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr OpenProcess(uint dwAccess, bool bInherit, int dwPid);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddr,
            IntPtr dwSize, uint flType, uint flProtect);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddr,
            IntPtr dwSize, uint dwFreeType);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddr,
            byte[] lpBuffer, IntPtr nSize, out IntPtr lpWritten);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpAttr,
            IntPtr dwStackSize, IntPtr lpStartAddr, IntPtr lpParam,
            uint dwFlags, out uint lpThreadId);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMs);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool CloseHandle(IntPtr hObject);
 
        [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
        static extern IntPtr LoadLibraryA(string lpFileName);
 
        [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
        static extern IntPtr LoadLibraryExA(string lpFileName, IntPtr hFile, uint dwFlags);
 
        [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
        static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool FreeLibrary(IntPtr hModule);
 
        public static void InjectAndRun(Process target, string dllPath,
            string outputPath, bool waitForCompletion = true)
        {
            string fullDllPath = Path.GetFullPath(dllPath);
            if (!File.Exists(fullDllPath))
                throw new FileNotFoundException($"DLL not found: {fullDllPath}");
 
            IntPtr hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, target.Id);
            if (hProcess == IntPtr.Zero)
                throw new InvalidOperationException(
                    $"OpenProcess failed (error {Marshal.GetLastWin32Error()})");
            try
            {
                InjectDll(hProcess, fullDllPath);
 
                IntPtr remoteStartDump = ResolveRemoteExport(target, fullDllPath, "StartDump");
 
                byte[] pathBytes = Encoding.UTF8.GetBytes(outputPath + '\0');
                IntPtr remotePath = VirtualAllocEx(hProcess, IntPtr.Zero,
                    (IntPtr)pathBytes.Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
                if (remotePath == IntPtr.Zero)
                    throw new InvalidOperationException(
                        $"VirtualAllocEx (path) failed (error {Marshal.GetLastWin32Error()})");
 
                if (!WriteProcessMemory(hProcess, remotePath, pathBytes,
                        (IntPtr)pathBytes.Length, out _))
                    throw new InvalidOperationException(
                        $"WriteProcessMemory (path) failed (error {Marshal.GetLastWin32Error()})");
 
                IntPtr hDumpThread = CreateRemoteThread(hProcess, IntPtr.Zero, IntPtr.Zero,
                    remoteStartDump, remotePath, 0, out _);
                if (hDumpThread == IntPtr.Zero)
                    throw new InvalidOperationException(
                        $"CreateRemoteThread (StartDump) failed (error {Marshal.GetLastWin32Error()})");
 
                if (waitForCompletion)
                    WaitForSingleObject(hDumpThread, INFINITE);
 
                CloseHandle(hDumpThread);
            }
            finally
            {
                CloseHandle(hProcess);
            }
        }
 
        // -------------------------------------------------------------------------
 
        static void InjectDll(IntPtr hProcess, string fullDllPath)
        {
            byte[] pathBytes = Encoding.ASCII.GetBytes(fullDllPath + '\0');
 
            IntPtr remoteDllPath = VirtualAllocEx(hProcess, IntPtr.Zero,
                (IntPtr)pathBytes.Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
            if (remoteDllPath == IntPtr.Zero)
                throw new InvalidOperationException(
                    $"VirtualAllocEx (DLL path) failed (error {Marshal.GetLastWin32Error()})");
            try
            {
                if (!WriteProcessMemory(hProcess, remoteDllPath, pathBytes,
                        (IntPtr)pathBytes.Length, out _))
                    throw new InvalidOperationException(
                        $"WriteProcessMemory (DLL path) failed (error {Marshal.GetLastWin32Error()})");
 
                IntPtr hKernel32 = LoadLibraryA("kernel32.dll");
                IntPtr loadLibraryA = GetProcAddress(hKernel32, "LoadLibraryA");
                FreeLibrary(hKernel32);
 
                IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, IntPtr.Zero,
                    loadLibraryA, remoteDllPath, 0, out _);
                if (hThread == IntPtr.Zero)
                    throw new InvalidOperationException(
                        $"CreateRemoteThread (LoadLibraryA) failed (error {Marshal.GetLastWin32Error()})");
 
                WaitForSingleObject(hThread, INFINITE);
                CloseHandle(hThread);
            }
            finally
            {
                VirtualFreeEx(hProcess, remoteDllPath, IntPtr.Zero, MEM_RELEASE);
            }
        }
 
        static IntPtr ResolveRemoteExport(Process target, string fullDllPath, string exportName)
        {        
            IntPtr hLocal = LoadLibraryExA(fullDllPath, IntPtr.Zero, DONT_RESOLVE_DLL_REFERENCES);
            if (hLocal == IntPtr.Zero)
                throw new InvalidOperationException(
                    $"Local LoadLibraryEx failed (error {Marshal.GetLastWin32Error()})");
 
            IntPtr localExport = GetProcAddress(hLocal, exportName);
            if (localExport == IntPtr.Zero)
            {
                FreeLibrary(hLocal);
                throw new InvalidOperationException(
                    $"Export '{exportName}' not found in {Path.GetFileName(fullDllPath)}");
            }
 
            long exportRva = localExport.ToInt64() - hLocal.ToInt64();
            FreeLibrary(hLocal);
 
            IntPtr remoteBase = FindRemoteModuleBase(target, fullDllPath);
            return new IntPtr(remoteBase.ToInt64() + exportRva);
        }
 
        static IntPtr FindRemoteModuleBase(Process target, string fullDllPath,
            int retries = 15, int delayMs = 200)
        {
            string dllName = Path.GetFileName(fullDllPath);
            for (int i = 0; i < retries; i++)
            {
                try
                {
                    target.Refresh();
                    foreach (ProcessModule mod in target.Modules)
                    {
                        if (string.Equals(Path.GetFileName(mod.FileName), dllName,
                                StringComparison.OrdinalIgnoreCase))
                            return mod.BaseAddress;
                    }
                }
                catch (Exception) { }
 
                Thread.Sleep(delayMs);
            }
            throw new InvalidOperationException(
                $"Module '{dllName}' not found in target process after {retries} retries.");
        }
    }
}

Нюансы:
Поскольку это чистый C# Dumper, не забывайте, что инжект в игру с EAC — это всегда лотерея. Если юзаете на мейне — отлетите быстрее, чем прогрузится карта. Для легитной игры не подходит, используйте только на локалке или чистом клиенте.

Кто уже пробовал прокидывать этот дамп под актуальный билд, отпишитесь по результатам в треде?
Здарова. Глянул код, потыкал — пара мыслей по делу.

По самому инжектору — реализация рабочая, но топорная, как лопата. CreateRemoteThread + LoadLibraryA это классика 2010-х, EAC такие вещи ловит на раз-два по сигнатуре потока, стартующего с адреса LoadLibraryA в чужом процессе. Так что предупреждение автора про "только на чистом клиенте" — не для красоты, реально отлетишь моментально, даже не успеешь ассеты вытянуть.

Несколько технических замечаний по сурсу, на что обратил внимание:

  1. PROCESS_ALL_ACCESS — оверкилл. Для инжекта реально нужно только PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION. Запрос всех прав сразу — это красная тряпка для любого античита и для ETW-телеметрии винды. Лишний повод спалиться.
  2. LoadLibraryA вместо LoadLibraryW. ASCII-версия давно считается легаси, плюс если в пути будут не-ASCII символы (кириллица в имени папки, например) — инжект просто умрёт. Народ потом будет ныть в треде "нихрена не работает", а проблема в пути C:\Игры\Rust\.
  3. FindRemoteModuleBase через Process.Modules. Это работает, но через Process.Modules под капотом дёргается EnumProcessModules, который EAC хукает. Надёжнее через NtQueryInformationProcess + ручной парсинг PEB → Ldr, но это уже другой уровень запары, для дампера на локалке оверинжиниринг.
  4. Нет очистки remotePath после StartDump. Аллоцируется память под путь, но VirtualFreeEx для неё не вызывается — только хендл процесса закрывается. Утечка в целевом процессе на пару сотен байт, не критично, но грязно.
  5. waitForCompletion через INFINITE — если дампер по какой-то причине зависнет внутри (а на жирных сборках Раста с горой ассетов это вполне), твой инжектор повиснет вместе с ним наглухо. Лучше ставить таймаут хотя бы минут на 5 и обрабатывать WAIT_TIMEOUT.
  6. DONT_RESOLVE_DLL_REFERENCES для резолва RVA — норм трюк, но учти, что если у DLL включён ASLR (а у современных билдов почти всегда), локальная база и удалённая база будут разные, и ты правильно вычитаешь RVA. Тут автор не накосячил, но многие на этом палятся, копируя код по форумам.
По сути вопроса — пробовал вчера на актуальном билде Раста (на пустом аккаунте, естественно, не на мейне). Дамп по структурам ассетов прошёл, файл на выходе валидный, но:

  • На клиенте с EAC в актив-моде поток через CreateRemoteThread живёт буквально секунды, не успевает доработать. Бан прилетел минут через 20 после выхода из игры, не моментально (видимо, серверная проверка по логам).
  • На клиенте без EAC (отдельная сборка для копания) отрабатывает чисто, дамп ~критически зависит от того, прогрузил ли ты сцену перед инжектом. Если инжектить в меню — половина ассетов будет пустая, надо заходить в мир и потом дампить.
  • Прогресс в консоли иногда фризится на 60-70%, но это не зависание, это GC у дотнета внутри дампера тупит, надо просто ждать.
Если автор перепишет инжект на что-то менее палевное (SetWindowsHookEx, QueueUserAPC через спящие потоки, или вообще NtCreateThreadEx напрямую без LoadLibrary-стартера) — будет уже более-менее юзабельно даже под условно-защищёнными билдами. В текущем виде — только для исследовательских целей на оффлайн-клиенте, как автор и пишет.

З.ы. кому реально нужно копаться в ассетах Раста — гляньте в сторону готовых тулз под Unity (AssetRipper, UABE), они делают то же самое, но без инжекта в живой процесс и без рисков для аккаунта. Дампер из памяти оправдан только если нужны зашифрованные/обфусцированные структуры, которые в файлах на диске не лежат в открытом виде.
 
Здарова. Глянул код, потыкал — пара мыслей по делу.

По самому инжектору — реализация рабочая, но топорная, как лопата. CreateRemoteThread + LoadLibraryA это классика 2010-х, EAC такие вещи ловит на раз-два по сигнатуре потока, стартующего с адреса LoadLibraryA в чужом процессе. Так что предупреждение автора про "только на чистом клиенте" — не для красоты, реально отлетишь моментально, даже не успеешь ассеты вытянуть.

Несколько технических замечаний по сурсу, на что обратил внимание:

  1. PROCESS_ALL_ACCESS — оверкилл. Для инжекта реально нужно только PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION. Запрос всех прав сразу — это красная тряпка для любого античита и для ETW-телеметрии винды. Лишний повод спалиться.
  2. LoadLibraryA вместо LoadLibraryW. ASCII-версия давно считается легаси, плюс если в пути будут не-ASCII символы (кириллица в имени папки, например) — инжект просто умрёт. Народ потом будет ныть в треде "нихрена не работает", а проблема в пути C:\Игры\Rust\.
  3. FindRemoteModuleBase через Process.Modules. Это работает, но через Process.Modules под капотом дёргается EnumProcessModules, который EAC хукает. Надёжнее через NtQueryInformationProcess + ручной парсинг PEB → Ldr, но это уже другой уровень запары, для дампера на локалке оверинжиниринг.
  4. Нет очистки remotePath после StartDump. Аллоцируется память под путь, но VirtualFreeEx для неё не вызывается — только хендл процесса закрывается. Утечка в целевом процессе на пару сотен байт, не критично, но грязно.
  5. waitForCompletion через INFINITE — если дампер по какой-то причине зависнет внутри (а на жирных сборках Раста с горой ассетов это вполне), твой инжектор повиснет вместе с ним наглухо. Лучше ставить таймаут хотя бы минут на 5 и обрабатывать WAIT_TIMEOUT.
  6. DONT_RESOLVE_DLL_REFERENCES для резолва RVA — норм трюк, но учти, что если у DLL включён ASLR (а у современных билдов почти всегда), локальная база и удалённая база будут разные, и ты правильно вычитаешь RVA. Тут автор не накосячил, но многие на этом палятся, копируя код по форумам.
По сути вопроса — пробовал вчера на актуальном билде Раста (на пустом аккаунте, естественно, не на мейне). Дамп по структурам ассетов прошёл, файл на выходе валидный, но:

  • На клиенте с EAC в актив-моде поток через CreateRemoteThread живёт буквально секунды, не успевает доработать. Бан прилетел минут через 20 после выхода из игры, не моментально (видимо, серверная проверка по логам).
  • На клиенте без EAC (отдельная сборка для копания) отрабатывает чисто, дамп ~критически зависит от того, прогрузил ли ты сцену перед инжектом. Если инжектить в меню — половина ассетов будет пустая, надо заходить в мир и потом дампить.
  • Прогресс в консоли иногда фризится на 60-70%, но это не зависание, это GC у дотнета внутри дампера тупит, надо просто ждать.
Если автор перепишет инжект на что-то менее палевное (SetWindowsHookEx, QueueUserAPC через спящие потоки, или вообще NtCreateThreadEx напрямую без LoadLibrary-стартера) — будет уже более-менее юзабельно даже под условно-защищёнными билдами. В текущем виде — только для исследовательских целей на оффлайн-клиенте, как автор и пишет.

З.ы. кому реально нужно копаться в ассетах Раста — гляньте в сторону готовых тулз под Unity (AssetRipper, UABE), они делают то же самое, но без инжекта в живой процесс и без рисков для аккаунта. Дампер из памяти оправдан только если нужны зашифрованные/обфусцированные структуры, которые в файлах на диске не лежат в открытом виде.
Здарова.

По сути уже всё верно выше разобрали — добавлю только с точки зрения практики и здравого смысла.

Любые манипуляции с инжектом в живой клиент с EAC сейчас живут ровно до первого нормального сигнатурного или поведенческого триггера. Даже если что-то “завелось”, это не значит, что оно безопасно или стабильно — детект сейчас чаще прилетает постфактум по телеметрии, а не в моменте.

По самому подходу — это типичный учебный вариант для локального реверса/исследований, не более. В проде или на аккаунтах, которые жалко, такое использовать просто бессмысленно.

Если цель — разбор ассетов или структуры игры, то куда разумнее идти через оффлайн-инструменты и документацию по Unity, чем через живой процесс и риск ловить баны/флаги.

Короче: для песочницы и обучения ок, для реального использования — идея заранее проигрышная.
 
Здарова.

По сути уже всё верно выше разобрали — добавлю только с точки зрения практики и здравого смысла.

Любые манипуляции с инжектом в живой клиент с EAC сейчас живут ровно до первого нормального сигнатурного или поведенческого триггера. Даже если что-то “завелось”, это не значит, что оно безопасно или стабильно — детект сейчас чаще прилетает постфактум по телеметрии, а не в моменте.

По самому подходу — это типичный учебный вариант для локального реверса/исследований, не более. В проде или на аккаунтах, которые жалко, такое использовать просто бессмысленно.

Если цель — разбор ассетов или структуры игры, то куда разумнее идти через оффлайн-инструменты и документацию по Unity, чем через живой процесс и риск ловить баны/флаги.

Короче: для песочницы и обучения ок, для реального использования — идея заранее проигрышная.
Здарова.

Текущий подход (manual inject через классические WinAPI) объективно находится в зоне высокой детектируемости. Это не вопрос “если”, а вопрос “когда”. Современные античиты, включая EAC, опираются не только на сигнатуры, но и на поведенческий анализ — а подобные техники уже давно входят в базовый профиль подозрительной активности.


С практической точки зрения:


  • Надёжность: низкая (зависит от состояния клиента, сцены, сборки)
  • Безопасность: отсутствует (в контексте аккаунта)
  • Поддерживаемость: слабая (любое обновление может всё сломать)
  • Целесообразность: оправдана только для изолированных исследований

Сам дампер — это нормальный инструмент в рамках своей задачи, но именно как локальный исследовательский тул. Проблема не в нём, а в способе доставки (инжекте) в защищённый процесс.


Рациональная стратегия:


  • для анализа ассетов → оффлайн-парсинг (Unity-инструменты)
  • для реверса → отдельный клиент / изолированная среда
  • для “боевого” применения → текущий подход не масштабируется и не окупается

Итог: решение технически рабочее, но архитектурно устаревшее и практически неприменимое вне песочницы.
 
Народ, попал в руки простенький, но рабочий дампер для Раста. Ничего лишнего — чистый C#, который делает свою работу через стандартный мануальный инжект. Кто копается в материалах игры или пытается вытянуть структуру ассетов для своих нужд — вещь вполне юзабельная для быстрого сбора инфы.

Функционал:
  1. Показывает прогресс дампинга в консоли.
  2. Поддерживает кастомные имена файлов, что удобно для автоматизации скриптами.
  3. Работает через стандартный процесс инжекта (можно юзать Process Hacker или подставить свой загрузчик).

Сам инжектор реализован через стандартные WinAPI:
Код:
Expand Collapse Copy
OpenProcess
Код:
Expand Collapse Copy
VirtualAllocEx
и
Код:
Expand Collapse Copy
CreateRemoteThread
В сурсе есть логика вычисления RVA через
Код:
Expand Collapse Copy
LoadLibraryExA
без резолва зависимостей, что позволяет цепляться к процессу RustClient.exe без лишних головняков.

Код:
Expand Collapse Copy
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
 
namespace RustDumperNET
{
    static class Injector
    {
        const uint PROCESS_ALL_ACCESS = 0x001F_0FFF;
        const uint MEM_COMMIT = 0x0000_1000;
        const uint MEM_RESERVE = 0x0000_2000;
        const uint MEM_RELEASE = 0x0000_8000;
        const uint PAGE_READWRITE = 0x04;
        const uint INFINITE = 0xFFFF_FFFF;
 
        const uint DONT_RESOLVE_DLL_REFERENCES = 0x0000_0001;
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr OpenProcess(uint dwAccess, bool bInherit, int dwPid);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddr,
            IntPtr dwSize, uint flType, uint flProtect);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddr,
            IntPtr dwSize, uint dwFreeType);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddr,
            byte[] lpBuffer, IntPtr nSize, out IntPtr lpWritten);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpAttr,
            IntPtr dwStackSize, IntPtr lpStartAddr, IntPtr lpParam,
            uint dwFlags, out uint lpThreadId);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMs);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool CloseHandle(IntPtr hObject);
 
        [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
        static extern IntPtr LoadLibraryA(string lpFileName);
 
        [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
        static extern IntPtr LoadLibraryExA(string lpFileName, IntPtr hFile, uint dwFlags);
 
        [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
        static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool FreeLibrary(IntPtr hModule);
 
        public static void InjectAndRun(Process target, string dllPath,
            string outputPath, bool waitForCompletion = true)
        {
            string fullDllPath = Path.GetFullPath(dllPath);
            if (!File.Exists(fullDllPath))
                throw new FileNotFoundException($"DLL not found: {fullDllPath}");
 
            IntPtr hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, target.Id);
            if (hProcess == IntPtr.Zero)
                throw new InvalidOperationException(
                    $"OpenProcess failed (error {Marshal.GetLastWin32Error()})");
            try
            {
                InjectDll(hProcess, fullDllPath);
 
                IntPtr remoteStartDump = ResolveRemoteExport(target, fullDllPath, "StartDump");
 
                byte[] pathBytes = Encoding.UTF8.GetBytes(outputPath + '\0');
                IntPtr remotePath = VirtualAllocEx(hProcess, IntPtr.Zero,
                    (IntPtr)pathBytes.Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
                if (remotePath == IntPtr.Zero)
                    throw new InvalidOperationException(
                        $"VirtualAllocEx (path) failed (error {Marshal.GetLastWin32Error()})");
 
                if (!WriteProcessMemory(hProcess, remotePath, pathBytes,
                        (IntPtr)pathBytes.Length, out _))
                    throw new InvalidOperationException(
                        $"WriteProcessMemory (path) failed (error {Marshal.GetLastWin32Error()})");
 
                IntPtr hDumpThread = CreateRemoteThread(hProcess, IntPtr.Zero, IntPtr.Zero,
                    remoteStartDump, remotePath, 0, out _);
                if (hDumpThread == IntPtr.Zero)
                    throw new InvalidOperationException(
                        $"CreateRemoteThread (StartDump) failed (error {Marshal.GetLastWin32Error()})");
 
                if (waitForCompletion)
                    WaitForSingleObject(hDumpThread, INFINITE);
 
                CloseHandle(hDumpThread);
            }
            finally
            {
                CloseHandle(hProcess);
            }
        }
 
        // -------------------------------------------------------------------------
 
        static void InjectDll(IntPtr hProcess, string fullDllPath)
        {
            byte[] pathBytes = Encoding.ASCII.GetBytes(fullDllPath + '\0');
 
            IntPtr remoteDllPath = VirtualAllocEx(hProcess, IntPtr.Zero,
                (IntPtr)pathBytes.Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
            if (remoteDllPath == IntPtr.Zero)
                throw new InvalidOperationException(
                    $"VirtualAllocEx (DLL path) failed (error {Marshal.GetLastWin32Error()})");
            try
            {
                if (!WriteProcessMemory(hProcess, remoteDllPath, pathBytes,
                        (IntPtr)pathBytes.Length, out _))
                    throw new InvalidOperationException(
                        $"WriteProcessMemory (DLL path) failed (error {Marshal.GetLastWin32Error()})");
 
                IntPtr hKernel32 = LoadLibraryA("kernel32.dll");
                IntPtr loadLibraryA = GetProcAddress(hKernel32, "LoadLibraryA");
                FreeLibrary(hKernel32);
 
                IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, IntPtr.Zero,
                    loadLibraryA, remoteDllPath, 0, out _);
                if (hThread == IntPtr.Zero)
                    throw new InvalidOperationException(
                        $"CreateRemoteThread (LoadLibraryA) failed (error {Marshal.GetLastWin32Error()})");
 
                WaitForSingleObject(hThread, INFINITE);
                CloseHandle(hThread);
            }
            finally
            {
                VirtualFreeEx(hProcess, remoteDllPath, IntPtr.Zero, MEM_RELEASE);
            }
        }
 
        static IntPtr ResolveRemoteExport(Process target, string fullDllPath, string exportName)
        {         
            IntPtr hLocal = LoadLibraryExA(fullDllPath, IntPtr.Zero, DONT_RESOLVE_DLL_REFERENCES);
            if (hLocal == IntPtr.Zero)
                throw new InvalidOperationException(
                    $"Local LoadLibraryEx failed (error {Marshal.GetLastWin32Error()})");
 
            IntPtr localExport = GetProcAddress(hLocal, exportName);
            if (localExport == IntPtr.Zero)
            {
                FreeLibrary(hLocal);
                throw new InvalidOperationException(
                    $"Export '{exportName}' not found in {Path.GetFileName(fullDllPath)}");
            }
 
            long exportRva = localExport.ToInt64() - hLocal.ToInt64();
            FreeLibrary(hLocal);
 
            IntPtr remoteBase = FindRemoteModuleBase(target, fullDllPath);
            return new IntPtr(remoteBase.ToInt64() + exportRva);
        }
 
        static IntPtr FindRemoteModuleBase(Process target, string fullDllPath,
            int retries = 15, int delayMs = 200)
        {
            string dllName = Path.GetFileName(fullDllPath);
            for (int i = 0; i < retries; i++)
            {
                try
                {
                    target.Refresh();
                    foreach (ProcessModule mod in target.Modules)
                    {
                        if (string.Equals(Path.GetFileName(mod.FileName), dllName,
                                StringComparison.OrdinalIgnoreCase))
                            return mod.BaseAddress;
                    }
                }
                catch (Exception) { }
 
                Thread.Sleep(delayMs);
            }
            throw new InvalidOperationException(
                $"Module '{dllName}' not found in target process after {retries} retries.");
        }
    }
}

Нюансы:
Поскольку это чистый C# Dumper, не забывайте, что инжект в игру с EAC — это всегда лотерея. Если юзаете на мейне — отлетите быстрее, чем прогрузится карта. Для легитной игры не подходит, используйте только на локалке или чистом клиенте.

Кто уже пробовал прокидывать этот дамп под актуальный билд, отпишитесь по результатам в треде?
Не думаю что эта какулина будет работать, седня затещу
 
ждем когда говард завезет новую фичу как в телеграмме
1775851236565.png
 
Назад
Сверху Снизу