635 lines
15 KiB
C
635 lines
15 KiB
C
/*
|
|
* assembly parser
|
|
*
|
|
* Copyright 2008 James Hawkins
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
|
|
#include "windef.h"
|
|
#include "winbase.h"
|
|
#include "winuser.h"
|
|
#include "winver.h"
|
|
#include "wincrypt.h"
|
|
#include "dbghelp.h"
|
|
#include "ole2.h"
|
|
#include "fusion.h"
|
|
#include "corhdr.h"
|
|
|
|
#include "fusionpriv.h"
|
|
#include "wine/debug.h"
|
|
#include "wine/unicode.h"
|
|
|
|
#define TableFromToken(tk) (TypeFromToken(tk) >> 24)
|
|
|
|
#define MAX_CLR_TABLES 64
|
|
|
|
typedef struct tagCLRTABLE
|
|
{
|
|
DWORD rows;
|
|
DWORD offset;
|
|
} CLRTABLE;
|
|
|
|
struct tagASSEMBLY
|
|
{
|
|
LPSTR path;
|
|
|
|
HANDLE hfile;
|
|
HANDLE hmap;
|
|
BYTE *data;
|
|
|
|
IMAGE_NT_HEADERS32 *nthdr;
|
|
IMAGE_COR20_HEADER *corhdr;
|
|
|
|
METADATAHDR *metadatahdr;
|
|
|
|
METADATATABLESHDR *tableshdr;
|
|
DWORD numtables;
|
|
DWORD *numrows;
|
|
CLRTABLE tables[MAX_CLR_TABLES];
|
|
|
|
BYTE *strings;
|
|
BYTE *blobs;
|
|
};
|
|
|
|
const DWORD COR_TABLE_SIZES[64] =
|
|
{
|
|
sizeof(MODULETABLE),
|
|
sizeof(TYPEREFTABLE),
|
|
sizeof(TYPEDEFTABLE),
|
|
0,
|
|
sizeof(FIELDTABLE),
|
|
0,
|
|
sizeof(METHODDEFTABLE),
|
|
0,
|
|
sizeof(PARAMTABLE),
|
|
sizeof(INTERFACEIMPLTABLE),
|
|
sizeof(MEMBERREFTABLE),
|
|
sizeof(CONSTANTTABLE),
|
|
sizeof(CUSTOMATTRIBUTETABLE),
|
|
sizeof(FIELDMARSHALTABLE),
|
|
sizeof(DECLSECURITYTABLE),
|
|
sizeof(CLASSLAYOUTTABLE),
|
|
sizeof(FIELDLAYOUTTABLE),
|
|
sizeof(STANDALONESIGTABLE),
|
|
sizeof(EVENTMAPTABLE),
|
|
0,
|
|
sizeof(EVENTTABLE),
|
|
sizeof(PROPERTYMAPTABLE),
|
|
0,
|
|
sizeof(PROPERTYTABLE),
|
|
sizeof(METHODSEMANTICSTABLE),
|
|
sizeof(METHODIMPLTABLE),
|
|
sizeof(MODULEREFTABLE),
|
|
sizeof(TYPESPECTABLE),
|
|
sizeof(IMPLMAPTABLE),
|
|
sizeof(FIELDRVATABLE),
|
|
0,
|
|
0,
|
|
sizeof(ASSEMBLYTABLE),
|
|
sizeof(ASSEMBLYPROCESSORTABLE),
|
|
sizeof(ASSEMBLYOSTABLE),
|
|
sizeof(ASSEMBLYREFTABLE),
|
|
sizeof(ASSEMBLYREFPROCESSORTABLE),
|
|
sizeof(ASSEMBLYREFOSTABLE),
|
|
sizeof(FILETABLE),
|
|
sizeof(EXPORTEDTYPETABLE),
|
|
sizeof(MANIFESTRESTABLE),
|
|
sizeof(NESTEDCLASSTABLE),
|
|
sizeof(GENERICPARAMTABLE),
|
|
sizeof(METHODSPECTABLE),
|
|
sizeof(GENERICPARAMCONSTRAINTTABLE),
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0
|
|
};
|
|
|
|
static LPSTR strdupWtoA(LPCWSTR str)
|
|
{
|
|
LPSTR ret = NULL;
|
|
DWORD len;
|
|
|
|
if (!str)
|
|
return ret;
|
|
|
|
len = WideCharToMultiByte(CP_ACP, 0, str, -1, NULL, 0, NULL, NULL);
|
|
ret = HeapAlloc(GetProcessHeap(), 0, len);
|
|
if (ret)
|
|
WideCharToMultiByte(CP_ACP, 0, str, -1, ret, len, NULL, NULL);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static DWORD rva_to_offset(IMAGE_NT_HEADERS *nthdrs, DWORD rva)
|
|
{
|
|
DWORD offset = rva, limit;
|
|
IMAGE_SECTION_HEADER *img;
|
|
WORD i;
|
|
|
|
img = IMAGE_FIRST_SECTION(nthdrs);
|
|
|
|
if (rva < img->PointerToRawData)
|
|
return rva;
|
|
|
|
for (i = 0; i < nthdrs->FileHeader.NumberOfSections; i++)
|
|
{
|
|
if (img[i].SizeOfRawData)
|
|
limit = img[i].SizeOfRawData;
|
|
else
|
|
limit = img[i].Misc.VirtualSize;
|
|
|
|
if (rva >= img[i].VirtualAddress &&
|
|
rva < (img[i].VirtualAddress + limit))
|
|
{
|
|
if (img[i].PointerToRawData != 0)
|
|
{
|
|
offset -= img[i].VirtualAddress;
|
|
offset += img[i].PointerToRawData;
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static BYTE *GetData(BYTE *pData, ULONG *pLength)
|
|
{
|
|
if ((*pData & 0x80) == 0x00)
|
|
{
|
|
*pLength = (*pData & 0x7f);
|
|
return pData + 1;
|
|
}
|
|
|
|
if ((*pData & 0xC0) == 0x80)
|
|
{
|
|
*pLength = ((*pData & 0x3f) << 8 | *(pData + 1));
|
|
return pData + 2;
|
|
}
|
|
|
|
if ((*pData & 0xE0) == 0xC0)
|
|
{
|
|
*pLength = ((*pData & 0x1f) << 24 | *(pData + 1) << 16 |
|
|
*(pData + 2) << 8 | *(pData + 3));
|
|
return pData + 4;
|
|
}
|
|
|
|
*pLength = (ULONG)-1;
|
|
return 0;
|
|
}
|
|
|
|
static VOID *assembly_data_offset(ASSEMBLY *assembly, ULONG offset)
|
|
{
|
|
return (VOID *)&assembly->data[offset];
|
|
}
|
|
|
|
static HRESULT parse_clr_tables(ASSEMBLY *assembly, ULONG offset)
|
|
{
|
|
DWORD i, previ, offidx;
|
|
ULONG currofs;
|
|
|
|
currofs = offset;
|
|
assembly->tableshdr = (METADATATABLESHDR *)assembly_data_offset(assembly, currofs);
|
|
if (!assembly->tableshdr)
|
|
return E_FAIL;
|
|
|
|
currofs += sizeof(METADATATABLESHDR);
|
|
assembly->numrows = (DWORD *)assembly_data_offset(assembly, currofs);
|
|
if (!assembly->numrows)
|
|
return E_FAIL;
|
|
|
|
assembly->numtables = 0;
|
|
for (i = 0; i < MAX_CLR_TABLES; i++)
|
|
{
|
|
if ((i < 32 && (assembly->tableshdr->MaskValid.u.LowPart >> i) & 1) ||
|
|
(i >= 32 && (assembly->tableshdr->MaskValid.u.HighPart >> i) & 1))
|
|
{
|
|
assembly->numtables++;
|
|
}
|
|
}
|
|
|
|
currofs += assembly->numtables * sizeof(DWORD);
|
|
memset(assembly->tables, -1, MAX_CLR_TABLES * sizeof(CLRTABLE));
|
|
|
|
if (assembly->tableshdr->MaskValid.u.LowPart & 1)
|
|
assembly->tables[0].offset = currofs;
|
|
|
|
offidx = 0;
|
|
for (i = 0; i < MAX_CLR_TABLES; i++)
|
|
{
|
|
if ((i < 32 && (assembly->tableshdr->MaskValid.u.LowPart >> i) & 1) ||
|
|
(i >= 32 && (assembly->tableshdr->MaskValid.u.HighPart >> i) & 1))
|
|
{
|
|
assembly->tables[i].rows = assembly->numrows[offidx];
|
|
offidx++;
|
|
}
|
|
}
|
|
|
|
previ = 0;
|
|
offidx = 1;
|
|
for (i = 1; i < MAX_CLR_TABLES; i++)
|
|
{
|
|
if ((i < 32 && (assembly->tableshdr->MaskValid.u.LowPart >> i) & 1) ||
|
|
(i >= 32 && (assembly->tableshdr->MaskValid.u.HighPart >> i) & 1))
|
|
{
|
|
currofs += COR_TABLE_SIZES[previ] * assembly->numrows[offidx - 1];
|
|
assembly->tables[i].offset = currofs;
|
|
offidx++;
|
|
previ = i;
|
|
}
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
static HRESULT parse_metadata_header(ASSEMBLY *assembly, DWORD *hdrsz)
|
|
{
|
|
METADATAHDR *metadatahdr;
|
|
BYTE *ptr, *dest;
|
|
DWORD size, ofs;
|
|
ULONG rva;
|
|
|
|
rva = assembly->corhdr->MetaData.VirtualAddress;
|
|
ptr = ImageRvaToVa(assembly->nthdr, assembly->data, rva, NULL);
|
|
if (!ptr)
|
|
return E_FAIL;
|
|
|
|
metadatahdr = (METADATAHDR *)ptr;
|
|
|
|
assembly->metadatahdr = HeapAlloc(GetProcessHeap(), 0, sizeof(METADATAHDR));
|
|
if (!assembly->metadatahdr)
|
|
return E_OUTOFMEMORY;
|
|
|
|
size = FIELD_OFFSET(METADATAHDR, Version);
|
|
memcpy(assembly->metadatahdr, metadatahdr, size);
|
|
|
|
/* we don't care about the version string */
|
|
|
|
ofs = FIELD_OFFSET(METADATAHDR, Flags);
|
|
ptr += FIELD_OFFSET(METADATAHDR, Version) + metadatahdr->VersionLength + 1;
|
|
dest = (BYTE *)assembly->metadatahdr + ofs;
|
|
memcpy(dest, ptr, sizeof(METADATAHDR) - ofs);
|
|
|
|
*hdrsz = sizeof(METADATAHDR) - sizeof(LPSTR) + metadatahdr->VersionLength + 1;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
static HRESULT parse_clr_metadata(ASSEMBLY *assembly)
|
|
{
|
|
METADATASTREAMHDR *streamhdr;
|
|
ULONG rva, i, ofs;
|
|
LPSTR stream;
|
|
HRESULT hr;
|
|
DWORD hdrsz;
|
|
BYTE *ptr;
|
|
|
|
hr = parse_metadata_header(assembly, &hdrsz);
|
|
if (FAILED(hr))
|
|
return hr;
|
|
|
|
rva = assembly->corhdr->MetaData.VirtualAddress;
|
|
ptr = ImageRvaToVa(assembly->nthdr, assembly->data, rva + hdrsz, NULL);
|
|
if (!ptr)
|
|
return E_FAIL;
|
|
|
|
for (i = 0; i < assembly->metadatahdr->Streams; i++)
|
|
{
|
|
streamhdr = (METADATASTREAMHDR *)ptr;
|
|
ofs = rva_to_offset(assembly->nthdr, rva + streamhdr->Offset);
|
|
|
|
ptr += sizeof(METADATASTREAMHDR);
|
|
stream = (LPSTR)ptr;
|
|
|
|
if (!lstrcmpA(stream, "#~"))
|
|
{
|
|
hr = parse_clr_tables(assembly, ofs);
|
|
if (FAILED(hr))
|
|
return hr;
|
|
}
|
|
else if (!lstrcmpA(stream, "#Strings") || !lstrcmpA(stream, "Strings"))
|
|
assembly->strings = (BYTE *)assembly_data_offset(assembly, ofs);
|
|
else if (!lstrcmpA(stream, "#Blob") || !lstrcmpA(stream, "Blob"))
|
|
assembly->blobs = (BYTE *)assembly_data_offset(assembly, ofs);
|
|
|
|
ptr += lstrlenA(stream);
|
|
while (!*ptr) ptr++;
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
static HRESULT parse_pe_header(ASSEMBLY *assembly)
|
|
{
|
|
IMAGE_DATA_DIRECTORY *datadirs;
|
|
|
|
assembly->nthdr = ImageNtHeader(assembly->data);
|
|
if (!assembly->nthdr)
|
|
return E_FAIL;
|
|
|
|
datadirs = assembly->nthdr->OptionalHeader.DataDirectory;
|
|
if (!datadirs)
|
|
return E_FAIL;
|
|
|
|
if (!datadirs[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress ||
|
|
!datadirs[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].Size)
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
|
|
assembly->corhdr = ImageRvaToVa(assembly->nthdr, assembly->data,
|
|
datadirs[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress, NULL);
|
|
if (!assembly->corhdr)
|
|
return E_FAIL;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT assembly_create(ASSEMBLY **out, LPCWSTR file)
|
|
{
|
|
ASSEMBLY *assembly;
|
|
HRESULT hr;
|
|
|
|
*out = NULL;
|
|
|
|
assembly = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ASSEMBLY));
|
|
if (!assembly)
|
|
return E_OUTOFMEMORY;
|
|
|
|
assembly->path = strdupWtoA(file);
|
|
if (!assembly->path)
|
|
{
|
|
hr = E_OUTOFMEMORY;
|
|
goto failed;
|
|
}
|
|
|
|
assembly->hfile = CreateFileW(file, GENERIC_READ, FILE_SHARE_READ,
|
|
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
|
if (assembly->hfile == INVALID_HANDLE_VALUE)
|
|
{
|
|
hr = HRESULT_FROM_WIN32(GetLastError());
|
|
goto failed;
|
|
}
|
|
|
|
assembly->hmap = CreateFileMappingW(assembly->hfile, NULL, PAGE_READONLY,
|
|
0, 0, NULL);
|
|
if (!assembly->hmap)
|
|
{
|
|
hr = HRESULT_FROM_WIN32(GetLastError());
|
|
goto failed;
|
|
}
|
|
|
|
assembly->data = MapViewOfFile(assembly->hmap, FILE_MAP_READ, 0, 0, 0);
|
|
if (!assembly->data)
|
|
{
|
|
hr = HRESULT_FROM_WIN32(GetLastError());
|
|
goto failed;
|
|
}
|
|
|
|
hr = parse_pe_header(assembly);
|
|
if (FAILED(hr)) goto failed;
|
|
|
|
hr = parse_clr_metadata(assembly);
|
|
if (FAILED(hr)) goto failed;
|
|
|
|
*out = assembly;
|
|
return S_OK;
|
|
|
|
failed:
|
|
assembly_release(assembly);
|
|
return hr;
|
|
}
|
|
|
|
HRESULT assembly_release(ASSEMBLY *assembly)
|
|
{
|
|
if (!assembly)
|
|
return S_OK;
|
|
|
|
HeapFree(GetProcessHeap(), 0, assembly->metadatahdr);
|
|
HeapFree(GetProcessHeap(), 0, assembly->path);
|
|
UnmapViewOfFile(assembly->data);
|
|
CloseHandle(assembly->hmap);
|
|
CloseHandle(assembly->hfile);
|
|
HeapFree(GetProcessHeap(), 0, assembly);
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
static LPSTR assembly_dup_str(ASSEMBLY *assembly, WORD index)
|
|
{
|
|
LPSTR str = (LPSTR)&assembly->strings[index];
|
|
LPSTR cpy = HeapAlloc(GetProcessHeap(), 0, strlen(str)+1);
|
|
if (cpy)
|
|
strcpy(cpy, str);
|
|
return cpy;
|
|
}
|
|
|
|
HRESULT assembly_get_name(ASSEMBLY *assembly, LPSTR *name)
|
|
{
|
|
ASSEMBLYTABLE *asmtbl;
|
|
ULONG offset;
|
|
|
|
offset = assembly->tables[TableFromToken(mdtAssembly)].offset;
|
|
if (offset == -1)
|
|
return E_FAIL;
|
|
|
|
asmtbl = (ASSEMBLYTABLE *)assembly_data_offset(assembly, offset);
|
|
if (!asmtbl)
|
|
return E_FAIL;
|
|
|
|
*name = assembly_dup_str(assembly, asmtbl->Name);
|
|
if (!*name)
|
|
return E_OUTOFMEMORY;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT assembly_get_path(ASSEMBLY *assembly, LPSTR *path)
|
|
{
|
|
LPSTR cpy = HeapAlloc(GetProcessHeap(), 0, strlen(assembly->path)+1);
|
|
*path = cpy;
|
|
if (cpy)
|
|
strcpy(cpy, assembly->path);
|
|
else
|
|
return E_OUTOFMEMORY;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT assembly_get_version(ASSEMBLY *assembly, LPSTR *version)
|
|
{
|
|
LPSTR verdata;
|
|
VS_FIXEDFILEINFO *ffi;
|
|
HRESULT hr = S_OK;
|
|
DWORD size;
|
|
|
|
size = GetFileVersionInfoSizeA(assembly->path, NULL);
|
|
if (!size)
|
|
return HRESULT_FROM_WIN32(GetLastError());
|
|
|
|
verdata = HeapAlloc(GetProcessHeap(), 0, size);
|
|
if (!verdata)
|
|
return E_OUTOFMEMORY;
|
|
|
|
if (!GetFileVersionInfoA(assembly->path, 0, size, verdata))
|
|
{
|
|
hr = HRESULT_FROM_WIN32(GetLastError());
|
|
goto done;
|
|
}
|
|
|
|
if (!VerQueryValueA(verdata, "\\", (LPVOID *)&ffi, &size))
|
|
{
|
|
hr = HRESULT_FROM_WIN32(GetLastError());
|
|
goto done;
|
|
}
|
|
|
|
*version = HeapAlloc(GetProcessHeap(), 0, MAX_PATH);
|
|
if (!*version)
|
|
{
|
|
hr = E_OUTOFMEMORY;
|
|
goto done;
|
|
}
|
|
|
|
sprintf(*version, "%d.%d.%d.%d", HIWORD(ffi->dwFileVersionMS),
|
|
LOWORD(ffi->dwFileVersionMS), HIWORD(ffi->dwFileVersionLS),
|
|
LOWORD(ffi->dwFileVersionLS));
|
|
|
|
done:
|
|
HeapFree(GetProcessHeap(), 0, verdata);
|
|
return hr;
|
|
}
|
|
|
|
HRESULT assembly_get_architecture(ASSEMBLY *assembly, DWORD fixme)
|
|
{
|
|
/* FIXME */
|
|
return S_OK;
|
|
}
|
|
|
|
static BYTE *assembly_get_blob(ASSEMBLY *assembly, WORD index, ULONG *size)
|
|
{
|
|
return GetData(&assembly->blobs[index], size);
|
|
}
|
|
|
|
static void bytes_to_str(BYTE *bytes, DWORD len, LPSTR str)
|
|
{
|
|
int i;
|
|
|
|
static const char hexval[16] = {
|
|
'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'
|
|
};
|
|
|
|
for(i = 0; i < len; i++)
|
|
{
|
|
str[i * 2] = hexval[((bytes[i] >> 4) & 0xF)];
|
|
str[i * 2 + 1] = hexval[(bytes[i]) & 0x0F];
|
|
}
|
|
}
|
|
|
|
#define BYTES_PER_TOKEN 8
|
|
#define CHARS_PER_BYTE 2
|
|
#define TOKEN_LENGTH (BYTES_PER_TOKEN * CHARS_PER_BYTE + 1)
|
|
|
|
HRESULT assembly_get_pubkey_token(ASSEMBLY *assembly, LPSTR *token)
|
|
{
|
|
ASSEMBLYTABLE *asmtbl;
|
|
ULONG i, offset, size;
|
|
BYTE *hashdata;
|
|
HCRYPTPROV crypt;
|
|
HCRYPTHASH hash;
|
|
BYTE *pubkey;
|
|
BYTE tokbytes[BYTES_PER_TOKEN];
|
|
HRESULT hr = E_FAIL;
|
|
LPSTR tok;
|
|
|
|
*token = NULL;
|
|
|
|
offset = assembly->tables[TableFromToken(mdtAssembly)].offset;
|
|
if (offset == -1)
|
|
return E_FAIL;
|
|
|
|
asmtbl = (ASSEMBLYTABLE *)assembly_data_offset(assembly, offset);
|
|
if (!asmtbl)
|
|
return E_FAIL;
|
|
|
|
pubkey = assembly_get_blob(assembly, asmtbl->PublicKey, &size);
|
|
|
|
if (!CryptAcquireContextA(&crypt, NULL, NULL, PROV_RSA_FULL,
|
|
CRYPT_VERIFYCONTEXT))
|
|
return E_FAIL;
|
|
|
|
if (!CryptCreateHash(crypt, CALG_SHA1, 0, 0, &hash))
|
|
return E_FAIL;
|
|
|
|
if (!CryptHashData(hash, pubkey, size, 0))
|
|
return E_FAIL;
|
|
|
|
size = 0;
|
|
if (!CryptGetHashParam(hash, HP_HASHVAL, NULL, &size, 0))
|
|
return E_FAIL;
|
|
|
|
hashdata = HeapAlloc(GetProcessHeap(), 0, size);
|
|
if (!hashdata)
|
|
{
|
|
hr = E_OUTOFMEMORY;
|
|
goto done;
|
|
}
|
|
|
|
if (!CryptGetHashParam(hash, HP_HASHVAL, hashdata, &size, 0))
|
|
goto done;
|
|
|
|
for (i = size - 1; i >= size - 8; i--)
|
|
tokbytes[size - i - 1] = hashdata[i];
|
|
|
|
tok = HeapAlloc(GetProcessHeap(), 0, TOKEN_LENGTH);
|
|
if (!tok)
|
|
{
|
|
hr = E_OUTOFMEMORY;
|
|
goto done;
|
|
}
|
|
|
|
bytes_to_str(tokbytes, BYTES_PER_TOKEN, tok);
|
|
tok[TOKEN_LENGTH - 1] = '\0';
|
|
|
|
*token = tok;
|
|
hr = S_OK;
|
|
|
|
done:
|
|
HeapFree(GetProcessHeap(), 0, hashdata);
|
|
CryptDestroyHash(hash);
|
|
CryptReleaseContext(crypt, 0);
|
|
|
|
return hr;
|
|
}
|