Исследование функции SetTimer


На днях, во время реверса одной программы, задалась вопросом, как вывести список таймеров, установленных win32 — функцией SetTimer (а если быть конкретнее, то мне нужны были адреса процедур — lpTimerFunc). Конечно, с практической точки зрения это малополезно, а с теоретической — очень даже. Что потребуется для успешного восприятия нижеизложенного? Из инструментов:

  • IDA
  • Наборы системных файлов для разных билдов Windows
  • Отладочные символы
  • msdn

Приступим непосредственно к исследованию. Сначала приведу прототип функции SetTimer (в процессе изложения удобно иметь его перед глазами)

UINT_PTR SetTimer(      
    HWND hWnd,
    UINT_PTR nIDEvent,
    UINT uElapse,
    TIMERPROC lpTimerFunc
);

Путешествия в недры дизассемблированного кода начнем с юзермода. Когда мы вызываем функцию SetTimer из user32.dll вызывается сервис win32k.sys (это видно по номеру сервиса, который >1000h) NtUserSetTimer

; UINT __stdcall NtUserSetTimer(HWND hWnd, UINT nIDEvent, UINT uElapse, TIMERPROC lpTimerFunc)
public _NtUserSetTimer@16
_NtUserSetTimer@16 proc near

hWnd= dword ptr  4
nIDEvent= dword ptr  8
uElapse= dword ptr  0Ch
lpTimerFunc= dword ptr  10h

mov     eax, 121Eh
mov     edx, 7FFE0300h
call    dword ptr [edx]
retn    10h
_NtUserSetTimer@16 endp

Далее управление передается в ядро. Что ж, грузим в IDA win32k.sys

Обратимся к листингу NtUserSetTimer:

... 
.text:BF803A10                 mov     ecx, [ebp+hwnd]
.text:BF803A13                 test    ecx, ecx
.text:BF803A15                 jz      short loc_BF803A4C
.text:BF803A17                 call    @ValidateHwnd@4 ; ValidateHwnd(x)
.text:BF803A1C                 test    eax, eax
.text:BF803A1E                 jz      short loc_BF8039F3
.text:BF803A20
.text:BF803A20 loc_BF803A20:                           ; CODE XREF: NtUserSetTimer(x,x,x,x)+49j
.text:BF803A20                 mov     edx, [ebp+uElapse]
.text:BF803A23                 cmp     edx, USER_TIMER_MINIMUM
.text:BF803A26                 jb      short set_minimum
.text:BF803A28
.text:BF803A28 loc_BF803A28:                           ; CODE XREF: NtUserSetTimer(x,x,x,x)-7j
.text:BF803A28                 mov     ecx, USER_TIMER_MAXIMUM
.text:BF803A2D                 cmp     edx, ecx
.text:BF803A2F                 ja      short set_maximum
.text:BF803A31
.text:BF803A31 loc_BF803A31:                           ; CODE XREF: NtUserSetTimer(x,x,x,x)-Cj
.text:BF803A31                 push    [ebp+lpTimerFunc]
.text:BF803A34                 push    edx
.text:BF803A35                 push    [ebp+nIDEvent]
.text:BF803A38                 push    eax
.text:BF803A39                 call    __SetTimer@16   ; _SetTimer(x,x,x,x)

...

Первым делом вызывается функция ValidateHwnd (подробнее о ней можно почитать в статье Twister-а //www.wasm.ru/article.php?article=window_inject). Функция возвращает указатель на структуру tagWND. Этот указатель затем передается во внутреннюю функцию __SetTimer. В NtUserSetTimer проверяется параметр uElapse (об этом написано в msdn). Если быть точнее, то он должен быть USER_TIMER_MINIMUM<=uElapse<=USER_TIMER_MAXIMUM. Константы USER_TIMER_MINIMUM и USER_TIMER_MAXIMUM равны соответственно 0xA и 0x7FFFFFFF. Идем дальше… Заглянем в функцию __SetTimer, которая сводится к вызову InternalSetTimer.

... 
.text:BF803A5B                 mov     esi, [ebp+ptagWND]
...
.text:BF803A77 loc_BF803A77:                           ; CODE XREF: _SetTimer(x,x,x,x)+Bj
.text:BF803A77                 push    0
.text:BF803A79                 push    [ebp+lpTimerFunc]
.text:BF803A7C                 push    [ebp+uElapse]
.text:BF803A7F                 push    [ebp+nIDEvent]
.text:BF803A82                 push    esi
.text:BF803A83                 call    _InternalSetTimer@20 ; InternalSetTimer(x,x,x,x,x)

Далее смотрим _InternalSetTimer. Здесь-то и начинается самое интересное:

.text:BF803894 ; __stdcall InternalSetTimer(x, x, x, x, x)
.text:BF803894 _InternalSetTimer@20 proc near          ; CODE XREF: zzzUpdateCursorImage()-14p
.text:BF803894                                         ; _SetTimer(x,x,x,x)+2Ep ...
.text:BF803894
.text:BF803894 hwnd            = dword ptr  8
.text:BF803894 IDEvent         = dword ptr  0Ch
.text:BF803894 uElapse         = dword ptr  10h
.text:BF803894 TimerFunction   = dword ptr  14h
.text:BF803894 unknown         = dword ptr  18h
.text:BF803894
.text:BF803894 ; FUNCTION CHUNK AT .text:BF80384C SIZE 00000043 BYTES
.text:BF803894
.text:BF803894                 mov     edi, edi
.text:BF803896                 push    ebp
.text:BF803897                 mov     ebp, esp
.text:BF803899                 push    ebx
.text:BF80389A                 push    esi
.text:BF80389B                 push    edi
.text:BF80389C                 push    USER_TIMER_MINIMUM
.text:BF80389E                 pop     eax
.text:BF80389F                 xor     edi, edi
.text:BF8038A1                 cmp     [ebp+uElapse], eax
.text:BF8038A4                 jb      short set_el_minimum
.text:BF8038A6
.text:BF8038A6 loc_BF8038A6:                           ; CODE XREF: InternalSetTimer(x,x,x,x,x)-45j
.text:BF8038A6                 mov     eax, USER_TIMER_MAXIMUM
.text:BF8038AB                 cmp     [ebp+uElapse], eax
.text:BF8038AE                 ja      short set_el_maximum

Первым делом, как видно, проводятся уже знакомые проверки параметра uElapse. Затем вызывается внутренняя функция _FindTimer. Затем выделяется память под структуру, содержащую информацию о таймере

.text:BF8038C6                 push    30h             ; NumberOfBytes
.text:BF8038C8                 push    10h             ; char
.text:BF8038CA                 push    edi             ; int
.text:BF8038CB                 push    edi             ; int
.text:BF8038CC                 call    _HMAllocObject@16 ; HMAllocObject(x,x,x,x)
.text:BF8038D1                 mov     esi, eax
.text:BF8038D3                 cmp     esi, edi
.text:BF8038D5                 jz      error_allocate

Заполнение структуры (указываю только на интересные поля) — это процедура таймера, ид таймера и следующий таймер в списке

.text:BF8038E7                 mov     eax, [ebp+nIDEvent]
.text:BF8038EA
.text:BF8038EA loc_BF8038EA:                           ; CODE XREF: InternalSetTimer(x,x,x,x,x)+150j
; записываем в структуру по смещению 18h ид таймера
.text:BF8038EA                 mov     [esi+18h], eax
.text:BF8038ED                 mov     eax, _gptmrFirst
; записываем адрес следующей за нами структуры таймера
.text:BF8038F2                 mov     [esi+8], eax
.text:BF8038F5                 mov     [esi+0Ch], edi
.text:BF8038F8                 mov     eax, _gptmrFirst
.text:BF8038FD                 cmp     eax, edi
.text:BF8038FF                 jz      short ifFirst
.text:BF803901                 mov     [eax+0Ch], esi
.text:BF803904
.text:BF803904 ifFirst:                                ; CODE XREF: InternalSetTimer(x,x,x,x,x)+6Bj
.text:BF803904                 mov     _gptmrFirst, esi
.text:BF80390A

Тут появляется весьма интересная переменная _gptmrFirst — которая представляет собой указатель на односвязный список таймеров, зарегистрированных в системе. Последний зарегистрированный таймер помещается в начало списка.

По смещению 0x28 в нашей структуре содержится процедура таймера (ради которой и пришлось расковырять эту функцию):

.text:BF80394E                 mov     eax, [ebp+TimerFunction]
.text:BF803951                 mov     [esi+28h], eax

Приведенной выше информации достаточно для вывода информации о таймерах в winxp. Объявим такую структуру

typedef struct _timer_struct_xp
{
 DWORD unkn01[2]; 
 _timer_struct_xp * nextTimer;
 DWORD unkn02[2];
 PULONG ptagWND;
 DWORD nIDEvent;
 DWORD unkn03[3];
 PVOID TimerFunc;  
 DWORD unkn04;
}timer_struct_xp,*ptimer_struct_xp;

Обратите внимание на поле ptagWND – указатель на структуру tagWND для окна, хендл которого передается как первый аргумент SetTimer. Нужно оно для идентификации процесса, связанного с таймером. Дело в том, что в структуре tagWND по смещению 8 лежит указатель на структуру WIN32THREAD, первое поле которой — указатель ETHREAD. А имея в наличии ETHREAD можно определить процесс, которому принадлежит нить.

Все бы ничего, но сначала необходимо найти переменную _gptmrFirst. Из вышеизложенного видно, что дизассемблировать процедуру за процедурой будет не очень рационально, да и ненадежно. Более красивого способа поиска этой переменной я не нашла, поэтому я определила только адреса в разных версиях Windows.

На этом можно было бы поставить точку, но тогда статья была бы неполной. Стоит сказать несколько слов о том, как обстоят дела в Windows 7 (бета и RC). Какие же изменения произошли в этой версии Винды? Во-первых структура под таймер теперь выделяется размером не 30h, а 44h

.text:BF8D73C2                 push    44h             ; NumberOfBytes
.text:BF8D73C4                 push    10h             ; char
.text:BF8D73C6                 push    esi             ; int
.text:BF8D73C7                 push    [ebp+var_4]     ; int
.text:BF8D73CA                 call    _HMAllocObject@16 ; HMAllocObject(x,x,x,x)
.text:BF8D73CF                 mov     esi, eax

Во-вторых переменная, хранящая указатель на список таймеров (который, в-третьих, из односвязного трансформировался в двусвязный) теперь именуется _gtmrListHead.

.text:BF8D741E                 mov     [esi+1Ch], eax
.text:BF8D7421                 mov     ecx, _gtmrListHead
.text:BF8D7427                 lea     eax, [esi+0Ch]
.text:BF8D742A                 mov     [eax], ecx
.text:BF8D742C                 mov     dword ptr [eax+4], offset _gtmrListHead
.text:BF8D7433                 mov     [ecx+4], eax
.text:BF8D7436                 mov     _gtmrListHead, eax

Ну и, в-четвертых, смещение нужного нам поля с адресом процедуры таймера тоже поменялось

.text:BF8D7483                 mov     eax, [ebp+lpTimerFunc]
.text:BF8D7486                 mov     [esi+2Ch], eax

Кстати говоря, в Висте дела обстоят аналогичным образом. Все поля структуры реверсить незачем (может как-нибудь в другой раз), следующего объявления будет вполне достаточно

typedef struct _timer_struct_vistawin7
{
 DWORD unkn01[3]; 
 LIST_ENTRY leTimerList;
 PULONG ptagWND;
 DWORD unkn03[5];  
 PVOID TimerFunc;
/*---*/
}timer_struct_vistawin7,*ptimer_struct_vistawin7;

Теперь, когда известно где брать нужную информацию, в коде драйвера, выводящего DbgPrint – ом поля структуры таймера, пропишем в коде RVA переменных (_gptmrFirst и _gtmrListHead) для разных билдов

switch(*NtBuildNumber)
	{
	case 7100:
		// Win7 RC
		uRVA = 0x21D970;
		break;
	case 6001:
		// Vista
		uRVA = 0x1E2110;
		break;
	case 7000:
		// Beta Win7
		uRVA = 0x2189F0;
		break;

	case 2600:
		// SP2
		uRVA = 0x1A8914;
		break;

	default:
		break;

	}

К сожалению, у меня не было в наличии большого числа различных win32k.sys, но, как мне кажется и этого достаточно. Я не люблю прописывать смещения, но в данном случае это оправдано. Кто хочет, может встроить любой понравившийся дизасм длин и отдельно написать код поиска для XP и Vista. Перед этим желающим дизассемблировать надо будет найти ShadowSSDT и определить номер сервиса NtUserSetTimer.

Так же необходимо определить базу win32k.sys. Привожу две процедуры вывода

VOID KePrintTimersWin7Vista(DWORD dwBase,DWORD dwRVA)
{
	PLIST_ENTRY pleTimerHead;

	ptimer_struct_vistawin7 ptsCurrent;

	DWORD pWND;

	ULONG ptCurThread,pProcess;

	if(!dwBase) return;

	// RVA переменной + база win32k.sys
	pleTimerHead = (PLIST_ENTRY)(dwRVA+dwBase);
	
	ptsCurrent = CONTAINING_RECORD(pleTimerHead->Flink,
				timer_struct_vistawin7,
				leTimerList); 

	DWORD dwCount = 0;

	do
	{
	
		if(MmIsAddressValid((PVOID)ptsCurrent))
	 
		{

			DbgPrint("KePrintTimers: Current timer %X; Next timer %X; Timer func %X\n",
				ptsCurrent,
				ptsCurrent->leTimerList.Flink,
				ptsCurrent->TimerFunc
				);

			pWND = (DWORD)ptsCurrent->ptagWND;

			// определение по указателю на tagWND имени процесса
			if(pWND)
			{

				DbgPrint(" Timer window handle %X\n",
					*(PULONG)(pWND));
				// по смещению 8 в структуре tagWND лежит указатель на Win32Thread
				// в этой структуре первый дворд - указатель на ETHREAD
				ptCurThread = *(*(PULONG *)(pWND+8));

				if(ptCurThread)
				{
				  DbgPrint(" Timer ETHREAD %X\n",ptCurThread);

				  // смещение KPROCESS в ETHREAD
				  if(*NtBuildNumber<7100)
				  {
				    
					  pProcess = *(PULONG)(ptCurThread+0x144);
				  
				  }
				  else
				  {
				  
					  pProcess = *(PULONG)(ptCurThread+0x150);

				  }
				  DbgPrint(" EPROCESS =%X\n",pProcess);

				  if(pProcess)
				  {
					  DbgPrint(" Process name = %s\n",
						  PsGetProcessImageFileName((PEPROCESS)pProcess));
				  }

				}
			}


			ptsCurrent = CONTAINING_RECORD(ptsCurrent->leTimerList.Flink,
				timer_struct_vistawin7,
				leTimerList);

			dwCount ++;
	
	}
	else
	{
			DbgPrint("KePrintTimers: Invalid address\n");

			break;
	}

	}while(ptsCurrent->leTimerList.Flink!=pleTimerHead);

	DbgPrint("KePrintTimers: Timers count =%d\n",dwCount);


}

Как вы могли заметить смещение PKPROCESS в ETHREAD поменялось в Windows 7 RC, поэтому введена дополнительная проверка.

VOID KePrintTimersXP(DWORD dwBase,DWORD dwRVA)
{
	ptimer_struct_xp ptsInstalledTimers;

	if(!dwBase) return;

	ptsInstalledTimers = *(ptimer_struct_xp *)(dwRVA + dwBase);

	DWORD dwCount = 0;

	DWORD pWND;

	PETHREAD ptCurThread;


	while(ptsInstalledTimers)
	{
	
		if(MmIsAddressValid((PVOID)ptsInstalledTimers))
	 
		{
			DbgPrint("KePrintTimers: Address valid\n");

			DbgPrint("KePrintTimers: Current timer %X; Next timer %X; Timer func %X; nIDEvent %X\n",
				ptsInstalledTimers,
				ptsInstalledTimers->nextTimer,
				ptsInstalledTimers->TimerFunc,
				ptsInstalledTimers->nIDEvent);

			pWND = (DWORD)ptsInstalledTimers->ptagWND;

			// определение по указателю на tagWND имени процесса
			if(pWND)
			{

				DbgPrint(" Timer window handle %X\n",
					*(PULONG)(pWND));
				// по смещению 8 в структуре tagWND лежит указатель на Win32Thread
				// в этой структуре первый дворд - указатель на ETHREAD
				ptCurThread = *(PETHREAD *)(*(PULONG)(pWND+8));

				if(ptCurThread)
				{
				  DbgPrint(" Timer ETHREAD %X\n",ptCurThread);

				  DbgPrint(" EPROCESS =%X\n",ptCurThread->ThreadsProcess);

				  if(ptCurThread->ThreadsProcess)
				  {
					  DbgPrint(" Process name = %s\n",
						  PsGetProcessImageFileName(ptCurThread->ThreadsProcess));
				  }

				}
			}
			ptsInstalledTimers = ptsInstalledTimers->nextTimer;

			dwCount ++;
	
	}
	else
	{
			DbgPrint("KePrintTimers: Invalid address\n");

			break;
	}

}

	DbgPrint("KePrintTimers: Timers count =%d\n",dwCount);

	
}

Как видите все предельно просто.

Как проверить, что все правильно? Инсталлировать таймер в юзермоде и посмотреть будет ли он присутствовать в выводе.

nIDEvent – то, что вернула SetTimer.

На этом статью можно завершить, надеюсь, кому-нибудь было интересно.

[C] lhc645

 

Источник WASM.RU /11.08.2009/

Поделиться в соц сетях

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

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

Есть идеи, замечания, предложения? Воспользуйтесь формой Обратная связь или отправьте сообщение по адресу replay@sciencestory.ru
© 2017 Истории науки. Информация на сайте опубликована в ознакомительных целях может иметь ограничение 18+