Any sufficiently advanced uninstaller is indistinguishable from malware

Raymond Chen

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.

27 comments

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

  • skSdnW 1

    Vbscript is on its way to becoming a not-installed-by-default optional component, does this apply to Jscript as well?

    FILE_DISPOSITION_POSIX_SEMANTICS almost helps.

    Why not just give people what they want/need? BOOL WINAPI DeleteSelf(BOOL TryDeleteDirectory);

    • Jan Ringoš 0

      I usually move myself (as in the uninstaller.exe) to Temp, with randomized filename, and schedule deletion on Restart.

    • Piotr Siódmak 0

      What if a DLL calls it? How do you define “Self” (not in a philosophical manner)?

      • skSdnW 0

        The current process main module (.exe). I’m guessing you feel like it should have a HMODULE parameter? Microsoft has refused to provide this function for over 20 years, I doubt we will get it any time soon.

        • Piotr Siódmak 0

          Would it really be OK for a plugin (DLL) to be able to order the host to self-destruct? Sounds like a security nightmare.
          Would a user-scoped joke.dll be able to make system-wide rundll32.exe to remove itself?

          • skSdnW 0

            A normal user can’t delete rundll32.exe when it is not running, why would this be any different? DeleteSelf would just ask the filesystem to remove the directory entry for the file with the same access checks as DeleteFile and mark the handle as delete-on-close.

  • Alex Guo 1

    Ah uninstallers… Had some fun with those.

    When our product’s uninstaller sees an undeletable file (possibly a DLL loaded in another process), it uses the MOVEFILE_DELAY_UNTIL_REBOOT flag to mark it for deletion, and warns the user “Please reboot as soon as possible to remove the remaining files.”

    However, some user uninstall our product, just to be able to reinstall it later, to the same location. And of course they ignored the warning. Once they reinstalled it, everything works, until a reboot.

    Imagine figuring that out without knowing the uninstaller trick. The install log says everything is OK, and the software log initially shows nothing wrong, but all of a sudden, some files just disappear into thin air.

    We finally managed to fix it by first renaming that undeletable file to “something.del”, and then marking that for deletion. We also added a registry check in our installer. If it sees a PendingFileRenameOperations entry located in the install destination, but without the “del” extension, it will abort.

    (We are also experimenting with MSIX! However it changes everything a bit more than we currently can handle.)

  • Michael Taylor 0

    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 remove the file before a reboot then that is up to them but it will eventually get cleaned up and works as the system was intending. It is also not reliant on script settings.

    • skSdnW 1

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

      • Michael Taylor 0

        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 you are recommending doesn’t work for a normal user either.

        • Raymond ChenMicrosoft employee 3

          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.)

  • Brian Friesen 0

    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 it for deletion on close when it is busy.

    • skSdnW 0

      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 0

        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.

  • Georg Rottensteiner 0

    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 0

      Batch files can be disabled by Group Policy.

  • Richard Deeming 1

    Who needs JavaScript when you can have a self-deleting batch file? 🙂

    https://stackoverflow.com/a/20333575/124386

    (goto) 2>nul & del "%~f0"
  • Marcus André 0

    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.

  • Shawn Van Ness 0

    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 0

      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).

  • Dan Bugglin 0

    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 a delay itself and abuse the hell out of it for our own purposes.

    ping.exe will work just fine. You just need to craft a command you know will always fail via timeout, and do a single ping with your desired delay as the timeout.

    Turns out localhost subnet’s multicast IP address works fine for this. Multicast addresses are reserved and can’t actually point to a single device. And apparently ICMP doesn’t multicast, I guess.

    ping -n 1 -w [delay in milliseconds] 127.255.255.255

    Another completely unrelated approach is to use MoveFileEx with no new file name and MOVEFILE_DELAY_UNTIL_REBOOT flag, to indicate the file should be deleted next boot (If you’re patient.)

    • Chris Iverson 0

      “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?

  • Youfu Zhang 1

    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.”

  • Marek Knápek 0

    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 be deleted), delete itself from disk and finally exit the rundll.exe process. This is maybe too much overcomplicated, but I cannot see any reason why this could not work. I didn’t implement this, yet.

    • Raymond ChenMicrosoft employee 0

      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 1

        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:

        “`
        push rbp
        mov rbp, rsp
        sub rsp, 32
        mov rbi, rcx
        .x mov rax, [rbx]
        mov rcx, [rbx + 8]
        call rax
        add rbx, 16
        jmp short .x
        “`

        The problem is there’s no way to register this function for dynamic execution because the unwind is not encodable in the tables at all. (It looks like you could encode correct the stack, pop rbp, and return but you can’t because the after the fist call instruction the next stack frame is *gone*.) SInce crash the process is the right thing to do if there’s any exception thrown from `FreeLibrary`, `DeleteFile`, `RemoveDirctory`, or `ExitProcess`, it doesn’t actually matter.

        As the guy said above, RemoveSelf() would have been a decent API; however I don’t _quite_ favor that route. Better would be to add IMAGE_FILE_RUN_FROM_SWAP to go with IMAGE_FILE_NET_RUN_FROM_SWAP that means always copy this binary to swap and run from there; *never* keep a lock on this binary while it’s running.

        Note that the wscript version doesn’t work reliably because wscript might not be there, or the code may be running from the service desktop or something more exotic.

  • Bruce Davidson 1

    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.

Feedback usabilla icon