skip to Main Content

I was taking a look at this Github project: https://github.com/LloydLabs/delete-self-poc

This project uses the SetFileInformationByHandle API (https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileinformationbyhandle) in a somewhat creative way to allow the deletion from disk of a locked file. I am attempting to implement this as part of a larger program, however I have ran into an issue when compiling for x86. I use mingw-w64 on a debian machine to compile my program and when doing compatibility checks for x86, I found a very strange issue.

#include "main.h"

static
HANDLE
ds_open_handle(
    PWCHAR pwPath
)
{
    return CreateFileW(pwPath, DELETE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
}

static
BOOL
ds_rename_handle(
    HANDLE hHandle
)
{
    FILE_RENAME_INFO fRename;
    RtlSecureZeroMemory(&fRename, sizeof(fRename));

    // set our FileNameLength and FileName to DS_STREAM_RENAME
    LPWSTR lpwStream = DS_STREAM_RENAME;
    fRename.FileNameLength = sizeof(lpwStream);
    RtlCopyMemory(fRename.FileName, lpwStream, sizeof(lpwStream));

    return SetFileInformationByHandle(hHandle, FileRenameInfo, &fRename, sizeof(fRename) + sizeof(lpwStream));
}

static
BOOL 
ds_deposite_handle(
    HANDLE hHandle
)
{
    // set FILE_DISPOSITION_INFO::DeleteFile to TRUE
    FILE_DISPOSITION_INFO fDelete;
    RtlSecureZeroMemory(&fDelete, sizeof(fDelete));

    fDelete.DeleteFile = TRUE;

    return SetFileInformationByHandle(hHandle, FileDispositionInfo, &fDelete, sizeof(fDelete));
}

int
main(
    int argc,
    char** argv
)
{
    WCHAR wcPath[MAX_PATH + 1];
    RtlSecureZeroMemory(wcPath, sizeof(wcPath));

    // get the path to the current running process ctx
    if (GetModuleFileNameW(NULL, wcPath, MAX_PATH) == 0)
    {
        DS_DEBUG_LOG(L"failed to get the current module handle");
        return 0;
    }

    HANDLE hCurrent = ds_open_handle(wcPath);
    if (hCurrent == INVALID_HANDLE_VALUE)
    {
        DS_DEBUG_LOG(L"failed to acquire handle to current running process");
        return 0;
    }

    // rename the associated HANDLE's file name
    DS_DEBUG_LOG(L"attempting to rename file name");
    if (!ds_rename_handle(hCurrent))
    {
        DS_DEBUG_LOG(L"failed to rename to stream");
        return 0;
    }

    DS_DEBUG_LOG(L"successfully renamed file primary :$DATA ADS to specified stream, closing initial handle");
    CloseHandle(hCurrent);

    // open another handle, trigger deletion on close
    hCurrent = ds_open_handle(wcPath);
    if (hCurrent == INVALID_HANDLE_VALUE)
    {
        DS_DEBUG_LOG(L"failed to reopen current module");
        return 0;
    }

    if (!ds_deposite_handle(hCurrent))
    {
        DS_DEBUG_LOG(L"failed to set delete deposition");
        return 0;
    }

    // trigger the deletion deposition on hCurrent
    DS_DEBUG_LOG(L"closing handle to trigger deletion deposition");
    CloseHandle(hCurrent);

    // verify we've been deleted
    if (PathFileExistsW(wcPath))
    {
        DS_DEBUG_LOG(L"failed to delete copy, file still exists");
        return 0;
    }

    DS_DEBUG_LOG(L"successfully deleted self from disk");
    return 1;
}

When compiling the base code found in the linked repository (and shown above) as x86, attempting to run the program fails at the SetFileInformationByHandle call in the ds_rename_handle function. Calling GetLastError() returns 123:

ERROR_INVALID_NAME

123 (0x7B)

The filename, directory name, or volume label syntax is incorrect.

The very bizarre part is that the program succeeds when ran from an Administrator prompt. Even stranger, compiling the same code for x64 works both in a normal and an Administrator prompt.

As a sanity check I copied the code verbatim over to VS2019 and compiled there, and the resulting x86 program was able to run without Administrator privileges.

The only changes to the source code made on the debian system were made in the header file:

#pragma once

#pragma comment(lib, "Shlwapi.lib")

#include <Windows.h>
#include <shlwapi.h>
#include <stdio.h>
#include <stdlib.h>

#define DS_STREAM_RENAME L":wtfbbq"
#define DS_DEBUG_LOG(msg) wprintf(L"[LOG] - %sn", msg)

Where <Windows.h> was changed to <windows.h> and the DS_DEBUG_LOG line changed to %ls so that the entire log message would print.

The GCC command used to compile for x86 was:

i686-w64-mingw32-gcc main.c -o delete32.exe -s -DUNICODE -Os -lshlwapi

I have tried removing all switches and compiling and it still fails.

As a note, the shlwapi library is only required for the very last call in main(), PathFileExistsW. I have commented out that portion and removed shlwapi from imports and from the gcc command to no effect.

The x64 gcc command which succeeded was:

x86_64-w64-mingw32-gcc main.c -o delete32.exe -s -DUNICODE -Os -lshlwapi

In the issues tab of the github repo there are some mentions of errors in the code which I have looked at separately. However I would desperately like to know why mingw is causing an issue with the 32 bit version of this program. Unfortunately "just compile with VS" isn’t an option, as I use a program to generate and compile the program that this bit of code will be part of on my linux machine.

Thanks for any insight.

2

Answers


  1. RtlCopyMemory(fRename.FileName, lpwStream, sizeof(lpwStream)); writes 4 or 8 bytes into a 2 byte buffer! Who knows where the remaining bytes are going, the program behavior is probably undefined.

    The concept of deleting a running exe like this might work but the code indicates that the author does not have a full understanding of Windows and C. You are better off rewriting it from scratch…

    Edit:

    After playing with your uploaded files I can with confidence say that it is the lack of a application manifest that causes it to fail when it is not elevated. Your vc file has a requestedExecutionLevel element which gives it Vista operating system context. If you remove the manifest resource in the vc exe it stops working. If you add a manifest to the mingw exe it starts working.

    Login or Signup to reply.
  2. the ds_rename_handle function is wrong implemented.

    the FILE_RENAME_INFO is variable size structure.

    as result declaration

    FILE_RENAME_INFO fRename;
    

    almost always wrong (it will ok only if FileName containing 1 or 2 symbols)

    really we need first calculate FileNameLength and then allocate PFILE_RENAME_INFO based on this
    as example:

    ULONG FileNameLength = (ULONG)wcslen(DS_STREAM_RENAME) * sizeof(WCHAR);
    
    ULONG dwBufferSize = FIELD_OFFSET(FILE_RENAME_INFO, FileName) + FileNameLength;
    
    PFILE_RENAME_INFORMATION fRename = (PFILE_RENAME_INFORMATION)alloca(dwBufferSize);
    

    so complete code for ds_rename_handle can be next:

    ULONG ds_rename_handle(HANDLE hHandle, PCWSTR DS_STREAM_RENAME)
    {
        ULONG FileNameLength = (ULONG)wcslen(DS_STREAM_RENAME) * sizeof(WCHAR);
        
        ULONG dwBufferSize = FIELD_OFFSET(FILE_RENAME_INFO, FileName) + FileNameLength;
        
        PFILE_RENAME_INFO fRename = (PFILE_RENAME_INFO)alloca(dwBufferSize);
        
        fRename->ReplaceIfExists = TRUE;
        fRename->RootDirectory = 0;
        fRename->FileNameLength = FileNameLength;
        memcpy(fRename->FileName, DS_STREAM_RENAME, FileNameLength);
    
        return SetFileInformationByHandle(hHandle, FileRenameInfo, 
            fRename, dwBufferSize) ? NOERROR : GetLastError();
    }
    

    but documentation of FILE_RENAME_INFO is very bad. unclear – in what form – full pathname, file name or a relative pathname – must be FileName ?!

    from my research – it must be full pathname only (not file name) or begin with a colon : ( The new name for the stream )

    much more better use NtSetInformationFile with FileRenameInformation

    compare description of FILE_RENAME_INFORMATION structure with FILE_RENAME_INFO !

    here exist detailed description – in what form FileName it should be.

    so i always use

    NTSTATUS ds_rename_handle_nt(HANDLE hHandle, PCWSTR DS_STREAM_RENAME)
    {
        ULONG FileNameLength = (ULONG)wcslen(DS_STREAM_RENAME) * sizeof(WCHAR);
    
        ULONG dwBufferSize = FIELD_OFFSET(FILE_RENAME_INFO, FileName) + FileNameLength;
    
        PFILE_RENAME_INFORMATION fRename = (PFILE_RENAME_INFORMATION)alloca(dwBufferSize);
    
        fRename->ReplaceIfExists = TRUE;
        fRename->RootDirectory = 0;
        fRename->FileNameLength = FileNameLength;
        memcpy(fRename->FileName, DS_STREAM_RENAME, FileNameLength);
    
        IO_STATUS_BLOCK iosb;
        return NtSetInformationFile(
            hHandle, &iosb, fRename, dwBufferSize, FileRenameInformation);
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search