Статья ASM. Путешествие во времени Windows

Marylin

Mod.Assembler
Red Team
05.06.2019
340
1 500
BIT
971
TimeLogo.webp

Одним из многочисленных недостатков Windows является огромный зоопарк форматов времени, с которым поневоле приходится иметь дело прикладным программистам. Как результат, обычная на первый взгляд задача превращается в нетривиальную, с поиском единственной из многочисленных API. В данной статье рассматриваются 7 известных мне типов времени, хотя на полноту изложения материал никак не претендует.

Win32API возвращают время в одном из нескольких форматов. Когда нужно, мы можем использовать специальные функции для преобразования из одного формата в другой, например для сравнения, или отображения на устройствах вывода (окно, консоль, принтер). В следующей таблице обобщены основные из них:

WinTime.webp


1. Системное время – это текущая дата\время суток по Гринвичу UTC. Его возвращает функция GetSystemTime() в структуру SYSTEMTIME, которая содержит по 2 байта в удобном для вывода формате: год, месяц, день недели, день, час, минута, секунда и миллисекунда. Системное время можно установить вызовом SetSystemTime(). Несколько вариантов обозначения одного и того-же системного времени создаёт путаницу, а потому разложим всё по полочкам..

На сегодняшний день, мировое время регулируется выпущенным в 1972 году стандартом UTC. Это время синхронизируется с движением электронов в атоме Цезия, и является точным вплоть до тясячной доли секунд (миллисек=1\1000). Когда такая точность не требуется, альтернативой UTC выступает время по Гринвичу GMT, которое в качестве отправной точки использует уже период вращения Земли за 24 часа. Стоит отметить, что UTC играет роль эталонных часов, и к нему не применяются поправки на зимнее\летнее время.
  • UTC: Universal Time Coordinated – всемирный стандарт времени.
  • GMT: Greenwich Mean Time – подразумевает часовой пояс на планете Земля.
  • STD: Standart Time – локальное время в дефолте.
  • DST: Daylight Saving Time – локальное время, с поправкой на летнее.

2. Локальное время – является датой и временем для текущего часового пояса, которое мы видим в правом\нижнем углу на панели задач Windows. GetLocalTime() под капотом берёт значение у GetSystemTime(), и преобразует его в местное время на основе настроек часового пояса системы. Поправки можно запросить функцией GetTimeZoneInformation() – на входе она ожидает одноимённую структуру TIME_ZONE_INFORMATION. Для установки лок.времени предусмотрена SetLocalTime().

В таблице ниже перечислены основные города мира относительно GMT(0). Поскольку формат придумали в университете Гринвича в Англии, то конечно они выбрали для себя точку отсчёта нуль. Но если учесть, что Земля круглая, а на карте мира часовые пояса разделены вертикальными блоками, то GMT(0) мог быть назначен кому угодно:

GMT_0.webp

C-подобный:
  invoke  GetSystemTime, out_lpSYSTEMTIME
  invoke  GetLocalTime,  out_lpSYSTEMTIME

struct SYSTEMTIME
  wYear          dw  0
  wMonth         dw  0
  wDayOfWeek     dw  0
  wDay           dw  0
  wHour          dw  0
  wMinute        dw  0
  wSecond        dw  0
  wMilliseconds  dw  0
ends

;//---------------------------

  invoke  GetTimeZoneInformation, out_TIME_ZONE_INFORMATION

struct TIME_ZONE_INFORMATION
  Bias           dd  0          ;// значение поправки в минутах (UTC = LocalTime + Bias)
  StandardName   dw  32 dup(0)  ;// Unicode-имя текущего часового пояса
  StandardDate   SYSTEMTIME     ;// дата перехода с летнего на зимнее время
  StandardBias   dd  0          ;// 0
  DaylightName   dw  32 dup(0)  ;// Unicode-имя летнего времени
  DaylightDate   SYSTEMTIME     ;// дата перехода с зимнего на летнее время
  DaylightBias   dd  0          ;// 0
ends

Пример вызова функций для чтения системного и локального времени может выглядеть так:

C-подобный:
;//----- Прочитаем системное и локальное время ----------------
         invoke  GetSystemTime,sTime
         invoke  GetLocalTime, lTime

        cinvoke  printf,<10,'     System time: ',0>
         mov     rsi,sTime
         call    PrintDateTime

        cinvoke  printf,<10,'      Local time: ',0>
         mov     rsi,lTime
         call    PrintDateTime

;//----- Инфа о часовом поясе ---------------------------------
         invoke  GetTimeZoneInformation,tzInfo
         mov     eax,[tzInfo.Bias]
         or      eax,eax
         jns     @f
         mov     byte[tZone+24],'+'
         neg     eax
@@:      mov     ecx,60
         xor     edx,edx
         div     ecx
         push    rax
         lea     rbx,[tzInfo.DaylightName]
         invoke  CharToOemW,rbx,buff
         pop     rax
        cinvoke  printf,tZone,rax,buff

time1.webp


3. Время Windows – это количество миллисекунд, прошедших с момента последнего запуска системы. По сути представляет собой время работы самой ОС Windows, а потому никак не связано с датой\временем UTC. На 32-битных системах, функция GetTickCount() возвращала миллисекунды в регистр EAX, и соответственно макс.значение не могло превышать 0xFFFFFFFF= 4.294.967.295 тиков таймера. Тогда простой арифметикой можно вычислить и примерное время работы ОС, после которого счётчик переполнялся:

4294967295 /1000 = 4294967 сек /60 = 71582 минут /60 = 1193 часов /24 = ~49 дней.

Но с приходом архитектуры х64, разрядность EAX увеличилась вдвое, и функция GetTickCount64() возвращает теперь в RAX счётчик с астрономическим значением – его хватит на 585 миллионов лет безотказной работы системы:

18446744073709551615 /1000 = 18446744073709551 сек /60 = 307445734561825 мин
307445734561825 мин /60 = 5124095576030 часов /24 = 213503982334 дней /365 = 584.942.417 лет.

Функции GetTickCount() считают по тикам системного таймера, разрешение которого составляет ~16 милли\сек.
На выходе получаем общее время работы ОС, включая спящий режим и гибернацию.


4. Время прерывания – аналогично времени Windows, только выхлоп не в миллисекундах, а в 100 наносекундных блоках (1 наносек = 0.000000001 сек). Чтобы получить время в секундах, нужно будет результат разделить на 10 млн. Отсчет начинается с нуля при запуске ОС, и увеличивается при каждом прерывании часов, на длину их тактов. Точная величина 1 такта зависит от текущих настроек в BIOS аппаратного оборудования (см.ACPI-таймер), а потому может различаться на разных системах.

Для чтения тиков прерываний предусмотрены функции QueryInterruptTime() и QueryUnbiasedInterruptTime(). Несмещённая «Unbiased» версия возвращает время, когда система находилась именно в рабочем состоянии, без учёта спящего режима и гибернации. В Win10 подвезли ещё 2 варианта этих функций с суффиксами «Precise», для более точных измерений интервалов: QueryInterruptTimePrecise(), и QueryUnbiasedInterruptTimePrecise(). Отличаются они тем, что если первые читают данные из уже готовой структуры системы KUSER_SHARED_DATA (отображается в пространство юзера), то расширенные версии напрямую из портов физ.оборудования.

C-подобный:
;//----- Время работы Windows по тикам ------------------------
         invoke  GetTickCount64
         push    rax
         mov     rbx,1000
         xor     rdx,rdx
         div     rbx        ;// в секундах
         mov     rbx,60
         xor     rdx,rdx
         div     rbx        ;// в минутах
         xor     rdx,rdx
         div     rbx        ;// в часах
         mov     r10,rdx
         pop     rbx
@@:     cinvoke  printf,<10,\
                         10,'     TickCount64:  %u',\
                         10,'    Windows time:  %d hour, %02d minutes',10,0>,rbx,rax,r10

;//----- Время работы Windows по прерываниям ------------------
         invoke  QueryUnbiasedInterruptTime,intTime
         mov     rax,[intTime]
         mov     ebx,10000000
         xor     edx,edx
         div     rbx
         mov     ebx,60
         xor     edx,edx
         div     rbx
         xor     edx,edx
         div     rbx
         xchg    rbx,rdx
        cinvoke  printf,<10,' Interrupt ticks:  %I64d',\
                         10,'   Unibased time:  %d hour, %02d minutes',0>,[intTime],rax,rbx

time2.webp



5. Время файла – это 64-битное значение, представляющее количество 100 нано\сек интервалов, прошедших с 01.01.1601 года по Григорианскому календарю. Данный тип времени лежит в компетенции файловой системы, которая обновляет его в момент создания, записи и попытки доступа к файлам. NTFS хранит время в формате UTC без поправок на часовой пояс, в то время как FAT32 использует локальное время компьютера. Все функции работы с файлами возвращают его в 8-байтную структуру FILETIME.

Специально предназначенная для этих целей GetFileTime() извлекает время файла по открытому дескриптору сразу в три структуры FILETIME, отдельно для времени создания, записи и доступа (см.аргументы out_xx ниже). Но это не единственная API в своём роде. Например FindFirst\NextFile() в свою структуру WIN32_FIND_DATA возвращает намного больше информации о файле, в том числе и три указанных варианта файлового времени. Если мы хотим вывести его на консоль или в форточку гуя, нужно функцией FileTimeToSystemTime() преобразовать его сначала в системное. В обратную сторону работает GetSystemTimeAsFileTime(), которая на автомате считывает текущее системное время, и переводит его в формат файлового.

