Sunday 18 September 2016

38. File Shredder

Introduction

This article describes a C style shredder in C# .NET.
Pizza van with tinted windows parked across the road for days? Strange clicking sound on the landline? Crooked CEO maybe? Then this is exactly the tool you have been searching for..

Background

I originally published v1 in VB6 as 'SDS' on Planet Source Code.

NShred 2.0

I decided to rewrite the SDS file shredder as one of my first forays into the C# .NET language, the reason being that it is a fairly compact and class-driven application. This time around, however, not being saddled with the limitations of the VB6 language, I was able to create a faster and more thorough application engine. ThecShredder class is almost completely Win32 driven, using virtual memory buffers and the WriteFile API to overwrite the file with several passes of 0s, 1s, and random data. After some initial preamble of file path checks, attribute stripping, and enabling key access tokens within the process, we create the buffer:
...
hFile = CreateFileW(pName, GENERIC_ALL, FILE_SHARE_NONE, 
        IntPtr.Zero, OPEN_EXISTING, WRITE_THROUGH, IntPtr.Zero);
// get the file size
nFileLen = fileSize(hFile);
if (nFileLen > BUFFER_SIZE)
    nFileLen = BUFFER_SIZE;
if (hFile.ToInt32() == -1)
    return false;
// set the table
SetFilePointerEx(hFile, 0, IntPtr.Zero, FILE_BEGIN);
pBuffer = VirtualAlloc(IntPtr.Zero, nFileLen, MEM_COMMIT, PAGE_READWRITE);
if (pBuffer == IntPtr.Zero)
    return false;
// fill the buffer with zeros
RtlZeroMemory(pBuffer, nFileLen);
...
Once the buffer is allocated and written to, call the overwriteFile method that uses WriteFile to overwrite the contents in buffered 'chunks'. Note that the file was opened with the WRITE_THROUGH flag, which causes the file to be written through the buffers and straight to disk. Also, all APIs used are of the 'W' flavor, so the shredder should be fully Unicode compliant.
private Boolean overwriteFile(IntPtr hFile, IntPtr pBuffer)
{
    UInt32 nFileLen = fileSize(hFile);
    UInt32 dwSeek = 0;
    UInt32 btWritten = 0;

    try
    {
        if (nFileLen < BUFFER_SIZE)
        {
            SetFilePointerEx(hFile, dwSeek, IntPtr.Zero, FILE_BEGIN);
            WriteFile(hFile, pBuffer, nFileLen, ref btWritten, IntPtr.Zero);
        }
        else
        {
            do
            {
                SetFilePointerEx(hFile, dwSeek, IntPtr.Zero, FILE_BEGIN);
                WriteFile(hFile, pBuffer, BUFFER_SIZE, ref btWritten, IntPtrZero);
                dwSeek += btWritten;
            } while ((nFileLen - dwSeek) > BUFFER_SIZE);
            WriteFile(hFile, pBuffer, (nFileLen - dwSeek), ref btWritten, IntPtr.Zero);
        }
        // reset file pointer
        SetFilePointerEx(hFile, 0, IntPtr.Zero, FILE_BEGIN);
        // add it up
        if ((btWritten + dwSeek) == nFileLen)
            return true;
        return false;
    }
    catch
    {
        return false;
    }
}
The buffers are filled first with zeros, then ones, random data, then zeros again; this ensures that even with the most sophisticated software techniques, using a modern hard drive, all data will be rendered permanently unreadable. The random data phase uses the Crypto API to fill the buffer; intended for secure key creation, it also works well in this implementation.
private Boolean randomData(IntPtr pBuffer, UInt32 nSize)
{
    IntPtr iProv = IntPtr.Zero;

    try
    {
        // acquire context
        if (CryptAcquireContextW(ref iProv, "", MS_ENHANCED_PROV, 
            PROV_RSA_FULL, CRYPT_VERIFYCONTEXT) != true)
            return false;
        // generate random block
        if (CryptGenRandom(iProv, nSize, pBuffer) != true)
            return false;
        return true;
    }
    finally
    {
        // release crypto engine
        if (iProv != IntPtr.Zero)
            CryptReleaseContext(iProv, 0);
    }
}
One thing that most open source shredders I have seen fail to do, is to verify the read on the file. This is accomplished by comparing the buffer with the file contents using RtlCompareMemory:
private Boolean writeVerify(IntPtr hFile, IntPtr pCompare, UInt32 pSize)
{
    IntPtr pBuffer = IntPtr.Zero;
    UInt32 iRead = 0;

    try
    {
        pBuffer = VirtualAlloc(IntPtr.Zero, pSize, MEM_COMMIT, PAGE_READWRITE);
        SetFilePointerEx(hFile, 0, IntPtr.Zero, FILE_BEGIN);
        if (ReadFile(hFile, pBuffer, pSize, ref iRead, IntPtr.Zero) == 0)
        {
            if (InError != null)
                InError(004, "The file write failed verification test.");
            return false; // bad read
        }
        if (RtlCompareMemory(pCompare, pBuffer, pSize) == pSize)
            return true; // equal
        return false;
    }
    finally
    {
        if (pBuffer != IntPtr.Zero)
            VirtualFree(pBuffer, pSize, MEM_RELEASE);
    }
}
After the overwrite cycles, the file is then zero sized ten times, and renamed thirty times:
private Boolean zeroFile(IntPtr pName)
{
    for (Int32 i = 0; i < 10; i++)
    {
        IntPtr hFile = CreateFileW(pName, GENERIC_ALL, FILE_SHARE_NONE,
            IntPtr.Zero, OPEN_EXISTING, WRITE_THROUGH, IntPtr.Zero);
        if (hFile == IntPtr.Zero)
            return false;
        SetFilePointerEx(hFile, 0, IntPtr.Zero, FILE_BEGIN);
        // unnecessary but..
        FlushFileBuffers(hFile);
        CloseHandle(hFile);
    }
    return true;
}

private Boolean renameFile(string sPath)
{
    string sNewName = String.Empty;
    string sPartial = sPath.Substring(0, sPath.LastIndexOf(@"\") + 1);
    Int32 nLen = 10;
    char[] cName = new char[nLen];
    for (Int32 i = 0; i < 30; i++)
    {
        for (Int32 j = 97; j < 123; j++)
        {
            for (Int32 k = 0; k < nLen; k++)
            {
                if (k == (nLen - 4))
                    sNewName += ".";
                else
                    sNewName += (char)j;
            }
            if (MoveFileExW(sPath, sPartial + sNewName, 
  MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0)
                sPath = sPartial + sNewName;
            sNewName = String.Empty;
        }
    }
    // last step: delete the file
    if (deleteFile(sPath) != true)
        return false;
    return true;
}
For the truly paranoid user, there is a hidden startup switch, '/p', that enables the paranoid mode. With this setting, after the overwrite cycles, the files object identifier is deleted, effectively orphaning the file from the file and security subsystems:
private Boolean orphanFile(IntPtr pName)
{
    UInt32 lpBytesReturned = 0;
    IntPtr hFile = CreateFileW(pName, GENERIC_WRITE, FILE_SHARE_NONE,
        IntPtr.Zero, OPEN_EXISTING, WRITE_THROUGH, IntPtr.Zero);
    if (DeviceIoControl(hFile, FsctlDeleteObjectId, IntPtr.Zero, 
        0, IntPtr.Zero, 0, out lpBytesReturned, IntPtr.Zero))
        return false;
    return true;
}

No comments:

Post a Comment