На днях, во время реверса одной программы, задалась вопросом, как
вывести список таймеров, установленных win32 - функцией SetTimer (а
если быть конкретнее, то мне нужны были адреса процедур - lpTimerFunc).
Конечно, с практической точки зрения это малополезно, а с теоретической
- очень даже. Что потребуется для успешного восприятия нижеизложенного?
Из инструментов:
IDA
Наборы системных файлов для разных билдов Windows
Отладочные символы
msdn
Приступим непосредственно к исследованию. Сначала приведу прототип
функции SetTimer (в процессе изложения удобно иметь его перед глазами)
Путешествия в недры дизассемблированного кода начнем с юзермода.
Когда мы вызываем функцию 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
Первым делом вызывается функция 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.
Далее смотрим _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 в нашей структуре содержится процедура таймера (ради которой и пришлось расковырять эту функцию):
Обратите внимание на поле 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.
Кстати говоря, в Висте дела обстоят аналогичным образом. Все поля
структуры реверсить незачем (может как-нибудь в другой раз), следующего
объявления будет вполне достаточно
Теперь, когда известно где брать нужную информацию, в коде драйвера,
выводящего 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. Привожу две процедуры вывода
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));
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));