By default, .NET System.IO.File.Open exclusively opens and NO_SHARE
locks files. While not regularly a problem for developers, this can
frustrate interested parties down the line, and here we are. There are
essentially three ways to get around an exclusive file lock and they
all suck.
- Ignore the lock via raw disk
access
This solution sucks; writing raw disk access applications requires
substantial development overhead and brings an entire universe of
gotchas with it. The Win32 API provides access to this option via
CreateFile on an MS-DOS device name [technically, it returns a Direct
Access Storage Device handle usable by DeviceIoControl and standard
Win32 APIs like WriteFile].
Note: 2008/Vista+
restrict direct disk access
- Ignore the lock via custom kernel driver
This approach sucks less, but writing an entire kernel driver as a
workaround to an exclusive lock sucks, not to mention security
implications or signing requirements. That said, Eldos apparently
recognized this problem and provide a commercial solution,
RawDisk, which
doesn't look terrible.
- Borrow the exclusively locked file handle
How much this approach sucks depends on the operating environment. A
truly generic solution is extremely difficult and will probably flag
antivirus someday. A highly specific solution can be quick and pretty
easy, but any changes in the program/environment risk breakage.
Analysis
<disclaimer>I'll refer to the other program as IE for
simplicity, but no browsers were harmed in the making of this article.
That said, don't forget local laws and regulations when testing these
techniques.</disclaimer>
In our scenario, IE rarely updates and we start in its thread of
execution using other techniques. The third option works best in these
circumstances given minimal development overhead, easy access to target
resources, and high specificity. This option imposes three
requirements; find the handle, ensure IE doesn't use the handle
concurrently, and restore the handle's original state (from IE's
perspective).
The entire process can be abstracted as the 5-step process below. IE
doesn't use the target file outside the thread of execution we live in,
allowing us to safely skip steps 1 and 5. If one were to include them,
the easiest solution is to suspend relevant threads during lock and
resume them during unlock.
1. Lock the Program
2. Acquire the Handle
3. Use the Handle (Read/write)
4. Reset/Release the Handle
5. Unlock the Program
Acquire the Handle
We'll avail ourselves of the most straightforward and simple method to
find the already opened handle; searching the open handles of every
running process. NTDLL.dll exposes the NtQuerySystemInformationi
interface which makes this fairly simple, although it is undocumented
and of questionable long-term viabilityii. The interface resembles
typical kernel development in that users allocate the buffer and it
returns failure if the buffer was too short, but a quick wrapper can
make the function straightforward.
pNtQuerySystemInformation NtQuerySystemInformation = NULL;
bool GetNtQuerySystemInformation()
{
if(NtQuerySystemInformation) return true;
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
LogAssert(!ntdll,false,"failed to load ntdll: "<<GetLastError());
NtQuerySystemInformation = (pNtQuerySystemInformation) GetProcAddress(ntdll, "NtQuerySystemInformation");
LogAssert(!NtQuerySystemInformation,false,"failed to load QuerySystemInfo: "<<GetLastError());
return true;
}
NTSTATUS QuerySystemInfo(SYSTEM_INFORMATION_CLASS SysClass, void **SysInfo, ULONG *SysLength)
{
if(!NtQuerySystemInformation) GetNtQuerySystemInformation();
ULONG StructSize = sizeof(ULONG) + 16*1024*sizeof(SysClass);
while(true)
{
*SysInfo = malloc(StructSize);
NTSTATUS status = NtQuerySystemInformation(SysClass,*SysInfo,StructSize,SysLength);
if(status != STATUS_BUFFER_TOO_SMALL && status != STATUS_INFO_LENGTH_MISMATCH) return status;
if(*SysLength > StructSize) StructSize = *SysLength;
else StructSize *= 2;
free(*SysInfo);
}
}
With that wrapper down, a SystemHandleInformation query will return an
array of all open handles held by any process. After filtering for IE's
handles, the problem of identifying which handle correlates to which
file appears. This was not as straightforward I originally imagined.
Internet advice suggested using DuplicateHandle to copy the handle, but
that does not [appear to] work for SHARE_NONE handles. The easiest fix
was to call
GetFileAttributesEx and
GetFileInformationByHandle
for 6 criteria to tell apart files, although it is technically possible
to receive a match on the wrong file. A FlushFileBuffers may be
necessary in some cases, such as when IE has written to the file.
bool sameFile(HANDLE F1, const char *F2)
{
WIN32_FILE_ATTRIBUTE_DATA FileAttribs;
BY_HANDLE_FILE_INFORMATION HandleInfo;
FlushFileBuffers(F1);
GetFileAttributesExA(F2,GetFileExInfoStandard,&FileAttribs);
if(!GetFileInformationByHandle(F1,&HandleInfo)) return false;
if(HandleInfo.dwFileAttributes != FileAttribs.dwFileAttributes) return false;
if(HandleInfo.nFileSizeHigh != FileAttribs.nFileSizeHigh) return false;
if(HandleInfo.nFileSizeLow != FileAttribs.nFileSizeLow) return false;
if(memcmp(&HandleInfo.ftCreationTime, &FileAttribs.ftCreationTime, sizeof(FileAttribs.ftCreationTime))) return false;
if(memcmp(&HandleInfo.ftLastAccessTime, &FileAttribs.ftLastAccessTime, sizeof(FileAttribs.ftLastAccessTime))) return false;
if(memcmp(&HandleInfo.ftLastWriteTime, &FileAttribs.ftLastWriteTime, sizeof(FileAttribs.ftLastWriteTime))) return false;
return true;
}
Put all together:
HANDLE GetProcessFileHandle(const char *Filename)
{
ULONG ProcessID = GetCurrentProcessId();
ULONG SysLength = 0;
ULONG *SysInfo = NULL;
SYSTEM_HANDLE_INFORMATION *Handles;
NTSTATUS status = QuerySystemInfo(SystemHandleInformation,(void**)&SysInfo,&SysLength);
LogAssert(status != STATUS_SUCCESS,INVALID_HANDLE_VALUE,"failed to query sysinfo "<<status);
ULONG entries = *SysInfo;
Handles = (SYSTEM_HANDLE_INFORMATION*)(SysInfo+1);
for(ULONG i=0;i<entries;i++)
{
if(Handles[i].ProcessId != ProcessID) continue;
if(Handles[i].ObjectTypeNumber != 28) continue;
if(!sameFile((HANDLE)Handles[i].Handle,Filename)) continue;
// probably the right file; return it!
HANDLE target = (HANDLE)Handles[i].Handle;
free(SysInfo);
return target;
}
LogError("no matching file handles found");
return INVALID_HANDLE_VALUE;
}
Use the Handle (Read/Write)
SetFilePointer makes using and resetting the handle easy; pass 0 offset with FILE_CURRENT and we have the starting state.
bool easyread(HANDLE fp, void *buf, u64 len)
{
DWORD read = 0;
if(ReadFile(fp,buf,(DWORD)len,&read,NULL) && read == len) return true;
LogErrno("failed easyread ("<<read<<" of "<<len<<" bytes): "<<GetLastError());
return false;
}
bool easyseek(HANDLE fp, u64 offset, DWORD method, u64 *offptr)
{
LONG seekHi(offset >> 32);
DWORD seekLo((DWORD)offset);
seekLo = SetFilePointer(fp,seekLo,&seekHi,method);
LogAssert(seekLo == INVALID_SET_FILE_POINTER,false,"easyseek ["<<offset<<"] failed: "<<GetLastError());
if(offptr) { *offptr = seekHi; *offptr = (*offptr << 32) + seekLo; }
return true;
}
bool parseFile(char *target, u64 *offset)
{
HANDLE fp = GetProcessFileHandle(target);
LogAssert(fp == INVALID_HANDLE_VALUE,false,"failed to open imgdb");
LogDebug("acquired handle: "<<fp);
u64 iniOffset(0), seekOffset(0);
LogAssert(!easyseek(fp,0,FILE_CURRENT,&iniOffset),false,"failed to get offset");
LogTrace("current position: "<<iniOffset);
if(offset && *offset) seekOffset = *offset;
LogAssert(!easyseek(fp,seekOffset),false,"failed to seek to start");
LogTrace("file pointer starting at "<<seekOffset);
try { while(true)
{
if(!extractInfo(fp)) break;
} } catch(...) { }
if(!easyseek(fp,iniOffset)) LogError("failed to reset fp offset");
return true;
}
- Kelson (kelson
@shysecurity.com)