Maxim Menshikov

Static analysis reseacher and startup founder


Как это работает: Full Unlock (WP7) How it works, Retrospects, По-русски

Интересовались ли вы когда-нибудь, как работает Full Unlock (“Полная разблокировка”) для Windows Phone 7? Меня много раз спрашивали об этом, но, кажется, время рассказать это пришло только сейчас. Статья рассчитана не на программистов/хакеров и так далее. Думаю, суть сможет понять любой более-менее подготовленный пользователь ПК. К сожалению, код Full Unlock не выложен и выложен не будет. Так что пройдёмся по коду в “полуслепом” режиме. Сразу оговорюсь, что в русском переводе используется нестандартная терминология, призванная облегчить понимание. Full Unlock состоит из нескольких частей.

  1. Cloaking Filter (clkflt) - скрывающий фильтр файловой системы.
  2. Loader Verifier (ulv) - проверяльщик загружаемых модулей.
  3. Policy Engine (upl) - модуль, обеспечивающий проверку соответствия разрешений, данных приложению, требованиям запрашиваемых им функциям.
  4. Policy Engine Helper (uplhlp) - вспомогательный модуль, который всего-то информирует пользователя о приложениях, которых “обломал” Policy Engine.
  5. Account Manager (accman) - исполняемый файл, запускающийся единственный раз - при первом запуске системы - и дающий права доступа определенным программам.
  6. Root Manager (“Права доступа”, “Менеджер прав доступа”, accountmanager) - известное приложение, которое позволяет пользователю выдавать права доступа нужным ему приложениям. Поговорим о каждой части по отдельности. Но сначала поговорим о требованиях к анлоку.

Целостность системных файлов

Это самое важное, и это нужно, чтобы системе было непросто заметить “навороты” нашего анлока. ## Обновляемость

Второе по важности. В Windows Phone для обновления системы используются специальные архивы в формате “.CAB”. В них содержатся так называемые “DIFF-файлы”, суть которых в том, что они содержат информацию об изменениях файла X между версиями %n% и %n+1%. Замечу: именно поэтому системные файлы не должны изменяться анлоком!

Производительность

Проверки политик безопасности проводятся каждую секунду и даже чаще. Поэтому если анлок становится узким местом всей системы, то в этом, безусловно, нет ничего хорошего..

Стабильность

Во многом пересекается с предыдущим пунктом. Если какой-нибудь системный модуль “падает” в режиме ядра, то это приведет к перезагрузке всей системы. А анлок всё-таки работает в режиме ядра. (Перезагрузку можно отключить, но это плохая идея)

Как выглядит штатный порядок работы системы безопасности?

  1. Система загружает Loader Verifier (“проверяльщик файлов”) и Policy Engine (“проверяльщик разрешений”).

  2. Система использует Loader Verifier для проверки, имеет ли выбранный(ая) EXE/DLL право загружаться.

  3. Когда DLL или EXE вызывают определенные системные функции, система делает запрос Policy Engine’у “а может ли программа вызывать данный метод?” В общем, достаточно всё просто. Тут вполне можно привести следующую аналогию из жизни: Loader Verifier - пограничник. Вместо паспортов - цифровые подписи файлов. Policy Engine - полиция. Вместо законодательной базы - база разрешений.

Интересные моменты из оригинального порядка работы:

  1. Целостность Loader Verifier’а не гарантирована, так как он представлен в виде “модуля”, а не файла. Модули в Windows Mobile и Windows Phone не содержат цифровых подписей. Однако система предполагает, что память, в которой находится LV, не является перезаписываемой. Так и есть, в общем-то.

  2. Недостаточно подкорректировать поведение одного лишь LV.

  3. Policy Engine представлен в виде обычной библиотеки с цифровой подписью. Поэтому LV проверяет его целостность по полной программе.

  4. Что будет, если мы заменим Loader Verifier? Ведь мы тогда сможем загрузить” злой” Policy Engine. В этом и идея.

Вот примерно так выглядит модифицированная цепочка

