Clay Clay - 1 month ago 25
C# Question

How to invoke NtSetInformationFile (w/ FILE_LINK_INFORMATION) in c#

The following is an attempt to reproduce the CreateHardLink functionality as described here.

The reason I even need to do this is because this is the only way that I know I'll have the necessary permissions (this code is running in .Net, in WinPE and has asserted the necessary privileges for restore). In particular, I'm using the BackupSemantics flag and the SE_RESTORE_NAME privilege. The normal pInvoke mechanism to CreateHardLink has no provisions for a restore program to use BackupSemantics...and there are scads of files my account doesn't have "normal" access to - hence, this mess.

unsafe bool CreateLink( string linkName, string existingFileName )
{
var access =
NativeMethods.EFileAccess.AccessSystemSecurity |
NativeMethods.EFileAccess.WriteAttributes |
NativeMethods.EFileAccess.Synchronize;

var disp = NativeMethods.ECreationDisposition.OpenExisting;

var flags =
NativeMethods.EFileAttributes.BackupSemantics |
NativeMethods.EFileAttributes.OpenReparsePoint;

var share =
FileShare.ReadWrite |
FileShare.Delete;

var handle = NativeMethods.CreateFile
(
existingFileName,
access,
( uint ) share,
IntPtr.Zero,
( uint ) disp,
( uint ) flags,
IntPtr.Zero
);

if ( !handle.IsInvalid )
{
var mem = Marshal.AllocHGlobal( 1024 );
try
{
var linkInfo = new NativeMethods.FILE_LINK_INFORMATION( );
var ioStatus = new NativeMethods.IO_STATUS_BLOCK( );
linkInfo.replaceIfExisting = false;
linkInfo.directoryHandle = IntPtr.Zero;
linkInfo.fileName = linkName;
linkInfo.fileNameLength = ( uint )
Encoding
.Unicode
.GetByteCount( linkInfo.fileName );

Marshal.StructureToPtr( linkInfo, mem, true );
var result = NativeMethods.NtSetInformationFile
(
handle.DangerousGetHandle( ),
ref ioStatus,
mem.ToPointer( ),
1024,
NativeMethods.FILE_INFORMATION_CLASS.FileLinkInformation
);

return result == 0;
}
finally
{
Marshal.FreeHGlobal( mem );
}
}
return false;
}


I keep getting a result from NtSetInformationFile that says I have specified an invalid parameter to a system function. (Result=0xC000000D). I'm unsure about how I've declared the structures - as one of 'em has a file name's length is followed by the "first character" of the name. It's documented here.

Here's how I've declared the structures - and the import. This is just best-guess stuff, as I've not found anyone who's declared this in c# (pinvoke.net and other places) I've messed with a number of permutations...all with the exact same error:

[StructLayout( LayoutKind.Sequential, Pack = 4 )]
internal struct FILE_LINK_INFORMATION
{
[MarshalAs( UnmanagedType.Bool )]
public bool replaceIfExisting;
public IntPtr directoryHandle;
public uint fileNameLength;
[MarshalAs( UnmanagedType.ByValTStr, SizeConst = MAX_PATH )]
public string fileName;
}

internal struct IO_STATUS_BLOCK
{
uint status;
ulong information;
}

[DllImport( "ntdll.dll", CharSet = CharSet.Unicode )]
unsafe internal static extern uint NtSetInformationFile
(
IntPtr fileHandle,
ref IO_STATUS_BLOCK IoStatusBlock,
void* infoBlock,
uint length,
FILE_INFORMATION_CLASS fileInformation
);


Any light you can shed on the dumb thing I've done would be most appreciated.

EDIT:

At the risk of drawing more downvotes, I'll explain the context, without which, might have had some believing I was looking for a hack. It's a selective backup/restore program that lives in the midst of state-management software - mostly kiosks and POS terminals and library computers. Backup and restore operations happen in a pre-boot environment (WinPE).

What ended up working, with respect to using the function was the need to change the structure
FILE_LINK_INFORMATION
and a twist in the file naming. First, the working
FILE_LINK_INFORMATION
needs to go like this:

[StructLayout( LayoutKind.Sequential, CharSet = CharSet.Unicode )]
internal struct FILE_LINK_INFORMATION
{
[MarshalAs( UnmanagedType.U1 )]
public bool ReplaceIfExists;
public IntPtr RootDirectory;
public uint FileNameLength;
[MarshalAs( UnmanagedType.ByValTStr, SizeConst = MAX_PATH )]
public string FileName;
}


As Harry Johnson mentioned, the Pack=4 was wrong - and the marshalling of the bool needed to be a little different. The
MAX_PATH
is 260.

Then, when calling
NtSetInformationFile
in the context of a file that is opened with Read,Write, and Delete access and sharing:

unsafe bool CreateLink( DirectoryEntry linkEntry, DirectoryEntry existingEntry, SafeFileHandle existingFileHandle )
{
var statusBlock = new NativeMethods.IO_STATUS_BLOCK( );
var linkInfo = new NativeMethods.FILE_LINK_INFORMATION( );
linkInfo.ReplaceIfExists = true;
linkInfo.FileName = @"\??\" + storage.VolumeQualifiedName( streamCatalog.FullName( linkEntry ) );
linkInfo.FileNameLength = ( uint ) linkInfo.FileName.Length * 2;
var size = Marshal.SizeOf( linkInfo );
var buffer = Marshal.AllocHGlobal( size );
try
{
Marshal.StructureToPtr( linkInfo, buffer, false );
var result = NativeMethods.NtSetInformationFile
(
existingFileHandle.DangerousGetHandle( ),
statusBlock,
buffer,
( uint ) size,
NativeMethods.FILE_INFORMATION_CLASS.FileLinkInformation
);
if ( result != 0 )
{
Session.Emit( "{0:x8}: {1}\n{2}", result, linkInfo.FileName, streamCatalog.FullName( existingEntry ) );
}
return ( result == 0 );
}
finally
{
Marshal.FreeHGlobal( buffer );
}
}


Note, in particular, the namespace prefix - didn't work until I added that.

By the way, the
DirectoryEntry
describes a file that was supposed to be on disk as of the last backup.

With respect to not using
CreateHardLink
, as the original article describes, there was a vulnerability illustrated using
NtSetInformationFile
where there caller didn't need any particular permissions to add the link. Bummer! I suspect that when Microsoft closed the hole, they also introduced an issue with
CreateHardLink
. I will revisit this posting when I know more.

Answer

While I wouldn't recommend using the kernel API except as a last resort, I believe your immediate problem is that you are packing the FILE_LINK_INFO structure incorrectly.

You have specified packing of 4 bytes, which according to the documentation will put directoryHandle at an offset of 4. However, you are probably running on a 64-bit system, in which case the correct offset is 8.

I'm not certain how to fix this, but my best guess is that you need to use the default packing rules, i.e., not specify Pack at all. (Note that if you specify a packing of 8 bytes, FileName will presumably be put at offset 24 when it should be at offset 20.)