s          ..                                 
    :8    < .z@8"`        ..                       
   .88     !@88E         @L             u.    u.   
  :888ooo  '888E   u    9888i   .dL   x@88k u@88c. 
-*8888888   888E u@8NL  `Y888k:*888. ^"8888""8888" 
  8888      888E`"88*"    888E  888I   8888  888R  
  8888      888E .dN.     888E  888I   8888  888R  
  8888      888E~8888     888E  888I   8888  888R  
 .8888Lu=   888E '888&    888E  888I   8888  888R  
 ^%888*     888E  9888.  x888N><888'  "*88*" 8888" 
   'Y"    '"888*" 4888"   "88"  888     ""   'Y"   
             ""    ""           88F                
                               98"                 
                             ./"                   
                            ~`                     
Musings from a mediocre hacker

The Not So Self Deleting Executable on 24h2

TL;DR :

When executing malware in contested territory clearing your tracks is very important. Hence the Lloyd Labs self delete technique which has had interpretations published by many researchers throughout the years.

Today we explore why this doesn’t work as expected in 24H2 and how to fix it!

Overview:

I was first made aware of this technique from a Xeet from JonasLyk.
https://x.com/jonasLyk/status/1350401461985955840

https://pbs.twimg.com/media/Er2W8NFXIAAWZ5a?format=png&name=4096x4096

It follows this pattern:

1. Open a file with DELETE desired access
2. Rename the unnamed primary :$DATA stream
3. Close the first handle
4. Open the original filename again with DELETE
5. Set the disposition to delete = true
6. Close the handle
7. File deleted

The public PoCs for this follow the same pattern.

- **`GetModuleFileNameW(NULL, wcPath, MAX_PATH)`**
    → Get path of current executable.

- **`ds_open_handle(wcPath)`**
    → Calls: `CreateFileW(wcPath, DELETE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)`
    → Opens the file with delete permissions.

- **`ds_rename_handle(hCurrent)`**
    → Allocates memory for `FILE_RENAME_INFO`
    → `RtlSecureZeroMemory(...)` to zero out struct
    → `RtlCopyMemory(...)` to copy `DS_STREAM_RENAME`
    → `SetFileInformationByHandle(hHandle, FileRenameInfo, pfRename, size)`
    → ==Renames file to alternate data stream (ADS).==

- **`CloseHandle(hCurrent)`**
    → Closes renamed handle.

- **`ds_open_handle(wcPath)`**
    → Reopens the file.

- **`ds_deposite_handle(hCurrent)`**
    → Sets `FILE_DISPOSITION_INFO.DeleteFile = TRUE`
    → `SetFileInformationByHandle(hHandle, FileDispositionInfo, &fDelete, sizeof(fDelete))`

- **`CloseHandle(hCurrent)`**
    → Triggers file deletion.

- **`PathFileExistsW(wcPath)`**
    → Verifies whether the file has been deleted.

Investigation:

While this technique works perfectly on Windows version 23H2 (for reasons we’ll explore later), Windows 11 24H2 exhibits unexpected behavior.

This image shows a comparison between Windows 23H2 (left) and Windows 11 24H2 (right). In 24H2, while the file appears empty, it actually still exists on disk its contents have merely been moved to an alternate data stream instead of being deleted. The data persists in this alternate stream rather than the default one, which defeats the purpose of self deletion.

Let’s examine what this looks like in Procmon.
23H2:

24H2:

Based on these observations, we can see that the technique produces different results on 24H2, specifically failing during the SetDispositionInformationFile call.

To investigate further, I downloaded the NTFS.sys samples for both 23H2 and 24H2 from https://winbindex.m417z.com/?file=ntfs.sys. After analyzing these files in Ghidra, I identified NtfsSetDispositionInfo as the function responsible for the error.

I then set up Kernel debugging on a fresh machine using NTSTATUS debugging. This allows NTFS to trigger a breakpoint when returning specific NTSTATUS codes. The required commands are ed Ntfs!NtfsStatusDebugFlags 2 followed by ed Ntfs!NtfsStatusBreakOnStatus 0xc0000121 to catch our error.
https://www.osr.com/blog/2018/10/17/ntfs-status-debugging/

https://www.osr.com/blog/2021/01/21/mitigating-the-i30bitmap-ntfs-bug/

Here there are a couple things to make note of:

This is the disassembly that leads to the breakpoint triggered by the error. The specific error code is 0xF216D. Microsoft provides these debugging codes to help track the exact sequence of events that cause the error.

Using WinDbg’s disassembly output, I matched the instructions to Ghidra’s code flow graph. I traced the execution path backward from the 0xF216D debug code to its origin.

I then compared this with the 23H2 code to identify differences. Though I attempted to use automated diffing tools (https://github.com/clearbluejar/ghidriff), my lack of experience with them led me to manually compare how 23H2’s NtfsSetDispositionInfo and 24H2’s version handled file deletion decisions.

I investigated numerous rabbit holes here that I would like to explore further in the future, such as how 24H2 improved the handling of setting delete disposition on directories rather than files.

Ideally, I would have set a breakpoint in a kernel debugging session for 23H2 to directly compare execution paths, but I hadn’t set up a VM for this purpose.

By using the POSIX SEMANTICS flag this allows the deletion to continue.

FILE_DISPOSITION_POSIX_SEMANTICS 0x00000002 Specifies the system should perform a POSIX-style delete. See more info in Remarks.

Here’s some code to do it! I uploaded the whole project to Github but this is the most important part:

FILE_DISPOSITION_INFORMATION_EX dispo = {};
dispo.Flags = FILE_DISPOSITION_DELETE | FILE_DISPOSITION_POSIX_SEMANTICS;

IO_STATUS_BLOCK iosb = {};
NTSTATUS status = NtSetInformationFile(hFile, &iosb, &dispo, sizeof(dispo), FileDispositionInformationEx);
if (status < 0) {
    DWORD err = RtlNtStatusToDosError(status);
    std::wcerr << L"[!] NtSetInformationFile failed. NTSTATUS: 0x" << std::hex << status
        << L", Win32: " << std::dec << err << std::endl;
    CloseHandle(hFile);
    HeapFree(GetProcessHeap(), 0, pRename);
    return FALSE;
}

https://github.com/MaangoTaachyon/SelfDeletion-Updated

Now working on 24H2!

BOF implementation demo:
https://youtu.be/Ai99vNO4nEY

I was made aware of this through a discord message and an issue that was raised in the Github repo
https://github.com/LloydLabs/delete-self-poc/issues/6

Extra info!

Big thanks to sixtyvividtails!