-1. Система загружает драйвера файловой системы. И в этот момент наш Cloaking Filter (“скрывающий фильтр”) вступает в игру! Он перехватывает все вызовы системных функций, связанных с файловой системой, и подгружается не те библиотеки, которые запрашивает система, а наши собственные. Но потом сообщает системе, что загружает правильные исполняемые файлы.

  1. Система загружает оригинальные Loader Verifier и Policy Engine, но на деле вызываются наши.

  2. Теперь все DLL и EXE загружаются без каких-либо вопросов.

  3. Модифицированный Policy Engine использует не только базу правил от Microsoft, но и нашу собственную, которая основывается на пользовательских предпочтениях, заданных в программе “Права доступа” А теперь подробнее о каждом компоненте.

Скрывающий фильтр CLKFLT.dll

В моём коде он до сих пор назван “FsPerf” - от слов “File System Performance” (“Производительность файловой системы”) - так как он изначально задумывался для улучшения производительности файловой системы в Windows Mobile 6.x. Во время перехода на Windows Phone 7 он “слегка” видоизменился и потерял изначальную функцию. Фильтр основывается на публичном примере фильтра файловой системы от Microsoft. Все отличия в функциях CreateFile (“Создание/открытие файла”), GetFileAttributes (“Получение атрибутов файла”) и CloseFile (“Закрытие файла”). Вот, например, что происходит в CreateFile:

HANDLE FILTER_CreateFileW(
  PVOLUME               pvol,
  HANDLE                hProc,
  LPCWSTR               lpFileName,
  DWORD                 dwAccess,
  DWORD                 dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD                 dwCreate,
  DWORD                 dwFlagsAndAttributes,
  HANDLE                hTemplateFile
  )
{
  lpFileName = Cloaking_GetNewPath((wchar_t*)lpFileName);
  EnsureCacheLoaded();
  ...
}

Надеюсь, идея понятна. Мы всего лишь подменяем название файла. Внутри функции Cloaking_GetNewPath - простой список пар %старое название файла%-%новое название файла% Таблица пар записана в реестре. Если вы хоть раз собирали/открывали прошивки Dynamics для Window Phone 7, то вы наверняка видели папку “Pkg_XipUnlock” в разделе “NK” (прим.: Native Kernel - “ядро системы”; XIP - eXecute In Place - область памяти, из которой модули могут исполняться напрямую без подгрузки в ОЗУ). FILTER_Package В пакете немного чего интересного, на самом-то деле. Файл DSM - это всего лишь описание пакета. Папка CLKFLT.dll - наш модуль, разбитый на отдельные секции (секция с кодом, секция с данными, …). RGU - набор записей реестра.

REGEDIT4

\[HKEY_LOCAL_MACHINE\\System\\StorageManager\\IMGFS\\Filters\\clkflt\]
"Dll"="clkflt.dll" "Order"=dword:0

\[HKEY_LOCAL_MACHINE\\Software\\OEM\\Cloaking\]
"\\\windows\\\lvmod.dll"="\\\windows\\\ulv.dll"
"\\\windows\\\mslvmod.dll"="\\\windows\\\ulvmod.dll"

Последние три строчки ведут нас к описанию uLoaderVerifier и прочих вещей.

uLV - ultrashot’s Loader Verifier - “проверяльщик исполняемых файлов”

Loader Verifier проверяет все исполняемые файлы, загружаемые системой. Не буду вдаваться в подробности его работы. Наша версия разрешает всем файлам загружаться. Внимание! Только загружаться, но не делать всё, что душе угодно. Основное здесь:

HRESULT LVModProvisionSecurityForApplication(...)
{
    ...
    HRESULT res = extLVModProvisionSecurityForApplication(szSID,
      szAppFriendlyName, pszCaps, dwCaps, szOnePEFilePath, fNativeApp);

    if (fNativeApp == FALSE)
    {
      if (szSID)
      {
        AddToThirdPartyGroup((LPWSTR)szSID);
      }
    }
    res = S_OK;
    SetLastError(S_OK);
    ...
}

Здесь мы просто пометили все устанавливаемые приложения (кроме системных и магазиновских) как внешние. Если же мы говорим о такой важной функции, как LVModAuthenticateFile - то её главная задача заключается в принципе “Разрешать загружать всех, кроме ненастоящего менеджера прав доступа”. Файлы Менеджера прав доступа защищены кастомным алгоритмом цифровой подписи, и если файл кем-то изменен, то приложение никогда не запустится.

