September 11th, 2023

Any sufficiently advanced uninstaller is indistinguishable from malware

There was a spike in Explorer crashes that resulted in the instruction pointer out in the middle of nowhere.

0:000> r
eax=00000001 ebx=008bf8aa ecx=77231cf3 edx=00000000 esi=008bf680 edi=008bf8a8
eip=7077c100 esp=008bf664 ebp=008bf678 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
7077c100 ??              ???

Maybe the return address tells us something.

0:000> u poi esp
008bf6d4 test    eax,eax
008bf6d6 je      008bf6b9
008bf6d8 xor     edi,edi
008bf6da cmp     dword ptr [esi+430h],edi

It’s strange that we’re executing from someplace that has no name. If you look closely, you’ll see that we are executing code from the stack: esp is 008bf664, so the code that went haywire is on the stack.

Who executes code from the stack?

Malware, that’s who.

Let’s see what this malware is trying to do.

Disassembling around the last known good code address gives us this:

008bf6c4 call    dword ptr [esi+214h]
008bf6ca inc     dword ptr [ebp+8]
008bf6cd push    edi
008bf6ce call    dword ptr [esi+210h]   ; this called into space
008bf6d4 test    eax,eax
008bf6d6 je      008bf6b9
008bf6d8 xor     edi,edi
008bf6da cmp     dword ptr [esi+430h],edi
008bf6e0 je      008bf70d

It looks like the payload stored function pointers at esi+210 and esi+214. Let’s see what’s there. This is probably where the payload stashed all its call targets.

0:000> dps @esi+200
008bf880  1475ff71
008bf884  00000004
008bf888  76daecf0 kernel32!WaitForSingleObject
008bf88c  76daeb00 kernel32!CloseHandle
008bf890  7077c100
008bf894  76dada90 kernel32!SleepStub
008bf898  76db6a40 kernel32!ExitProcessImplementation
008bf89c  76daf140 kernel32!RemoveDirectoryW
008bf8a0  76da6e30 kernel32!GetLastErrorStub
008bf8a4  770d53f0 user32!ExitWindowsEx
008bf8a8  003a0043
008bf8ac  0050005c
008bf8b0  006f0072
008bf8b4  00720067
008bf8b8  006d0061

Yup, there’s a payload of function pointers here. It looks like this malware is going to wait for something, and then exit the process, or remove a directory, or exit Windows. Those bytes after user32!ExitWindowsEx look like a Unicode string, so let’s dump them as a string:

0:000> du 008bf8a8  
008bf8a8  "C:\Program Files\Contoso\contoso_update.exe"

Wait, what? It is trying to mess around with Contoso’s auto-updater?

Let’s take a look at more of the malware payload. Maybe we can figure out what it’s doing. It looks like it’s using esi as its base of operations, so let’s disassemble from esi.

008bf684 push    ebp                        ; build stack frame
008bf685 mov     ebp,esp
008bf687 push    ebx                        ; save ebx
008bf688 push    esi                        ; save esi
008bf689 mov     esi,dword ptr [ebp+8]      ; parameter
008bf68c push    edi                        ; save edi
008bf68d push    0FFFFFFFFh                 ; INFINITE
008bf68f push    dword ptr [esi+204h]       ; data->hProcess
008bf695 lea     ebx,[esi+22Ah]             ; address of path + 2
008bf69b call    dword ptr [esi+208h]       ; WaitForSingleObject
008bf6a1 push    dword ptr [esi+204h]       ; data->hProcess
008bf6a7 call    dword ptr [esi+20Ch]       ; CloseHandle

