Исследование функции 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-а http://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/