Современные решения

для защиты Windows приложений

и восстановления исходного кода
Автор: Ms-Rem. Дата публикации: 21.07.2005

Современные технологии дампинга и защиты от него


Зачем нужна эта статья
Все мы любим пользоваться программами бесплатно, а значит кто-то должен уметь их ломать. При этом часто приходится сталкиваться с различными упаковщиками и протекторами. Принципы работы упаковщиков и основные методы их распаковки неплохо рассмотрены в статьях "Об упаковщиках в последний раз (NEOx). Там подробно рассмотрен формат PE файла и то, что протектор может с ним делать. Несомненно, важной частью процесса распаковки является получение дампа. О получении дампа и методах противодействия ему также написано в этих статьях, все описанные там методы антидампинга PE Tools умеет обходить. Но к сожалению, все это справедливо лишь для простых защит, а более сложные протекторы (eXtreme Protector, Armadillo) применяют совсем иные методы против получения дампа, и PE Tools с ними уже не справляется. В этой статье я хотел бы рассмотреть современные методы противодействия дампингу и методы их обхода, что несомненно пригодится тому, кто хочет научиться распаковывать что-то сложнее ASPack’а.

Антидамп в нулевом кольце
Все дамперы процессов построены на функциях OpenProcess/ReadProcessMemory/VirtualQueryEx e.t.c. Для получения списка модулей загруженных в процесс обычно используются функции ToolHelp API, которые в свою очередь читают память процесса через ReadProcessMemory. На уровне NativeAPI при этом происходит вызов функций ZwOpenProcess и ZwReadVirtualMemory. Очевидный способ противодействия дампу - это установить драйвер, который перехватит в ядре эти функции и запретит доступ к защищаемому процессу.

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

NTSTATUS NewNtOpenProcess ( OUT PHANDLE ProcessHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId OPTIONAL) { HANDLE ProcessId; if ((ULONG)ClientId > *MmUserProbeAddress) return STATUS_INVALID_PARAMETER; __try { ProcessId = ClientId->UniqueProcess; } __except(EXCEPTION_EXECUTE_HANDLER) { DPRINT("Exception"); return STATUS_INVALID_PARAMETER; } if (IsAdded(wLastItem, ProcessId)) { DPRINT("Access Denied!"); return STATUS_ACCESS_DENIED; } else return TrueNtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId); }

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

typedef struct _ProcessList { PVOID NextItem; HANDLE Pid; } TProcessList, *PProcessList;

Вот код управляющий связанным списком этих структур:

BOOLEAN IsAdded(PProcessList List, HANDLE Pid) { PProcessList Item = List; while (Item) { if (Pid == Item->Pid) return TRUE; Item = Item->NextItem; } return FALSE; } void DelItem(PProcessList *List, HANDLE Pid) { PProcessList Item = *List; PProcessList Prev = NULL; while (Item) { if (Pid == Item->Pid) { if (Prev) Prev->NextItem = Item->NextItem; else *List = Item->NextItem; ExFreePool(Item); return; } Prev = Item; Item = Item->NextItem; } return; } void FreePointers(PProcessList List) { PProcessList Item = List; PVOID Mem; while (Item) { Mem = Item; Item = Item->NextItem; ExFreePool(Mem); } return; } void AddItem(PProcessList *List, HANDLE Pid) { PProcessList wNewItem; wNewItem = ExAllocatePool(NonPagedPool, sizeof(TProcessList)); wNewItem->NextItem = *List; *List = wNewItem; wNewItem->Pid = Pid; return; }

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

Для более надежной защиты, следует еще перехватывать ZwReadVirtualMemory/ZwWriteVirtualMemory и ZwCreateThread, но тогда возникнет необходимость получать по хэндлу процесса его ProcessID. Это можно сделать через ZwQueryInformationProcess, но это не будет работать для хэндлов не имеющих флага доступа PROCESS_QUERY_INFORMATION, поэтому лучше с помошью ObReferenceObjectByHandle получать указатель на структуру EPROCESS связаную с нужным процессов и извлекать ProcessID непосредственно из нее. Это делает следующий код:

