None
Breaking the Exclusive Lock
WinXP Exclusive File Lock Workaround
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.

  1. 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

  1. 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.

  1. 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)