uPL - ultrashot’s Policy Engine

Вот это самый интересный элемент анлока. Policy Engine проверяет, имеет ли определенное приложение право использовать функции, которые он пытается вызывать. По части кода тут всё несколько неинтересно. Проверок действительно много. Замечу: правила, о которых система спрашивает Policy Engine, здесь называются “ветками” (branch), так как они имеют иерархическую структуру и выглядят примерно так: /LOADERVERIFIER/GLOBAL/AUTHORIZATION/UNSIGNEDNATIVEDLL_AUTHZ, что, в общем-то, понятно, если начать переводить. Я провожу две проверки. Одна проверка совершается ДО вызова оригинального Policy Engine:

if (IsRevokeCertsBranch(hIri) == TRUE)
{
  // Запрещаем доступ
  ...
}
else if (IsInThirdPartyGroup(accountName))
{
  __try
  {
    if (IsUnsignedNativeDllBranch(hIri))
    {
      BOOL priv = GetPrivileged(accountName);
      if (priv)
      {
        // OK
        ...
      }
      else
      {
        FULLUNLOCK_POLICY_MESSAGE msg;
        memset(&msg, 0, sizeof(msg));
        msg.type = ACCESS_DENIED;
        wcscpy_s(msg.userAccount, 200, accountName);
        HRESULT hr = IriGetAsString(hIri, 0, 0xFFFFFFFF, 0,
          msg.requestedAccess, 500, NULL);
        if (hr == S_OK)
          PolicyMsgQueue_Write(msg);

        // REJECTED
        ...
      }
      *processed = TRUE;
    }
  }
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
    // Запрещаем доступ при любых неизвестных ошибках
  }
}
else
{
  // OK
}

Как можно видеть из кода выше, сначала код проверяет ветку “отозванных сертификатов”. Система просто имеет список правил для каждого конкретного сертификата. Если ветка для определенного сертификата существует, то он считается отозванным, и система не разрешает его загрузку. Но нам-то это не надо, поэтому мы говорим системе, что такой ветки не существует. Второй важный момент: мы сообщаем Помощнику Policy Engine’а о попытках доступа, чтобы тот мог отобразить это на экране. Проверка в случае принятия отрицательного решения майкрософтовским Policy Engine’ом:

if (hIri)
{
  if (IsWipeBranch(hIri) == TRUE)
  {
    // REJECTED
  }
}
if (IsFullTrustModeEnabled() == TRUE)
{
  // OK
  ...
}

if (wcscmp(accountName, ACCID_ACCOUNTMANAGER) == 0)
{
  // OK
  ...
}

result = GetPrivileged(accountName);
resultReason = 5;
if (hIri)
{
  if (result == TRUE)
  {
    if (IsAdbFunctionBranch(hIri) == TRUE)
    {
      // REJECTED
    }
  }
}

И тут есть важные моменты. Этот фрагмент вызывается абсолютно для всех самодельных (“homebrew”) приложений. Можно заметить, если включен режим “Полного доверия”, то мы действительно разрешаем все, кроме хард-резета. Если приложение загружается от лица Менеджера Прав Доступа, то оно может делать всё, что угодно, кроме опять же хард-резета. (снова замечу, что Менеджер прав доступа проверяется на целостность). Вот что нельзя делать другим приложениям, так это вызывать функции, работающие с базой аккаунтов системы. А теперь попроще.

Помощник Policy Engine - Policy Engine Helper (UPLHLP)

Прост как две копейки. Скриншот этого приложения: Policy Engine А вот и весь код:

int _tmain(int argc, _TCHAR* argv[])
{
  HANDLE hMsgQueue = GetPolicyMsgQueue(TRUE);
  if (hMsgQueue)
  {
    while (true)
    {
      if (WaitForSingleObject(hMsgQueue, INFINITE) == 0)
      {
        DWORD dwBytesRead = 0;
        DWORD dwMsgFlags = 0;
        FULLUNLOCK_POLICY_MESSAGE msg;
        if (ReadMsgQueue(hMsgQueue, &msg,
                         sizeof(FULLUNLOCK_POLICY_MESSAGE), &dwBytesRead,
                         5000, &dwMsgFlags) == TRUE)
        {
          wchar_t productID[100];
          GetProductID(msg.userAccount, productID, 100);
          GUID guid;
          CLSIDFromString(productID, &guid);
          CApplicationInfo *info = NULL;
          GetApplicationInfoByProductID(guid, &info);
          if (info)
          {
            if (info->AppInstallType() == 3)
            {
              MESSAGETOASTDATA data;
              data.appId = info->GetAppID();

              wchar_t *newTitle = GetLocalizedString(info->GetTitle());
              if (newTitle)
              {
                data.pszText1 = newTitle;
                data.pszText2 = GetTranslation(RequiresRootAccess);

                wchar_t uri[2000];
                swprintf(uri,
                         L"app://794eb6ae-b2f9-4bfb-9277-4161e4d9e3f5/"
                         L"_default?promote=%ls&promoteaccess=%ls",
                         productID, msg.requestedAccess);
                data.pszTaskUri = uri;
                data.pszSound = NULL;
                HRESULT hr = SHPostMessageToast(&data);
                delete[] newTitle;
              }
            }
            delete info;
          }
        }
      }
    }
  }
  CloseHandle(hMsgQueue);


  return 0;
}

Говоря простым языком: мы ждём сообщений от Policy Engine, получаем название приложения, которое запросило права доступа, переводим его заголовок (если это требуется) и выводим сообщение на экран. Очень просто.

Менеджер аккаунтов - Account Manager (accman)

Совсем скучная штука. Загружается при первом запуске системы и дает права доступа к приложениям, список которых записан в реестре примерно так:

[HKEY_LOCAL_MACHINE\Software\OEM\accman]
"{9cefc0bf-7060-45b0-ba66-2d1dcad8dc3c}"="" ; это уникальный идентификатор
                                            ; программы "Мелодии звонка"

Менеджер прав доступа - Root Manager (Account Manager)

Самое известное обывателю приложение. Дает возможность изменять список привилегированных приложений. Работает через COM/Interop, а в остальном работает также, как и Менеджер аккаунтов. Интерфейс - на Silverlight. ## Всякие интересные мелочи

Первая версия вообще была написана за два дня. Она состояла всего лишь из Loader Verifier’а и Policy Engine, а количество кода в них стремилось к нулю. Версия, обозреваемая в этом посте, имеет номер 4.0. Во время разработки были следующие проблемы:

  1. Надо убедиться, что компилятор действительно нормальный. Компилятора ARMv4/ARMv5 из Windows Mobile 6 SDK вполне достаточно, хотя в последних версиях анлока я использовал версию из Windows CE 7.0 Platform Builder.
  2. Надо убедиться, что в компиляторе включена опция /LARGEADDRESSAWARE. Всё просто: модули ядра загружаются по адресам 0x80000000-0xFFFFFFFF - и, соответственно, надо, чтобы компилятор это учитывал.

  3. /NXCOMPAT - поддержка DEP (Предотвращение выполнения данных — функция безопасности, которая не позволяет приложению исполнять код из области памяти, помеченной как “только для данных”. -wiki). Windows Phone 7 поддерживает эту функцию в полном объеме, поэтому её необходимо включать и для всех модулей ядра.

  4. Преобразование из файла в модули. Слава богу: в наших модулях не используется так называемые “Z-таблицы релокации”. Но тем не менее, файлы всё равно надо разбивать на секции. С этим прекрасно справляется OSBuilder c параметром “-reversmod %путь к файлу%”.

  5. В манифесте пакета с файлами обязательно нужно сохранять флаг “K”. Этот флаг говорит о том, что это модуль уровня ядра (“Kernel module”). В противном случае модуль не загрузится.

Выводы

В целом, идея анлока может показаться пугающей, хотя реализация на самом-то деле очень простая. Если что-то из приведенных объяснений не до конца понятно - то это вполне нормально, не надо беспокоиться. Я показал лишь самые важные моменты из всех модулей анлока. Надеюсь, экскурс в анлок показался хоть немного интересным.