008bf6ad and     dword ptr [ebp+8],0        ; count = 0
008bf6b1 lea     edi,[esi+228h]             ; address of path
008bf6b7 jmp     008bf6cd                   ; enter loop
008bf6b9 cmp     dword ptr [ebp+8],28h      ; waited too long?
008bf6bd jge     008bf6d8                   ; then stop
008bf6bf push    1F4h                       ; 500
008bf6c4 call    dword ptr [esi+214h]       ; Sleep
008bf6ca inc     dword ptr [ebp+8]          ; count++
008bf6cd push    edi                        ; path
008bf6ce call    dword ptr [esi+210h]       ; DeleteFile
008bf6d4 test    eax,eax                    ; Q: Did it delete?
008bf6d6 je      008bf6b9                   ; N: Loop and try again

008bf6d8 xor     edi,edi
008bf6da cmp     dword ptr [esi+430h],edi   ; data->fRemoveDirectory?
008bf6e0 je      008bf70d                   ; N: Skip
008bf6e2 jmp     008bf6f0                   ; Enter loop for trimming file name
008bf6e4 cmp     ax,5Ch                     ; Q: Backslash?
008bf6e8 jne     008bf6ed                   ; N: Ignore
008bf6ea mov     dword ptr [ebp+8],ebx      ; Remember location of last backslash
008bf6ed add     ebx,2                      ; Move to character
008bf6f0 movzx   eax,word ptr [ebx]         ; Fetch next character
008bf6f3 cmp     ax,di                      ; Q: End of string?
008bf6f6 jne     008bf6e4                   ; N: Keep looking

008bf6f8 mov     ecx,dword ptr [ebp+8]      ; Get location of last backslash
008bf6fb xor     eax,eax                    ; eax = 0
008bf6fd mov     word ptr [ecx],ax          ; Terminate string at last backslash
008bf700 lea     eax,[esi+228h]             ; Get path (now without file name)
008bf706 push    eax                        ; Push address
008bf707 call    dword ptr [esi+21Ch]       ; RemoveDirectory

008bf70d cmp     dword ptr [esi+434h],edi   ; data->fExitWindows?
008bf713 je      008bf71e                   ; N: Skip
008bf715 push    edi                        ; dwReason = 0
008bf716 push    12h                        ; EWX_REBOOT | EWX_FORCEIFHUNG
008bf718 call    dword ptr [esi+224h]       ; ExitWindowsEx

008bf71e push    edi                        ; dwExitCode = 0
008bf71f call    dword ptr [esi+218h]       ; ExitProcess
008bf725 pop     edi
008bf726 pop     esi
008bf727 pop     ebx
008bf728 pop     ebp
008bf729 ret

; This code appears to be unused
008bf72a push    ebp
008bf72b mov     ebp,esp
008bf72d push    esi
008bf72e mov     esi,dword ptr [ebp+10h]
008bf731 test    esi,esi
008bf733 jle     008bf746
...

Reverse-compiling back to C, we have

struct Data
{
    char code[0x0204];
    HANDLE hProcess;
    DWORD (CALLBACK* WaitForSingleObject)(HANDLE, DWORD);
    BOOL (CALLBACK* CloseHandle)(HANDLE);
    DWORD (CALLBACK* MysteryFunction)(PCWSTR);
    void (CALLBACK* Sleep)(DWORD);
    void (CALLBACK* ExitProcess)(UINT);
    BOOL (CALLBACK* RemoveDirectoryW)(PCWSTR);
    DWORD (CALLBACK* GetLastError)();
    BOOL (CALLBACK* ExitWindowsEx)(UINT, DWORD);
    wchar_t path[MAX_PATH];
    BOOL fRemoveDirectory;
    BOOL fExitWindows;
};
void Payload(Data* data)
{
    // Wait for the process to exit
    data->WaitForSingleObject(data->hProcess, INFINITE);
    data->CloseHandle(data->hProcess);

    // Try up to 20 seconds to do something with the file
    for (int count = 0;
        !data->MysteryFunction(data->path) && count < 40;
        count++) {
        Sleep(500);
    }

    if (data->fRemoveDirectory) {
        PWSTR p = &data->path[1];
        PWSTR lastBackslash = p;
        while (*p != L'\0') {
            if (*p == L'\\') lastBackslash = p;
            p++;
        }
        *lastBackslash = L'\0';
        RemoveDirectoryW(data->path);
    }

    if (data->fExitWindows) {
        ExitWindowsEx(EWX_REBOOT | EWX_FORCEIFHUNG, 0);
    }
}

Aha, this isn’t malware. This is an uninstaller!

The mystery function is almost certainly DeleteFileW. It’s waiting for the main uninstaller to exit, so it can delete the binary.

There is a page on CodeProject that shows how to write a self-deleting file, and it seems that multiple companies have decided to use that code to implement their own uninstallers. (Whether they follow the licensing terms for that code I do not know.)

Okay, so why did we crash? What went wrong with DeleteFileW?

According to the dump file, the spot where DeleteFileW was supposed to be instead holds 7077c100. This is a function pointer into some mystery DLL that isn’t loaded. How did that happen?

My guess is that the DeleteFileW function was detoured in the Contoso uninstaller. When the uninstaller tried to built its table of useful functions, it ended up getting not the address of DeleteFileW but the address of a detour. It then tried to call that detour from its payload, but since the detour is not installed in Explorer (or if it is, the detour is in some other location), it ended up calling into space.

Neither code injection nor detouring is officially supported. I can’t tell who did the detouring. Maybe somebody added a detour to the uninstaller, unaware that the uninstaller is going to inject a call to the detour into Explorer. Or maybe the detour was injected by anti-malware software. Or maybe the detour was injected by Windows’ own application compatibility layer. Whatever the reason, the result was a crash in Explorer.

Which means that people like me spend a lot of time studying these crashes to figure out what is going on, only to conclude that they were caused by other people abusing the system.

If you want to create a self-deleting binary, please don’t use code injection into somebody else’s process. Here’s a way to delete a binary and leave no trace:

Create a temporary file called cleanup.js that goes like this:

var fso = new ActiveXObject("Scripting.FileSystemObject");
fso.DeleteFile("C:\\Users\\Name\\AppData\\Local\\Temp\\cleanup.js");

var path = "C:\\Program Files\\Contoso\\contoso_update.exe";
for (var count = 0; fso.FileExists(path) && count < 40; count++) {
    try { fso.DeleteFile(path); break; } catch (e) { }
    WSH.Sleep(500);
}

This script deletes itself and then tries to delete contoso_update.exe for 20 seconds. Run it with wscript cleanup.js and let it do its thing. No code injection, no detours, all documented.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

27 comments