ULONG GetPid(HANDLE PHanlde) { NTSTATUS st = 15; PEPROCESS process = 0; ULONG pId; st = ObReferenceObjectByHandle(PHanlde, 0, NULL, UserMode, &process, NULL); if (st == STATUS_SUCCESS) { pId = *(PULONG)(((ULONG)process) + pIdOffset); ObDereferenceObject(process); return pId; } return 0; }

pIdOffset - это смещение поля ProcessId в структуре EPROCESS, эта константа будет различной в разных версиях ядра системы, поэтому следует при старте драйвера проверять версию ядра и соответственно заполнять значение переменной. При написании обработчиков следует учесть, что ZwOpenProcess должен работать, если процесс пытается открыть сам себя (сравнение с PsGetCurrentProcessId), а при обработке функций принимающих хэндлы нужно учесть, что существует псевдохэндл (-1) который означает текущий процесс.

Все хорошо в теории, но при практической реализации этого метода встречаются подводные камни. Например, в Windows XP существует служба стилей. Для правильной ее работы нужно разрешать доступ процесса сервера подсистемы (csrss.exe) к памяти защищаемого процесса. В связи с этим возникает вопрос - как определить Id этого процесса? Вопрос этот не так прост, как это кажется поначалу, по имени определять нельзя, так как процессов csrss.exe в системе может быть несколько (например кракер может переименовать так свой дампер), поэтому нужен более надежный способ однозначного определения этого процесса. Для реализации этого я решил использовать то, что сервер подсистемы владеет некоторыми именованными обьектами, по которым его и можно определить. Для примера возьмем LPC порт \Windows\ApiPort, который создается csrss во всех версиях Windows NT. Для его определения нужно перечислить все открытые хэндлы с помощью ZwQuerySystemInformation, скопировать каждый из них в таблицу хэндлов ядра, вызвать ZwQueryObject и сравнить полученное имя с искомым. При совпадении имен, Id процесса владельца хэндла и будет csrss. Все это осуществляет следующий код:

