{"id":108749,"date":"2023-09-11T07:00:00","date_gmt":"2023-09-11T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=108749"},"modified":"2023-09-11T06:53:49","modified_gmt":"2023-09-11T13:53:49","slug":"20230911-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20230911-00\/?p=108749","title":{"rendered":"Any sufficiently advanced uninstaller is indistinguishable from malware"},"content":{"rendered":"<p>There was a spike in Explorer crashes that resulted in the instruction pointer out in the middle of nowhere.<\/p>\n<pre>0:000&gt; r\r\neax=00000001 ebx=008bf8aa ecx=77231cf3 edx=00000000 esi=008bf680 edi=008bf8a8\r\neip=7077c100 esp=008bf664 ebp=008bf678 iopl=0         nv up ei pl zr na pe nc\r\ncs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246\r\n7077c100 ??              ???\r\n<\/pre>\n<p>Maybe the return address tells us something.<\/p>\n<pre>0:000&gt; u poi esp\r\n008bf6d4 test    eax,eax\r\n008bf6d6 je      008bf6b9\r\n008bf6d8 xor     edi,edi\r\n008bf6da cmp     dword ptr [esi+430h],edi\r\n<\/pre>\n<p>It&#8217;s strange that we&#8217;re executing from someplace that has no name. If you look closely, you&#8217;ll see that we are executing code <i>from the stack<\/i>: <code>esp<\/code> is <code>008bf664<\/code>, so the code that went haywire is on the stack.<\/p>\n<p>Who executes code from the stack?<\/p>\n<p>Malware, that&#8217;s who.<\/p>\n<p>Let&#8217;s see what this malware is trying to do.<\/p>\n<p>Disassembling around the last known good code address gives us this:<\/p>\n<pre>008bf6c4 call    dword ptr [esi+214h]\r\n008bf6ca inc     dword ptr [ebp+8]\r\n008bf6cd push    edi\r\n008bf6ce call    dword ptr [esi+210h]   ; this called into space\r\n008bf6d4 test    eax,eax\r\n008bf6d6 je      008bf6b9\r\n008bf6d8 xor     edi,edi\r\n008bf6da cmp     dword ptr [esi+430h],edi\r\n008bf6e0 je      008bf70d\r\n<\/pre>\n<p>It looks like the payload stored function pointers at <code>esi+210<\/code> and <code>esi+214<\/code>. Let&#8217;s see what&#8217;s there. This is probably where the payload stashed all its call targets.<\/p>\n<pre>0:000&gt; dps @esi+200\r\n008bf880  1475ff71\r\n008bf884  00000004\r\n008bf888  76daecf0 kernel32!WaitForSingleObject\r\n008bf88c  76daeb00 kernel32!CloseHandle\r\n<span style=\"border: solid 1px currentcolor; border-bottom: none;\">008bf890  7077c100<\/span>\r\n<span style=\"border: solid 1px currentcolor; border-top: none;\">008bf894  76dada90<\/span> kernel32!SleepStub\r\n008bf898  76db6a40 kernel32!ExitProcessImplementation\r\n008bf89c  76daf140 kernel32!RemoveDirectoryW\r\n008bf8a0  76da6e30 kernel32!GetLastErrorStub\r\n008bf8a4  770d53f0 user32!ExitWindowsEx\r\n008bf8a8  003a0043\r\n008bf8ac  0050005c\r\n008bf8b0  006f0072\r\n008bf8b4  00720067\r\n008bf8b8  006d0061\r\n<\/pre>\n<p>Yup, there&#8217;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 <code>user32!ExitWindowsEx<\/code> look like a Unicode string, so let&#8217;s dump them as a string:<\/p>\n<pre>0:000&gt; du 008bf8a8  \r\n008bf8a8  \"C:\\Program Files\\Contoso\\contoso_update.exe\"\r\n<\/pre>\n<p>Wait, what? It is trying to mess around with Contoso&#8217;s auto-updater?<\/p>\n<p>Let&#8217;s take a look at more of the malware payload. Maybe we can figure out what it&#8217;s doing. It looks like it&#8217;s using <code>esi<\/code> as its base of operations, so let&#8217;s disassemble from <code>esi<\/code>.<\/p>\n<pre>008bf684 push    ebp                        ; build stack frame\r\n008bf685 mov     ebp,esp\r\n008bf687 push    ebx                        ; save ebx\r\n008bf688 push    esi                        ; save esi\r\n008bf689 mov     esi,dword ptr [ebp+8]      ; parameter\r\n008bf68c push    edi                        ; save edi\r\n008bf68d push    0FFFFFFFFh                 ; INFINITE\r\n008bf68f push    dword ptr [esi+204h]       ; data-&gt;hProcess\r\n008bf695 lea     ebx,[esi+22Ah]             ; address of path + 2\r\n008bf69b call    dword ptr [esi+208h]       ; WaitForSingleObject\r\n008bf6a1 push    dword ptr [esi+204h]       ; data-&gt;hProcess\r\n008bf6a7 call    dword ptr [esi+20Ch]       ; CloseHandle\r\n\r\n008bf6ad and     dword ptr [ebp+8],0        ; count = 0\r\n008bf6b1 lea     edi,[esi+228h]             ; address of path\r\n008bf6b7 jmp     008bf6cd                   ; enter loop\r\n008bf6b9 cmp     dword ptr [ebp+8],28h      ; waited too long?\r\n008bf6bd jge     008bf6d8                   ; then stop\r\n008bf6bf push    1F4h                       ; 500\r\n008bf6c4 call    dword ptr [esi+214h]       ; Sleep\r\n008bf6ca inc     dword ptr [ebp+8]          ; count++\r\n008bf6cd push    edi                        ; path\r\n008bf6ce call    dword ptr [esi+210h]       ; DeleteFile\r\n008bf6d4 test    eax,eax                    ; Q: Did it delete?\r\n008bf6d6 je      008bf6b9                   ; N: Loop and try again\r\n\r\n008bf6d8 xor     edi,edi\r\n008bf6da cmp     dword ptr [esi+430h],edi   ; data-&gt;fRemoveDirectory?\r\n008bf6e0 je      008bf70d                   ; N: Skip\r\n008bf6e2 jmp     008bf6f0                   ; Enter loop for trimming file name\r\n008bf6e4 cmp     ax,5Ch                     ; Q: Backslash?\r\n008bf6e8 jne     008bf6ed                   ; N: Ignore\r\n008bf6ea mov     dword ptr [ebp+8],ebx      ; Remember location of last backslash\r\n008bf6ed add     ebx,2                      ; Move to character\r\n008bf6f0 movzx   eax,word ptr [ebx]         ; Fetch next character\r\n008bf6f3 cmp     ax,di                      ; Q: End of string?\r\n008bf6f6 jne     008bf6e4                   ; N: Keep looking\r\n\r\n008bf6f8 mov     ecx,dword ptr [ebp+8]      ; Get location of last backslash\r\n008bf6fb xor     eax,eax                    ; eax = 0\r\n008bf6fd mov     word ptr [ecx],ax          ; Terminate string at last backslash\r\n008bf700 lea     eax,[esi+228h]             ; Get path (now without file name)\r\n008bf706 push    eax                        ; Push address\r\n008bf707 call    dword ptr [esi+21Ch]       ; RemoveDirectory\r\n\r\n008bf70d cmp     dword ptr [esi+434h],edi   ; data-&gt;fExitWindows?\r\n008bf713 je      008bf71e                   ; N: Skip\r\n008bf715 push    edi                        ; dwReason = 0\r\n008bf716 push    12h                        ; EWX_REBOOT | EWX_FORCEIFHUNG\r\n008bf718 call    dword ptr [esi+224h]       ; ExitWindowsEx\r\n\r\n008bf71e push    edi                        ; dwExitCode = 0\r\n008bf71f call    dword ptr [esi+218h]       ; ExitProcess\r\n008bf725 pop     edi\r\n008bf726 pop     esi\r\n008bf727 pop     ebx\r\n008bf728 pop     ebp\r\n008bf729 ret\r\n\r\n; This code appears to be unused\r\n008bf72a push    ebp\r\n008bf72b mov     ebp,esp\r\n008bf72d push    esi\r\n008bf72e mov     esi,dword ptr [ebp+10h]\r\n008bf731 test    esi,esi\r\n008bf733 jle     008bf746\r\n...\r\n<\/pre>\n<p>Reverse-compiling back to C, we have<\/p>\n<pre>struct Data\r\n{\r\n    char code[0x0204];\r\n    HANDLE hProcess;\r\n    DWORD (CALLBACK* WaitForSingleObject)(HANDLE, DWORD);\r\n    BOOL (CALLBACK* CloseHandle)(HANDLE);\r\n    DWORD (CALLBACK* MysteryFunction)(PCWSTR);\r\n    void (CALLBACK* Sleep)(DWORD);\r\n    void (CALLBACK* ExitProcess)(UINT);\r\n    BOOL (CALLBACK* RemoveDirectoryW)(PCWSTR);\r\n    DWORD (CALLBACK* GetLastError)();\r\n    BOOL (CALLBACK* ExitWindowsEx)(UINT, DWORD);\r\n    wchar_t path[MAX_PATH];\r\n    BOOL fRemoveDirectory;\r\n    BOOL fExitWindows;\r\n};\r\nvoid Payload(Data* data)\r\n{\r\n    \/\/ Wait for the process to exit\r\n    data-&gt;WaitForSingleObject(data-&gt;hProcess, INFINITE);\r\n    data-&gt;CloseHandle(data-&gt;hProcess);\r\n\r\n    \/\/ Try up to 20 seconds to do something with the file\r\n    for (int count = 0;\r\n        !data-&gt;MysteryFunction(data-&gt;path) &amp;&amp; count &lt; 40;\r\n        count++) {\r\n        Sleep(500);\r\n    }\r\n\r\n    if (data-&gt;fRemoveDirectory) {\r\n        PWSTR p = &amp;data-&gt;path[1];\r\n        PWSTR lastBackslash = p;\r\n        while (*p != L'\\0') {\r\n            if (*p == L'\\\\') lastBackslash = p;\r\n            p++;\r\n        }\r\n        *lastBackslash = L'\\0';\r\n        RemoveDirectoryW(data-&gt;path);\r\n    }\r\n\r\n    if (data-&gt;fExitWindows) {\r\n        ExitWindowsEx(EWX_REBOOT | EWX_FORCEIFHUNG, 0);\r\n    }\r\n}\r\n<\/pre>\n<p>Aha, this isn&#8217;t malware. This is an uninstaller!<\/p>\n<p>The mystery function is almost certainly <code>DeleteFileW<\/code>. It&#8217;s waiting for the main uninstaller to exit, so it can delete the binary.<\/p>\n<p>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.)<\/p>\n<p>Okay, so why did we crash? What went wrong with <code>DeleteFileW<\/code>?<\/p>\n<p>According to the dump file, the spot where <code>DeleteFileW<\/code> was supposed to be instead holds <code>7077c100<\/code>. This is a function pointer into some mystery DLL that isn&#8217;t loaded. How did that happen?<\/p>\n<p>My guess is that the <code>DeleteFileW<\/code> 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 <code>DeleteFileW<\/code> 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.<\/p>\n<p>Neither code injection nor detouring is officially supported. I can&#8217;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&#8217; own application compatibility layer. Whatever the reason, the result was a crash in Explorer.<\/p>\n<p>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.<\/p>\n<p>If you want to create a self-deleting binary, please don&#8217;t use code injection into somebody else&#8217;s process. Here&#8217;s a way to delete a binary and leave no trace:<\/p>\n<p>Create a temporary file called <code>cleanup.js<\/code> that goes like this:<\/p>\n<pre>var fso = new ActiveXObject(\"Scripting.FileSystemObject\");\r\nfso.DeleteFile(\"C:\\\\Users\\\\Name\\\\AppData\\\\Local\\\\Temp\\\\cleanup.js\");\r\n\r\nvar path = \"C:\\\\Program Files\\\\Contoso\\\\contoso_update.exe\";\r\nfor (var count = 0; fso.FileExists(path) &amp;&amp; count &lt; 40; count++) {\r\n    try { fso.DeleteFile(path); break; } catch (e) { }\r\n    WSH.Sleep(500);\r\n}\r\n<\/pre>\n<p>This script deletes itself and then tries to delete <code>contoso_update.exe<\/code> for 20 seconds. Run it with <code>wscript cleanup.js<\/code> and let it do its thing. No code injection, no detours, all documented.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The common pattern of trying to delete yourself.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-108749","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>The common pattern of trying to delete yourself.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/108749","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=108749"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/108749\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=108749"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=108749"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=108749"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}