UACME/Source/Yuubari/fusion.c

681 lines
23 KiB
C

/*******************************************************************************
*
* (C) COPYRIGHT AUTHORS, 2014 - 2018
*
* TITLE: FUSION.C
*
* VERSION: 1.29
*
* DATE: 15 June 2018
*
* THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
* ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED
* TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*
*******************************************************************************/
#include "global.h"
ptrWTGetSignatureInfo WTGetSignatureInfo = NULL;
/*
* SxsGetTocHeaderFromActivationContext
*
* Purpose:
*
* Locate and return pointer to Toc header in activation context.
*
*/
NTSTATUS SxsGetTocHeaderFromActivationContext(
_In_ PACTIVATION_CONTEXT ActivationContext,
_Out_ PACTIVATION_CONTEXT_DATA_TOC_HEADER *TocHeader,
_Out_opt_ PACTIVATION_CONTEXT_DATA *ActivationContextData
)
{
BOOL bCond = FALSE;
NTSTATUS result = STATUS_UNSUCCESSFUL;
ACTIVATION_CONTEXT_DATA *ContextData = NULL;
ACTIVATION_CONTEXT_DATA_TOC_HEADER *Header;
WCHAR szLog[0x100];
if (ActivationContext == NULL)
return STATUS_INVALID_PARAMETER_1;
if (TocHeader == NULL)
return STATUS_INVALID_PARAMETER_2;
__try {
do {
RtlSecureZeroMemory(szLog, sizeof(szLog));
ContextData = ActivationContext->ActivationContextData;
if (ContextData->Magic != ACTIVATION_CONTEXT_DATA_MAGIC) {
wsprintf(szLog, TEXT("ActivationContext Magic = %lx invalid"), ContextData->Magic);
break;
}
if (
(ContextData->HeaderSize != sizeof(ACTIVATION_CONTEXT_DATA)) ||
(ContextData->HeaderSize > ContextData->TotalSize)
)
{
wsprintf(szLog, TEXT("Unexpected data HeaderSize = %lu"), ContextData->HeaderSize);
break;
}
if (ContextData->DefaultTocOffset > ContextData->TotalSize) {
wsprintf(szLog, TEXT("Unexpected Toc offset %lx"), ContextData->DefaultTocOffset);
break;
}
Header = (ACTIVATION_CONTEXT_DATA_TOC_HEADER *)(((LPBYTE)ContextData) + ContextData->DefaultTocOffset);
if (Header->HeaderSize != sizeof(ACTIVATION_CONTEXT_DATA_TOC_HEADER)) {
wsprintf(szLog, TEXT("Unexpected Toc HeaderSize %lu"), Header->HeaderSize);
break;
}
if ((Header->FirstEntryOffset != 0) && (Header->EntryCount == 0)) {
wsprintf(szLog, TEXT("Unexpected EntryCount %lu"), Header->EntryCount);
break;
}
if ((Header->EntryCount > 0) && (Header->FirstEntryOffset == 0)) {
wsprintf(szLog, TEXT("Unexpected Toc FirstEntryOffset %lu"), Header->FirstEntryOffset);
break;
}
if (Header->FirstEntryOffset > ContextData->TotalSize) {
wsprintf(szLog, TEXT("Toc FirstEntry offset = %lu invalid"), Header->FirstEntryOffset);
break;
}
*TocHeader = Header;
if (ActivationContextData != NULL)
*ActivationContextData = ContextData;
result = STATUS_SUCCESS;
} while (bCond);
if (!NT_SUCCESS(result)) {
OutputDebugString(szLog);
return STATUS_SXS_CORRUPTION;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return STATUS_SXS_CORRUPTION;
}
return result;
}
/*
* SxsGetStringSectionRedirectionDlls
*
* Purpose:
*
* Extract redirection dlls from string section entry.
*
*/
NTSTATUS SxsGetStringSectionRedirectionDlls(
_In_ ACTIVATION_CONTEXT_STRING_SECTION_HEADER *SectionHeader,
_In_ ACTIVATION_CONTEXT_STRING_SECTION_ENTRY *StringEntry,
_Inout_ PDLL_REDIRECTION_LIST DllList
)
{
ULONG SegmentIndex;
NTSTATUS result = STATUS_SXS_KEY_NOT_FOUND;
ACTIVATION_CONTEXT_DATA_DLL_REDIRECTION *DataDll = NULL;
ACTIVATION_CONTEXT_DATA_DLL_REDIRECTION_PATH_SEGMENT *DllPathSegment = NULL;
DLL_REDIRECTION_LIST_ENTRY *DllListEntry = NULL;
WCHAR *wszDllName = NULL;
if (DllList == NULL)
return STATUS_INVALID_PARAMETER;
__try {
DataDll = (ACTIVATION_CONTEXT_DATA_DLL_REDIRECTION*)(((LPBYTE)SectionHeader) + StringEntry->Offset);
if (DataDll->PathSegmentOffset) {
DllPathSegment = (ACTIVATION_CONTEXT_DATA_DLL_REDIRECTION_PATH_SEGMENT*)(((LPBYTE)SectionHeader) + DataDll->PathSegmentOffset);
if (DllPathSegment) {
for (SegmentIndex = 0; SegmentIndex < DataDll->PathSegmentCount; SegmentIndex++) {
if ((DllPathSegment->Length) && (DllPathSegment->Offset)) {
DllListEntry = RtlAllocateHeap(NtCurrentPeb()->ProcessHeap, HEAP_ZERO_MEMORY, sizeof(DLL_REDIRECTION_LIST_ENTRY));
if (DllListEntry) {
wszDllName = RtlAllocateHeap(NtCurrentPeb()->ProcessHeap, HEAP_ZERO_MEMORY, DllPathSegment->Length);
if (wszDllName) {
RtlCopyMemory(wszDllName, (((PBYTE)SectionHeader) + DllPathSegment->Offset), DllPathSegment->Length);
RtlInitUnicodeString(&DllListEntry->DllName, wszDllName);
}
RtlInterlockedPushEntrySList(&DllList->Header, &DllListEntry->ListEntry);
}
}
DllPathSegment = (ACTIVATION_CONTEXT_DATA_DLL_REDIRECTION_PATH_SEGMENT*)(((LPBYTE)SectionHeader) + DataDll->Size);
}
}
result = STATUS_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return STATUS_SXS_CORRUPTION;
}
return result;
}
/*
* SxsGetDllRedirectionFromActivationContext
*
* Purpose:
*
* Query redirection dll list from activation context data.
*
*/
NTSTATUS SxsGetDllRedirectionFromActivationContext(
_In_ PACTIVATION_CONTEXT ActivationContext,
_In_ PDLL_REDIRECTION_LIST DllList
)
{
BOOL bCond = FALSE;
ULONG i, j;
NTSTATUS result = STATUS_UNSUCCESSFUL, status;
ACTIVATION_CONTEXT_DATA *ContextData = NULL;
ACTIVATION_CONTEXT_DATA_TOC_HEADER *TocHeader = NULL;
ACTIVATION_CONTEXT_DATA_TOC_ENTRY *TocEntry = NULL;
ACTIVATION_CONTEXT_STRING_SECTION_HEADER *SectionHeader = NULL;
ACTIVATION_CONTEXT_STRING_SECTION_ENTRY *StringEntry = NULL;
WCHAR szLog[0x100];
__try {
if (ActivationContext == NULL)
return STATUS_INVALID_PARAMETER_1;
if (DllList == NULL)
return STATUS_INVALID_PARAMETER_2;
do {
if (!NT_SUCCESS(SxsGetTocHeaderFromActivationContext(ActivationContext, &TocHeader, &ContextData)))
break;
TocEntry = (ACTIVATION_CONTEXT_DATA_TOC_ENTRY*)(((LPBYTE)ContextData) + TocHeader->FirstEntryOffset);
RtlInitializeSListHead(&DllList->Header);
i = 1;
while (i < TocHeader->EntryCount) {
if (TocEntry->Format == ACTIVATION_CONTEXT_SECTION_FORMAT_STRING) {
SectionHeader = (ACTIVATION_CONTEXT_STRING_SECTION_HEADER*)(((LPBYTE)ContextData) + TocEntry->Offset);
if (SectionHeader->Magic != ACTIVATION_CONTEXT_STRING_SECTION_MAGIC) {
wsprintf(szLog, TEXT("Section Magic = %lx invalid"), SectionHeader->Magic);
OutputDebugString(szLog);
break;
}
if (SectionHeader->HeaderSize != sizeof(ACTIVATION_CONTEXT_STRING_SECTION_HEADER)) {
wsprintf(szLog, TEXT("Unexpected Section HeaderSize = %lu"), SectionHeader->HeaderSize);
OutputDebugString(szLog);
break;
}
if (TocEntry->Id == ACTIVATION_CONTEXT_SECTION_DLL_REDIRECTION) {
StringEntry = (ACTIVATION_CONTEXT_STRING_SECTION_ENTRY*)(((LPBYTE)SectionHeader) + SectionHeader->ElementListOffset);
status = SxsGetStringSectionRedirectionDlls(SectionHeader, StringEntry, DllList);
if (status == STATUS_SXS_CORRUPTION)
continue;
for (j = 1; j < SectionHeader->ElementCount; j++) {
StringEntry = (ACTIVATION_CONTEXT_STRING_SECTION_ENTRY*)(((LPBYTE)StringEntry) + sizeof(ACTIVATION_CONTEXT_STRING_SECTION_ENTRY));
status = SxsGetStringSectionRedirectionDlls(SectionHeader, StringEntry, DllList);
if (status == STATUS_SXS_CORRUPTION)
continue;
}
}
}
TocEntry = (ACTIVATION_CONTEXT_DATA_TOC_ENTRY*)(((LPBYTE)TocEntry) + sizeof(ACTIVATION_CONTEXT_DATA_TOC_ENTRY));
i += 1;
} //while (i < TocHeader->EntryCount)
DllList->Depth = RtlQueryDepthSList(&DllList->Header);
if (DllList->Depth == 0)
result = STATUS_SXS_SECTION_NOT_FOUND;
else
result = STATUS_SUCCESS;
} while (bCond);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return STATUS_SXS_CORRUPTION;
}
return result;
}
/*
* FusionProbeForRedirectedDlls
*
* Purpose:
*
* Probe activation context for redirection dlls and output them if found.
*
*/
NTSTATUS FusionProbeForRedirectedDlls(
_In_ LPWSTR lpFileName,
_In_ ACTIVATION_CONTEXT *ActivationContext,
_In_ FUSIONCALLBACK OutputCallback
)
{
NTSTATUS status;
SLIST_ENTRY *ListEntry = NULL;
DLL_REDIRECTION_LIST_ENTRY *DllData = NULL;
UAC_FUSION_DATA_DLL FusionRedirectedDll;
DLL_REDIRECTION_LIST DllList;
__try {
RtlSecureZeroMemory(&DllList, sizeof(DllList));
status = SxsGetDllRedirectionFromActivationContext(ActivationContext, &DllList);
if (NT_SUCCESS(status)) {
while (DllList.Depth) {
ListEntry = RtlInterlockedPopEntrySList(&DllList.Header);
if (ListEntry) {
DllData = (PDLL_REDIRECTION_ENTRY)ListEntry;
RtlSecureZeroMemory(&FusionRedirectedDll, sizeof(FusionRedirectedDll));
FusionRedirectedDll.DataType = UacFusionDataRedirectedDllType;
FusionRedirectedDll.FileName = lpFileName;
FusionRedirectedDll.DllName = DllData->DllName.Buffer;
OutputCallback((UAC_FUSION_DATA*)&FusionRedirectedDll);
RtlFreeUnicodeString(&DllData->DllName);
RtlFreeHeap(NtCurrentPeb()->ProcessHeap, 0, DllData);
}
DllList.Depth--;
}
RtlInterlockedFlushSList(&DllList.Header);
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return STATUS_SXS_CORRUPTION;
}
return status;
}
/*
* FusionCheckFile
*
* Purpose:
*
* Query file manifest data related to security.
*
*/
VOID FusionCheckFile(
LPWSTR lpDirectory,
WIN32_FIND_DATA *fdata,
FUSIONCALLBACK OutputCallback
)
{
BOOL bCond = FALSE;
DWORD lastError;
NTSTATUS status;
HANDLE hFile = NULL, hSection = NULL, hActCtx = NULL;
LPWSTR FileName = NULL, pt = NULL;
PBYTE DllBase = NULL;
SIZE_T DllVirtualSize, sz, l;
OBJECT_ATTRIBUTES attr;
UNICODE_STRING usFileName;
IO_STATUS_BLOCK iosb;
ULONG_PTR ResourceSize = 0;
ULONG_PTR IdPath[3];
ACTCTX ctx;
SIGNATURE_INFO sigData;
UAC_FUSION_DATA FusionCommonData;
ACTIVATION_CONTEXT_RUN_LEVEL_INFORMATION ctxrl;
WCHAR szValue[100];
usFileName.Buffer = NULL;
do {
if ((lpDirectory == NULL) || (fdata == NULL) || (OutputCallback == NULL))
break;
if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
break;
sz = (_strlen(lpDirectory) + _strlen(fdata->cFileName)) * sizeof(WCHAR) + sizeof(UNICODE_NULL);
sz = ALIGN_UP(sz, 0x1000);
FileName = VirtualAlloc(NULL, sz, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (FileName == NULL)
break;
pt = FileName;
_strcpy(FileName, lpDirectory);
l = _strlen(FileName);
if (pt[l - 1] != L'\\') {
pt[l] = L'\\';
pt[l + 1] = 0;
}
_strcat(FileName, fdata->cFileName);
if (RtlDosPathNameToNtPathName_U(FileName, &usFileName, NULL, NULL) == FALSE)
break;
InitializeObjectAttributes(&attr, &usFileName,
OBJ_CASE_INSENSITIVE, NULL, NULL);
RtlSecureZeroMemory(&iosb, sizeof(iosb));
//
// Open file and map it.
//
status = NtCreateFile(&hFile, SYNCHRONIZE | FILE_READ_DATA,
&attr, &iosb, NULL, 0, FILE_SHARE_READ, FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
if (!NT_SUCCESS(status))
break;
status = NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL,
NULL, PAGE_READONLY, SEC_IMAGE, hFile);
if (!NT_SUCCESS(status))
break;
DllBase = NULL;
DllVirtualSize = 0;
status = NtMapViewOfSection(hSection, NtCurrentProcess(), &DllBase,
0, 0, NULL, &DllVirtualSize, ViewUnmap, 0, PAGE_READONLY);
if (!NT_SUCCESS(status))
break;
RtlSecureZeroMemory(&FusionCommonData, sizeof(FusionCommonData));
FusionCommonData.Name = FileName;
//
// Look for embedded manifest resource
//
IdPath[0] = (ULONG_PTR)RT_MANIFEST;
IdPath[1] = (ULONG_PTR)CREATEPROCESS_MANIFEST_RESOURCE_ID;
IdPath[2] = 0;
status = LdrResSearchResource(DllBase, (ULONG_PTR*)&IdPath, 3, 0, &pt, (ULONG_PTR*)&ResourceSize, NULL, NULL);
FusionCommonData.IsFusion = NT_SUCCESS(status);
//
// File has no manifest embedded.
//
if (FusionCommonData.IsFusion == FALSE) {
switch (status) {
case STATUS_RESOURCE_TYPE_NOT_FOUND:
OutputDebugString(TEXT("LdrResSearchResource: resource type not found\r\n"));
break;
case STATUS_RESOURCE_DATA_NOT_FOUND:
OutputDebugString(TEXT("LdrResSearchResource: resource data not found\r\n"));
break;
case STATUS_RESOURCE_NAME_NOT_FOUND:
OutputDebugString(TEXT("LdrResSearchResource: resource name not found\r\n"));
break;
default:
break;
}
//
// No embedded manifest, possible manifest hijacking for versions below RS1
//
if (
(status == STATUS_RESOURCE_TYPE_NOT_FOUND) ||
(status == STATUS_RESOURCE_DATA_NOT_FOUND) ||
(status == STATUS_RESOURCE_NAME_NOT_FOUND)
) {
if (WTGetSignatureInfo != NULL) {
//
// Check if file is signed as part of an operation system
//
RtlSecureZeroMemory(&sigData, sizeof(sigData));
sigData.cbSize = sizeof(sigData);
status = WTGetSignatureInfo(FileName, hFile,
SIF_BASE_VERIFICATION | SIF_CHECK_OS_BINARY | SIF_CATALOG_SIGNED,
&sigData,
NULL, NULL);
if (NT_SUCCESS(status)) {
if (sigData.fOSBinary != FALSE) {
RtlSecureZeroMemory(&FusionCommonData, sizeof(FusionCommonData));
FusionCommonData.Name = FileName;
FusionCommonData.IsOSBinary = TRUE;
//
// Check if signature valid or trusted
//
FusionCommonData.IsSignatureValidOrTrusted = ((sigData.SignatureState == SIGNATURE_STATE_TRUSTED) ||
(sigData.SignatureState == SIGNATURE_STATE_VALID));
OutputCallback(&FusionCommonData);
}
}
}
else { //WTGetSignatureInfo != NULL
//
// On Windows 7 this API is not available, just output result.
//
RtlSecureZeroMemory(&FusionCommonData, sizeof(FusionCommonData));
FusionCommonData.Name = FileName;
OutputCallback(&FusionCommonData);
}
}
//break the global loop
break;
}
//
// File has manifest, create activation context for it.
//
RtlSecureZeroMemory(&ctx, sizeof(ctx));
ctx.cbSize = sizeof(ACTCTX);
ctx.dwFlags = ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_HMODULE_VALID;
ctx.lpResourceName = CREATEPROCESS_MANIFEST_RESOURCE_ID;
ctx.lpSource = FileName;
ctx.hModule = (HMODULE)DllBase;
hActCtx = CreateActCtx(&ctx);
if (hActCtx == INVALID_HANDLE_VALUE) {
lastError = GetLastError();
RtlSecureZeroMemory(szValue, sizeof(szValue));
_strcpy(szValue, TEXT("Unexpected activation context failure ="));
ultostr(lastError, _strend(szValue));
_strcat(szValue, TEXT("\r\n"));
OutputDebugString(szValue);
break;
}
//
// Query run level and uiAccess information.
//
RtlSecureZeroMemory(&ctxrl, sizeof(ctxrl));
status = RtlQueryInformationActivationContext(
RTL_QUERY_INFORMATION_ACTIVATION_CONTEXT_FLAG_NO_ADDREF,
hActCtx, NULL, RunlevelInformationInActivationContext,
(PVOID)&ctxrl, sizeof(ACTIVATION_CONTEXT_RUN_LEVEL_INFORMATION), NULL);
if (NT_SUCCESS(status)) {
RtlCopyMemory(&FusionCommonData.RunLevel, &ctxrl, sizeof(ACTIVATION_CONTEXT_RUN_LEVEL_INFORMATION));
}
//
// DotNet application highly vulnerable for Dll Hijacking attacks.
// Always check if file is DotNet origin.
//
FusionCommonData.IsDotNet = supIsCorImageFile(DllBase);
//
// Query autoelevate setting.
//
l = 0;
RtlSecureZeroMemory(&szValue, sizeof(szValue));
status = RtlQueryActivationContextApplicationSettings(0, hActCtx, NULL, TEXT("autoElevate"), (PWSTR)&szValue, sizeof(szValue), &l);
if (NT_SUCCESS(status)) {
//
// Actually appinfo only looks for 'T' or 't' symbol
// for performance reasons perhaps
//
if (_strcmpi(szValue, TEXT("true")) == 0)
FusionCommonData.AutoElevateState = AutoElevateEnabled;
else
//
// Several former autoelevate applications has autoelevated strictly
// disabled in manifest as part of their UAC fixes.
//
if (_strcmpi(szValue, TEXT("false")) == 0)
FusionCommonData.AutoElevateState = AutoElevateDisabled;
}
else {
//
// Query settings failed, check if it known error like sxs key not exist.
//
if (status != STATUS_SXS_KEY_NOT_FOUND) {
RtlSecureZeroMemory(szValue, sizeof(szValue));
_strcpy(szValue, TEXT("QueryActivationContext error ="));
ultostr(status, _strend(szValue));
_strcat(szValue, TEXT("\r\n"));
OutputDebugString(szValue);
//
// Don't output anything, just break, it is unexpected situation.
//
break;
}
}
//
// Even if autoElevate key could be not found, application still can be in whitelist.
// As in case of inetmgr.exe on RS1+, so check if it has redirection dlls.
//
OutputCallback(&FusionCommonData);
//
// Print redirection dlls from activation context
//
FusionProbeForRedirectedDlls(FileName, (PACTIVATION_CONTEXT)hActCtx, OutputCallback);
} while (bCond);
if (hActCtx != NULL)
ReleaseActCtx(hActCtx);
if (usFileName.Buffer != NULL)
RtlFreeUnicodeString(&usFileName);
if (DllBase != NULL)
NtUnmapViewOfSection(NtCurrentProcess(), DllBase);
if (hSection != NULL)
NtClose(hSection);
if (hFile != NULL)
NtClose(hFile);
if (FileName != NULL)
VirtualFree(FileName, 0, MEM_RELEASE);
}
/*
* FusionScanFiles
*
* Purpose:
*
* Scan directory for files of given type.
*
*/
VOID FusionScanFiles(
LPWSTR lpDirectory,
FUSIONCALLBACK OutputCallback
)
{
HANDLE hFile;
LPWSTR lpLookupDirectory = NULL;
SIZE_T sz;
WIN32_FIND_DATA fdata;
sz = (_strlen(lpDirectory) + MAX_PATH) * sizeof(WCHAR);
lpLookupDirectory = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sz);
if (lpLookupDirectory) {
_strncpy(lpLookupDirectory, MAX_PATH, lpDirectory, MAX_PATH);
_strcat(lpLookupDirectory, TEXT("\\*.exe"));
RtlSecureZeroMemory(&fdata, sizeof(fdata));
hFile = FindFirstFile(lpLookupDirectory, &fdata);
if (hFile != INVALID_HANDLE_VALUE) {
do {
FusionCheckFile(lpDirectory, &fdata, OutputCallback);
} while (FindNextFile(hFile, &fdata));
FindClose(hFile);
}
HeapFree(GetProcessHeap(), 0, lpLookupDirectory);
}
}
/*
* FusionScanDirectory
*
* Purpose:
*
* Recursively scan directories.
*
*/
VOID FusionScanDirectory(
LPWSTR lpDirectory,
FUSIONCALLBACK OutputCallback
)
{
SIZE_T l;
HANDLE hDirectory;
WCHAR dirbuf[MAX_PATH * 2];
WCHAR textbuf[MAX_PATH * 2];
WIN32_FIND_DATA fdata;
if ((lpDirectory == NULL) || (OutputCallback == NULL))
return;
FusionScanFiles(lpDirectory, OutputCallback);
RtlSecureZeroMemory(dirbuf, sizeof(dirbuf));
RtlSecureZeroMemory(textbuf, sizeof(textbuf));
_strncpy(dirbuf, MAX_PATH, lpDirectory, MAX_PATH);
l = _strlen(dirbuf);
if (dirbuf[l - 1] != L'\\') {
dirbuf[l] = L'\\';
dirbuf[l + 1] = 0;
l++;
}
_strcpy(textbuf, dirbuf);
textbuf[l] = L'*';
textbuf[l + 1] = 0;
l++;
RtlSecureZeroMemory(&fdata, sizeof(fdata));
hDirectory = FindFirstFile(textbuf, &fdata);
if (hDirectory != INVALID_HANDLE_VALUE) {
do {
if ((fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
(fdata.cFileName[0] != L'.')
)
{
_strcpy(textbuf, dirbuf);
_strcat(textbuf, fdata.cFileName);
FusionScanDirectory(textbuf, OutputCallback);
}
} while (FindNextFile(hDirectory, &fdata));
FindClose(hDirectory);
}
}