PVOID GetInfoTable(ULONG ATableType) { ULONG mSize = 0x4000; PVOID mPtr = NULL; NTSTATUS St; do { mPtr = ExAllocatePool(PagedPool, mSize); if (mPtr != NULL) { St = ZwQuerySystemInformation(ATableType, mPtr, mSize, NULL); } else return NULL; if (St == STATUS_INFO_LENGTH_MISMATCH) { ExFreePool(mPtr); mSize = mSize * 2; } } while (St == STATUS_INFO_LENGTH_MISMATCH); if (St == STATUS_SUCCESS) { DPRINT("GetInfoTable Success!"); DPRINT("Info table in memory size - %d", mSize); return mPtr; } else ExFreePool(mPtr); DPRINT("Error on GetInfoTable %X", St); return NULL; } ULONG GetCsrPid() { int r; HANDLE Process, hObject; NTSTATUS St; ULONG CsrId = 0; OBJECT_ATTRIBUTES obj; CLIENT_ID cid; POBJECT_NAME_INFORMATION ObjName; UNICODE_STRING ApiPortName; PSYSTEM_HANDLE_INFORMATION_EX Handles; RtlInitUnicodeString(&ApiPortName, L"\\Windows\\ApiPort"); DPRINT("Get handles info"); Handles = GetInfoTable(SystemHandleInformation); if (Handles == NULL) return 0; ObjName = ExAllocatePool(PagedPool, 0x2000); DPRINT("Number of handles %d", Handles->NumberOfHandles); for (r = 0; r != Handles->NumberOfHandles; r++) { if (Handles->Information[r].ObjectTypeNumber == 21) //Port object { InitializeObjectAttributes(&obj, NULL, OBJ_KERNEL_HANDLE, NULL, NULL); cid.UniqueProcess = (HANDLE)Handles->Information[r].ProcessId; cid.UniqueThread = 0; if (ZwOpenProcess(&Process, PROCESS_DUP_HANDLE, &obj, &cid) == STATUS_SUCCESS) { if (ZwDuplicateObject(Process, (HANDLE)Handles->Information[r].Handle, NtCurrentProcess(), &hObject, 0, 0, DUPLICATE_SAME_ACCESS) == STATUS_SUCCESS) { if (ZwQueryObject(hObject, ObjectNameInformation, ObjName, 0x2000, NULL) == STATUS_SUCCESS) { if (ObjName->Name.Buffer != NULL) if (wcsncmp(ApiPortName.Buffer, ObjName->Name.Buffer, 20) == 0) { DPRINT("Csrss %d", Handles->Information[r].ProcessId); DPRINT("csr port - %ws", ObjName->Name.Buffer); CsrId = Handles->Information[r].ProcessId; ZwClose(Process); ZwClose(hObject); CsrId = Handles->Information[r].ProcessId; ExFreePool(Handles); ExFreePool(ObjName); return CsrId; } } else DPRINT("Error in Query Object"); ZwClose(hObject); } else DPRINT("Error on duplicating object"); ZwClose(Process); } else DPRINT("Could not open process"); } } ExFreePool(Handles); ExFreePool(ObjName); return 0; }

Такая методика антидампинга присеняется в протекторе Extreme Protector, в ранних версиях протектора для борьбы с проблемами работы GUI применялся метод подобный описанному выше, но в последних версиях разработчикам пришлось от него отказаться (что ухудшило защиту), последние версии Extreme Protector запрещают доступ не ко всему адресному пространству защищаемого процесса, а только по диапазону адресов в котором находится EXE файл, а все загруженные в его память DLL остатются никак не защищены.

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

Кончно этот метод показал себя достаточно хорошо, так как большинство кракеров даже не представляют, что творится в ядре системы, но при наличии минимальных знаний такой антидамп проблемой не будет. Один из методов снятия этой защиты - найти адрес оригинального обработчика, и убрать хук пропатчив SDT. Пpоблема в том, что этот способ не универсален, так как хук может быть установлен не только через SDT, но и перехватом int 2Eh (win2000) или изменением обработчика sysenter (win XP), также может применяться сплайсинг кода оригинального обработчика. Убирать хуки проектора - это не лучший вариант, так как он может проверять их наличие, и при снятии предпринимать враждебные действия (этим сильно грешит StarForce), поэтому я предлагаю другой вариант - дампить с помощью драйвера, не используя при этом ZwReadVirtualMermory.

В ядре Windows NT есть недокументированные функции KeAttachProcess/KeDetachProcess которые позволяют драйверу менять текущее адресное пространство. Вот прототипы этих функций:

extern
void KeAttachProcess(PEPROCESS Process);

extern
void KeDetachProcess(void);

С их помощью можно подключиться к защищенному процессу и прочитать его память, но к сожалению последняя версия фемиды патчит эти функции запрещая дамп своего процесса таким способом. Но защитой это назвать трудно, так как подключиться к другому процессу можно например с помощью функций KeStackAttachProcess/KeUnstackDetachProcess которые протектор перехватывать забывает (и защититься от этого будет проблемой, так как эти функции используются системными драйверами), поэтому для чтения памяти мы будем использовать именно их. Вот прототипы этих функций и структура необходимая для их работы:

typedef struct _KAPC_STATE { LIST_ENTRY ApcListHead[2]; PVOID Process; BOOLEAN KernelApcInProgress; BOOLEAN KernelApcPending; BOOLEAN UserApcPending; } KAPC_STATE, *PKAPC_STATE; extern NTKERNELAPI void KeStackAttachProcess(PVOID Process, PKAPC_STATE ApcState); extern NTKERNELAPI void KeUnstackDetachProcess(PKAPC_STATE ApcState);
Если в следующей версии фемиды будет реализован перехват и этих функций, то несложным делом будет подключиться к нужному процессу вручную, изменив регистр cr3.

Для чтения памяти процесса, мы будем передавать нашему драйверу через IOCTL хэндл процесса, адрес читаемой памяти, адрес буффера в текущем процессе и число читаемых байт, после чего драйвер получит указатель на EPROCESS дампимого процесса, подключится к нему и прочитает запрошенные данные. Тоесть фактически, мы реализуем ZwReadVirtualMemory вручную. Код делающий все это будет выглядеть так:

void CopyProcessMem(HANDLE hProcess, PVOID SrcAddr, PVOID DstAddr, ULONG *Size) { PEPROCESS process = NULL; NTSTATUS st; PUCHAR pMem = NULL; ULONG Addr, Bytes; PUCHAR cPtr, dPtr; KAPC_STATE ApcState; st = ObReferenceObjectByHandle(hProcess, 0, NULL, UserMode, &process, NULL); if (NT_SUCCESS(st)) { Bytes = *Size; pMem = ExAllocatePool(NonPagedPool, Bytes); dPtr = pMem; cPtr = (PUCHAR)SrcAddr; KeStackAttachProcess(process, &ApcState); __try { while (Bytes) { *dPtr = *cPtr; cPtr++; dPtr++; Bytes --; } } __except(EXCEPTION_EXECUTE_HANDLER) { } KeUnstackDetachProcess(&ApcState); Bytes = *Size - Bytes; __try { memcpy(DstAddr, pMem, Bytes); *Size = Bytes; } __except(EXCEPTION_EXECUTE_HANDLER) { } ExFreePool(pMem); ObDereferenceObject(process); } return; }

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

К сожалению, заменить ZwWriteVirtualMemory своим аналогом во многих случаях будет недостаточно, так как некоторые протекторы перехватывают еще и ZwOpenProcess. Чтобы обойти это, можно не открывать процесс, а получить его хэндл неявным поиском в процессе сервера подсистемы (см. "Перехват API функций в Windows NT, Методы внедрения кода). В моей библиотеке advApiHook соответствующая функция называется OpenProcessEx. Также такой метод поможет против защит, которые изменяют маркер безопасности процесса, запрещая открытие его памяти на чтение. Если этот метод получения хэндлов получит распостранение, то авторы защит скорее всего начнут закрывать хэндлы в сервере подсистемы, только защиты от этого они не получат, а лишь одни дополнительные глюки. Для получения хэндла процеса в драйвере, достаточно получить указатель на его EPROCESS с момощью PsLookupProcessByProcessId, после чего его можно будет добавить в таблицу хэндлов нашего процесса с помощью ObOpenObjectByPointer. Это действия осуществляет следующий код:

HANDLE MyOpenProcess(HANDLE ProcessId) { PEPROCESS Process; NTSTATUS St; HANDLE hProcess = NULL; PsLookupProcessByProcessId(ProcessId, &Process); ObOpenObjectByPointer(Process, 0, NULL, 0, NULL, UserMode, &hProcess); ObDereferenceObject(Process); return hProcess; }

Авторы защит могут конечно перехватывать PsLookupProcessByProcessId, но это только отсрочит получение дампа на пять минут, так как получить хэндл можно вручную покопавшись в структурах ядра, или вообще снять дамп без получения хэндла и без использования API.

Защита в нулевом кольце открывает действительно богатые возможности (ограниченные только воображением). Например весьма неплохой способ антидампа - разрушение таблицы страниц защищаемого процесса. Для этого надо вмешаться в работу планировщика и перехватить неэкспортируемую функцию SwapContext, которая вызывается при смене рабочего потока, в обработчике перехвата, при переключении на защищаемый процесс нужно восстанавливать таблицу страниц, а при отключении от него - разрушать. Это простейшее, что можно противопоставить драйверному дампу. Обойти такую защиту тоже несложно, нужно просто заставить работать дампер в контексте защищенного процесса, что можно сделать как в ядре (перехват обработчика чичтемных вызовов), так и в юзермоде (Code Injection). Я думаю, при желании нетрудно будет придумать свой, оригинальный способ снятия дампа.

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

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

Получение дампа модулей ядра
При реверсинге протекторов работающих в нулевом кольце часто возникает необходимость в снятии дампа PE файла загруженного в память ядра. Например, если протектор патчит ядро системы, то это легко обнаружить сдампив ядро и сравнив его с файлом на диске. Или можно сдампить сам драйвер протектора и изучить его расшифрованный код. Только с этим будет уже не так просто, так как все современные протекторы уже давно научились прятать свои драйвера удаляя упоминание о них из всех системных структур, поэтому самый реальный способ снять дамп такого драйвера - это сдампить всю память ядра, а потом искать в ней то, что нужно. Но зачастую это не сильно поможет, так как разработчики защит уже перестают паковать драйвера (например в StarForce 3), переходя на более перспективные способы защиты, такие как метаморфный код и виртуальные машины.

На превый взгляд, снять дамп с модуля ядра можно просто скопировав с помощью copymem его память, но не все так просто, так как здесь существует один подводный камень. При попытке чтения памяти модуля от начала до конца, удается получить только синий экран. При попытке создать MDL для нужного участка памяти и выполнить MmProbeAndLockPages возникает исключение, а если я делаю MmBuildMdlForNonPagedPool, то MDL создается, но при попытке ее чтения опять выскакивает синий экран. Связано это с тем, что в native PE файле у секции может присутствовать атрибут "Discardable as needed", такие секции удаляются из памяти сразу же после завершения Driver Entry. Такая система позволяет экономить NonPaged Pool, которого в системе всегда немного. При попытке обращения по такому адресу возникает исключение, которое не обрабатывается SEH и приводит к падению системы. Поэтому перед чтением памяти мы всегда будем с помощью MmIsAddressValid проверять валидность адресов. Вот код, который позволяет безопасно читать неподкачиваему память ядра:

NTSTATUS DumpKernelMemory(PVOID SrcAddr, PVOID DstAddr, ULONG Size) { PMDL pSrcMdl, pDstMdl; PUCHAR pAddress, pDstDddress; NTSTATUS st = STATUS_UNSUCCESSFUL; ULONG r; pSrcMdl = IoAllocateMdl(SrcAddr, Size, FALSE, FALSE, NULL); if (pSrcMdl) { MmBuildMdlForNonPagedPool(pSrcMdl); pAddress = MmGetSystemAddressForMdlSafe(pSrcMdl, NormalPagePriority); if (pAddress) { pDstMdl = IoAllocateMdl(DstAddr, Size, FALSE, FALSE, NULL); if (pDstMdl) { __try { MmProbeAndLockPages(pDstMdl, UserMode, IoWriteAccess); pDstDddress = MmGetSystemAddressForMdlSafe(pDstMdl, NormalPagePriority); if (pDstDddress) { memset(pDstDddress, 0, Size); for (r = 1; r < Size; r++) { if (MmIsAddressValid(pAddress)) *pDstDddress = *pAddress; pAddress++; pDstDddress++; } st = STATUS_SUCCESS; } MmUnlockPages(pDstMdl); } __except(EXCEPTION_EXECUTE_HANDLER) { } IoFreeMdl(pDstMdl); } } IoFreeMdl(pSrcMdl); } return st; }

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

Делаем процесс снятия дампа удобнее
Писать свой дампер - довольно хлопотное занятие, поэтому я решил не изобретать очередной велосипед, а использовать PE Tools, так как он учитывает многие тонкости PE формата и позволяет быстро и удобно сдампить практически что угодно. Поэтому я написал плагин eXtreme dumper (по аналогу с Extreme Protector’ом), который реализует методику драйверного дампа и дампа методом DllInjection. Для этого я перехватил функции ZwOpenProcess и ZwReadVirtualMemory в PE Tools и заменил их своими аналогами, которые используют описаные выше технологии. Например приведу код обработчика ZwReadVirtualMemory:

function NewZwReadVirtualMemory(ProcessHandle: dword; BaseAddress: pointer; Buffer: pointer; BufferLength: dword; ReturnLength: pdword): NTStatus; stdcall; var hPipe, Bytes, Len: dword; Req: TXDumpRequest; PipeName: array [0..128] of Char; sPid: array [0..8] of Char; ProcessId: dword; Query: TDriverQuery; begin if DriverMethod then begin Result := dword(-1); Len := BufferLength; Query.QueryType := 2; Query.Param1 := ProcessHandle; Query.Param2 := dword(@Len); Query.Param3 := dword(BaseAddress); Query.Param4 := dword(Buffer); WriteFile(hDriver, Query, SizeOf(TDriverQuery), Bytes, nil); if ReturnLength <> nil then ReturnLength^ := Len; if Len > 0 then Result := STATUS_SUCCESS; end else begin ProcessId := GetPid(ProcessHandle); StrCpy(PipeName, bPipeName); ToHex(ProcessId, 8, sPid); StrCat(PipeName, sPid); hPipe := CreateFile(@PipeName, GENERIC_WRITE or GENERIC_READ, 0, nil, OPEN_EXISTING, 0, 0); if hPipe <> INVALID_HANDLE_VALUE then begin Req.Address := BaseAddress; Req.Length := BufferLength; WriteFile(hPipe, Req, SizeOf(TXDumpRequest), Bytes, nil); ReadFile(hPipe, Len, SizeOf(dword), Bytes, nil); ReadFile(hPipe, Buffer^, Len, Bytes, nil); if ReturnLength <> nil then ReturnLength^ := Len; Result := 0; CloseHandle(hPipe); end else Result := TrueZwReadVirtualMemory(ProcessHandle, BaseAddress, Buffer, BufferLength, ReturnLength); end; end;

Как вы видите, для взаимодействия дампера с Dll внедряемой в защищенный процесс используются именованые каналы, имя канала формируется из строки eXtremeDumper и Id процесса в HEX коде. Запрос на чтение памяти отправляется в дампимый процесс, где серверная часть читает запрошенный участок памяти и возвращает данные и число прочитанных байт. Серверная часть дампера выглядит так:

library xDump; uses Windows, advApiHook, NativeApi; {$include string.inc} type PXDumpRequest = ^TXDumpRequest; TXDumpRequest = packed record ReqType: dword; Address: pointer; Length: dword; end; const bPipeName = ’\\.\pipe\eXtremeDumper’#0; function SafeReadMemory(Addr, Buff: pointer; Size: dword): dword; asm push ebx push edx push ecx push esi push edi push ebp push offset @Handler push fs:[0] mov fs:[0], esp mov esi, eax mov edi, edx rep movsb mov eax, ecx pop fs:[0] add esp, 4 pop ebp pop edi pop esi pop ecx pop edx pop ebx ret @handler: mov ecx, [esp + $0C] add [ecx + $B8], 2 ret end; function PipeThread(hPipe: dword): dword; stdcall; var Req: TXDumpRequest; Bytes, Len: dword; pBuff: pointer; begin ReadFile(hPipe, Req, SizeOf(TXDumpRequest), Bytes, nil); GetMem(pBuff, Req.Length); Len := Req.Length - SafeReadMemory(Req.Address, pBuff, Req.Length); WriteFile(hPipe, Len, SizeOf(dword), Bytes, nil); WriteFile(hPipe, pBuff^, Req.Length, Bytes, nil); FreeMem(pBuff); CloseHandle(hPipe); end; var TrId: dword; procedure PipeServerThread(); var hPipe: dword; PipeName: array [0..128] of Char; sPid: array [0..8] of Char; begin StrCpy(PipeName, bPipeName); ToHex(GetCurrentProcessId(), 8, sPid); StrCat(PipeName, sPid); repeat hPipe := CreateNamedPipe(PipeName, PIPE_ACCESS_DUPLEX or WRITE_DAC, PIPE_TYPE_MESSAGE or PIPE_READMODE_MESSAGE or PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 1024, 1024, 5000, nil); if hPipe = INVALID_HANDLE_VALUE then Exit; if ConnectNamedPipe(hPipe, nil) then CreateThread(nil, 0, @PipeThread, pointer(hPipe), 0, TrId); until false; end; begin CreateThread(nil, 0, @PipeServerThread, nil, 0, TrId); end.

При подключении клиента к пайпу создается поток, который читает запрошенные данные внутри SEH обработчика, после чего данные направляются клиенту. Единственный недостаток этого кода - низкая производительность, так как каждый запрос обслуживается в отдельном потоке. Но, по сути, высокая производительность тут и не нужна, так как важно просто получить дамп, хотя кого это не устраивает, может просто взять и написать лучше, а мне было влом :).

А теперь вернемся к теме получения информации о модулях ядра. Для этой цели я буду использовать функцию ZwQuerySystemInformation с классом SystemModuleInformation, при этом функция возвращает мне массив структур следующего типа:

PSYSTEM_MODULE_INFORMATION = ^SYSTEM_MODULE_INFORMATION; SYSTEM_MODULE_INFORMATION = packed record // Information Class 11 Reserved: array[0..1] of ULONG; Base: PVOID; Size: ULONG; Flags: ULONG; Index: USHORT; Unknown: USHORT; LoadCount: USHORT; ModuleNameOffset: USHORT; ImageName: array [0..255] of Char; end; PSYSTEM_MODULE_INFORMATION_EX = ^SYSTEM_MODULE_INFORMATION_EX; SYSTEM_MODULE_INFORMATION_EX = packed record ModulesCount: dword; Modules: array[0..0] of SYSTEM_MODULE_INFORMATION; end;

Нас интересуют поля ImageName, Base и Size. Замечу, что в KernelMode получить эту информацию можно через связанный список PsLoadedModulesList, но в данном случае такая задача не стоит.

Для перечисления загруженных в процесс модулей, PE Tools использует функции GetModuleFirst/GetModuleNext из Procs32.dll. Для отображения своего списка модулей мы будем их перехватывать и подставлять наши модули в процесс System. Код обработчиков этих функций будет выглядеть так:

function NewGetModuleNext(dwPID: dword; mEntry: PMODULE_ENTRY): bool; stdcall; begin if dwPID = SystemPid then begin Result := false; lstrcpy(mEntry^.lpFileName, Modules^.Modules[CurrentModule].ImageName); mEntry^.dwImageBase := dword(Modules^.Modules[CurrentModule].Base); mEntry^.dwImageSize := Modules^.Modules[CurrentModule].Size; mEntry^.bSystemProcess := true; Inc(CurrentModule); if CurrentModule > Modules^.ModulesCount then ReleaseModulesInfo() else Result := true; end else Result := TrueGetModuleNext(dwPID, mEntry); end; function NewGetModuleFirst(dwPID: dword; mEntry: PMODULE_ENTRY): bool; stdcall; begin if dwPID = SystemPid then begin if CurrentModule > 0 then ReleaseModulesInfo(); Modules := GetInfoTable(SystemModuleInformation); Result := NewGetModuleNext(dwPID, mEntry); end else Result := TrueGetModuleFirst(dwPID, mEntry); end;

Для снятия дампа PE Tools использует функцию DumpProcess из NDump.dll, нужно перехватить ее и отправлять запросы на дамп своему драйверу. Код ее обработчика выглядит так:

function NewDumpProcess(dwProcessId: dword; pStartAddr: pointer; dwcBytes: dword; pDumpedBytes: pointer): bool; stdcall; var Query: TDriverQuery; Bytes: dword; begin if dwProcessId = SystemPid then begin Query.QueryType := IOCTL_DUMP_KERNEL_MEM; Query.Param1 := dword(pStartAddr); Query.Param2 := dwcBytes; Query.Param3 := dword(pDumpedBytes); Result := WriteFile(hDriver, Query, SizeOf(TDriverQuery), Bytes, nil); end else Result := TrueDumpProcess(dwProcessId, pStartAddr, dwcBytes, pDumpedBytes); end;

--------------------------------------------------------------------------------

Из фич плагина eXtreme dumper можно выделить следующие:

2 Метода дампа (дамп с помошью Dll-Injection и с помошью драйвера)
Неявное получение хэндлов (OpenProcessEx)
Защита PE Tools от воздействия со стороны других процессов (Protect PE Tools)
Снятие дампа модулей ядра (Enable kernel modules dumping)

Короче, плагин этот в хозяйстве несомненно пригодится.


--------------------------------------------------------------------------------

Хочу заметить, что автор статьи взломом программ и прочими незаконными по ук. РФ действиями не занимается. Вся приведенная здесь информация может быть использована ТОЛЬКО В УЧЕБНО-ПОЗНАВАТЕЛЬНЫХ ЦЕЛЯХ, за любое незаконное применение этой информации автор никакой ответственности не несет.

Комментарии

отсутствуют

Добавление комментария


Ваше имя (на форуме):

Ваш пароль (на форуме):

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

Комментарий: