четверг, 14 ноября 2013 г.

Карта памяти процесса

Задумывались ли вы над тем, как именно используется память, доступная вашей программе, да и вообще, что именно размещается в этих двух-трех гигабайтах виртуальной памяти, с которыми работает ваше ПО?

Спросите, зачем?
Ну как же, для 32-битного приложения 2-3 гигабайта – это ваш лимит за пределы которого без использования AWE вы выбраться не сможете, а контролировать собственные ресурсы все же желательно. Но даже и без этого просто с целью разобраться...

В прошлых статьях я описывал работу отладчика, где производились модификации памяти приложения, находящегося под отладкой. Эта статья является продолжением данного материала. И хотя к отладчику она не будет иметь отношения, но вот к процессу отладки – самое непосредственное...

Давайте посмотрим, как именно программист работает с памятью при отладке (особенно при отладке стороннего приложения, проще говоря, при реверсе):

1. Как правило, самой частой операцией будет поиск значения в памяти приложения и, к сожалению, данный функционал почему-то не предоставлен в отладчике Delphi (собственно, как и в MS VC++).
2. Модификация системных структур (PEB/TEB/SEHChain/Unwind/директорий PE-файлов etc...) будет происходить гораздо проще, когда поля структур размаплены на занимаемые ими адреса и представлены в читабельном виде.
3. Отслеживание изменений в памяти процесса (практически никем не предоставляемый функционал, реализованный в виде плагинов к популярным отладчикам). Действительно, зачем трассировать до посинения, когда достаточно сравнить два снимка карты памяти, чтобы понять, тут ли происходит нужная нам модификация данных или нет?

Да, собственно, вариантов использования много.

Впрочем, если без лирики, утилит отображающих более-менее вменяемую информацию о карте памяти процесса, которую можно применить для отладки, очень мало.

Самая удобная реализация от OllyDebug 2, но, к сожалению, она не отображает данные по 64 битам (все еще ждем).

OllyDebug 2 - Process Environment Block Dump

VMMap от Марка Руссиновича выполняет чисто декоративные свойства, да красиво, да за подписью Microsoft, но практически применить выводимые ей данные тяжеловато.

VMMap отображает только самую базовую информацию не вникая в детали.

ProcessHacker – хороший инструмент, но его автор не ставил перед собой задач по работе с выводом данных о памяти, поэтому выводимая им информация можно сказать вообще самая простая.

Process Hacker - Memory Dump

Ну а к карте памяти от IDA Pro за столько лет работы с ней я так и не привык (мне не удобно) :)

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

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


Содержание




1. Получаем список доступных регионов


Вся виртуальная память процесса представлена в виде страниц.
Страницы бывают маленькие (4096 байт) и большие. (Подробнее можно узнать в MSDN)
В большинстве случаев идущие подряд страницы имеют одинаковые атрибуты.

Что есть регион?
Грубо (если взять за основу MSDN) – это набор всех страниц имеющих одинаковые атрибуты, которые начинающихся с переданного функции VirtualQuery адреса.

В самом простейшем виде получить список регионов нашего процесса можно вот таким кодом:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Windows,
  SysUtils;

var
  MBI: TMemoryBasicInformation;
  dwLength: NativeUInt;
  Address: PByte;
begin
  Address := nil;
  dwLength := SizeOf(TMemoryBasicInformation);
  while VirtualQuery(Address, MBI, dwLength) <> 0 do
  begin
    Writeln(
      'AllocationBase: ', IntToHex(NativeUInt(MBI.AllocationBase), 8),
      ', BaseAddress: ', IntToHex(NativeUInt(MBI.BaseAddress), 8),
      ', RegionSize: ', MBI.RegionSize);
    Inc(Address, MBI.RegionSize);
  end;
  Readln;
end.

К примеру, изначально мы передали первым параметром адрес nil. После вызова функции переменная MBI примет следующие значения:

  • BaseAddress = nil
  • AllocationBase = nil
  • AllocationProtect = 0
  • RegionSize = $10000
  • State = $10000
  • Protect = 1
  • Type_9 = 0

Размер региона равен $10000 (64 кб), это соответствует 16 страницам, идущим подряд, начиная с адреса ноль, состояние которых (State) равно MEM_FREE ($10000) и выставлен атрибут защиты PAGE_NO_ACCESS (1) в параметре Protect.

Если переписать код вот таким образом:

function ExtractAccessString(const Value: DWORD): string;
const
  PAGE_WRITECOMBINE = $400;
begin
  Result := 'Unknown access';
  if (Value and PAGE_EXECUTE) = PAGE_EXECUTE then Result := 'E';
  if (Value and PAGE_EXECUTE_READ) = PAGE_EXECUTE_READ then Result := 'RE';
  if (Value and PAGE_EXECUTE_READWRITE) = PAGE_EXECUTE_READWRITE then
     Result := 'RWE';
  if (Value and PAGE_EXECUTE_WRITECOPY) = PAGE_EXECUTE_WRITECOPY then
    Result := 'RE, Write copy';
  if (Value and PAGE_NOACCESS) = PAGE_NOACCESS then Result := 'No access';
  if (Value and PAGE_READONLY) = PAGE_READONLY then Result := 'R';
  if (Value and PAGE_READWRITE) = PAGE_READWRITE then Result := 'RW';
  if (Value and PAGE_WRITECOPY) = PAGE_WRITECOPY then Result := 'Write copy';
  if (Value and PAGE_GUARD) = PAGE_GUARD then
    Result := Result + ', Guarded';
  if (Value and PAGE_NOCACHE) = PAGE_NOCACHE then
    Result := Result + ', No cache';
  if (Value and PAGE_WRITECOMBINE) = PAGE_WRITECOMBINE then
    Result := Result + ', Write Combine';
end;

function ExtractRegionTypeString(Value: TMemoryBasicInformation): string;
begin
  Result := '';
  case Value.State of
    MEM_FREE: Result := 'Free';
    MEM_RESERVE: Result := 'Reserved';
    MEM_COMMIT:
    case Value.Type_9 of
      MEM_IMAGE: Result := 'Image';
      MEM_MAPPED: Result := 'Mapped';
      MEM_PRIVATE: Result := 'Private';
    end;
  end;
  Result := Result + ', ' + ExtractAccessString(Value.Protect);
end;

var
  MBI: TMemoryBasicInformation;
  dwLength: NativeUInt;
  Address: PByte;
begin
  Address := nil;
  dwLength := SizeOf(TMemoryBasicInformation);
  while VirtualQuery(Address, MBI, dwLength) <> 0 do
  begin
    Writeln(
      'AllocationBase: ', IntToHex(NativeUInt(MBI.AllocationBase), 8),
      ', BaseAddress: ', IntToHex(NativeUInt(MBI.BaseAddress), 8),
      ' - ', ExtractRegionTypeString(MBI));
    Inc(Address, MBI.RegionSize);
  end;

... то можно наглядно увидеть принцип разбиения на регионы функцией VirtualAlloc:

Страницы объединяются в регионы по совокупности свойств

К примеру, у второго и третьего региона атрибуты доступа одинаковые (чтение запись), но разная AllocationBase. AllocationBase назначается страницам при выделении памяти посредством VirtualAlloc, объединяя их таким образом в отдельный регион.

2. Собираем данные о потоках


Пришла пора начать заполнять полученные нами регионы информацией о том, что они хранят, и начнем мы с потоков (нитей – кому как удобнее).

Код получения списка потоков простой – через CreateToolhelp32Snapshot.

const
  THREAD_GET_CONTEXT = 8;
  THREAD_SUSPEND_RESUME = 2;
  THREAD_QUERY_INFORMATION = $40;
  ThreadBasicInformation = 0;
  ThreadQuerySetWin32StartAddress = 9;
  STATUS_SUCCESS = 0;
var
  hSnap, hThread: THandle;
  ThreadEntry: TThreadEntry32;
  TBI: TThreadBasicInformation;
  TIB: NT_TIB;
  lpNumberOfBytesRead: NativeUInt;
  ThreadStartAddress: Pointer;
begin

  // Делаем снимок нитей в системе
  hSnap := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, GetCurrentProcessId);
  if hSnap <> INVALID_HANDLE_VALUE then
  try
    ThreadEntry.dwSize := SizeOf(TThreadEntry32);
    if Thread32First(hSnap, ThreadEntry) then
    repeat
      if ThreadEntry.th32OwnerProcessID <> GetCurrentProcessId then Continue;

      Writeln('ThreadID: ', ThreadEntry.th32ThreadID);

      // Открываем нить
      hThread := OpenThread(THREAD_GET_CONTEXT or
        THREAD_SUSPEND_RESUME or THREAD_QUERY_INFORMATION,
        False, ThreadEntry.th32ThreadID);
      if hThread <> 0 then
      try
        // Получаем адрес ThreadProc()
        if NtQueryInformationThread(hThread, ThreadQuerySetWin32StartAddress,
          @ThreadStartAddress, SizeOf(ThreadStartAddress), nil) = STATUS_SUCCESS then
          Writeln('ThreadProcAddr: ', IntToHex(NativeUInt(ThreadStartAddress), 1));

        // Получаем информацию по нити
        if NtQueryInformationThread(hThread, ThreadBasicInformation, @TBI,
          SizeOf(TThreadBasicInformation), nil) = STATUS_SUCCESS then
        begin

          Writeln('Thread Environment Block (TEB) Addr: ',
            IntToHex(NativeUInt(TBI.TebBaseAddress), 1));

          // Читаем из удаленного адресного пространства
          // TIB (Thread Information Block) открытой нити
          if ReadProcessMemory(GetCurrentProcess,
            TBI.TebBaseAddress, @TIB, SizeOf(NT_TIB),
            lpNumberOfBytesRead) then
          begin
            Writeln('Thread StackBase Addr: ',
              IntToHex(NativeUInt(TIB.StackBase), 1));
            Writeln('Thread StackLimit Addr: ',
              IntToHex(NativeUInt(TIB.StackLimit), 1));
          end;

        end;

      finally
        CloseHandle(hThread);
      end;
    until not Thread32Next(hSnap, ThreadEntry);
  finally
     CloseHandle(hSnap);
  end;
  Readln;
end.

