Расширяем контекстное меню кнопки «Пуск» в Windows 8.1

от автора

В этой статье мне хотелось бы рассказать о своем опыте расширения 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 Не являюсь профессионалом в системном программировании, так что могут быть не точности.

ссылка на оригинал статьи http://habrahabr.ru/post/194716/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *