Analytics

Windows: достучаться до железа


      Windows: достучаться до железа

Меня всегда интересовало низкоуровневое программирование – общаться напрямую с оборудованием, жонглировать регистрами, детально разбираться как что устроено… Увы, современные операционные системы максимально изолируют железо от пользователя, и просто так в физическую память или регистры устройств что-то записать нельзя. Точнее я так думал, а на самом деле оказалось, что чуть ли не каждый производитель железа так делает!

В чём суть, капитан?

В архитектуре x86 есть понятие «колец защиты» («Ring») – режимов работы процессора. Чем ниже номер текущего режима, тем больше возможностей доступно исполняемому коду. Самым ограниченным «кольцом» является «Ring 3», самым привилегированным – «Ring -2» (режим SMM). Исторически сложилось, что все пользовательские программы работают в режиме «Ring 3», а ядро ОС – в «Ring 0»:


      Windows: достучаться до железа

Режимы работы x86 процессора

В «Ring 3» программам запрещены потенциально опасные действия, такие как доступ к I/O портам и физической памяти. По логике разработчиков, настолько низкоуровневый доступ обычным программам не нужен. Доступ к этим возможностям имеют только операционная система и её компоненты (службы и драйверы). И всё бы ничего, но однажды я наткнулся на программу RW Everything:


      Windows: достучаться до железа

RW Everything действительно читает и пишет практически всё

Эта программа была буквально напичкана именно теми функциями, которые обычно запрещаются программам «Ring 3» — полный доступ к физической памяти, I/O портам, конфигурационному пространству PCI (и многое другое). Естественно, мне стало интересно, как это работает. И выяснилось, что RW Everything устанавливает в систему прокси-драйвер:


      Windows: достучаться до железа

Смотрим последний установленный драйвер через OSR Driver Loader

Прокси-драйвера

В итоге получается обходной манёвр – всё, что программе запрещено делать, разработчик вынес в драйвер, программа устанавливает драйвер в систему и уже через него программа делает, что хочет! Более того – выяснилось, что RW Everything далеко не единственная программа, которая так делает. Таких программ не просто много, они буквально повсюду. У меня возникло ощущение, что каждый уважающий себя производитель железа имеет подобный драйвер:

  • Софт для обновления BIOS (Asrock, Gigabyte, HP, Dell, AMI, Intel, Insyde…)

  • Софт для разгона и конфигурации железа (AMD, Intel, ASUS, ASRock, Gigabyte)

  • Софт для просмотра сведений о железе (CPU-Z, GPU-Z, AIDA64)

  • Софт для обновления PCI устройств (Nvidia, Asmedia)

  • Во многих из них практически та же самая модель поведения – драйвер получает команды по типу «считай-ка вот этот физический адрес», а основная логика – в пользовательском софте. Ниже в табличке я собрал некоторые прокси-драйвера и их возможности:

    
      Windows: достучаться до железа

    Результаты краткого анализа пары десятков драйверов. Могут быть ошибки!

    Небольшая легенда:

  • Mem – чтение / запись физической памяти

  • PCI – чтение / запись PCI Configuration Space

  • I/O – чтение / запись портов I/O

  • Alloc – аллокация и освобождение физической памяти

  • Map – прямая трансляция физического адреса в вирутальный

  • MSR – чтение / запись x86 MSR (Model Specific Register)

  • Жёлтым обозначены возможности, которых явно нет, но их можно использовать через другие (чтение или маппинг памяти). Мой фаворит из этого списка – AsrDrv101 от ASRock. Он устроен наиболее просто и обладает просто огромным списком возможностей, включая даже функцию поиска шаблона по физической памяти (!!)

    Неполный перечень возможностей AsrDrv101

  • Чтение / запись RAM

  • Чтение / запись IO

  • Чтение / запись PCI Configuration Space

  • Чтение / запись MSR (Model-Specific Register)

  • Чтение / запись CR (Control Register)

  • Чтение TSC (Time Stamp Counter)

  • Чтение PMC (Performance Monitoring Counter)

  • Чтение CPUID

  • Alloc / Free физической памяти

  • Поиск по физической памяти

  • Самое нехорошее в такой ситуации — если подобный драйвер остаётся запущенным на ПК пользователя, для обращения к нему не нужно даже прав администратора! То есть любая программа с правами пользователя сможет читать и писать физическую память — хоть пароли красть, хоть ядро пропатчить. Именно на это уже ругались другие исследователи. Представьте, что висящая в фоне софтина, красиво моргающая светодиодиками на матплате, открывает доступ ко всей вашей системе. Или вирусы намеренно ставят подобный драйвер, чтобы закрепиться в системе. Впрочем, любой мощный инструмент можно в нехороших целях использовать.

    Через Python в дебри

    Конечно же я захотел сделать свой небольшой «тулкит» для различных исследований и экспериментов на базе такого драйвера. Причём на Python, мне уж очень нравится, как просто выглядит реализация сложных вещей на этом языке.

    Первым делом нужно установить драйвер в систему и запустить его. Делаем «как положено» и сначала кладём драйвер (нужной разрядности!) в System32:

    #puts the driver into Windows/System32/drivers folder
    def SaveDriverFile(self):
      winPath = os.environ['WINDIR']
      sys32Path = os.path.join(winPath, "System32")
      targetPath = os.path.join(sys32Path, "drivers\" + self.name + ".sys")
      file_data = open(self.file_path, "rb").read()
      open(targetPath, "wb").write(file_data)

    Раньше в похожих ситуациях я извращался с папкой %WINDIR%Sysnative, но почему-то на моей текущей системе такого алиаса не оказалось, хотя Python 32-битный. (по идее, на 64-битных системах обращения 32-битных программ к папке System32 перенаправляются в папку SysWOW64, и чтобы положить файлик именно в System32, нужно обращаться по имени Sysnative).

    Затем регистрируем драйвер в системе и запускаем его:

    #registers the driver for further startup
    def RegisterDriver(self):
      serviceManager = win32service.OpenSCManager(None, None, 
                                                  win32service.SC_MANAGER_ALL_ACCESS)
      driverPath = os.path.join(os.environ['WINDIR'], 'system32\drivers\' + 
                                self.name + '.sys')
      serviceHandle = win32service.CreateService(serviceManager,self.name,self.name,
                                                 win32service.SERVICE_ALL_ACCESS, 
                                                 win32service.SERVICE_KERNEL_DRIVER, 
                                                 win32service.SERVICE_DEMAND_START, 
                                                 win32service.SERVICE_ERROR_NORMAL,
                                                 driverPath, None,0,None,None,None)
      win32service.CloseServiceHandle(serviceManager)
      win32service.CloseServiceHandle(serviceHandle)
    
    #starts the driver
    def RunDriver(self):
      win32serviceutil.StartService(self.name)

    А дальше запущенный драйвер создаёт виртуальный файл (кстати, та самая колонка «имя» в таблице с анализом дров), через запросы к которому и осуществляются дальнейшие действия:

    
      Windows: достучаться до железа

    И ещё одна полезная программа для ползания по системе, WinObj

    Тоже ничего особенного, открываем файл и делаем ему IoCtl:

    #tries to open the driver by name
    def OpenDriver(self):
        handle = win32file.CreateFile("\.\" + self.name, 
                                      win32file.FILE_SHARE_READ | 
                                      win32file.FILE_SHARE_WRITE, 
                                      0, None, win32file.OPEN_EXISTING, 
                                      win32file.FILE_ATTRIBUTE_NORMAL | 
                                      win32file.FILE_FLAG_OVERLAPPED, 
                                      None)
        if handle == win32file.INVALID_HANDLE_VALUE:
              return None
        return handle
    
    #performs IOCTL!
    def IoCtl(self, ioctlCode, inData, outLen=0x1100):
        out_buf = win32file.DeviceIoControl(self.dh,ioctlCode,inData,outLen,None)
        return out_buf

    Вот здесь чутка подробнее. Я долго думал, как же обеспечить адекватную обработку ситуации, когда таких «скриптов» запущено несколько. Не останавливать драйвер при выходе нехорошо, в идеале нужно смотреть, не использует ли драйвер кто-то ещё и останавливать его только если наш скрипт «последний». Долгие упорные попытки получить количество открытых ссылок на виртуальный файл драйвера ни к чему не привели (я получал только количество ссылок в рамках своего процесса). Причём сама система точно умеет это делать — при остановке драйвера с открытым файлом, он остаётся висеть в «Pending Stop». Если у кого есть идеи — буду благодарен.

    В конечном итоге я «подсмотрел», как это делают другие программы. Выяснилось, что большинство либо не заморачиваются, либо просто ищут запущенные процессы с тем же именем. Но одна из исследованных программ имела кардинально другой подход, который я себе и перенял. Вместо того, чтобы переживать по количеству ссылок на файл, просто на каждый запрос открываем и закрываем файл! А если файла нет, значит кто-то остановил драйвер и пытаемся его перезапустить:

    #perform IOCTL!
    def IoCtl(self, ioctlCode, inData, outLen=0x1100):
      #open driver file link
      driverHandle = self.OpenDriver()
      if driverHandle is None:
        self.ReinstallDriver()
        driverHandle = self.OpenDriver()
        #second try
        if driverHandle is None:
          return None
      #perform IOCTL
      out_buf = win32file.DeviceIoControl(driverHandle,ioctlCode,inData,outLen,None)
      #close driver file link
      win32file.CloseHandle(driverHandle)
      return out_buf

    А дальше просто реверсим драйвер и реализуем все нужные нам вызовы:

    class PmxInterface:
      def __init__(self):
        self.d = PmxDriver("AsrDrv101")
    
        def MemRead(self, address, size, access=U8):
          buf = ctypes.c_buffer(size)
          request = struct.pack("<QIIQ", address, size, access, 
                                ctypes.addressof(buf))
          if self.d.IoCtl(0x222808, request, len(request)):
            return bytearray(buf)
          else:
            return None
    
          def MemWrite(self, address, data, access=U8):
            buf = ctypes.c_buffer(data, len(data))
            request = struct.pack("<QIIQ", address, len(data), access, 
                                  ctypes.addressof(buf))
            return self.d.IoCtl(0x22280C, request, len(request)) is not None
          # (и все остальные тоже)

    И вуаля:

    
      Windows: достучаться до железа

    Легко и непринуждённо в пару команд читаем физическую память

    PCI Express Config Space

    Немного отвлечёмся на один нюанс про PCIE Config Space. С этим адресным пространством не всё так просто — со времён шины PCI для доступа к её конфигурационному пространству используется метод с использованием I/O портов 0xCF8 / 0xCFC. Он применён и в нашем драйвере AsrDrv101:

    
      Windows: достучаться до железа

    Чтение и запись PCI Config Space

    Но через этот метод доступны только 0x100 байт конфигурационного пространства, в то время как в стандарте PCI Express размер Config Space у устройств может быть достигать 0x1000 байт! И полноценно вычитать их можно только обращением к PCI Extended Config Space, которая замаплена где-то в адресном пространстве, обычно чуть пониже BIOS:

    
      Windows: достучаться до железа

    Адресное пространство современного x86 компа, 0-4 ГБ

    На чипсетах Intel (ну, в их большинстве) указатель на эту область адресного пространства можно взять из конфига PCI устройства 0:0:0 по смещению 0x60, подробнее описано в даташитах:

    
      Windows: достучаться до железа

    У AMD я такого не нашёл (наверняка есть, плохо искал), но сам факт неуниверсальности пнул меня в сторону поиска другого решения. Погуглив стандарты, я обнаружил, что указатель на эту область передаётся системе через ACPI таблицу MCFG

    А сами ACPI таблицы можно найти через запись RSDP, поискав её сигнатуру по адресам 0xE0000-0xFFFFF, а затем распарсив табличку RSDT. Отлично, здесь нам и пригодится функционал поиска по памяти. Получаем нечто такое:

    rsdp = self.PhysSearch(0xE0000, 0x20000, b"RSD PTR ", step=0x10)
    #use rsdt only for simplicity
    rsdt = self.MemRead32(rsdp + 0x10)
    (rsdtSign, rsdtLen) = struct.unpack("<II", self.MemRead(rsdt, 8, U32))
    if rsdtSign == 0x54445352: #RSDT
      headerSize = 0x24
      rsdtData = self.MemRead(rsdt + headerSize, rsdtLen - headerSize, U32)
      #iterate through all ACPI tables
      for i in range(len(rsdtData) // 4):
        pa = struct.unpack("<I", rsdtData[i*4:(i+1)*4])[0]
        table = self.MemRead(pa, 0x40, U32)
        if table[0:4] == b"MCFG":
          #we have found the right table, parse it
          (self.pciMmAddress, pciSeg, botBus, self.pciMmTopBus) = 
          	struct.unpack("<QHBB", table[0x2C:0x38])

    На всякий случай оставляем вариант для чипсетов Intel

    if self.PciRead16(PciAddress(0,0,0,0)) == 0x8086:
      #try intel way
      pciexbar = self.PciRead64(PciAddress(0,0,0,0x60))
      if pciexbar & 1:
        self.pciMmTopBus = (1 << (8 - ((pciexbar >> 1) & 3))) - 1
        self.pciMmAddress = pciexbar & 0xFFFF0000

    Всё, теперь осталось при необходимости заменить чтение PCI Express Config Space через драйвер на чтение через память. Теперь-то разгуляемся!

    Читаем BIOS

    В качестве примера применения нашего «тулкита», попробуем набросать скрипт чтения BIOS. Он должен быть «замаплен» где-то в конце 32-битного адресного пространства, потому что компьютер начинает его исполнение с адреса 0xFFFFFFF0. Обычно в ПК стоит флеш-память объёмом 4-16 МБ, поэтому будем «сканировать» адресное пространство с адреса 0xFF000000, как только найдём что-нибудь непустое, будем считать, что тут начался BIOS:

    from PyPmx import PmxInterface
    pmx = PmxInterface()
    
    for i in range(0xFF000000, 0x100000000, 0x10000):
      data = pmx.MemRead(i, 0x20)
      if data != b"xFF"*0x20 and data != b"x00"*0x20:
        biosLen = 0x100000000-i
        print("BIOS Found at 0x%x" % i)
        f = open("dump.bin", "wb")
        for j in range(0, biosLen, 0x1000):
          data = pmx.MemRead(i + j, 0x1000)
          f.write(data)
          break

    В результате получаем:

    
      Windows: достучаться до железа

    Вот так в 10 строчек мы считали BIOS

    Но подождите-ка, получилось всего 6 мегабайт, а должно быть 4 или 8 что-то не сходится. А вот так, у чипсетов Intel в адресное пространство мапится не вся флешка BIOS, а только один её регион. И чтобы считать всё остальное, нужно уже использовать SPI интерфейс.

    Не беда, лезем в даташит, выясняем, что SPI интерфейс висит на PCI Express:

    
      Windows: достучаться до железа

    И для его использования, нужно взаимодействовать с регистрами в BAR0 MMIO по алгоритму:

    1. Задать адрес для чтения в BIOS_FADDR

    2. Задать параметры команды в BIOS_HSFTS_CTL

    3. Прочитать данные из BIOS_FDATA

    Пилим новый скрипт для чтения через чипсет:

    from PyPmx import PmxInterface, PciAddress, U32
    
    spi = PciAddress(0, 31, 5)
    pmx = PmxInterface()
    spiMmio = pmx.PciRead32(spi + 0x10) & 0xFFFFF000
    f = open("dump.bin", "wb")
    
    for i in range(0, 0x800000, 0x40):
      # write BIOS_FADDR
      pmx.MemWrite32(spiMmio + 0x08, i)
      # write BIOS_HSFTS_CTL
      #        read      0x40 bytes      start     clear fcerr & fgo
      cmd = (0 << 17) | (0x3F << 24) | (1 << 16) |         3
      pmx.MemWrite32(spiMmio + 0x04, cmd)
      # wait for read or error
      curCmd = pmx.MemRead32(spiMmio + 0x04)
      while curCmd & 3 == 0:
        curCmd = pmx.MemRead32(spiMmio + 0x04)
      # read BIOS_FDATA
      data = pmx.MemRead(spiMmio + 0x10, 0x40, U32)
      f.write(data)

    Исполняем и вуаля — в 20 строчек кода считаны все 8 МБ флешки BIOS! (нюанс — в зависимости от настроек, регион ME может быть недоступен для чтения).

    Точно так же можно делать всё, что заблагорассудится — делать снифер USB пакетов, посылать произвольные ATA команды диску, повышать частоту процессора и переключать видеокарты. И это всё — с обычными правами администратора:

    
      Windows: достучаться до железа

    Немного помучившись, получаем ответ от SSD на команду идентификации

    А если написать свой драйвер?

    Некоторые из вас наверняка уже подумали — зачем так изворачиваться, реверсить чужие драйвера, если можно написать свой? И я о таком думал. Более того, есть Open-Source проект chipsec, в котором подобный драйвер уже разработан.

    Зайдя на страницу с кодом драйвера, вы сразу наткнетесь на предупреждение:

    WARNING

    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    !!
    !! Chipsec should only be run on test systems! 
    !! It should not be installed/deployed on end-user systems!
    !! 
    !! There are multiple reasons for that:
    !! 
    !! 1. Chipsec kernel drivers provide raw access to HW resources to 
    !! user-mode applications (like access to physical memory). This would 
    !! allow malware to compromise the OS kernel.
    !! 2. The driver is distributed as a source code. In order to load it
    !! on OS which requires signed drivers (e.g. x64 Microsoft Windows 7 
    !! and higher), you'll need to enable TestSigning mode and self-sign 
    !! the driver binary. Enabling TestSigning (or equivalent) mode also 
    !! turns off important protection of OS kernel.
    !!
    !! 3. Due to the nature of access to HW resources, if any chipsec module 
    !! issues incorrect access to these HW resources, OS can crash/hang.
    !!
    !! If, for any reason, you want to production sign chipsec driver and 
    !! deploy chipsec on end-user systems,
    !! DON'T!
    !!
    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

    В этом предупреждении как раз и описываются все опасности, о которых я рассказывал в начале статьи — инструмент мощный и опасный, следует использовать только в Windows режиме Test Mode, и ни в коем случае не подписывать. Да, без специальной подписи на обычной системе просто так запустить драйвер не получится. Поэтому в примере выше мы и использовали заранее подписанный драйвер от ASRock.

    Если кто сильно захочет подписать собственный драйвер — понадобится регистрировать собственную компанию и платить Microsoft. Насколько я нагуглил, физическим лицам такое развлечение недоступно.

    Точнее я так думал, до вот этой статьи, глаз зацепился за крайне интересный абзац:

    У меня под рукой нет Windows DDK, так что я взял 64-битный vfd.sys, скомпилированный неким critical0, и попросил dartraiden подписать его «древне-китайским способом». Такой драйвер успешно загружается и работает, если vfdwin запущена с правами администратора

    Драйвер из статьи действительно подписан, и действительно неким китайским ключом:

    
      Windows: достучаться до железа

    Как оказалось, сведения о подписи можно просто посмотреть в свойствах.. А я в HEX изучал

    Немного поиска этого имени в гугле, и я натыкаюсь на вот эту ссылку, откуда узнаю, что:

  • есть давно утёкшие и отозванные ключи для подписи драйверов

  • если ими подписать драйвер — он прекрасно принимается системой

  • малварщики по всему миру используют это для создания вирусни

  • Основная загвоздка — заставить майкрософтский SignTool подписать драйвер истёкшим ключом, но для этого даже нашёлся проект на GitHub. Более того, я нашёл даже проект на GitHub для другой утилиты подписи драйверов от TrustAsia, с помощью которого можно подставить для подписи вообще любую дату.

    Несколько минут мучений с гугл-переводчиком на телефоне, и мне удалось разобраться в этой утилите и подписать драйвер одним из утекших ключей (который довольно легко отыскался в китайском поисковике):

    
      Windows: достучаться до железа

    И в самом деле, китайская азбука

    И точно так же, как и AsrDrv101, драйвер удалось без проблем запустить!

    
      Windows: достучаться до железа

    А вот и наш драйвер запустился

    Из чего делаю вывод, что старая идея с написанием своего драйвера вполне себе годная. Как раз не хватает функции маппинга памяти. Но да ладно, оставлю как TODO.

    Выводы?

    Как видите, имея права администратора, можно делать с компьютером практически что угодно. Будьте внимательны — установка утилит от производителя вашего железа может обернуться дырой в системе. Ну а желающие поэкспериментировать со своим ПК — добро пожаловать на низкий уровень! Наработки выложил на GitHub. Осторожно, бездумное использование чревато BSODами.

    Related posts

    Строим твёрдотельный лазер без регистрации и смс

    admin

    Как и ожидалось, в полночь Adobe Flash превратился в тыкву

    admin

    Аспирантка решила топологическую задачу полувековой давности

    admin

    Leave a Comment