C-подобный:
  invoke  GetFileTime, in_hFile, out_lpCreate, out_lpAccess, out_lpWrite

struct FILETIME
  dwLowDateTime       dd  0
  dwHighDateTime      dd  0
ends

;//---------------------------------------
  invoke  FindFirstFile,<'*.exe',0>, WIN32_FIND_DATA

struct WIN32_FIND_DATA
  dwFileAttributes    dd  0
  ftCreationTime      FILETIME
  ftLastAccessTime    FILETIME
  ftLastWriteTime     FILETIME
  nFileSize           dq  0
  dwReserved          dq  0
  cFileName           db  256 dup(0)
  cAlternateFileName  db  14  dup(0)
ends

invoke  FileTimeToSystemTime, in_lpFileTime, out_lpSystemTime
invoke  GetSystemTimeAsFileTime, out_lpFILETIME

time3.webp



6. Дата и время MS-DOS – является упакованным 32-битным значением (4 байта). В старших 16-бит кодируется дата, а в младших – время. Как и предыдущем случае, контроль над этих типом времени лежит на плечах файловой системы – FAT обновляет его в момент, когда файл создаётся или модифицируется. Приложения MS-DOS извлекают эту дату и время с помощью функции AH=2Ah прерывания int-21h. Что касается защищённого режима Win, то при работе с файлами DOS, функция GetFileTime() автоматически преобразует дату и время MS-DOS в формат времени UTC. Более того, в клинических случаях можно позвать на помощь и функцию DosDateTimeToFileTime(), которая сбросит время эпохи неолита в структуру FILETIME. Для обратного преобразования имеется так-же и FileTimeToDosDateTime(). В общем здесь есть из чего выбирать.


7. Дата и время UNIX – в документации и различного рода утилитах по сбору информации, это время известно как TimeDateStamp. Очень интересный персонаж, а потому уделим ему чуть больше внимания, не в пример программерам из Microsoft, которые решили не заворачить проблему в Win32API. Как результат, все манипуляции с данным типом времени приходится реализовать вручную.

Дело в том, что формат используется в заголовке РЕ-файлов, где для него выделено специальное поле. Как видно из скрина ниже, время создания файла кодируется 32-битным значением DWORD, а поскольку точкой отчёта является 01.01.1970 года (т.н. эпоха Unix), то переполнение поля получим уже в начале 2038 года.

time4.webp

Время внутри заголовка РЕ-файла никак не связано с файловым временем NTFS – его прописывает компилятор при сборке EXE\DLL\SYS. Например, исполняемый файл может быть реально создан в 2000 году, а скопирован поверх файловой системы диска намного позже в 2023-м. Так-вот функция GetFileTime() вернёт дату\время именно как 2023 год.

time5.webp

Более того, отметку о времени создания компилятор прописывает не только в заголовок PE-файла. Отдельное поле TimeDataStamp имеется ещё и в заголовках буквально всех остальные секций экзешника, например: экспорта, в импортах IAT\Delay\Bound, ресурсах, отладки, безопасности, и прочих. Такой зоопарк позволяет находить заражённые файлы на диске, поскольку вышедшая из под пера студентов малварь не обращает внимания на многочисленные поля с отметкой времени внутри бинаря. В идеале, в заголовках всех секций время должно совпадать, в противном случае исполняемый файл должен вызывать подозрение.

Выше упоминалось, что в штатной поставке Win нет библиотечных функции для преобразования Unix-времени в системное UTC. Однако имеется специальная формула, по которой можно вручную получить желаемый результат в структуру FILETIME. Далее остаётся вызвать FileTimeToSystemTime(), и выводить готовое время на консоль\окошки. Сама формула имеет такой вид:
FileTime = (UnixTime * 10000000) + 116444736000000000

time7.webp



8. Послесловие

Время играет огромную роль в системе, начиная от примитивной организации задержек, и заканчивая производительности ОС в целом. При желании можно замедлить или ускорить ход часов, что повлечёт за собой глобальные последствия. Поэтому нужно аккуратней относиться к функциями API с префиксами Set_xx(). Кстати многие сетевые адаптеры NIC могут генерить отметки TimeDataStamp не отходя от кассы прямо в своём оборудовании, читая тики аппаратных часов сетевого контролёра (не обращаясь к системным часам на материнской плате). NIC использует такие штампы например для вычисления времени, на которое пакет застрял в сетевом стеке перед его отправкой\получением.

В скрепку положил исходник небольшой программы с вызовом всех перечисленных выше Win32API, и уже скомпилированный пример для тестов. До скорого, пока!
 

Вложения

Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!