Pull to refresh

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

Reading time 5 min
Views 31K


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

Articles