Contents of this directory is archived and no longer updated.

Ссылки на другие материалы из этой серии:

В первой части мы кратенько рассмотрели процесс трансформации неуправляемых функций и структур в управляемые методы и объекты классов .NET. Прежде чем продолжать рассказывать про создание самоподписанного сертификата, я посчитал нужным поговорить о довольно важной вещи, как управление неуправляемой памятью. В CryptoAPI (равно как и в других подмножествах WinAPI) чаще всего используется 3 основных метода.

Получение данных неизвестного размера

Самый первый и самый простой — выгрузка данных прямо в заранее подготовленную переменную. Вот небольшой пример из моего буржуйского бложека — Get registered CSPs on the system. Выполним фрагмент кода до Add-Type включительно:

$signature = @"
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool CryptEnumProviders(
    uint dwIndex,
    uint pdwReserved,
    uint dwFlags,
    ref uint pdwProvType,
    System.Text.StringBuilder pszProvName,
    ref uint pcbProvName
);
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool CryptEnumProviderTypes(
    uint dwIndex,
    uint pdwReserved,
    uint dwFlags,
    ref uint pdwProvType,
    System.Text.StringBuilder pszTypeName,
    ref uint pcbTypeName
);
"@
Add-Type -MemberDefinition $signature -Namespace PKI -Name CSP

И посмотрим на функцию CryptEnumProviders, которая использовалась в том примере. Функция, как мы видим, выгружает данные в 3 аргумента сразу (которые помечены как __out и __inout). Если смотреть на определение функции в PowerShell, можно увидеть, что префикс ref установлен только на двух из них: pdwProvType и pcbProvName. А у pszProvName его нет. Почему? А потому что префикс ref можно ставить только для значений имеющих фиксированную длину. Например, для типов char, int, uint, byte и т.д. Для значений имеющих неопределённую длину, например, массивы простых типов данных, int[], char[], byte[]. Фактически в аргумент pszProvName выгружается массив байтов, которые потом StringBuilder сложит в строку. Но в неуправляемом мире всё очень сложно и программист должен заботиться обо всём, включая выделение памяти, изоляцию и освобождение памяти, когда данные больше не нужны. Там нет таких прекрасных товарищей как garbage collector. Следовательно здесь нам придётся вручную позаботиться об этом тоже.

Итак, суть этого метода заключается в простом: функция выполняется минимум дважды. В первый раз аргумент, получающий данные неопределённой длины выставляется в null. В процессе первого выполнения функции в аргумент pb* будет выгружена длина данных неизвестной длины. Т.е. после первого запуска данные станут известной длины.

Примечание: в CryptoAPI очень легко узнать функции использующие этот метод получения данных. У таких функций последний аргумент будет иметь приставку pcb<имя_аргумента>, cb<имя_аргумента> и определять размер данных, выгружаемых в предпоследний аргумент. А предпоследний будет иметь префикс pb. Вот табличка наиболее популярных префиксов используемых в C++:
http://www.cse.iitk.ac.in/users/dsrkg/cs245/html/Guide.htm

pdwProvType нас сейчас не интересует совсем. Значит, при первом запуске мы должны получить размер данных, которые будут выгружены в pszProvName. Как мы договорились, все переменные, которые используются как [ref] должны быть заранее объявлены:

$pdwProvType = 0
$pszTypeName = New-Object Text.StringBuilder
$pcbTypeName = 0

Т.к. мы первый раз запускаем функцию, мы не знаем сколько данных упадёт в pszTypeName, поэтому выставим его в null

[↓] [vPodans] [PKI.CSP]::CryptEnumProviderTypes(0,0,0,[ref]$pdwProvType,$null,[ref]$pcbTypeName)
True
[↓] [vPodans] $pcbTypeName
76
[↓] [vPodans]

Вот! Функция нам говорит, что в pszTypeName выгрузит 76 байт (хотя, на самом деле меньше, просто функции перестраховываются от переполнения буфера). Вот теперь очень важный момент. Нам нужно заранее выделить память под это дело. Памяти выделять не меньше, чем указано. Следовательно мы задаём размер для StringBuilder'а:

$pszTypeName.Length = $pcbTypeName

Если этого не сделать перед повторным вызовом функции, случится переполнение буфера и приложение, в контексте которого этот код исполняется (в нашем случае консоль PowerShell или редактор) тихо свалятся. Причём конкретное поведение отличается от систем. На моей домашней Windows 7 консоль просто крашится, а на рабочей Windows Server 2003 я получаю эксепшн (вполне себе нормальный), что случилось переполнение буфера и всё резко стало плохо. После того как мы выделили необходимую память под это, можно идти за самими данными. Поскольку данные теперь известной длины, заменяем $null на отмеренный объект StringBuilder:

[↓] [vPodans] $pszTypeName.Length = $pcbTypeName
[↓] [vPodans] [PKI.CSP]::CryptEnumProviderTypes(0,0,0,[ref]$pdwProvType,$pszTypeName,[ref]$pcbTypeName)
True
[↓] [vPodans] $pszTypeName

                               Capacity                             MaxCapacity                                  Length
                               --------                             -----------                                  ------
                                     76                              2147483647                                      37


[↓] [vPodans] $pszTypeName.ToString()
RSA Full (Signature and Key Exchange)
[↓] [vPodans]

На MSDN есть заметка на эту тему. Возможно она более понятна будет, чем моё объяснение: Retrieving Data of Unknown Length.

Если бы функция возвращала последовательный набор байтов, вызов функции был бы следующим:

# вычисляем размер возвращаемых данных в аргумент pbTypeName
[PKI.CSP]::CryptEnumProviderTypes(0,0,0,[ref]$pdwProvType,$null,[ref]$pcbTypeName)
# создаём массив байтов нужного размера
$pbTypeName = New-Object byte[] -ArgumentList $pcbTypeName
# выполняем ещё раз функцию и вместо $null подставляем только что созданный массив байтов
[PKI.CSP]::CryptEnumProviderTypes(0,0,0,[ref]$pdwProvType,$pbTypeName,[ref]$pcbTypeName)

Маршалинг — копирование данных в неуправляемую память

Иногда может потребоваться передать некоторые данные из управляемого кода в неуправляемую функцию. Например, в процессе создания сертификата мы создадим поле Subject. Это поле представляет X.500 distinguished name записанное в нотации ASN.1. Для этого есть соответствующие неуправляемые функции (CertNameToStr и CryptEncodeObject). Но как минимум на одной функции можно сэкономить, потому что у нас есть замечательный класс X500DistinguishedName у которого свойство RawData содержит как раз массив байтов записанных в нотации ASN.1. Однако, неуправляемые функции не поддерживают стандартные массивы .NET и требуется некий механизм, который сможет преобразовать массив байтов .NET в массив байтов пригодный для неуправляемых функций. Эти функции, как правило, используют структуры для хранения массивов данных.

Итак, смотрим описание аргумента pSubjectIssuerBlob для CertCreateSelfSignCertificate:

A pointer to a BLOB that contains the distinguished name (DN) for the certificate subject. This parameter cannot be NULL. Minimally, a pointer to an empty DN must be provided. This BLOB is normally created by using the CertStrToName function. It can also be created by using the CryptEncodeObject function and specifying either the X509_NAME or X509_UNICODE_NAME StructType.

Если речь идёт о BLOB'ах, значит это гарантированно будет структура CRYPTOAPI_BLOB. Как мы знаем, эта структура имеет два свойства: адрес (указатель) в памяти, где находятся данные и размер этих данных в байтах. Вот здесь нам понадобится маршалинг для копирования стандартного массива .NET в последовательность байтов в памяти. Весь этот процесс сводится к трём этапам:

  1. Получение массива байтов (или других данных. Просто в CryptoAPI вы чаще всего будете оперировать байтами);
  2. Выделение места в неуправляемой памяти под эти данные;
  3. Копирование данных в выделенную память.

В .NET есть просто замечательный класс: System.Runtime.InteropServices.Marshal. Можете посмотреть какая там пачка статических методов :-). Давайте проследуем указанную схему поэтапно. Сначала получим массив данных, которые будем потом записывать в неуправляемую память:

[↓] [vPodans] $Subject = [Security.Cryptography.X509Certificates.X500DistinguishedName]"CN=I am a cool user"
[↓] [vPodans] $Subject | ft -a

Name                Oid                              RawData
----                ---                              -------
CN=I am a cool user System.Security.Cryptography.Oid {48, 27, 49, 25...}


[↓] [vPodans]

Вы можете увидеть немного байтов в свойстве RawData. Это как раз те самые байты, записанные в нотации ASN.1 и которые нам нужно передать в функцию CertCreateSelfSignCertificate. Имея сам массив данных мы можем узнать их размер (размер массива) по свойству Count или Length. Следовательно мы можем выделить нужное количество памяти для хранения этого массива. Существует несколько методов выделения неуправляемой памяти, но я использую только один (нет никаких причин, просто мне он нравится) — AllocHGlobal(Int32), где в качестве аргумента говорим, сколько памяти надо выделить:

[↓] [vPodans] $ptrSubject = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($Subject.RawData.Length)
[↓] [vPodans] $ptrSubject
37641136 [↓] [vPodans]

Мы молучили указатель на адрес в памяти. Но, пока самих данных там нет. Мы воспользуемся методом Copy(array Byte [], Int32, IntPtr, Int32), чтобы скопировать .NET'овский массив в неуправляемую память:

[Runtime.InteropServices.Marshal]::Copy(
    $Subject.RawData, # указываем массив, который будем копировать
    0, # указываем стартовый индекс, если мы не весь массив хотим скопировать
    $ptrSubject, # указываем указатель на выделенную под эту операцию память
    $Subject.RawData.Length # количество байтов, которые будем копировать
)

Метод ничего не возвращает, кроме ошибок, следовательно ничего никуда присваивать не надо. Теперь можно сделать наш BLOB. Я уже говорил, как создаются объекты неуправляемых структур в PowerShell:

$ptrName = New-Object Quest.PowerGUI+CRYPTOAPI_BLOB -Property @{
    cbData = $Subject.RawData.Length;
    pbData = $ptrSubject
}

Как уже говорилось выше, в cbData мы указываем размер данных, а в pbData указываем указатель на память, где эти данные находятся. Вот этот объект ($ptrName) мы и подсунем в нашу исходную функцию.

Заметка: чтобы лучше понять смысл этой структуры представьте себе память, как линейную последовательность байтов. Структура CRYPTOAPI_BLOB показывает где находится начало конкретных данных (по указателю) и сколько байтов относятся именно к конкретным данным. Т.е. мы просто нарезаем память на кусочки и выбираем только те, которые нам нужны.

Но это ещё не всё. Поскольку в неуправляемой памяти нет таких товарищей, как garbage collector, вы должны самостоятельно освобождать выделенную память после её использования. Память освобождается в зависимости от метода её выделения. Если мы выделяем память через AllocHGlobal, освобождается она через FreeHGlobal:

[Runtime.InteropServices.Marshal]::FreeHGlobal($ptrSubject)

Этот метод возвращает True или False. Я обычно освобождаю всю память в конце исполняемого кода.

Маршалинг — копирование данных из неуправляемой памяти

Существует и обратная ситуация — скопировать данные из неуправляемой памяти. Например, мы получили некоторую структуру вида CRYPTOAPI_BLOB. В этом случае у нас уже есть фрагмент памяти, содержащий конкретные данные. Свойство pbData скажет нам, где находится начало данных и сколько их. Для этого мы можем воспользоваться методом Copy класса Marshal, но с другим конструктором, а именно — Copy(IntPtr, array<Byte>[], Int32, Int32). Одно из отличий этого метода является следующая ремарка:

Unmanaged, C-style arrays do not contain bounds information, which prevents the startIndex and length parameters from being validated. Thus, the unmanaged data corresponding to the source parameter populates the managed array regardless of its usefulness. You must initialize the managed array with the appropriate size before calling this method.

Я вам предлагаю простой примерчик. Скопировать массив байтов в неуправляемую память и прочитать её обратно. Вот код примера:

$Subject = [Security.Cryptography.X509Certificates.X500DistinguishedName]"CN=I am a cool user"
# запись данных в неуправляемую память
$ptr = [Runtime.InteropServices.Marshal]::AllocHGlobal($Subject.RawData.Length)
[Runtime.InteropServices.Marshal]::Copy($Subject.RawData,0,$ptr,$Subject.RawData.Length)
# чтение данных из неуправляемой памяти
$bytes = New-Object byte[] -ArgumentList $Subject.RawData.Length
[Runtime.InteropServices.Marshal]::Copy($ptr,$bytes,0,$Subject.RawData.Length)
# освобождение памяти
[Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)

в результате в переменной $bytes должен быть точно такой же массив, что и в свойстве RawData переменной Subject.

Я очень допускаю, что с первого раза что-то будет непонятно. Но это самые основы, которые нужно знать, если вы хотите самостоятельно работать с p/invoke. Лёгкой жизни никто никому не обещал, но если понять описанные здесь моменты, можно значительно облегчить себе жизнь. В следующей части я расскажу ещё что-нибудь интересное :-)


Share this article:

Comments:

Comments are closed.