None
Going Below Exclusive Locks
WinXP Exclusive File Lock Workaround
The previous article addressed working around Exclusive File Locks in Windows XP by directly accessing a target's in-memory file handle. This quick solution meant minimal development time and immediate results, but also guaranteed periodic crashes due to the potential race condition. Specifically, we couldn't ensure the application didn't read or write while we were reading/writing with its file handle.

As recap from last article, .NET System.IO.File.Open opens files exclusively locks by default. This has made a lot of people very angry and been widely regarded as a bad move. We previously presented three routes to workaround the problem, relisted below,  but today we'll take option 1 and go beneath the Exclusive Lock to access our data.

  1. Ignore the lock via raw disk access
  2. Ignore the lock via custom kernel driver
  3. Borrow the exclusively locked file handle

Analysis

<disclaimer>As before, I'll refer to the 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>

Fortunately, Windows XP makes raw disk access pretty straight forward. Executing CreateFile with the volume name will return a handle capable of reading every sector on the disk, even without Administrator rights!

HANDLE hVolume = CreateFile(L"\\\\.\\C:",GENERIC_READ,FILE_SHARE_READ|FILE_SHARE_WRITE, NULL,
                   OPEN_EXISTING, FILE_ATTRIBUTE_READONLY | FILE_FLAG_BACKUP_SEMANTICS, NULL);

Unfortunately, this is where everything gets dicey. One problem is that the no-permission access is curtailed in Vista and above, but targeting Windows XP has saved us from that risk. The bigger problem is that Windows won't parse the returned bytes and, to get what we want, that means we must parse raw NTFS (New Technology File System). This is non-trivial work that would require many, many hours of development and by itself explains why we initially took option 3. In times such as these, Google is highly recommended to find another poor sap who sunk the time to make a semi-working solution.

That poor sap, this time, was cyb70289 (courtesy of CodeProject). cyb70289 developed a basic parser for NTFS to support reading (and potentially writing) to raw disks encoded with NTFS. This tool provided a huge stepping stone to get around the file lock, except that we found some reads "randomly" crashed the system. An hour of debugging NTFS reads later, we had found two subtle bugs due to limitations to raw disk reads imposed by Windows. First, raw disk reads must be sector aligned. Second, and more insidiously, raw disk reads must read into sector aligned buffers. These bugs, and several others, are resolved in the linked library below, but I'd note a third "bug". The library isn't truly header files; NTFS_FileRecord contains real code that restricts including it twice in the same project. Workaround as needed.

Example

struct RawHandle
{
class CNTFSVolume  *Volume;
class CFileRecord  *File;
class CAttrBase *Data;
u64   DataOffset;
};

RawHandle *GetRawHandle(const char *Filename)
{
RawHandle *file = new RawHandle;
if(!file)  return NULL;

file->Volume = new CNTFSVolume(wchar_t(Filename[0]));
if(!file->Volume) { delete file; return NULL; }
if(!file->Volume->IsVolumeOK()) { delete file; return NULL; }

file->File = new CFileRecord(file->Volume);
if(!GetRawFileByPath(file->File,Filename)) {delete file->Volume; delete file; return NULL; }

file->Data = (CAttrBase*)file->File->FindStream();
if(!file->Data) { delete file->Volume; delete file->File; delete file; return NULL; }

file->DataOffset = 0;
return file;
}

bool GetRawFileByPath(CFileRecord *File, const char *Filename)
{
if(Filename[1] != ':') return false;
if(Filename[2] != '\\') return false;

File->SetAttrMask(MASK_INDEX_ROOT | MASK_INDEX_ALLOCATION);
if(!File->ParseFileRecord(MFT_IDX_ROOT)) return false;
if(!File->ParseAttrs()) return false;

char *context = NULL;
char path[32*1024]; // max per MSDN, but not really
strncpy_s(path,Filename+3,_TRUNCATE);
for(char *token = strtok_s(path,"\\",&context);token;token = strtok_s(NULL,"\\",&context))
{
size_t bytes;
CIndexEntry entry;
wchar_t wide[MAX_PATH+1];
errno_t err = mbstowcs_s(&bytes,wide,sizeof(wide)/sizeof(wide[0]),token,_TRUNCATE);
if(err) return false;
if(!File->FindSubEntry(wide,entry)) return false;
if(!File->ParseFileRecord(entry.GetFileReference())) return false;
if(!File->ParseAttrs()) return false;
}
if(!File->IsDirectory())
{
File->SetAttrMask(MASK_ALL);
if(!File->ParseAttrs()) fprintf(stderr,"failed MASK_ALL file reparse");
}
return true;
}

bool RawRead(RawHandle *File, void *Buf, size_t Len)
{
ULONGLONG read = 0;
if(!File->Data->ReadData(File->DataOffset,Buf,Len,&read) || read < Len) // make sure we read at least the requested amount
{
return false;
}
return true;
}

int main(int argc, char **argv, char **env)
{
RawHandle *raw = GetRawHandle("C:\\dir1\\dir2\\file");
if(!raw) return 1;

char buf[1024];
if(RawRead(raw,buf,sizeof(buf))) return 2;

printf("file contents: %s\n",buf);
return 0;
}

Comments

Reading directly from disk circumvents a lot of headaches with file permissions, but introduces new headaches such as enormously slow small reads and differences between files on disk and memory due to OS-level caching. The first can, and should, be circumvented by reading data into a cache, but that is left as an exercise to the reader. The second proved more resistant to attack, but documentation stated that FlushFileBuffers on the volume handle caused all cached data to write to disk. My tests did not support that notion. A combination of FlushFileBuffers and appropriate time delays did circumvent the problem though. On the whole, this is a neat "little" workaround which is fading away due to smarter security practices.

NTFSlLib

- Kelson (kelson@shysecurity.com)