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:
The function
SHELL32!DeleteItemsInDataObject
is called with a pointer to anIDataObject
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); } }
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 to0x111
. If the user clicksYes
, thewParam
is0x6
otherwise, it is0x2
.
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:
- 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.
- It can’t verify the integrity of the PDB’s and that means, we hook the wrong functions whenever we update.
- It can’t auto-inject into
explorer.exe
.
The above things will be implemented soon.
Source code: spider2048/Nodelete