Чтобы сформировать понимание, как происходит получение списка процессов, просто заглянем в исходники самого Cheat Engine.

Здесь у нас есть процедура GetProcessList, в которую мы подаем массив строк, в который она запишем нам имена и айди процессов.

Первое, на что обратим внимание - структура, куда записывается информация о процесе, в СЕ она выглядит так

{$IFDEF WINDOWS}
type TProcessListInfo=record
  processID: dword;
  winhandle: HWND;
  processIcon: HICON;
  //issystemprocess: boolean;
end;
PProcessListInfo=^TProcessListInfo;
{$ENDIF}

Мы можем ее записать так.

typedef struct ProcessListInfo {
    DWORD processID;
    HWND winhandle; <-- не используется в GetProcessList
    HICON processIcon; <-- не используется в GetProcessList
} ProcessListInfo, *PProcessListInfo;

Далее в процедуре идет блок с переменными

var SNAPHandle: THandle;
    ProcessEntry: PROCESSENTRY32; <--- сюда запишем информацию о процессе
    Check: Boolean; <--- сюда будем записывать ответ от Process32First/Next;
{$IFDEF WINDOWS}
    lwindir: string; <--- путь в рабочий каталог Windows, так же не используется..
    HI: HICON; <--- хендл иконки не используется в процедуре..
    ProcessListInfo: PProcessListInfo; <--- наша структура
{$ENDIF}
    i,j: integer; <--- не упомянается в коде вообще...
    s,s2: string; <--- не упомянается в коде вообще...

Как итог я оставил так

    HANDLE snap_handle = nullptr;
    PROCESSENTRY32W process_entry;
    bool check;
    PProcessListInfo process_list_info;

Для замены под C++ объекта ProcessList: TStrings, я использовал std::unordered_map<std::wstring, PProcessListInfo>& process_list. Потому что в коде, была логика схожая с мапой, когда у нас по имени процесса идет связка с объектом, содержащим информацию о нем (фактически один ProcessID…)

     getmem(ProcessListInfo,sizeof(TProcessListInfo)); // Выделение блока памяти размером с TProcessListInfo и получаем указатель на него в ProcessListInfo 

// Установка значений полей структуры PProcessListInfo (идентификатор процесса, дескрипторы иконки и окна)
     ProcessListInfo.processID:=processentry.th32ProcessID;
     ProcessListInfo.processIcon:=0; 
     ProcessListInfo.winhandle:=0;

Что у нас эквивалентно

     process_list_info              = (PProcessListInfo)malloc(sizeof(ProcessListInfo));
     process_list_info->processID   = process_entry.th32ProcessID;
     process_list_info->processIcon = 0;
     process_list_info->winhandle   = 0;

И в конце при необходимости это включается в лист

{$ifdef windows}
// Если не требуется получение дополнительной информации о процессе
 (noProcessInfo=true), добавляем строку с именем исполняемого файла без объекта
        if noprocessinfo then

          ProcessList.Add(s)

 // В противном случае добавляем в список processlist новую запись состоящую из
  имени исполняемого файла и связанного с ней дополнительной информации (объекта)
       else 
         ProcessList.AddObject(s, TObject(ProcessListInfo));

{$else}

Заменил на

       if (no_process_info)
           process_list[ss.str()] = nullptr;
       else
           process_list[ss.str()] = process_list_info;

По этой процедуре особо добавить и нечего, она просто делает снимок и пробегается по процессам, выгружая данные…

Но тут мое любопытство увело меня в сторону от Cheat Engine, и я решил посмотреть, что там у Process Hacker, это утилита позволяет работать с процессами. Самой интересной частью является - список модулей. Потому, что там сразу можно увидеть кто-где и какой размер в памяти занимает каждый из них.

Через поиск по файла по фразе EnumModules я вышел на вот такую вот функцию

NTSTATUS PhpEnumProcessModules(
    _In_ HANDLE ProcessHandle,
    _In_ PPHP_ENUM_PROCESS_MODULES_CALLBACK Callback,
    _In_opt_ PVOID Context1,
    _In_opt_ PVOID Context2
    )

так же у нее есть 32битная реализация

NTSTATUS PhpEnumProcessModules32(
    _In_ HANDLE ProcessHandle,
    _In_ PPHP_ENUM_PROCESS_MODULES32_CALLBACK Callback,
    _In_opt_ PVOID Context1,
    _In_opt_ PVOID Context2
    )

Результатом работы оных будет вот такая вот структура