По шагам:

  1. При помощи CreateToolhelp32Snapshot/Thread32First/Thread32Next получаем список активных потоков у нашего приложения.
  2. Для получения более подробной информации потребуется хендл потока, который получаем посредством вызова OpenThread.
  3. При помощи NtQueryInformationThread получаем адрес процедуры потока, с которой он начал работу, и базовую информацию о потоке в виде структуры TThreadBasicInformation.
  4. Из этой структуры нас интересует только одно поле – TebBaseAddress, которое содержит адрес блока окружения потока, т.н. TEB (Thread Environment Block).
  5. Посредством вызова ReadProcessMemory (хотя для своего приложения это и избыточно) зачитываем данные по адресу TEB, а именно самый первый ее параметр, представляющий из себя структуру NT_TIB.

Декларация NT_TIB выглядит так:

  PNT_TIB = ^_NT_TIB;
  _NT_TIB = record
    ExceptionList: Pointer;
    StackBase,
    StackLimit,
    SubSystemTib: Pointer;
    case Integer of
      0: (
        FiberData: Pointer
        );
      1: (
        Version: ULONG;
        ArbitraryUserPointer: Pointer;
        Self: PNT_TIB;
        )
  end;
  NT_TIB = _NT_TIB;
  PPNT_TIB = ^PNT_TIB;

Ну или вот так, если описывать чуть подробнее:

  • ExceptionList – в 32-битном процессе указатель на адрес текущего SEH фрейма (структуру EXCEPTION_REGISTRATION). Основываясь на данной информации, мы будем раскручивать всю цепочку SEH фреймов.
    Если же TEB принадлежит 64-битному потоку, работающего в 32-битном приложении, то данное поле будет указывать на поле ExceptionList своего 32-битного аналога.
    В 64-битном процессе данное поле всегда обнилено, т.к. для 64 бит взамен механизма SEH работает немного другой механизм.
  • StackBase – база стека. Адрес от которого стек начинает расти в направлении StackLimit.
  • StackLimit – текущая верхушка стека.
  • ArbitraryUserPointer – что-то наподобие свободного TLS слота. Грубо говоря переменная принадлежащая потоку, значение которой может произвольно изменятся самим программистом для собственных нужд.
  • Self - параметр, содержащий адрес TEB (т.е. самого себя)

Остальные поля не нужны.

Ну, впрочем, как – не нужны?
Нужны, конечно, но пока что они для нас избыточны.
Кстати, вот ссылка, где вы сможете увидеть слегка устаревшее описание данной структуры: Thread Environment Block.

Данный код отобразит нам следующую картинку:



А вот так это будет видно в VMMap.

VMMap не отобразила информацию о TEB
Кстати, часть функций и структур из приведенного выше кода не задекларированы в стандартных исходниках Delphi, их декларацию вы сможете увидеть в демо-примерах, идущих в составе данной статьи. Но это не означает того, что они недокументированы в MSDN :)

Если мы захотим работать с TEB своего потока, то код очень сильно упростится из-за того что не нужно использовать функции ToolHelp32.dll, а достаточно использовать сегментный регистр FS (или GS для х64).
К примеру, очень часто встречается такая функция для получения адреса TEB:

function GetCurrentTEB: Pointer;
asm
{$IFDEF WIN64}
  // mov RAX, qword ptr GS:[30h] 
  // реализованно через машкоды, ввиду неверной генерации кода инструкции 64-битным компилятором
  DB $65, $48, $8B, $04, $25
  DD $00000030
{$ELSE}
  mov EAX, FS:[18h]
{$ENDIF}
end;

В данном случае происходит доступ к параметру NtTIB.Self структуры TEB, который расположен по смещению 0x18 (или 0x30 в случае 64-битного TEB) от ее начала.

Впрочем, продолжим...
Часть данных получили, но это не вся информация доступная нам.

На стеке каждого потока расположены SEH фреймы, которые генерируются автоматом при входе в блок try..finally/except, а также стек вызовов процедур. Было бы хорошо иметь эти данные на руках и выводить их в более наглядном виде – с привязкой к региону.

Раскруткой SEH фреймов у нас будет заниматься вот такая простенькая процедура:

procedure GetThreadSEHFrames(InitialAddr: Pointer);
type
  EXCEPTION_REGISTRATION = record
    prev, handler: Pointer;
  end;
var
  ER: EXCEPTION_REGISTRATION;
  lpNumberOfBytesRead: NativeUInt;
begin
  while ReadProcessMemory(GetCurrentProcess, InitialAddr, @ER,
    SizeOf(EXCEPTION_REGISTRATION), lpNumberOfBytesRead) do
  begin
    Writeln('SEH Frame at Addr: ',
      IntToHex(NativeUInt(InitialAddr), 1), ', handler at addr: ',
      IntToHex(NativeUInt(ER.handler), 1));
    InitialAddr := ER.prev;
    if DWORD(InitialAddr) <= 0 then Break;
  end;
end;

Получив в качестве параметра значение TEB.TIB.ExceptionList, которое указывает на первую структуру EXCEPTION_REGISTRATION, она бежит по цепочке данных структур, ориентируясь на значение prev данной структуры, которое содержит адрес предыдущей структуры EXCEPTION_REGISTRATION. А параметр handler содержит адрес обработчика исключения, если оно вдруг произойдет.

Выглядит все вот так:

Список SEH фреймов

Ну а CallStack будет получать следующая процедура:

procedure GetThreadCallStack(hThread: THandle);
var
  StackFrame: TStackFrame;
  ThreadContext: PContext;
  MachineType: DWORD;
begin
  // ThreadContext должен быть выровнен, поэтому используем VirtualAlloc
  // которая автоматически выделит память выровненную по началу страницы
  // в противном случае получим ERROR_NOACCESS (998)
  ThreadContext := VirtualAlloc(nil, SizeOf(TContext), MEM_COMMIT, PAGE_READWRITE);
  try
    ThreadContext^.ContextFlags := CONTEXT_FULL;
    if not GetThreadContext(hThread, ThreadContext^) then
      Exit;

    ZeroMemory(@StackFrame, SizeOf(TStackFrame));
    StackFrame.AddrPC.Mode := AddrModeFlat;
    StackFrame.AddrStack.Mode := AddrModeFlat;
    StackFrame.AddrFrame.Mode := AddrModeFlat;
    StackFrame.AddrPC.Offset := ThreadContext.Eip;
    StackFrame.AddrStack.Offset := ThreadContext.Esp;
    StackFrame.AddrFrame.Offset := ThreadContext.Ebp;
    MachineType := IMAGE_FILE_MACHINE_I386;

    while True do
    begin
      if not StackWalk(MachineType, GetCurrentProcess, hThread, @StackFrame,
        ThreadContext, nil, nil, nil, nil) then
        Break;
      if StackFrame.AddrPC.Offset <= 0 then Break;
      Writeln('CallStack Frame Addr: ',
        IntToHex(NativeUInt(StackFrame.AddrFrame.Offset), 1));
      Writeln('CallStack Handler: ',
        IntToHex(NativeUInt(StackFrame.AddrPC.Offset), 1));
      Writeln('CallStack Stack: ',
        IntToHex(NativeUInt(StackFrame.AddrStack.Offset), 1));
      Writeln('CallStack Return: ',
        IntToHex(NativeUInt(StackFrame.AddrReturn.Offset), 1));
    end;

  finally
    VirtualFree(ThreadContext, SizeOf(TContext), MEM_FREE);
  end;
end;

Правда, в отличие от отладчика Delphi, он будет выводить данные о процедурах, для которых сгенерирован стековый фрейм, остальные он пропустит.
За перечисление информации о стековых фреймах отвечает функция StackWalk (или StackWalk64).

Теперь нюанс: если мы применим данный код к самому себе, то он сможет оттрассировать только один стековый фрейм, после чего произойдет выход (можете проверить на демоприложении).

Произойдет это по следующей причине: для правильной трассировки функции StackWalk необходимо указать параметры текущего кадра стека (EBP и ESP/ RBP и RSP для х64) и, собственно, текущий адрес кода (регистр EIP или RIP для х64). Если мы будем брать эти данные с самого себя, то это произойдет в тот момент, когда бы вызвали функцию GetThreadContext, а раскручивать стек мы начнем уже после выхода из данной функции, где все три параметра станут, мягко говоря, не валидны. По этой причине сделать трассировку самого себя вызовом данной функции не получится.
Этот момент желательно учитывать...

На получении информации о потоках 32-битного процесса под 64-битной ОС включая 32 и 64-битные варианты я остановлюсь несколько позже, а сейчас...

3. Собираем данные о кучах


Само по себе Delphi приложение, как правило, кучи не использует, это больше прерогатива С++ приложений, но все-таки кучи присутствуют и здесь. Обычно их создают и используют различные сторонние библиотеки для своих нужд.

Нюанс при получении данных о кучах в том, что элементов HeapEntry, из которых состоит каждая куча, может быть несколько тысяч, а второй нюанс в том, что функция Heap32Next при каждом вызове заново перестраивает весь список, создавая при этом достаточно чувствительную задержку (вплоть до десятков секунд).

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

const
  RTL_HEAP_BUSY = 1;
  RTL_HEAP_SEGMENT = 2;
  RTL_HEAP_SETTABLE_VALUE = $10;
  RTL_HEAP_SETTABLE_FLAG1 = $20;
  RTL_HEAP_SETTABLE_FLAG2 = $40;
  RTL_HEAP_SETTABLE_FLAG3 = $80;
  RTL_HEAP_SETTABLE_FLAGS = $E0;
  RTL_HEAP_UNCOMMITTED_RANGE = $100;
  RTL_HEAP_PROTECTED_ENTRY = $200;
  RTL_HEAP_FIXED = (RTL_HEAP_BUSY or RTL_HEAP_SETTABLE_VALUE or
    RTL_HEAP_SETTABLE_FLAG2 or RTL_HEAP_SETTABLE_FLAG3 or
    RTL_HEAP_SETTABLE_FLAGS or RTL_HEAP_PROTECTED_ENTRY);
  STATUS_SUCCESS = 0;

  function CheckSmallBuff(Value: DWORD): Boolean;
  const
    STATUS_NO_MEMORY = $C0000017;
    STATUS_BUFFER_TOO_SMALL = $C0000023;
  begin
    Result := (Value = STATUS_NO_MEMORY) or (Value = STATUS_BUFFER_TOO_SMALL);
  end;

  function FlagToStr(Value: DWORD): string;
  begin
    case Value of
      LF32_FIXED: Result := 'LF32_FIXED';
      LF32_FREE: Result := 'LF32_FREE';
      LF32_MOVEABLE: Result := 'LF32_MOVEABLE';
    else
      Result := '';
    end;
  end;

var
  I, A: Integer;
  pDbgBuffer: PRtlDebugInformation;
  pHeapInformation: PRtlHeapInformation;
  pHeapEntry: PRtrHeapEntry;
  dwAddr, dwLastSize: ULONG_PTR;
  hit_seg_count: Integer;
  BuffSize: NativeUInt;
begin
  // Т.к. связка Heap32ListFirst, Heap32ListNext, Heap32First, Heap32Next
  // работает достаточно медленно, из-за постоянного вызова
  // RtlQueryProcessDebugInformation на каждой итерации, мы заменим ее вызов
  // аналогичным кодом без ненужного дубляжа
  // Создаем отладочный буфер
  BuffSize := $400000;
  pDbgBuffer := RtlCreateQueryDebugBuffer(BuffSize, False);
  // Запрашиваем информацию по списку куч процесса
  while CheckSmallBuff(RtlQueryProcessDebugInformation(GetCurrentProcessId,
    RTL_QUERY_PROCESS_HEAP_SUMMARY or RTL_QUERY_PROCESS_HEAP_ENTRIES,
    pDbgBuffer)) do
  begin
    // если размера буфера не хватает, увеличиваем...
    RtlDestroyQueryDebugBuffer(pDbgBuffer);
    BuffSize := BuffSize shl 1;
    pDbgBuffer := RtlCreateQueryDebugBuffer(BuffSize, False);
  end;

  if pDbgBuffer <> nil then
  try
    // Запрашиваем информацию по списку куч процесса
    if RtlQueryProcessDebugInformation(GetCurrentProcessId,
      RTL_QUERY_PROCESS_HEAP_SUMMARY or RTL_QUERY_PROCESS_HEAP_ENTRIES,
      pDbgBuffer) = STATUS_SUCCESS then
    begin
      // Получаем указатель на кучу по умолчанию
      pHeapInformation := @pDbgBuffer^.Heaps^.Heaps[0];
      // перечисляем все ее блоки...
      for I := 0 to pDbgBuffer^.Heaps^.NumberOfHeaps - 1 do
      begin

        // начиная с самого первого
        pHeapEntry := pHeapInformation^.Entries;
        dwAddr := DWORD(pHeapEntry^.u.s2.FirstBlock) +
          pHeapInformation^.EntryOverhead;
        dwLastSize := 0;

        A := 0;
        while A < Integer(pHeapInformation^.NumberOfEntries) do
        try
          hit_seg_count := 0;

          while (pHeapEntry^.Flags and RTL_HEAP_SEGMENT) = RTL_HEAP_SEGMENT do
          begin
            // Если блок отмечен флагом RTL_HEAP_SEGMENT,
            // то рассчитываем новый адрес на основе EntryOverhead
            dwAddr := DWORD(pHeapEntry^.u.s2.FirstBlock) +
              pHeapInformation^.EntryOverhead;
            Inc(pHeapEntry);
            Inc(A);
            Inc(hit_seg_count);
            // проверка выхода за границы блоков
            if A + hit_seg_count >=
              Integer(pHeapInformation^.NumberOfEntries - 1) then
              Continue;
          end;

          // Если блок не самый первый в сегменте, то текущий адрес блока равен,
          // адресу предыдущего блока + размер предыдущего блока
          if hit_seg_count = 0 then
            Inc(dwAddr, dwLastSize);

          // Выставляем флаги
          if pHeapEntry^.Flags and RTL_HEAP_FIXED <> 0 then
            pHeapEntry^.Flags := LF32_FIXED
          else
            if pHeapEntry^.Flags and RTL_HEAP_SETTABLE_FLAG1 <> 0 then
              pHeapEntry^.Flags := LF32_MOVEABLE
            else
              if pHeapEntry^.Flags and RTL_HEAP_UNCOMMITTED_RANGE <> 0 then
                pHeapEntry^.Flags := LF32_FREE;
          if pHeapEntry^.Flags = 0 then
            pHeapEntry^.Flags := LF32_FIXED;

          // Выводим данные
          Writeln('HeapID: ', I, ', entry addr: ', IntToHex(dwAddr, 8),
            ', size: ', IntToHex(pHeapEntry^.Size, 8), ' ', FlagToStr(pHeapEntry^.Flags));

         // Запоминаем адрес последнего блока
         dwLastSize := pHeapEntry^.Size;
         // Переходим к следующему блоку
         Inc(pHeapEntry);
        finally
          Inc(A);
        end;
        // Переходим к следующей куче
        Inc(pHeapInformation);
      end;
    end;
  finally
    RtlDestroyQueryDebugBuffer(pDbgBuffer);
  end;
  Readln;
end.

Вкратце, при помощи вызова функций RtlQueryProcessDebugInformation,  RtlCreateQueryDebugBuffer и RtlQueryProcessDebugInformation создается буфер, в котором содержится информация о текущих кучах процесса. После чего, зная структуру данных, хранящихся в нем, получаем эти данные в цикле.
pDbgBuffer^.Heaps - хранит в себе списки куч (аналог THeapList32), а сами записи хранятся в pDbgBuffer^.Heaps^.Heaps[N].Entries (аналог THeapEntry32).

Данный код выведет следующую информацию:

Сразу сверяемся с VMMap - выглядит похоже

В принципе, кучи я использую при отладке достаточно редко, но иногда и эта информация может пригодиться.

4. Собираем данные о загруженных PE файлах


Теперь пришла пора получить информацию о загруженных в адресное пространство процесса исполняемым файлах и библиотеках. Есть несколько способов сделать это (например, проанализировав PEB.LoaderData), но поступим проще.

Как правило, под PE файл выделяется отдельный регион (ну, по крайней мере, я еще не встречался с таким, чтобы PE образ был загружен без выравнивания по верхушке региона), поэтому, взяв за основу код из первой главы и проверив данные первой страницы региона на соответствие PE файлу, получим список всех загруженных библиотек и исполняемых файлов.

Следующий код детектирует наличие валидного PE файла по указанному адресу:

function CheckPEImage(hProcess: THandle;
  ImageBase: Pointer; var IsPEImage64: Boolean): Boolean;
var
  ReturnLength: NativeUInt;
  IDH: TImageDosHeader;
  NT: TImageNtHeaders;
begin
  Result := False;
  IsPEImage64 := False;
  if not ReadProcessMemory(hProcess, ImageBase,
    @IDH, SizeOf(TImageDosHeader), ReturnLength) then Exit;
  if IDH.e_magic <> IMAGE_DOS_SIGNATURE then Exit;
  ImageBase := Pointer(NativeInt(ImageBase) + IDH._lfanew);
  if not ReadProcessMemory(hProcess, ImageBase,
    @NT, SizeOf(TImageNtHeaders), ReturnLength) then Exit;
  Result := NT.Signature = IMAGE_NT_SIGNATURE;
  IsPEImage64 :=
    (NT.FileHeader.Machine = IMAGE_FILE_MACHINE_IA64) or
    (NT.FileHeader.Machine = IMAGE_FILE_MACHINE_ALPHA64) or
    (NT.FileHeader.Machine = IMAGE_FILE_MACHINE_AMD64);
end;

Ну точнее как, он просто проверяет наличие ImageDosHeader и ImageNTHeader, ориентируясь на их сигнатуры. В принципе для 99% случаев этого достаточно.

Третий параметр просто информационный, он показывает является ли PE файл 64-битным.

Получить путь к загруженному файлу можно вызовом функции GetMappedFileName:

function GetFileAtAddr(hProcess: THandle; ImageBase: Pointer): string;
begin
  SetLength(Result, MAX_PATH);
  SetLength(Result,
    GetMappedFileName(hProcess, ImageBase, @Result[1], MAX_PATH));
end;

А теперь попробуем посмотреть, что у нас загружается в обычное консольное приложение:

var
  MBI: TMemoryBasicInformation;
  dwLength: NativeUInt;
  Address: PByte;
  IsPEImage64: Boolean;
begin
  Address := nil;
  dwLength := SizeOf(TMemoryBasicInformation);
  while VirtualQuery(Address, MBI, dwLength) <> 0 do
  begin
    if CheckPEImage(GetCurrentProcess, MBI.BaseAddress, IsPEImage64) then
    begin
      Write(IntToHex(NativeUInt(MBI.BaseAddress), 8), ': ',
        GetFileAtAddr(GetCurrentProcess, MBI.BaseAddress));
      if IsPEImage64 then
        Writeln(' (x64)')
      else
        Writeln(' (x32)');
    end;
    Inc(Address, MBI.RegionSize);
  end;
  Readln;
end.

Получится вот такая картинка:

64-битная библиотека в 32-битном приложении? Да проще простого :)

Приложение у меня 32-битное, операционная система Windows 7 x64. Судя по тому что отображено на картинке, в нашем 32-битном процессе спокойно живут и работают четыре 64-битных библиотеки, впрочем, тут ничего не обычного - это так называемый Wow64 (эмуляция Win32 в 64-разрядной Windows).

Зато сразу становится понятно, откуда появляются 64-битные аналоги 32-битных потоков и куч.

Теперь, по-хорошему, нужно получить адреса секций каждого PE файла, чтобы можно было их показать более наглядно. Все секции выравниваются по адресу начала страницы и не пересекаются друг с другом.

Сделаем это вот таким кодом:

procedure GetInfoFromImage(const FileName: string; ImageBase: Pointer);
var
  ImageInfo: TLoadedImage;
  ImageSectionHeader: PImageSectionHeader;
  I: Integer;
begin
  if MapAndLoad(PAnsiChar(AnsiString(FileName)), nil, @ImageInfo, True, True) then
  try
    ImageSectionHeader := ImageInfo.Sections;
    for I := 0 to Integer(ImageInfo.NumberOfSections) - 1 do
    begin
      Write(
        IntToHex((NativeUInt(ImageBase) + ImageSectionHeader^.VirtualAddress), 8), ': ',
        string(PAnsiChar(@ImageSectionHeader^.Name[0])));
      if IsExecute(ImageSectionHeader^.Characteristics) then
        Write(' Execute');
      if IsWrite(ImageSectionHeader^.Characteristics) then
        Write(' Writable');
      Writeln;
      Inc(ImageSectionHeader);
    end;
  finally
    UnMapAndLoad(@ImageInfo);
  end;
  Writeln;
end;

Здесь используется вызов функции MapAndLoad, которая, помимо загрузки файла и проверки его заголовков, производит также выравнивание секций посредством вызова NtMapViewOfSection.

Для своего собственного процесса, конечно, вызов данной функции избыточен, т.к. требуемый PE файл и так уже подгружен в адресное пространство процесса, но т.к. нам потребуется более универсальный код для работы и с другими процессами, то воспользуемся именно этим подходом.

MapAndLoad хороша еще и тем, что позволяет 64-битным процессам подгружать 32-битные PE файлы (правда, это не работает для 32-битных процессов), и в дальнейшем эта возможность нам еще пригодится.

Суть кода такова: после выполнения MapAndLoad у нас будет на руках заполненная структура TLoadedImage, параметр Sections которой указывает на массив из структур TImageSectionHeader. У каждой из этих структур есть поле VirtualAddress, которое является смещением от адреса загрузки библиотеки. Сложив значение этого поля с hInstance библиотеки, мы получим адрес секции.

Функции IsExecute и IsWrite проверяют характеристики секции и возвращают True в том случае, если секция содержит исполняемый код (IsExecute) или данные, доступные для модификации (IsWrite). Выглядят они следующим образом:

function IsExecute(const Value: DWORD): Boolean;
begin
  Result := False;
  if (Value and IMAGE_SCN_CNT_CODE) =
    IMAGE_SCN_CNT_CODE then Result := True;
  if (Value and IMAGE_SCN_MEM_EXECUTE) =
    IMAGE_SCN_MEM_EXECUTE then Result := True;
end;

function IsWrite(const Value: DWORD): Boolean;
begin
  Result := False;
  if (Value and IMAGE_SCN_CNT_UNINITIALIZED_DATA) =
    IMAGE_SCN_CNT_UNINITIALIZED_DATA then Result := True;
  if (Value and IMAGE_SCN_MEM_WRITE) = IMAGE_SCN_MEM_WRITE then
    Result := True;
end;

В результате работы данного кода мы увидим следующее:

Вывод загруженных библиотек и адресов их секций

Правда, с этим кодом есть еще один небольшой нюанс.
Как видно было на предыдущей картинке, функция GetMappedFileName возвращает путь к загруженному файлу в следующем виде: "\Device\HarddiskVolume2\Windows\System32\wow64cpu.dll", а функция MapAndLoad требует нормализированного пути вида "C:\Windows\System32\wow64cpu.dll".

За приведение пути к привычному виду отвечает следующий код:

function NormalizePath(const Value: string): string;
const
  OBJ_CASE_INSENSITIVE         = $00000040;
  STATUS_SUCCESS               = 0;
  FILE_SYNCHRONOUS_IO_NONALERT = $00000020;
  FILE_READ_DATA = 1;
  ObjectNameInformation = 1;
  DriveNameSize = 4;
  VolumeCount = 26;
  DriveTotalSize = DriveNameSize * VolumeCount;
var
  US: UNICODE_STRING;
  OA: OBJECT_ATTRIBUTES;
  IO: IO_STATUS_BLOCK;
  hFile: THandle;
  NTSTAT, dwReturn: DWORD;
  ObjectNameInfo: TOBJECT_NAME_INFORMATION;
  Buff, Volume: string;
  I, Count, dwQueryLength: Integer;
  lpQuery: array [0..MAX_PATH - 1] of Char;
  AnsiResult: AnsiString;
begin
  Result := Value;
  // Подготавливаем параметры для вызова ZwOpenFile
  RtlInitUnicodeString(@US, StringToOleStr(Value));
  // Аналог макроса InitializeObjectAttributes
  FillChar(OA, SizeOf(OBJECT_ATTRIBUTES), #0);
  OA.Length := SizeOf(OBJECT_ATTRIBUTES);
  OA.ObjectName := @US;
  OA.Attributes := OBJ_CASE_INSENSITIVE;
  // Функция ZwOpenFile спокойно открывает файлы, путь к которым представлен
  // с использованием символьных ссылок, например:
  // \SystemRoot\System32\ntdll.dll
  // \??\C:\Windows\System32\ntdll.dll
  // \Device\HarddiskVolume1\WINDOWS\system32\ntdll.dll
  // Поэтому будем использовать ее для получения хендла
  NTSTAT := ZwOpenFile(@hFile, FILE_READ_DATA or SYNCHRONIZE, @OA, @IO,
    FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
    FILE_SYNCHRONOUS_IO_NONALERT);
  if NTSTAT = STATUS_SUCCESS then
  try
    // Файл открыт, теперь смотрим его формализованный путь
    NTSTAT := NtQueryObject(hFile, ObjectNameInformation,
      @ObjectNameInfo, MAX_PATH * 2, @dwReturn);
    if NTSTAT = STATUS_SUCCESS then
    begin
      SetLength(AnsiResult, MAX_PATH);
      WideCharToMultiByte(CP_ACP, 0,
        @ObjectNameInfo.Name.Buffer[ObjectNameInfo.Name.MaximumLength -
        ObjectNameInfo.Name.Length {$IFDEF WIN64} + 4{$ENDIF}],
        ObjectNameInfo.Name.Length, @AnsiResult[1],
        MAX_PATH, nil, nil);
      Result := string(PAnsiChar(AnsiResult));
      // Путь на открытый через ZwOpenFile файл
      // возвращается в виде \Device\HarddiskVolumeХ\бла-бла
      // Осталось только его сопоставить с реальным диском
      SetLength(Buff, DriveTotalSize);
      Count := GetLogicalDriveStrings(DriveTotalSize, @Buff[1]) div DriveNameSize;
      for I := 0 to Count - 1 do
      begin
        Volume := PChar(@Buff[(I * DriveNameSize) + 1]);
        Volume[3] := #0;
        // Преобразуем имя каждого диска в символьную ссылку и
        // сравниваем с формализированным путем
        QueryDosDevice(PChar(Volume), @lpQuery[0], MAX_PATH);
        dwQueryLength := Length(string(lpQuery));
        if Copy(Result, 1, dwQueryLength) = string(lpQuery) then
        begin
          Volume[3] := '\';
          if lpQuery[dwQueryLength - 1] <> '\' then
            Inc(dwQueryLength);
          Delete(Result, 1, dwQueryLength);
          Result := Volume + Result;
          Break;
        end;
      end;
    end;
  finally
    ZwClose(hFile);
  end;
end;

Это уже достаточно старый код, постоянно применяемый мной для приведения к нормальному пути. Суть его в том чтобы из путей следующих видов:
  • \SystemRoot\System32\ntdll.dll
  • \??\C:\Windows\System32\ntdll.dll
  • \Device\HarddiskVolume1\WINDOWS\system32\ntdll.dll


... получить фиксированный "\Device\HarddiskVolume1\WINDOWS\system32\ntdll.dll".
Это делается посредством вызова ZwOpenFile + NtQueryObject, после чего просто перебираются все диски в системе и для каждого вызывается QueryDosDevice, который возвращает путь в таком же формате. После чего пути сравниваются и (при совпадении) к переданному пути подставляется соответствующая метка диска.

Но это лирика.
Чтобы быть полностью довольными собой, желательно вывести так же директории PE файла, чтобы было понятно сразу, где искать, к примеру, таблицу импорта, где сидит UNWIND и т.п.

Это делается довольно простым кодом:

procedure EnumDirectoryes(ImageBase: Pointer; ImageInfo: TLoadedImage;
  AddrStart, AddrEnd: NativeUInt);
const
  DirectoryStr: array [0..14] of string =
    ('export', 'import', 'resource', 'exception',
    'security', 'basereloc', 'debug', 'copyright',
    'globalptr', 'tls', 'load_config', 'bound_import',
    'iat', 'delay_import', 'com');
var
  I: Integer;
  dwDirSize: DWORD;
  DirAddr: Pointer;
  ReadlDirAddr: NativeUInt;
begin
  for I := 0 to 14 do
  begin
    DirAddr := ImageDirectoryEntryToData(ImageInfo.MappedAddress,
      True, I, dwDirSize);
    if DirAddr = nil then Continue;    
    ReadlDirAddr := NativeUint(ImageBase) +
        NativeUint(DirAddr) - NativeUint(ImageInfo.MappedAddress);
    if (ReadlDirAddr >= AddrStart) and (ReadlDirAddr < AddrEnd) then
      Writeln(
        IntToHex(ReadlDirAddr, 8), ': directory "', DirectoryStr[I], '"');
  end;
end;

Имея на руках структуру TLoadedImage, мы можем достаточно просто вызовом функции ImageDirectoryEntryToData получить её адрес, правда, он будет привязан к адресу, по которому отображен PE файл. Чтобы перевести его в реальный, нужно из текущего адреса вычесть адрес, по которому отображен образ, получив таким образом смещение от начала файла, и уже его сложить с ImageBase библиотеки.

В итоге получится вот такая картинка:

Вывод директорий и секций PE файла

Сразу видно, что, к примеру, в секции ".text" библиотеки msctf.dll расположены директории импорта/экспорта/отложенного импорта и т.п.
Директория с ресурсами сидит в секции ".rsrc", да и релоки тоже там где положено, однако выпадает из схемы директория "bound_import".

Да, действительно, данная директория не располагается непосредственно ни в одной из секций библиотеки, такова ее особенность. Она обычно идет сразу за PE заголовком (хотя иногда может встречаться и в промежутках между секциями). Данная директория служит для обеспечения механизма "привязанного импорта", который встречается в основном только у программ и библиотек, идущих в составе ОС.

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

5. Блок окружения процесса (PEB) + KUSER_SHARED_DATA


Имея на руках данные о потоках, кучах и исполняемых файлах, уже прямо сейчас можно сделать небольшую утилиту, которая выведет информацию в удобочитаемом виде, но что можно еще добавить?

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

Доступ к нему можно получить вызовом функции NtQueryInformationProcess с флагом ProcessBasicInformation (константа равная нулю). В этом случае на руках будет структура PROCESS_BASIC_INFORMATION, у которой поле PebBaseAddress и будет содержать адрес PEB.

Но это будет актуально, только если битности процессов (запрашивающего и о котором запрашиваем информацию) совпадут. Если мы вызовем данную функцию из 64-битного приложения применительно к 32-битному, то получим адрес 64-битного PEB, а не родного 32-битного.

Для того, чтобы из 64-битного приложения получить доступ к Wow64PEB (назовем его так), необходимо вызвать функцию NtQueryInformationProcess с параметром ProcessWow64Information (константа равная 26) и размером буфера равным SizeOf(ULONG_PTR). В этом случае вместо структуры PROCESS_BASIC_INFORMATION функция вернет указатель на 32-битный PEB, из которого и будем зачитывать нужную нам информацию посредством ReadProcessMemory.

Что такое PEB?
Грубо говоря, это не сильно документированная структура, в большинстве своем предназначенная для хранения данных, используемых непосредственно системой. Но это не означает, что она не интересна разработчику обычного прикладного приложения. В частности данная структура содержит ряд интересных полей, таких как: флаг BeingDebugged, указывающий подключен ли к процессу отладчик; указатель на PEB_LDR_DATA, в которой содержится информация о загруженных в процесс модулях; и много остальной достаточно полезной для программиста информации, особенно для того, кто знает как ее применить в своих целях :)

Выглядит данная структура примерно вот так (декларация для Windows7 x86/64):

  PPEB = ^TPEB;
  TPEB = record
    InheritedAddressSpace: BOOLEAN;
    ReadImageFileExecOptions: BOOLEAN;
    BeingDebugged: BOOLEAN;
    BitField: BOOLEAN;
        {
            BOOLEAN ImageUsesLargePages : 1;
            BOOLEAN IsProtectedProcess : 1;
            BOOLEAN IsLegacyProcess : 1;
            BOOLEAN IsImageDynamicallyRelocated : 1;
            BOOLEAN SkipPatchingUser32Forwarders : 1;
            BOOLEAN IsPackagedProcess : 1;
            BOOLEAN IsAppContainer : 1;
            BOOLEAN SpareBits : 1;
        }
    Mutant: HANDLE;
    ImageBaseAddress: PVOID;
    LoaderData: PVOID;
    ProcessParameters: PRTL_USER_PROCESS_PARAMETERS;
    SubSystemData: PVOID;
    ProcessHeap: PVOID;
    FastPebLock: PRTLCriticalSection;
    AtlThunkSListPtr: PVOID;
    IFEOKey: PVOID;
    EnvironmentUpdateCount: ULONG;
    UserSharedInfoPtr: PVOID;
    SystemReserved: ULONG;
    AtlThunkSListPtr32: ULONG;
    ApiSetMap: PVOID;
    TlsExpansionCounter: ULONG;
    TlsBitmap: PVOID;
    TlsBitmapBits: array[0..1] of ULONG;
    ReadOnlySharedMemoryBase: PVOID;
    HotpatchInformation: PVOID;
    ReadOnlyStaticServerData: PPVOID;
    AnsiCodePageData: PVOID;
    OemCodePageData: PVOID;
    UnicodeCaseTableData: PVOID;

    KeNumberOfProcessors: ULONG;
    NtGlobalFlag: ULONG;

    CriticalSectionTimeout: LARGE_INTEGER;
    HeapSegmentReserve: SIZE_T;
    HeapSegmentCommit: SIZE_T;
    HeapDeCommitTotalFreeThreshold: SIZE_T;
    HeapDeCommitFreeBlockThreshold: SIZE_T;

    NumberOfHeaps: ULONG;
    MaximumNumberOfHeaps: ULONG;
    ProcessHeaps: PPVOID;

    GdiSharedHandleTable: PVOID;
    ProcessStarterHelper: PVOID;
    GdiDCAttributeList: ULONG;

    LoaderLock: PRTLCriticalSection;

    NtMajorVersion: ULONG;
    NtMinorVersion: ULONG;
    NtBuildNumber: USHORT;
    NtCSDVersion: USHORT;
    PlatformId: ULONG;
    Subsystem: ULONG;
    MajorSubsystemVersion: ULONG;
    MinorSubsystemVersion: ULONG;
    AffinityMask: ULONG_PTR;
    {$IFDEF WIN32}
    GdiHandleBuffer: array [0..33] of ULONG;
    {$ELSE}
    GdiHandleBuffer: array [0..59] of ULONG;
    {$ENDIF}
    PostProcessInitRoutine: PVOID;

    TlsExpansionBitmap: PVOID;
    TlsExpansionBitmapBits: array [0..31] of ULONG;

    SessionId: ULONG;

    AppCompatFlags: ULARGE_INTEGER;
    AppCompatFlagsUser: ULARGE_INTEGER;
    pShimData: PVOID;
    AppCompatInfo: PVOID;

    CSDVersion: UNICODE_STRING;

    ActivationContextData: PVOID;
    ProcessAssemblyStorageMap: PVOID;
    SystemDefaultActivationContextData: PVOID;
    SystemAssemblyStorageMap: PVOID;

    MinimumStackCommit: SIZE_T;

    FlsCallback: PPVOID;
    FlsListHead: LIST_ENTRY;
    FlsBitmap: PVOID;
    FlsBitmapBits: array [1..FLS_MAXIMUM_AVAILABLE div SizeOf(ULONG) * 8] of ULONG;
    FlsHighIndex: ULONG;

    WerRegistrationData: PVOID;
    WerShipAssertPtr: PVOID;
    pContextData: PVOID;
    pImageHeaderHash: PVOID;

    TracingFlags: ULONG;
        {
            ULONG HeapTracingEnabled : 1;
            ULONG CritSecTracingEnabled : 1;
            ULONG LibLoaderTracingEnabled : 1;
            ULONG SpareTracingBits : 29;
        }
    CsrServerReadOnlySharedMemoryBase: ULONGLONG;
  end;

Кстати, сравните эту структуру с той, что официально доступна в MSDN.

Для Window 2000/XP/2003 будут небольшие изменения, но не сильно критичные.
Расписывать каждое поле я не буду, те кто работают с PEB и так знают? что именно им нужно, но на некоторых полях я заострю ваше внимание.

Итак:

  • Поле BeingDebugged - в третьей части статьи об отладчике я показывал один из вариантов обхода детектирования оного посредством патча памяти приложения. Суть подхода заключалась как раз в определении адреса PEB и изменения значения параметра BeingDebugged на ноль, после чего функция IsDebuggerPresent, ориентирующаяся на данное поле, начинала возвращать False, говоря о том? что отладчика она не обнаружила.
  • Поле ImageBaseAddress - указывает на hInstance приложения (оно может не совпадать с полем ImageBase в PE заголовке).
  • LoaderData - указатель на данные о загруженных модулях, в нем хранится достаточно полезная информация для тех, кто строит защиту приложения самостоятельно, но, к сожалению, пока что это выходит за рамки данной статьи. На этом поле я остановлюсь чуть подробнее, когда увидит свет статья о детектировании инжекта в ваше приложение :)
  • ProcessParameters - откуда берут информацию ParamStr/GetCurrentDir и т.п. функции? Именно отсюда. Здесь же сидит адрес переменных окружения.
  • А еще мы можем узнать сервиспак системы, не дергая реестр, в этом нам поможет поле CSDVersion. Да, впрочем, поля NtMajorVersion/NtMinorVersion/NtBuildNumber говорят сами за себя.

Ну и так далее - продолжать можно долго.

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

Если мы хотим визуализировать все это (а к этому я и веду), эти данные мы должны иметь на руках, чтобы было что отобразить в финальном приложении.

Согласитесь, гораздо приятней иметь на руках вместо некоего блока бинарных данных что-то в виде такого:

Блок окружения 32-битного процесса

А ведь есть еще и KUSER_SHARED_DATA.
Это тоже структура используемая системой, и вы постоянно встречаетесь с ней, вызывая тот же GetTickCount или IsProcessorFeaturePresent.
К примеру, NtSystemRoot сидит именно в ней, да и, опять же, зачем все перечислять, проще увидеть:

KUSER_SHARED_DATA как она и есть :)
  • Хотите узнать, что за процесс активен без вызова GetForegroundWindow – читайте ConsoleSessionForegroundProcessId.
  • Вам пытаются подсунуть левую версию Win, чтобы отключилась часть системы защиты, не рассчитанная на предыдущие OS? Читайте актуальные значения из полей NtMajorVersion/NtMinorVersion...

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

    6. TRegionData


    На этом теоретическая часть закончилась и пришла пора применить это все на практике.

    Прежде всего нужно определиться с тем, каким образом хранить информацию о регионах. Готовясь к статье, я написал набор классов, выделенных в общее пространство имен "MemoryMap", их вы сможете найти в составе демо-примеров.

    ВАЖНО!!!
    Данный набор классов разработан с учетом нововведений, присутствующих в Delphi XE4, под более старыми версиями Delphi его работоспособность не проверялась и не гарантируется.

    Информацию по каждому региону будет хранить класс TRegionData, реализованный в модуле "MemoryMap.RegionData.pas".

    Выглядит он примерно следующим образом (в процессе развития проекта декларация класса может меняться).

      TRegionData = class
      private
        FParent: TRegionData;
        FRegionType: TRegionType;
        FMBI: TMemoryBasicInformation;
        FDetails: string;
        FRegionVisible: Boolean;
        FHiddenRegionCount: Integer;
        FTotalRegionSize: NativeUInt;
        FHeap: THeapData;
        FThread: TThreadData;
        FPEBData: TSystemData;
        FSection: TSection;
        FContains: TList;
        FDirectories: TList;
        FShared: Boolean;
        FSharedCount: Integer;
        FFiltered: Boolean;
      protected
    ...
      public
        constructor Create;
        destructor Destroy; override;
        property RegionType: TRegionType read FRegionType;
        property MBI: TMemoryBasicInformation read FMBI;
        property Details: string read FDetails;
        property RegionVisible: Boolean read FRegionVisible;
        property HiddenRegionCount: Integer read FHiddenRegionCount;
        property Parent: TRegionData read FParent;
        property TotalRegionSize: NativeUInt read FTotalRegionSize;
        property Heap: THeapData read FHeap;
        property Thread: TThreadData read FThread;
        property SystemData: TSystemData read FPEBData;
        property Section: TSection read FSection;
        property Directory: TList read FDirectories;
        property Contains: TList read FContains;
      end;
    

    По порядку:

    Каждый регион, как правило, хранит в себе данные одного типа.
    Т.е. для куч, стеков потоков, PE файлов, выделяется свой собственный регион страниц.
    За хранение типа региона отвечает свойство RegionType. Это перечислимый тип, объявленный следующим образом:

      // Тип региона
      TRegionType = (
        rtDefault,
        rtHeap,                // регион содержит элементы кучи
        rtThread,              // регион содержит стек потока или TEB
        rtSystem,              // регион содержит системные данные (PEB/KUSER_SHARED_DATA и т.п.)
        rtExecutableImage      // регион содержит образ исполняемого PE файла
        );
    

    Параметры региона, полученные при помощи вызова VirtualQueryEx хранятся в поле MBI.

    Краткое описание региона хранится в Details. В нем можно хранить все что угодно, к примеру путь к отображенному PE файлу, если таковой присутствует, строковое описание ID потока и т.п..

    Следующие три параметра используются для организации древовидной структуры.
    Один из регионов является корневым узлом (рутом), остальные дочерние.
    Флаг RegionVisible указывает на то,является ли регион корневым узлом.
    Свойство HiddenRegionCount содержит в себе количество подрегионов (AllocationBase которых равен BaseAddress рута).
    Ну а параметр Parent хранит ссылку на рута.
    Сделано не совсем оптимально, можно было бы организовать и классическое дерево, но на текущий момент банально нет времени переделывать, может быть, когда-нибудь потом :)

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

    В случае, если регион содержит кучу, данные о первом ее элементе помещаются в параметр Heap, представляющий из себя следующую структуру:

      THeapEntry = record
        Address: ULONG_PTR;
        Size: SIZE_T;
        Flags: ULONG;
      end;
    
      THeapData = record
        ID: DWORD;
        Wow64: Boolean;
        Entry: THeapEntry;
      end;
    

    Остальные элементы кучи, расположенные в рамках региона, размещаются в поле Contains.

    Вообще поле Contains может содержать данные многих типов.

      TContainItemType = (itHeapBlock, itThreadData,
        itStackFrame, itSEHFrame, itSystem);
    
      TContainItem = record
        ItemType: TContainItemType;
        function Hash: string;
        case Integer of
          0: (Heap: THeapData);
          1: (ThreadData: TThreadData);
          2: (StackFrame: TThreadStackEntry);
          3: (SEH: TSEHEntry);
          4: (System: TSystemData);
      end;
    

    Далее идет поле Thread, в нем хранится информация о потоке, который использует регион для хранения собственных данных.

    type
      TThreadInfo = (tiNoData, tiExceptionList, tiStackBase,
        tiStackLimit, tiTEB, tiThreadProc);
    
    type
      TThreadData = record
        Flag: TThreadInfo;
        ThreadID: Integer;
        Address: Pointer;
        Wow64: Boolean;
      end;
    

    Если данных о потоке в пределах региона много (например список SEH фреймов или CallStack потока), они также помещаются в поле Contains.

    Данные из системных структур (поля структур PEB/TEB и т.п.) помещаются в поле SystemData, представляющее из себя запись из адреса данных и их описания.
    Также эти данные могут быть помещены в поле Contains.

    Если регион принадлежит одной из секций PE файла, данные о секции размещаются в параметре Section. Ну а список директорий файла размещается в поле Directory.

    Вот как-то так вкратце. Теперь для представления данных о карте памяти процесса нам необходимо получить список регионов, создать для каждого из них экземпляр класса TRegionData и инициализировать поля созданного объекта требуемой информацией.

    За это отвечает класс TMemoryMap...

    7. TMemoryMap


    Данный класс реализован в модуле "MemoryMap.Core.pas".
    Задача его сводится буквально к трем основным этапам:
    1. Получению списка всех выделенных регионов в памяти указанного приложения, данных по нитям/кучам/загруженным образам и т.п..
    2. Созданию списка TRegionData и заполнению его полей полученной информацией.
    3. Сохранение/загрузка данных, фильтрация данных.
    На практике все выглядит несколько сложнее.
    Основная процедура сбора информации выглядит вот так:

    function TMemoryMap.InitFromProcess(PID: Cardinal;
      const ProcessName: string): Boolean;
    var
      ProcessLock: TProcessLockHandleList;
    begin
      Result := False;
      FRegions.Clear;
      FModules.Clear;
      FFilter := fiNone;
      ProcessLock := nil;
      // Открываем процесс на чтение
      FProcess := OpenProcess(
        PROCESS_QUERY_INFORMATION or PROCESS_VM_READ,
        False, PID);
      if FProcess = 0 then
        RaiseLastOSError;
      try
        FPID := PID;
        FProcessName := ProcessName;
    
        // определяем битность процесса
        FProcess64 := False;
        {$IFDEF WIN64}
          if not IsWow64(FProcess) then
            FProcess64 := True;
        {$ELSE}
          // если наше приложение 32 битное, а исследуемый процесс 64-битный
          // кидаем исключение
          if Is64OS and not IsWow64(FProcess) then
            raise Exception.Create('Can''t scan process.');
        {$ENDIF}
    
        // проверяем необходимость суспенда процесса
        if SuspendProcessBeforeScan then
          ProcessLock := SuspendProcess(PID);
        try
          FSymbols := TSymbols.Create(FProcess);
          try
            FPEImage := TPEImage.Create;
            try
              FWorkset := TWorkset.Create(FProcess);;
              try
                // получаем данные по регионам и отмапленым файлам
                GetAllRegions;
              finally
                FWorkset.Free;
              end;
    
              {$IFDEF WIN64}
              // если есть возможность получаем данные о 32 битных кучах
              AddWow64HeapsData;
              {$ENDIF}
    
              // добавляем данные о потоках
              AddThreadsData;
              // добавляем данные о кучах
              AddHeapsData;
              // добавляем данные о Process Environment Block
              AddPEBData;
              // добавляем данные о загруженых PE файлах
              AddImagesData;
            finally
              FPEImage.Free;
            end;
          finally
            FSymbols.Free;
          end;
        finally
          if SuspendProcessBeforeScan then
            ResumeProcess(ProcessLock);
        end;
        // сортируем
        SortAllContainsBlocks;
        // считаем общую информацию о регионах
        CalcTotal;
        // применяем текущий фильтр
        UpdateRegionFilters;
      finally
        CloseHandle(FProcess);
      end;
    end;
    

    Примерный код процедур GetAllRegions/AddThreadsData/AddHeapsData и AddImagesData я приводил в первых четырех главах и на нем я заострять внимание не буду, а вот с остальным желательно разобраться.

    Самым первым шагом после открытия процесса происходит определение битности процесса.
    Это необходимо по той причине, что в случае, если битности процессов (текущего и по которому мы получаем информацию), не совпадают, то нужно предпринять некоторые дополнительные действия.

    Общая схема такая:
    1. 32-битный процесс может получить данные по 32-битному под 32-битной ОС в полном объеме.
    2. 64-битный процесс может получить данные по 64-битному в полном объеме.
    3. 32-битный процесс НЕ МОЖЕТ получить данные по 64-битному.
    4. 32-битный процесс может получить данные по 32-битному под 64-битной ОС, но частично.
    5. 64-битный процесс может получить данные по 32-битному, но частично.

    Если с первыми двумя пунктами все ясно, то остальные три рассмотрим поподробнее.

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

    А вот с получением данных из 32-битного процесса в 64-битной ОС все гораздо хитрее.
    Как я говорил ранее, в 32-битном приложении загружены четыре 64-битные библиотеки, которые создают свои кучи/потоки.

    Если мы будем получать список куч и потоков из 32-битного приложения, то увидим данные только относящиеся к 32 битам, данные по 64-битным аналогам получить не удастся.

    То же будет и в случае запроса данных о 32-битном процессе из 64-битного, вернутся только данные, относящиеся к 64 битам. Хотя в этом случае есть вариант получить их частично.
    В частности, доступ к 32-битному PEB производится вызовом такой функции:

    const
      ProcessWow64Information = 26;
    ...
      NtQueryInformationProcess(FProcess, ProcessWow64Information,
        @FPebWow64BaseAddress, SizeOf(ULONG_PTR), @ReturnLength)
    

    Доступ к 32-битному TEB можно получить, считав адрес из 64-битного TEB, который хранится в параметре NtTIB.ExceptionList.

            // в 64 битном TEB поле TIB.ExceptionList указывает на начало Wow64TEB
            if not ReadProcessMemory(hProcess,
              TIB.ExceptionList, @WOW64_NT_TIB, SizeOf(TWOW64_NT_TIB),
              lpNumberOfBytesRead) then Exit;
    

    Получить контекст 32-битного потока для раскрутки CallStack можно вот таким кодом:

    const
      ThreadWow64Context = 29;
    ...
        ThreadContext^.ContextFlags := CONTEXT_FULL;
        if NtQueryInformationThread(hThread, ThreadWow64Context, ThreadContext,
          SizeOf(TWow64Context), nil) <> STATUS_SUCCESS then Exit;
    

    Либо вызовом функции Wow64GetThreadContext.

    А вот как получить данные о 32-битных кучах из 64-битного процесса легальным способом, мне неизвестно. Единственный вариант, который я применяю сейчас, это передача команды 32-битному процессу, который собирает данные о 32-битных кучах и отдает их обратно в 64-битный (примерно этим и занимается обработчик в функции AddWow64HeapsData).

    Теперь, когда с определением битности процесса и для чего это нужно, разобрались, пойдем дальше, а именно к вызову функции SuspendProcess.

    По-хорошему, это нужно только для того, чтобы данные в удаленном процессе не изменились на неактуальные в момент их чтения. Правда, обычно я применяю этот набор классов в двух случаях, для своего собственного приложения или для приложения находящегося под отладчиком. В обоих случаях замораживать потоки не нужно, но если анализируется какое-то стороннее приложение, то почему бы и нет?

    После заморозки удаленного процесса создаются три вспомогательных класса.

    1. TSymbols - о нем я расскажу в следующей главе.
    2. TPEImage - этот класс содержит в себе методы, позволяющие получить информацию о PE файле, описанные в четвертой главе. Сделан исключительно для удобства.
    3. TWorkset - еще один вспомогательный класс, задача которого получить информацию об общедоступной памяти.

    По сути, TWorkset хранит в себе список структур вида:

      TShareInfo = record
        Shared: Boolean;
        SharedCount: Byte;
      end;
    

    Эти структуры хранятся в словаре и каждая ассоциируется с конкретным адресом страницы.
    Параметры простые:

    • Shared - является ли страница общедоступной
    • SharedCount - сколько ссылок есть на страницу

    Получаются эти данные следующим способом, в котором все сводится к вызову функции QueryWorkingSet:

    procedure TWorkset.InitWorksetData(hProcess: THandle);
    const
      {$IFDEF WIN64}
      AddrMask = $FFFFFFFFFFFFF000;
      {$ELSE}
      AddrMask = $FFFFF000;
      {$ENDIF}
      SharedBitMask = $100;
      SharedCountMask = $E0;
    
      function GetSharedCount(Value: ULONG_PTR): Byte; inline;
      begin
        Result := (Value and SharedCountMask) shr 5;
      end;
    
    var
      WorksetBuff: array of ULONG_PTR;
      I: Integer;
      ShareInfo: TShareInfo;
    begin
      SetLength(WorksetBuff, $400000);
      while not QueryWorkingSet(hProcess, @WorksetBuff[0],
        Length(WorksetBuff) * SizeOf(ULONG_PTR)) do
        SetLength(WorksetBuff, WorksetBuff[0] * 2);
      for I := 0 to WorksetBuff[0] - 1 do
      begin
        ShareInfo.Shared := WorksetBuff[I]  and SharedBitMask <> 0;
        ShareInfo.SharedCount := GetSharedCount(WorksetBuff[I]);
        try
          FData.Add(Pointer(WorksetBuff[I] and AddrMask), ShareInfo);
        except
          on E: EListError do ;
        else
          raise;
        end;
      end;
    end;
    

    Данная функция возвращает массив ULONG_PTR, каждый элемент которого хранит данные следующим образом: первые пять бит хранят в себе атрибуты защиты страницы; следующие три бита – количество процессов, которым доступна данная страница; еще один бит указывает общедоступность страницы; ну и далее идет адрес самой страницы.
    Более подробно можно прочитать тут: PSAPI_WORKING_SET_BLOCK.

    По сути, это просто информационный класс, ни больше ни меньше.

    Впрочем, вернемся к нашему коду.
    Следующими шагами идут:

    1. GetAllRegions - аналог кода из первой главы.
    2. AddThreadsData - аналог кода из второй главы.
    3. AddHeapsData - аналог кода из третьей главы.
    4. AddPEBData - вывод данных о структуре из пятой главы.
    5. AddImagesData - аналог кода из четвертой главы.

    Как видите, все интересное я уже рассказал (ну почти) :)

    Оставшиеся шаги не интересны, за исключением вызова UpdateRegionFilters.
    Он выполняет утилитарную функцию, а именно исключает из списка ненужные на текущий момент регионы (ну, к примеру, убирает регионы с невыделенной памятью и т.п).
    Данная процедура будет вызываться постоянно при изменении фильтра через свойство Filter.

    Впрочем, все это вы сможете при желании увидеть из кода самого класса.
    Работать с ним достаточно просто:

    var
      AMemoryMap: TMemoryMap;
      M: TMemoryStream;
      I: Integer;
    begin
      try
        M := TMemoryStream.Create;
        try
    
          // Создаем класс
          AMemoryMap := TMemoryMap.Create;
          try
            // получаем текущую карту памяти
            AMemoryMap.InitFromProcess(GetCurrentProcessId, '');
            // сохраняем ее,
            AMemoryMap.SaveToStream(M);
            // тут можно прикрутить дампы регионов и все что душе угодно
          finally
            AMemoryMap.Free;
          end;
    
          // тут якобы передали данные куда-то, теперь загружаем их и работаем
          M.Position := 0;
    
          // Создаем класс
          AMemoryMap := TMemoryMap.Create;
          try
            // загружаем данные
            AMemoryMap.LoadFromStream(M);
            // убираем вообще все фильтры
            AMemoryMap.Filter := fiNone; // не обязательно
            // говорим отображать регионы с невыделенной памятью
            AMemoryMap.ShowEmpty := True;
            // выводим список регионов
            for I := 0 to AMemoryMap.Count - 1 do
              Writeln(NativeUInt(AMemoryMap[I].MBI.BaseAddress));
          finally
            AMemoryMap.Free;
          end;
    
        finally
          M.Free;
        end;
      except
        on E: Exception do
          Writeln(E.ClassName, ': ', E.Message);
      end;
      Readln;
    end.
    

    Как говорится, писал сам для себя, поэтому и работать с этим классом проще простого :)

    8. TSymbols - работа с символами


    Суть данного класса заключается в получении более подробной информации об адресе в процессе. Ну, к примеру, во второй главе мы получали CallStack потока (или обработчики SEH фреймов) и это были просто некие адреса. Но гораздо интереснее вместо сухих чисел видеть что-то наподобие этой картинки:

    CallStack потока.

    Делается это очень просто - достаточно вызова функции SymGetSymFromAddr, но есть несколько нюансов.

    Давайте сначала посмотрим на код:

    function TSymbols.GetDescriptionAtAddr(Address, BaseAddress: ULONG_PTR;
      const ModuleName: string): string;
    const
      BuffSize = $7FF;
    {$IFDEF WIN64}
      SizeOfStruct = SizeOf(TImagehlpSymbol64);
      MaxNameLength = BuffSize - SizeOfStruct;
    var
      Symbol: PImagehlpSymbol64;
      Displacement: DWORD64;
    {$ELSE}
      SizeOfStruct = SizeOf(TImagehlpSymbol);
      MaxNameLength = BuffSize - SizeOfStruct;
    var
      Symbol: PImagehlpSymbol;
      Displacement: DWORD;
    {$ENDIF}
    begin
      Result := '';
      if not FInited then Exit;
      GetMem(Symbol, BuffSize);
      try
        Symbol^.SizeOfStruct := SizeOfStruct;
        Symbol^.MaxNameLength := MaxNameLength;
        Symbol^.Size := 0;
        SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)),
          nil, BaseAddress, 0);
        try
          if SymGetSymFromAddr(FProcess, Address, @Displacement, Symbol) then
            Result := string(PAnsiChar(@(Symbol^).Name[0])) + ' + 0x' + IntToHex(Displacement, 4)
          else
          begin
            // с первой попытки может и не получиться
            SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)), nil, BaseAddress, 0);
            if SymGetSymFromAddr(FProcess, Address, @Displacement, Symbol) then
              Result := string(PAnsiChar(@(Symbol^).Name[0])) + ' + 0x' + IntToHex(Displacement, 4);
          end;
        finally
          SymUnloadModule(FProcess, BaseAddress);
        end;
      finally
        FreeMem(Symbol);
      end;
      if Result = '' then
        Result := ExtractFileName(ModuleName) + ' + 0x' + IntToHex(Address - BaseAddress, 1);
    end;
    

    Для правильного получения описания имени функции, которой принадлежит адрес, необходимо знать путь к библиотеке, которой принадлежит функция, либо адрес, по которому данная библиотека подгружена (в коде используются оба параметра). Эти параметры необходимы для функции SymLoadModule.

    Второй нюанс заключается в том, что вызов функции SymGetSymFromAddr иногда может завершиться неуспешно. Причина мне не ясна, но в интернете периодически описывают данную ситуацию и как решение её - повторный вызов функции SymLoadModule без вызова SymUnloadModule. В таком странном поведении не разбирался – но действительно помогает.

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

    Эта информация не является сильно важной при отладке, но немного ее упрощает.
    Вот так, к примеру, выглядит стандартный стек потока браузера Chrome (CallStack + SEH фреймы):

    Стандартный стек потока.

    Более полезная информация, которую могут предоставить символы, это список экспортируемых функций библиотеки и их текущие адреса.
    В классе TSymbols эта информация получается вызовом процедуры GetExportFuncList и выглядит следующим образом:

    function SymEnumsymbolsCallback(SymbolName: LPSTR; SymbolAddress: ULONG_PTR;
      SymbolSize: ULONG; UserContext: Pointer): Bool; stdcall;
    var
      List: TStringList;
    begin
      List := UserContext;
      List.AddObject(string(SymbolName), Pointer(SymbolAddress));
      Result := True;
    end;
    
    procedure TSymbols.GetExportFuncList(const ModuleName: string;
      BaseAddress: ULONG_PTR; Value: TStringList);
    begin
      SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)),
        nil, BaseAddress, 0);
      try
        if not SymEnumerateSymbols(FProcess, BaseAddress,
          @SymEnumsymbolsCallback, Value) then
        begin
          SymLoadModule(FProcess, 0, PAnsiChar(AnsiString(ModuleName)),
            nil, BaseAddress, 0);
          SymEnumerateSymbols(FProcess, BaseAddress,
            @SymEnumsymbolsCallback, Value)
        end;
      finally
        SymUnloadModule(FProcess, BaseAddress);
      end;
    end;
    

    Все сводится к вызову SymEnumerateSymbols, которой передается адрес функции обратного вызова.
    При ее вызове параметр SymbolName будет содержать имя экспортируемой функции, а SymbolAddress ее адрес.

    Этого вполне достаточно для того, чтобы отобразить пользователю вот такую табличку:

    Список экспортируемых функций подгруженными в процесс библиотеками.

    Более подробно реализацию данного класса, включая опущенные вызовы SymSetOptions и SymInitialize, вы сможете увидеть в модуле "MemoryMap.Symbols.pas".

    9. ProcessMemoryMap


    Ну вот мы подошли и к заключительной части статьи.
    Как я и говорил ранее, я использую набор классов MemoryMap в двух вариантах:

    1. Интегрируя его в вывод EurekaLog посредством перекрытия ее обработчика OnAttachedFilesRequest, где добавляю текущую карту процесса актуальную на момент возникновения исключения, и дампы всех Private регионов (страниц, не ассоциированных с определенными данными, имеющих флаг MEM_PRIVATE) и стеков потоков, плюс часть информации из PEB. Обычно этого достаточно для разбора причин возникновения ошибки.
    2. Использую как альтернативный инструмент для анализа отлаживаемого приложения.

    Для второго варианта была реализована отдельная утилита, которая работает непосредственно с классами MemoryMap, плюс добавляет некий дополнительный функционал.


    Описывать ее исходный код я не буду, пройдусь только немного по функционалу.

    С интерфейсной части она практически один в один напоминает VMMap. Впрочем, так и планировалось изначально, ибо такой интерфейс наиболее удобен для анализа.

    В верхней части расположен список с общей информацией по регионам, сгруппированным по их типам, он же является фильтром.

    На текущий момент она представляет следующий функционал:

    1. Просмотр содержимого памяти по указанному адресу (Ctrl+Q).

    Свойства произвольного региона

    Этот функционал, в принципе, присутствует в отладчике Delphi в окне CPU View, но возможностей у этого режима гораздо больше. К примеру, в случае просмотра поля PEB, будут выводится данные в другом виде:

    Process Environment Block 64

    Вот так будет выглядеть блок параметров процесса:


    Ну и так далее. Всего на данный момент утилита может выводить размапленные данные по следующим структурам:
    • PEB - Process Environment Block (32/64)
    • TEB - Thread Environment Block (32/64)
    • KUSER_SHARED_DATA
    • PE Header (IMAGE_DOS_HEADER / IMAGE_NT_HEADER / IMAGE_FILE_HEADER / IMAGE_OPTIONAL_HEADER(32/64) / IMAGE_DATA_DIRECTORY / IMAGE_SECTION_HEADERS)
    • Process Parameters (32/64)
    Этот список не окончательный, периодически в него будут добавляться новые структуры.

    2. Поиск данных в памяти процесса (Ctrl+F):


    Этот функционал в отладчике Delphi, к сожалению, отсутствует.
    Поиск можно производить как по Ansi, так и по Unicode строке, либо просто по абстрактному HEX буферу. При поиске можно указывать адрес начала поиска, а так же флаг, указывающий на необходимость поиска в страницах, доступ к которым возможен только на чтение.
    Результат выводится в виде окна с дампом памяти, показанного выше.

    3. Компаратор двух карт памяти. Включается в настройках.

    Позволяет найти отличия между двумя картами памяти и выводит их в виде текста.


    Сравниваются только сами карты, а не данные. Т.е. если по какому-то адресу изменились 4 байта, это изменение не отобразится. Но вот в том случае, если изменился размер региона, удалилась куча, выгрузился/загрузился файл и т.п. - все это будет отображено в результатах сравнения.
    Сравнивать можно как текущий снимок карты с сохраненным ранее, так и при обновлении снимка по горячей клавише F5.

    4. Дамп памяти.

    Также отсутствующий в отладчике Delphi функционал. Позволяет сохранить на диск содержимое памяти указанного региона либо данные с указанного адреса.

    5. Вывод всех доступных экспортируемых функций из всех библиотек, подгруженных в анализируемый процесс (Ctrl+E).


    А также быстрый поиск функции по ее наименованию или адресу.

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

    ProcessMemoryMap является OpenSource проектом.
    Ее последний стабильный релиз всегда доступен по ссылке: http://rouse.drkb.ru/winapi.php#pmm2
    GitHub репозиторий с последними изменениями кода можно обнаружить здесь: https://github.com/AlexanderBagel/ProcessMemoryMap

    Прямая ссылка на исходный код: https://github.com/AlexanderBagel/ProcessMemoryMap/archive/master.zip
    Прямая ссылка на последнюю сборку:  http://rouse.drkb.ru/files/processmm_bin.zip

    Для самостоятельной сборки потребуется установленный пакет компонентов Virtual TreeView версии 5 и выше: http://www.soft-gems.net/.

    Сборка осуществляется с использованием Delphi XE4 и выше в режиме "Win32/Release", при этом автоматически будет собрана и подключена (в виде ресурса) 64-битная версия данной утилиты.
    Под более старыми версиями Delphi работоспособность ProcessMemoryMap не проверялась.

    10. В качестве заключения


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

    Поэтому вот вам несколько ссылок, по которым вы сможете узнать немного больше информации.

    Информацию о системных структурах TEB/PEB и т.п. можно найти здесь:

    Информация о PE файлах:

    Информация о SEH:

    Исходный код всех демо-примеров можно забрать по данной ссылке.

    Огромное СПАСИБО форуму "Мастера Дельфи" за неоднократную помощь в подготовке статьи.

    Персональное спасибо за вычитку материала Дмитрию aka "брат Птибурдукова", Андрею Васильеву aka Inovet, а также Сергею aka "Картман".

    Удачи.

    ---

    © Александр (Rouse_) Багель
    Ноябрь, 2013

    9 комментариев:

    1. Охренеть материал :)

      1. По GetCurrentTEB - есть подозрение, что ты не учёл, что 64-битный компилятор по умолчанию оперирует с относительной адресацией. Иными словами GS:[30h] в 64 битном режиме будет трактоваться как GS:[REL 30h] (он же - GS:[+30h], он же GS:[RIP + 30h]). Для использования абсолютной адресации (как в 32 битах) надо явно задавать модификатор: GS:[ABS 30h].

      Впрочем, через GS:[ABS 30h] компилятор сгенерирует хоть и корректный, но не самый оптимальный (по размеру) код. Он будет использовать 64-битный displacement, хотя в целом тут достаточно 32-битного.

      Кстати, Delphi не способна корректно дизассемблерить абсолютный 32-битный displacement, показывая [+value] (т.е. как если бы код был бы относительным, а не абсолютным, коим он и является) вместо [abs value] или [dword value].

      2. Трассировать самого себя в целом можно. Тут главное, чтобы параметры стека (EBP/RBP, ESP/RSP) соответствовали бы текущему коду (EIP/RIP) и находились бы в текущем стеке вызова. Последний пункт не будет выполняться, как ты и сказал. Но можно ведь вызовами других функций получить значения регистров EBP/ESP/EIP в текущей функции, а затем передать их в StackWalk64. Аналогичное справедливо для RtlVirtualUnwind. Собственно, эти функции можно заставить работать даже на не настоящем стеке. Лишь бы он "выглядел" как стек.

      ОтветитьУдалить
    2. >> Охренеть материал :)
      Спасибо, я старался :)
      Три месяца ушло (два на код и месяц на статью) - умаялся если честно :)))

      >> По GetCurrentTEB - есть подозрение, что ты не учёл, что 64-битный компилятор по умолчанию оперирует с относительной адресацией.
      Да, Сань, вероятно тут у меня промах, я пока что плотно 64-битным ассемблером не занимался и местами плаваю...

      >>2. Трассировать самого себя в целом можно.
      Ну тут больше я просто суть кода показал, который используется в Process Memory Map. А для трассировки самого себя у меня есть твоя EurekaLog :)

      ОтветитьУдалить
    3. Действительно впечатляет. Большое Спасибо автору.

      ОтветитьУдалить
    4. А что на хабрахабр не зальешь!? Отличная статья.

      ОтветитьУдалить
      Ответы
      1. Готово :)
        http://habrahabr.ru/post/202242/
        Щас пойдут минусовать, на Хабре любят дельфистов :)))

        Удалить
      2. "на Хабре любят дельфистов :)))"

        ;-)))

        Удалить
    5. Высший пилотаж. Спасибо, мистер _Rouse

      ОтветитьУдалить
    6. есть игра game.exe 64битная. в памяти этой игры нахожу значение...
      Нахожу указатель на это значение который имеет вид "GAME.exe"+01CDFBD0->0043DE40+$0->4A81F600+$790 и т.д до исходного значения.

      Если бы это была 32битная игра то game.exe+01CDFBD0 можно было найти через след. функцию:


      function GetModuleBaseAddress(ProcessID: Cardinal; MName: String): Pointer;
      var
      Modules : Array of HMODULE;
      cbNeeded, i : Cardinal;
      ModuleInfo : TModuleInfo;
      ModuleName : Array[0..MAX_PATH] of Char;
      PHandle : THandle;
      begin
      Result := nil;
      SetLength(Modules, 1024);
      PHandle := OpenProcess(PROCESS_QUERY_INFORMATION + PROCESS_VM_READ, False, ProcessID);
      if (PHandle <> 0) then
      begin
      EnumProcessModules(PHandle, @Modules[0], 1024 * SizeOf(HMODULE), cbNeeded);
      SetLength(Modules, cbNeeded div SizeOf(HMODULE));
      for i := 0 to Length(Modules) - 1 do //Start the bucle
      begin
      GetModuleBaseName(PHandle, Modules[i], ModuleName, SizeOf(ModuleName));
      if AnsiCompareText(MName, ModuleName) = 0 then
      begin
      GetModuleInformation(PHandle, Modules[i], @ModuleInfo, SizeOf(ModuleInfo));
      Result := ModuleInfo.lpBaseOfDll;
      CloseHandle(PHandle);
      Exit;
      end;
      end;
      end;
      end;


      и использовать как

      WHandle := FindWindow(nil, 'Название игры');
      if wHandle = 0 then
      begin
      ShowMessage('notfound');
      end else
      begin
      GetWindowThreadProcessId(WHandle, @ProcessID);
      Address := Integer(GetModuleBaseAddress(ProcessID, 'GAME.exe')) + $01CDFBD0;
      lbl2.Caption:='$'+inttohex(Address,8);


      а так как игры 64битная то функция выдает 0.
      Помогите переписать функцию под х64

      ОтветитьУдалить
      Ответы
      1. с 64 битным процессом нужно работать из 64 битного приложения (иначе из-за размера указателей получите не валидную информацию).

        Удалить