В этой статье мне хотелось бы рассказать о своем опыте расширения Windows Explorer, если конкретнее, контекстного меню, называемого «Power User Menu». Не скажу, что очень нуждаюсь в старом представлении меню кнопки «Пуск», но все таки хотелось бы иметь возможность быстрого и структурированного доступа к основным функциям необходимым в работе. Power User Menu можно вызвать двумя способами: 1. Щелкнуть правой кнопкой мыши на кнопке «Пуск». 2. Нажать комбинацию клавиш Windows Key + X. Корпорация Майкрософт предоставила возможность редактирования этого меню, однако эта возможность достаточно ограниченна и не позволяет создавать иерархию меню, пункты с пиктограммами, и поддерживает только ярлыки, да и то не всех типов. Для реализации описываемого функционала мы выполним dll инъекцию в процесс Windows Explorer, а так же осуществим перехват api вызовов управляющих работой контекстного меню. В качестве подопытной операционной системы будем использовать Windows 8.1 x64.
Итак начнем с процедуры позволяющей нам выполнить инъекцию dll в адресное пространство Windows Explorer. Способ инъекции, который мы будем использовать называется «Code cave dll injection» и представляет из себя инъекцию заранее подготовленного машинного кода в адресное пространство выбранного процесса. Данный машинный код выполнит API вызов LoadLibrary с нужной нам библиотекой и вернет управление приложению.
void InjectDLLx64( LPPROCESS_INFORMATION ppi, LPCTSTR dll )
{
CONTEXT threadContext;
DWORD length;
LPVOID memBuf;
DWORD64 loadLibApi;
union
{
PBYTE cC;
PDWORD64 cP;
} ip;
#define CODESIZE 92
static BYTE code[CODESIZE+SIZE_T(MAX_PATH)] = {
0,0,0,0,0,0,0,0, // original rip
0,0,0,0,0,0,0,0, // LoadLibraryW
0x9C, // pushfq
0x50, // push rax
0x51, // push rcx
0x52, // push rdx
0x53, // push rbx
0x55, // push rbp
0x56, // push rsi
0x57, // push rdi
0x41,0x50, // push r8
0x41,0x51, // push r9
0x41,0x52, // push r10
0x41,0x53, // push r11
0x41,0x54, // push r12
0x41,0x55, // push r13
0x41,0x56, // push r14
0x41,0x57, // push r15
0x48,0x83,0xEC,0x28, // sub rsp, 40
0x48,0x8D,0x0D,41,0,0,0, // lea ecx, L"path to dll"
0xFF,0x15,-49,-1,-1,-1, // call LoadLibraryW
0x48,0x83,0xC4,0x28, // add rsp, 40
0x41,0x5F, // pop r15
0x41,0x5E, // pop r14
0x41,0x5D, // pop r13
0x41,0x5C, // pop r12
0x41,0x5B, // pop r11
0x41,0x5A, // pop r10
0x41,0x59, // pop r9
0x41,0x58, // pop r8
0x5F, // pop rdi
0x5E, // pop rsi
0x5D, // pop rbp
0x5B, // pop rbx
0x5A, // pop rdx
0x59, // pop rcx
0x58, // pop rax
0x9D, // popfq
0xFF,0x25,-91,-1,-1,-1, // jmp original Rip
0, // dword alignment for loadLibApi
};
length = SIZE_T(lstrlen( dll ) + 1);
if (length > SIZE_T(MAX_PATH))
return;
RtlCopyMemory( code + CODESIZE, dll, length );
length += CODESIZE;
threadContext.ContextFlags = CONTEXT_CONTROL;
GetThreadContext( ppi->hThread, &threadContext );
memBuf = VirtualAllocEx( ppi->hProcess, NULL, length, MEM_COMMIT,
PAGE_EXECUTE_READWRITE );
loadLibApi = (DWORD64)LoadLibraryW;
ip.cC = code;
*ip.cP++ = threadContext.Rip;
*ip.cP++ = loadLibApi;
WriteProcessMemory( ppi->hProcess, memBuf, code, length, NULL );
FlushInstructionCache( ppi->hProcess, memBuf, length );
threadContext.Rip = (DWORD64)memBuf + 16;
SetThreadContext( ppi->hThread, &threadContext);
}
Машинный код выполняет сохранение регистров ЦП, затем загрузку нужной нам библиотеки при помощи API вызова LoadLibrary определенного на стадии выполнения программы, далее восстанавливает содержимое регистров и возвращает управление. Естественно, что в момент инъекции процесс должен быть в приостановленном состоянии.
Код остальных функций приложения выполняющего внедрение dll рассматривать не буду, так как они не представляет большого интереса.
Для того что бы иметь возможно расширять контекстное меню, необходимо получить его handle. Для вывода меню используется функция TrackPopupMenu, рассмотрим ее прототип:
int WINAPI TrackPopupMenu(
_In_ HMENU hMenu,
_In_ UINT uFlags,
_In_ int x,
_In_ int y,
_In_ int nReserved,
_In_ HWND hWnd,
_In_opt_ const RECT *prcRect)
Как видно тут есть и HWND окна, которому принадлежит меню и непосредственно HANDLE самого меню. Однако прежде чем реализовывать перехват посмотрим какие параметры получает эта функция при вызове. Воспользуемся приложением API Monitor. Скачать его можно на сайте производителя API Monitor. После конфигурации точки останова на функции в API Monitor, пытаемся открыть Power User Menu и получаем окно следующего вида:
Из вызова видно что Explorer открывает контекстное меню используя флаг TPM_RETURNCMD, это значит, что не нужно пытаться искать сообщения типа WM_COMMAND определяющие выбранный элемент. Элемент указанный пользователем вернет сама функция TrackPopupMenu, либо 0 если пользователь ничего не выбрал.
Для организации перехвата API вызовов я использую библиотеку Mini Hook Library. Однако в оригинале она тянет за собой Boost. Версию без привязки к Boost можно взять в приложении к статье.
Далее привожу код перехваченной функции:
int WINAPI HookedTrackPopupMenu(
_In_ HMENU hMenu,
_In_ UINT uFlags,
_In_ int x,
_In_ int y,
_In_ int nReserved,
_In_ HWND hWnd,
_In_opt_ const RECT *prcRect)
{
WCHAR className[250];
int command;
GetClassName(hWnd,className,250);
int cpount = GetMenuItemCount(hMenu);
if(wcscmp(L"ImmersiveSwitchList",className) == 0 && !isInizialized)
{
HMENU hsubMenu = CreatePopupMenu();
InsertMenu(hsubMenu, 0, MF_BYPOSITION | MF_STRING, 23, L"Item");
InsertMenu(hMenu, 0, MF_BYPOSITION | MF_POPUP , (UINT_PTR)hsubMenu, L"Group");
isInizialized = true;
}
command = originalTrackMenu(hMenu, uFlags, x, y, nReserved, hWnd, prcRect);
switch (command)
{
case 23 :
{
MessageBoxA(hWnd, "Test", "Test", MB_OK+MB_ICONINFORMATION);
return 0;
break;
}
default:
{
break;
}
}
return command;
}
Как видно тут мы проверяем что вызвано контекстное меню в нужном нам месте, а именно в окне с классом ImmersiveSwitchList. Значение класса окна было установлено при помощи утилиты Spy++ поставляемой вместе с Visual Studio. Далее производим расширение контекстного меню, вызываем оригинальную функцию вывода и ожидаем результата выполнения операции. При выборе нашего пункта меню сработает MessageBox. На следующем скриншоте показано как выглядит модифицированное Power User Menu.
Заключение.
Мы рассмотрели возможность модификации контекстного меню Windows Explorer при помощи dll инъекции и перехвата api функции. Таким же образом можно перехватить любое меню в контексте Windows Explorer или любого другого процесса. Однако если меню вызывается без флага TPM_RETURNCMD, то нужно так же выполнить расширение оконной процедуры окна родителя дабы обеспечить корректную обработку выбора созданного Вами элемента и не нарушить работу уже существующего функционала. Это можно реализовать при помощи API функции SetWindowLongPtr, передав указатель на расширяющую функцию, а так же не забыть вернуть управление родительской оконной процедуре.
Исходники к статье выполнены в Visual Studio 2012 и доступны по ссылке: DllInject.zip
Так же ссылка на статью о редактировании Power User Menu на уровне файловой системы на английском: Add Shutdown, Restart options to WinKey+X Power User Menu in Windows 8
P.S Не являюсь профессионалом в системном программировании, так что могут быть не точности.
Автор: LrdSpr