sp1d3r

Preventing deletes in the Windows Explorer

Introduction #

I know that we are a little unhappy with the Shift+Delete operation in explorer.exe. It can brutally delete your important files and folders and some times, we can’t even rely on the MFT table or tools like recuva for recovery. So, I’ve decided to solve this problem using my knowledge of hooking.

Approach #

I thought that explorer.exe calls the DeleteFile Windows API call on the selected item to delete it. But, this API call is used by the main process only to flush the shortcut cache in the %APPDATA%\Microsoft\Windows\Recent Items folder.

Now, I tried to catch the delete event in explorer. We know that, if we delete a file, we get a dialog box with the file details or we get the number of files we delete. So, I suspect the process to call DialogBoxParamW API call and it does.

Using a debugger like Windbg we get the following backtrace:

Backtrace
 # Child-SP          RetAddr               Call Site
00 00000000`0ad0ea08 00007ff9`bf5c962e     user32!DialogBoxParamW
01 00000000`0ad0ea10 00007ff9`bf4294c1     SHELL32!SHFusionDialogBoxParam+0x62
02 00000000`0ad0ea60 00007ff9`bf3f101a     SHELL32!CTransferConfirmation::Confirm+0xb1
03 00000000`0ad0eae0 00007ff9`bb2ec00d     SHELL32!CTransferConfirmationProxy::Confirm+0x13a
04 00000000`0ad0ec80 00007ff9`bb2e3005     windows_storage!TestHook_COperationStatusBroker_Confirm+0x33d
05 00000000`0ad0ed90 00007ff9`bb2e3404     windows_storage!CFileOperation::_Confirm+0xa9
06 00000000`0ad0ee30 00007ff9`bb2df71a     windows_storage!CFileOperation::_ConfirmOrResolve+0xb0
07 00000000`0ad0eef0 00007ff9`bb2e381b     windows_storage!CFileOperation::PromptUserOrQueue+0x812
08 00000000`0ad0f2b0 00007ff9`bb0aaf85     windows_storage!CCopyWorkItem::_ConfirmWorker+0x93
09 00000000`0ad0f320 00007ff9`bae22f63     windows_storage!CCopyWorkItem::_UpFrontDelete+0x23c881
0a 00000000`0ad0f3b0 00007ff9`bae245d2     windows_storage!CRecursiveFolderOperation::_UpFrontPrepareConfirmations+0x77
0b 00000000`0ad0f3e0 00007ff9`bae2c0c8     windows_storage!CRecursiveFolderOperation::Prepare+0x18e
0c 00000000`0ad0f4e0 00007ff9`bae2bb90     windows_storage!CFileOperation::_EnumRootPrepare+0x70
0d 00000000`0ad0f520 00007ff9`bae2bc2f     windows_storage!CFileOperation::_PrepareAllRoots+0x3c
0e 00000000`0ad0f550 00007ff9`bae2280a     windows_storage!CFileOperation::PrepareAndDoOperations+0x43
0f 00000000`0ad0f620 00007ff9`bf20d801     windows_storage!CFileOperation::PerformOperations+0xfa
10 00000000`0ad0f670 00007ff9`bf2276c9     SHELL32!DeleteItemsInDataObject+0x121
11 00000000`0ad0f700 00007ff9`bebb684d     SHELL32!DeleteItemsThreadProc+0xb9
12 00000000`0ad0f760 00007ff9`be9526ad     shcore!_WrapperThreadProc+
13 00000000`0ad0f840 00007ff9`bfa8a9f8     KERNEL32!BaseThreadInitThunk+0x1d
14 00000000`0ad0f870 00000000`00000000     ntdll!RtlUserThreadStart+0x28

Now, a few things are clear:

  1. The function SHELL32!DeleteItemsInDataObject is called with a pointer to an IDataObject which references the selected file(s). Using Ghidra, we can get the function definition:

    void __fastcall DeleteItemsInDataObject(HWND hwnd, unsigned int param2, void* param3, IDataObject* pdo) {
        // Perform a few checks
        // ...
        // Call SHPerformFileOperations(...)
    }
    

    (Just note that the calling conventions are kind of important when you are writing hooks) We can use the following code to extract file information from an IDataObject:

    namespace fs=std::filesystem;
    #define W(X) const_cast<char*>(X.c_str())
    
    void get_files_from_do(IDataObject *pDataObject, std::vector<fs::path> &dst) {
        FORMATETC formatEtc = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
        STGMEDIUM stgMedium = {TYMED_HGLOBAL, {nullptr}};
    
        auto hr = pDataObject->GetData(&formatEtc, &stgMedium);
        if (SUCCEEDED(hr)) {
            HDROP hDrop = (HDROP)stgMedium.hGlobal;
            UINT  fileCount = DragQueryFileA(hDrop, 0xFFFFFFFF, nullptr, 0);
            for (UINT i = 0; i < fileCount; i++) {
                std::string fp;
                fp.resize(MAX_PATH);
    
                UINT sz;
                if ((sz = DragQueryFileA(hDrop, i, W(fp), MAX_PATH)) > 0) {
                    fp.resize(sz);
                    dst.push_back(fs::path(fp));
                }
            }
    
            ReleaseStgMedium(&stgMedium);
        }
    }
    
  2. According to the API Documentation, user32!DialogBoxParamW is defined as:

INT_PTR  DialogBoxParamW(
    [in, optional] HINSTANCE hInstance,
    [in]           LPCWSTR   lpTemplateName,
    [in, optional] HWND      hWndParent,
    [in, optional] DLGPROC   lpDialogFunc,
    [in]           LPARAM    dwInitParam
);

The function lpDialogFunc which constantly receives events from the dialog box and is defined as follows:

INT_PTR Dlgproc(
    HWND hwnd,
    UINT message,
    WPARAM wParam,
    LPARAM lParam
) {
    switch(message) {
        case ...:
            ...
    }
}

Analyzing the messages sent to the above DlgProc, we can notice that:

  • If the user clicks a button, the message param is set to 0x111. If the user clicks Yes, the wParam is 0x6 otherwise, it is 0x2.

Upon some digging in ghidra, I found out that, the DlgProc function is actually shell32!CConfirmationDlgBase::s_ConfirmDialogProc

Therefore, we need to hook the above two undocumented functions to totally control the Delete operation in explorer.exe. I’ve used Microsoft Detours and frida (for testing) and built an application that does the thing. The application also handles errors in the hooks and reports them in the log files. I had to deal with a lot of stackoverflows and segmentation faults inside explorer.exe. But, Windows SEH came to the rescue. While C++ try...catch is not-so-great, Windows SEH handles the above errors perfectly. Sometimes you’ll need to eject the dll properly otherwise DetourDetach is not called, and the program jumps to some unallocated memory just when the hooked function is called.

Things to do #

The source code still lacks some functionality:

  1. The method used to get the GUID of a DLL and download symbols is inefficient and error prone. It has to be changed to a standard method.
  2. It can’t verify the integrity of the PDB’s and that means, we hook the wrong functions whenever we update.
  3. It can’t auto-inject into explorer.exe.

The above things will be implemented soon.

Source code: spider2048/Nodelete

comments powered by Disqus