typedef struct _LDR_DATA_TABLE_ENTRY_PHNT
{
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    union
    {
        LIST_ENTRY InInitializationOrderLinks;
        LIST_ENTRY InProgressLinks;
    };
    PVOID DllBase;
    PLDR_INIT_ROUTINE EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    union
    {
        UCHAR FlagGroup[4];
        ULONG Flags;
        struct
        {
            ULONG PackagedBinary : 1;
            ULONG MarkedForRemoval : 1;
            ULONG ImageDll : 1;
            ULONG LoadNotificationsSent : 1;
            ULONG TelemetryEntryProcessed : 1;
            ULONG ProcessStaticImport : 1;
            ULONG InLegacyLists : 1;
            ULONG InIndexes : 1;
            ULONG ShimDll : 1;
            ULONG InExceptionTable : 1;
            ULONG ReservedFlags1 : 2;
            ULONG LoadInProgress : 1;
            ULONG LoadConfigProcessed : 1;
            ULONG EntryProcessed : 1;
            ULONG ProtectDelayLoad : 1;
            ULONG ReservedFlags3 : 2;
            ULONG DontCallForThreads : 1;
            ULONG ProcessAttachCalled : 1;
            ULONG ProcessAttachFailed : 1;
            ULONG CorDeferredValidate : 1;
            ULONG CorImage : 1;
            ULONG DontRelocate : 1;
            ULONG CorILOnly : 1;
            ULONG ChpeImage : 1;
            ULONG ChpeEmulatorImage : 1;
            ULONG ReservedFlags5 : 1;
            ULONG Redirected : 1;
            ULONG ReservedFlags6 : 2;
            ULONG CompatDatabaseProcessed : 1;
        };
    };
    USHORT ObsoleteLoadCount;
    USHORT TlsIndex;
    LIST_ENTRY HashLinks;
    ULONG TimeDateStamp;
    struct _ACTIVATION_CONTEXT *EntryPointActivationContext;
    PVOID Lock; // RtlAcquireSRWLockExclusive
    PLDR_DDAG_NODE DdagNode;
    LIST_ENTRY NodeModuleLink;
    struct _LDRP_LOAD_CONTEXT *LoadContext;
    PVOID ParentDllBase;
    PVOID SwitchBackContext;
    RTL_BALANCED_NODE BaseAddressIndexNode;
    RTL_BALANCED_NODE MappingInfoIndexNode;
    ULONG_PTR OriginalBase;
    LARGE_INTEGER LoadTime;
    ULONG BaseNameHashValue;
    LDR_DLL_LOAD_REASON LoadReason; // since WIN8
    ULONG ImplicitPathOptions;
    ULONG ReferenceCount; // since WIN10
    ULONG DependentLoadFlags;
    UCHAR SigningLevel; // since REDSTONE2
    ULONG CheckSum; // since 22H1
    PVOID ActivePatchImageBase;
    LDR_HOT_PATCH_STATE HotPatchState;
} LDR_DATA_TABLE_ENTRY_PHNT, *PLDR_DATA_TABLE_ENTRY_PHNT;

Самыми интересными для нас будут PVOID DllBase - начало модуля относительно процесса
ULONG SizeOfImage - размер модуля (сколько байт он занимает внутри процесса) и UNICODE_STRING FullDllName. Но с именем все не так просто. Структура выглядит так

  typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
  } UNICODE_STRING;

Казалось бы, вот же она… PWSTR Buffer строка… выводись в студаут!! А вот и нет, здесь находится адрес в чужом пространстве, чтобы прочитать эту строку, придется сделать следующее.

    PWSTR name[MAX_PATH]; // зададим наш буффер для строки
    ZeroMemory(name, MAX_PATH*sizeof(WCHAR)); // зануляем
    if (NT_SUCCESS(status = NtReadVirtualMemory(
                                ProcessHandle, // хендл процесса
                                currentEntry.FullDllName.Buffer, // наш указатель
                                name, // буффер
                                currentEntry.FullDllName.Length, // размер строки
                                NULL
    )))
    {              
// по итогу можем вывести результат в консоль и порадоваться :з
    std::wcout 
        << std::hex << currentEntry.DllBase 
        << L' ' 
        << std::dec << currentEntry.SizeOfImage 
        << L' ' 
        << (WCHAR*)name 
        << L'\n';
    }

И тут я на радостях побежал смотреть все модули, но не тут-то было… Если получить список процессов вполне себе легитимная процедура, то читать память другого процесса уже не всегда дозволяется авторами софта. Но на этот случай у ProcessHacker есть свой собственный драйвер, на то он и хакер.

Все, до чего я докопался - это метод

PHLIBAPI
NTSTATUS
NTAPI
KphConnect(
    _In_opt_ PWSTR DeviceName
    );

Данные о драйвере выглядят так

// Device

#define KPH_DEVICE_SHORT_NAME L"KProcessHacker3"
#define KPH_DEVICE_TYPE 0x9999
#define KPH_DEVICE_NAME (L"\\Device\\" KPH_DEVICE_SHORT_NAME)

Но, к сожалению, с наскока подключиться к драйверу не удалось и лучший ответ, который я получилъ

#define STATUS_INVALID_DEVICE_REQUEST ((NTSTATUS)0xC0000010)

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

Конец! А кто слушал - можете прокачать свои навыки на крутейшем курсе по реверсу ММОРПГ :)

Комментарии (3)


  1. vilgeforce
    01.02.2025 18:58

    Не очень понятно зачем все это, если CreateToolhelp32Snapshot и соответствующие First- и Next- функции перечисляют все что что нужно и без чтения чужой памяти...


    1. osieman Автор
      01.02.2025 18:58

      Спасибо) Опыта у меня не много, делюсь тем, что вижу)
      Как я понимаю, это не всегда доступно.


      1. vilgeforce
        01.02.2025 18:58

        Доступно начиная с WinXP, статуса устаревшей на ней не висит. Вроде и от обычного пользователя работать должна...