Продолжаем цикл статей о хакерских разработках. В прошлой статье Крипто Про создание электронной подписи или как я перешел на темную сторону речь шла о создании Windows-службы для работы с Крипто Про и создании электронной подписи. В этой статье я расскажу о возникших проблемах и о том, как они были решены.
Первые неожиданные проблемы
Итак, служба была написана и даже работала, но тут случилось неожиданное – ее начали тестировать! И как часто бывает в процессе тестирования, выясняются особенности, о которых разработчик возможно даже не задумывался. Дело в том, что вся разработка и первоначальные проверки выполнялись в системе Windows 8.1, и на ней все прекрасно работало: подписание сертификатом, подписание токеном. И я даже и не предполагал, что могут возникнуть какие-то проблемы в Windows 7. Однако они возникли. Нет, в целом служба работала и даже подписывала ЭП если выбран для подписания сертификат, однако что-то непонятно происходило при попытке подписания токеном. Служба вела себя так, словно токен просто не был вставлен в компьютер. Начались долгие и мучительные поиски причин и попытки их устранения…
Первоначальное предположение было, что у службы почему-то просто нет доступа до внешних носителей, поэтому я сделал попытку просто прочитать текстовый файл с флешки. Естественно все читалось.
Дальнейшие попытки разобраться привели к написанию простого консольного приложения подписывающего ЭП текстовый файлик, с использованием токена. Все прекрасно подписывалось. В итоге я преобразовал службу в консольное приложение, в котором код отвечающий за подписания был абсолютно идентичным тому, что был в службе, отличался только запуск приложения. И снова все подписывалось. Идеи иссякли, почему один и тот же код, запущенный из консоли работает, а запущенный из службы – нет.
Написал письмо в техподдержку КриптоПро, с описанием проблемы, тех поддержка ответила: “Попробуйте, пожалуйста, перевести КриптоПро CSP в режим службы (Панель управления – КриптоПро CSP – Безопасность – Использовать службу хранения ключей – Перезагрузить компьютер).”. Попробовал, и о чудо, оно заработало! Правда я не понял почему, то есть получилось, что если КрипроПро запущено в режиме службы, то и другая служба с ним нормально работает, а если в режиме приложения то почему-то нет. Такое решение в принципе нас устраивало, однако не очень хотелось включать еще дополнительные настройки при установки КриптоПро и хотелось разобраться, почему все-таки так работает, а с первоначальными настройками нет.
Имперсонация (олицетворение)
Дальнейшая переписка с сотрудниками КриптоПро привела к их следующему ответу: “Если служба запущена не под SYSTEM, то доступа к смарт-картам, согласно документации MS, она не получит ни при каких условиях. Это, правда, немного противоречит тому, что вы наблюдаете в Windows 8.1. Тут два варианта: либо они сняли ограничение, либо вы наблюдаете частичную работоспособность, то есть undefined behavior.”. Да, действительно странно… Напомню, мы запускали службу под определенным пользователем, а не под системной учетной записью (SYSTEM) потому, что нам был необходим доступ к сертификатам пользователя, грубо говоря, мы вызывали функцию Crypto API, говоря: “дай мне мои сертификаты”, код:
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)
естественно у системной учетной записи не было никаких сертификатов, мы получали пустой список и ничего не могли подписать. Поэтому дальнейшая наша переписка с КриптоПро свелась, к вопросу, как же все-таки получить сертификаты нужного нам пользователя, когда процесс запущен от SYSTEM?
Конечно, пришлось объяснять, почему нам необходима служба, почему мы не можем вынести код подписания в приложение. Но в итоге, сотрудниками КриптоПро было предложено использовать “имперсонацию” (или “олицетворение”), вот большая статья по этому поводу https://msdn.microsoft.com/ru-ru/library/ms730088%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396, если коротко, то “импероснация” это такой механизм, встроенный в .NET, позволяющий выполнять код от имени пользователя, который авторизован в системе, при этом процесс может быть запущен от другого пользователя в нашем случае от SYSTEM. Этот подход повсеместно встречается в Windows, например, в сервисе IIS.
Согласно статье, для имеперсонации достаточно повесить на нужный метод атрибут:
[OperationBehavior(Impersonation = ImpersonationOption.Required)]
Что я и сделал и благополучно получил ошибку при старте службы:
A Windows identity that represents the caller is not provided by binding (‘BasicHttpBinding’)
Интерфейса взаимодействия BasicHttpBinding (который использовался по умолчанию) явно не хватало для имперсонации, поэтому пришлось переделать его на WSHttpBinding.
После этого служба запустилась, код: WindowsIdentity.GetCurrent().Name возвращал имя текущего авторизованного пользователя. То есть имперсонация заработала, но вот метод, отвечающий за непосредственно подписание теперь возвращал ошибку:
unable to load DLL 0x80070542
По каким-то причинам теперь не загружалась моя библиотека, написанная на C++, при этом, если запускать метод без имперсонаций, библиотека подгружалась, но при подписании валилось, т.к. внутри C++ кода еще раз открывается хранилище сертификатов пользователя. Временно решил эту проблему добавив пустой метод в С++ код, который вызвал в коде перед стартом службы, в итоге нужна dll стала загружаться.
Однако теперь при открытии хранишища сертификатов из C++ кода:
CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, CERT_SYSTEM_STORE_CURRENT_USER, _TEXT("MY"));
Я получал ошибку 1346. Точно такую же ошибку получал и при вызове метода получения сертификатов из C# кода:
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
Что-то явно было не так с этой имперсонацией. Перечитав еще раз, внимательно статью, приведенную выше, я понял что, в интерфейсе взаимодействия со службой не хватает
блока <behaviors>, добавил его и все заработало! И получение сертификатов для пользователя, и самое главное подписание с использованием токена. При этом WSHttpBinding оказался не обязательным, и я вернул на BasicHttpBinding, итоговый конфиг для WPF:
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IDigitalSignatureService">
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Windows" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="ImpersonationBehavior">
<clientCredentials>
<windows allowedImpersonationLevel="Impersonation" allowNtlm="true"/>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint address="http://localhost:8523/DigitalSignatureService"
behaviorConfiguration="ImpersonationBehavior" binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_IDigitalSignatureService"
contract="DigitalSignatureService.IDigitalSignatureService"
name="BasicHttpBinding_IDigitalSignatureService">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
</client>
</system.serviceModel>
Теперь служба работала нормально и на Windows 7 и на Windows 8.1 с WPF приложением.
Теперь это решение нужно было перенести на Windows Store приложение.
Перенос решения на Windows Store
Радость моя оказалась недолгой. Настройки интерфейса для взаимодействия со службой в WinRT задаются в коде (а не в xml-файле как в WPF), который автоматически генерируется при добавлении Servece Referense, поэтому нужно добавлять свой partioal класс, в котором можно расширить базовые настройки. В нем я написал следующее:
static partial void ConfigureEndpoint(System.ServiceModel.Description.ServiceEndpoint serviceEndpoint,
System.ServiceModel.Description.ClientCredentials clientCredentials)
{
clientCredentials.Windows.ClientCredential = new NetworkCredential("VShakhlin", "******", "CROC");
clientCredentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation;
}
Имперсонация включилась, запрос сертификатов для пользователя через службу работал нормально, возвращал список. Но при подписании опять начались проблемы. Если в настройках закомментирована первая строчка, в которой передавался конкретный пользователь, с логином и паролем, то на вызове функции из C++ кода:
CryptAcquireCertificatePrivateKey(context, CRYPT_ACQUIRE_SILENT_FLAG, 0, &hProv, &dwKeySpec, &mustFree)
Получал ошибку с кодом 2148073498. При этом значение поля ServiceSecurityContext.Current.WindowsIdentity.IsAuthenticated – false
Если же были указаны обе строчки (как в коде, приведенном выше), то получаю ошибку с кодом 5 на функции открытия хранилища:
CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, CERT_SYSTEM_STORE_CURRENT_USER, _TEXT("MY"));
В этом случае значение поля ServiceSecurityContext.Current.WindowsIdentity.IsAuthenticated – true (остальные значения WindowsIdentity совпадали)
Написал очередное письмо в КриптоПро, тут уже сотрудники развели руками, мол: “не знаем как там в Windows Store все устроено, пришлите исходники приложения и службы, тогда возможно, что-то и подскажем”.
Естественно, исходники по понятным причинам выслать было нельзя, и создавать тестовое приложение, делающее то же самое, тоже не очень хотелось. Решил сам пока попробовать разобраться, в чем может быть дело.
Загадочная особенность
Долго ли, коротко ли, пробовал разные варианты настроек, читал stackoerflow, посыпал голову пеплом и, в конце концов, случайно заметил довольно странную особенность. При попытке прочитать определенный файл из файловой системы, я получил ошибку “Нет доступа”, это было очень подозрительно, потому что я попробовал прочитать тот же самый файл, но вызов сделать не из Windows Store приложения, а из WPF, все прекрасно прочиталось. Как такое возможно? Один и тот же файл, один и тот же код в службе читающий этот файл, один и тот же пользователь “передается” в службу (то есть права на файл у пользователя точно есть), единственное различие, это то из какого приложения делается запрос в службу. Все это было очень похоже на те ограничения, которые существуют в Windows Store приложениях, которые запускаются в песочнице, там так же нет доступа к файлам, за исключением тех, которые лежат в специальной папке для этого приложения, то есть нельзя вот так просто взять и прочитать какой-нибудь файл на диске С. Но ведь файл читается не в коде Windows Store приложения, а в коде Windows-службы, у которой никаких подобных ограничений нет. Почему же он не читался? Единственное объяснение, которое приходило в голову, это то, что система накладывает ограничения не только на код приложения, но и на пользователя запустившего это приложение, в итоге в службу приходит пользователь со всеми этими ограничениями и поэтому не может ни прочитать файл, ни выполнить Crypto API (доступ к нему так же отсутствует в Windows Store). При этом, сколько я не искал в интернете, нигде не мог найти, что подобные ограничения существуют. Но факт оставался фактом. То для чего изначально задумывалась наша служба – это обойти ограничения платформы Windows Store. Но вся эта идея становилась бессмысленной, Windows оказалась не настолько глупа, и, по всей видимости, накладывала ограничения еще и на пользователя, без которого мы ничего не могли подписать. На глазах рушились все наши планы и мечты…
Гениальное решение
Продолжая поиск решений в интернете, уже без особой надежды, наткнулся на статью Speedy.Net: Impersonating CurrentUser from SYSTEM. Уже не помню, как именно я ее нашел, но это было просто гениально. Там было описано решение, которое позволяло сделать “имперсонацию”, но не стандартными средствами .NET, передавая пользователя из одного потока в другой, а получить текущий процесс “explorer”, из него получить токен текущего пользователя и выполнить “имепрсонацию” используя этот токен. В итоге пользуясь таким способом, можно было не передавать пользователя в службу, а получать его непосредственно внутри службы и выполнять нужный код от имени этого пользователя. Что естественно снимало все ограничения Windows Store и служба заработала и с WPF, и c Windows Store приложениями.
Решение очень крутое, в комментариях к статье кто-то предложил автору донейт, за это решение, на тот момент я был так рад найденному решению, что тоже бы задонатил, но автор скромно ответил в комментариях, что ему ничего не нужно.
Заключение
В итоге мы получили службу, которая теперь нормально работает и в Windows 7, и в Windows 8.1, и с токенами, и с сертификатами, и с WPF, и с Windows Store. Удалось обойти все возможные ограничения, накладываемые Windows Store. И то, что изначально было сделать нельзя, теперь стало возможным! Сейчас любое решение начинается с поиска в интернете, и наше, так же когда-то началось с поиска “как сделать подписание ЭП в Windows Store”, на что был получен четкий ответ, “В Windows Store это сделать нельзя, отсутствует доступ к Crypto API”. Можно было бы просто продублировать этот ответ и сказать: “Мы это сделать не сможем”, но мы сказали: “Попробуем” и в итоге все получилось! Возможно, решение получилось довольно сложным, конечно, оно не лишено своих недостатков, но оно работает и решает конкретные задачи на нее возложенные. А еще делая подобные вещи, появляется ощущение, что ты теперь можешь сделать вообще все, что угодно. Хорошо это или плохо? Пока не разобрался