Discussion is closed. Login to edit/delete existing comments.

  • Bruce Davidson

    I haven’t used an uninstaller program in years. Probably not since I stopped using Windows. Seems like poor OS design is the cause of this.

  • Marek Knápek

    I was thinking about such uninstaller recently. And came up with an idea: Uninstaller is a DLL, because running EXE cannot be deleted. That DLL gets loaded by rundll.exe. When invoked it allocates executable memory and copies (part of) itself over there. Possibly also allocates and copies both read-only and read-write sections of itself. Does pointer relocation fixups, jumps to the new code. Now the new code can unload the old self (the DLL to...

    Read more
    • Raymond ChenMicrosoft employee Author

      And then when there’s a problem with the code, there will be a spike of crashes in rundll.exe, and I will have to investigate them and come to the conclusion that they are coming from another “sufficiently advanced uninstaller”.

      • Joshua Hudson

        I actually did this. The 32 bit version builds a ROP chain and uses a single ASM instruction to enter it. All code frames are inside kernel32.dll (now kernelbase.dll) This works because in 32 bit, all arguments are on the stack so I can just return into the entry point of the next function.

        The 64 bit version however can't do that, so the asm code is allocated with VirtualAlloc and looks like this:

        ```
        ...

        Read more
  • Youfu Zhang

    I don’t like the idea of invoking wscript from uninstaller. Windows Script Host may be disabled on some hardened PC.
    Under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Script Host\Settings, create a new DWORD value named “Enabled” and set the value data to “0”.
    A pop up will show that “Windows Script Host access is disabled on this computer. Contact your administrator for details.”

  • Dan Bugglin · Edited

    If a jscript dependency is somehow a concern, you can fairly easily rewrite the script in batch. Then all you need is cmd.exe which everyone has.

    A bit of IF EXISTS, a bit of DEL, a GOTO loop and label, and finally run cmd.exe with no window to keep things nice and tidy.

    The only bit that could be trouble is there's no built in delay function. But it's fairly easy to find a command that implements...

    Read more
    • Chris Iverson

      “The only bit that could be trouble is there’s no built in delay function.”

      No built-in delay function? Does the TIMEOUT command not work, for some reason?

  • Shawn Van Ness

    Is there really no registry key or API to call, to have Windows cleanup the last bits, after custom uninstall process completes? I suppose I always assumed there was.. but I’ve never implemented an uninstaller, obvs.

    • Dan Bugglin · Edited

      MoveFileEx can, but it defers to next boot though. This is mostly useful for removing or replacing files that are in use at the time you’re trying to remove/replace them . Many installers or uninstallers use it for that purpose (any installer that asks you to reboot usually does so because it has done this, especially if you find the app in question does not work until you actually reboot).

  • Marcus André

    Insane way for simple tasks, lol. Usually in my case I just call the cmd with an safe timeout to delete the remaining files and never got trouble.

  • Georg Rottensteiner · Edited

    Noone?

    I’ve always used the age old method of the self deleting batch file to finally remove the uninstaller and then itself.
    Write a batch file to the temp folder, do your work and as last line a del on itself, without(!) line break.

    Voilá, everything gone, as intended.

    Works a treat since Win 3.x.

    • skSdnW

      Batch files can be disabled by Group Policy.

  • Brian Friesen

    Could a process mark itself for deletion using SetFileInformationByHandle() with FileDispositionInfo? I have not tried this, but if it worked when the last handle for the process was closed the system would delete the file. The same as if the file were created/opened with FILE_DELETE_ON_CLOSE. I don't think this requires special permissions either, as long as you could delete the file if it wasn't busy then you should be able to mark...

    Read more
    • skSdnW

      If FileDispositionInfo or FILE_DELETE_ON_CLOSE worked on a running exe file then neither this blog post nor the injection code would exist in the first place.

      CreateProcess does not open the file with FILE_SHARE_DELETE to stop you from doing this in the name of compatibility (with DOS?, Windows 3.x?).

      • LB

        You can create an executable file with the flag set to delete it on close, and then before closing the last handle to it, you can execute it. Then you can exit your own process, and that executable you created will be deleted once it exits too since that will close the last handle.

  • Michael Taylor

    In my experience most IT groups block VBS on machines because of security issues.

    Personally I always found it easier to just start a new batch script (or perhaps PS these days) that polled the desired file into it could be deleted. But that is only for cases where I needed the file deleted ASAP.

    I think the preferred approach is to use `PendingFileRenameOperations` to schedule the file for removal on restart. If the user opts to...

    Read more
    • skSdnW

      PendingFileRenameOperations (MoveFileEx) only works for administrators since it needs to write to HKLM, not usable for things installed in FOLDERID_UserProgramFiles.

      • Michael Taylor

        As does most other approaches that need to clean up files. Since there was no contextual information about user privileges the solution I used to use (pre per-user installs) is sufficient. Note that in modern IT systems you also cannot run scripts of any sort without being an admin (and even then) and JS doesn't work outside the sandbox unless you have Java installed (which, again most computers won't for security reasons). So the solution...

        Read more
      • Raymond ChenMicrosoft employee Author

        Not sure what you mean by “JS doesn’t work outside the sandbox unless you have Java installed.” The wscript program happily runs JS outside any sandbox. Go ahead and create a FileSystemObject like the one in the article. (And Java is completely unrelated to Javascript.)