У нас возник резкий рост количества вылетов Explorer из-за того, что указатель команд оказывался в пустоте.

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

Возможно, нам о чём-то скажет адрес возврата.

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

Странно, что мы исполняем код из какого-то места, не имеющего имени. Если приглядеться, то можно увидеть, что мы исполняем код из стекаesp — это  008bf664, то есть вызывающий проблемы код находится в стеке.

Кто исполняет код из стека?

Конечно, зловреды.

Давайте посмотрим, что пытается сделать это зловредное ПО.

Дизассемблирование кода рядом с последним известным хорошим кодом даёт нам следующее:

008bf6c4 call    dword ptr [esi+214h]
008bf6ca inc     dword ptr [ebp+8]
008bf6cd push    edi
008bf6ce call    dword ptr [esi+210h]   ; здесь выполняется вызов в пустоту
008bf6d4 test    eax,eax
008bf6d6 je      008bf6b9
008bf6d8 xor     edi,edi
008bf6da cmp     dword ptr [esi+430h],edi
008bf6e0 je      008bf70d

Похоже, полезная нагрузка сохранила указатели функций в esi+210 и esi+214. Давайте посмотрим, что там. Вероятно, там полезная нагрузка хранит все свои цели вызова.

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

Да, здесь находится полезная нагрузка указателей функций. Похоже, этот зловред собирается чего-то дождаться, а затем или выйти из процесса, или удалить папку, или выйти из Windows. Эти байты после user32!ExitWindowsEx похожи на строку Unicode, так что давайте сдампим их как строку:

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

Постойте-ка, что? Весь этот беспорядок наводит программа автоматического обновления Contoso?

Давайте внимательнее приглядимся к полезной нагрузке зловреда. Возможно, мы разберёмся, что происходит. Похоже, он использует в качестве оперативного плацдарма esi , так что давайте начнём дизассемблирование с esi.

008bf684 push    ebp                        ; создание кадра стека
008bf685 mov     ebp,esp
008bf687 push    ebx                        ; сохранение ebx
008bf688 push    esi                        ; сохранение esi
008bf689 mov     esi,dword ptr [ebp+8]      ; параметр
008bf68c push    edi                        ; сохранение edi
008bf68d push    0FFFFFFFFh                 ; INFINITE
008bf68f push    dword ptr [esi+204h]       ; данные->hProcess
008bf695 lea     ebx,[esi+22Ah]             ; адрес пути + 2
008bf69b call    dword ptr [esi+208h]       ; WaitForSingleObject
008bf6a1 push    dword ptr [esi+204h]       ; данные->hProcess
008bf6a7 call    dword ptr [esi+20Ch]       ; CloseHandle
008bf6ad and     dword ptr [ebp+8],0        ; count = 0
008bf6b1 lea     edi,[esi+228h]             ; адрес пути
008bf6b7 jmp     008bf6cd                   ; вход в цикл
008bf6b9 cmp     dword ptr [ebp+8],28h      ; ждали слишком долго?
008bf6bd jge     008bf6d8                   ; тогда останов
008bf6bf push    1F4h                       ; 500
008bf6c4 call    dword ptr [esi+214h]       ; Sleep
008bf6ca inc     dword ptr [ebp+8]          ; count++
008bf6cd push    edi                        ; путь
008bf6ce call    dword ptr [esi+210h]       ; DeleteFile
008bf6d4 test    eax,eax                    ; Удалён ли файл?
008bf6d6 je      008bf6b9                   ; Если нет (N), входим в цикл и пробуем снова
008bf6d8 xor     edi,edi
008bf6da cmp     dword ptr [esi+430h],edi   ; данные->fRemoveDirectory?
008bf6e0 je      008bf70d                   ; Нет? Пропускаем
008bf6e2 jmp     008bf6f0                   ; Входим в цикл для урезания имени файла
008bf6e4 cmp     ax,5Ch                     ; Обратная косая черта?
008bf6e8 jne     008bf6ed                   ; Нет? Игнорируем
008bf6ea mov     dword ptr [ebp+8],ebx      ; Запоминаем место последней обратной косой черты
008bf6ed add     ebx,2                      ; Переходим к символу
008bf6f0 movzx   eax,word ptr [ebx]         ; Получаем следующий символ
008bf6f3 cmp     ax,di                      ; Конец строки?
008bf6f6 jne     008bf6e4                   ; Нет? Продолжаем искать
008bf6f8 mov     ecx,dword ptr [ebp+8]      ; Получаем место последней обратной косой черты
008bf6fb xor     eax,eax                    ; eax = 0
008bf6fd mov     word ptr [ecx],ax          ; Завершаем строку на последней обратной косой черте
008bf700 lea     eax,[esi+228h]             ; Получаем путь (теперь без имени файла)
008bf706 push    eax                        ; Push адреса
008bf707 call    dword ptr [esi+21Ch]       ; RemoveDirectory
008bf70d cmp     dword ptr [esi+434h],edi   ; данные->fExitWindows?
008bf713 je      008bf71e                   ; Нет? Пропускаем
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
; Похоже, этот код не используется
008bf72a push    ebp
008bf72b mov     ebp,esp
008bf72d push    esi
008bf72e mov     esi,dword ptr [ebp+10h]
008bf731 test    esi,esi
008bf733 jle     008bf746
...

Выполнив реверс-компиляцию обратно на C, получаем

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)
{
// Ждём, пока процесс выполнит выход
data->WaitForSingleObject(data->hProcess, INFINITE);
data->CloseHandle(data->hProcess);
// 20 секунд пытаемся сделать что-то с файлом
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);
}

}

Ага! Это не зловредное ПО, это деинсталлятор!

Скорее всего, загадочная функция — это DeleteFileW. Она ждёт, пока будет выполнен выход из основного деинсталлятора, чтобы удалить двоичный файл.

На CodeProject есть страница, показывающая, как написать самоудаляющийся файл; похоже многие компании решили использовать этот код в своих деинсталляторах. (Не знаю, соблюдают ли они лицензионные требования этого кода.)

Ну ладно, а почему же происходит вылет? Что не так с DeleteFileW?

Согласно файлу дампа, место, где должен находиться DeleteFileW , содержит 7077c100. Это указатель функции в какой-то загадочной DLL, которая не загружена. Как же так получилось?

Предположу, что функция DeleteFileW создала обходной путь (detour) в деинсталляторе Contoso. Когда деинсталлятор пытался создать таблицу полезных функций, он получил не адрес DeleteFileW , а адрес обходного пути. Затем он попытался вызвать из полезной нагрузки этот обходной путь, но поскольку обходной путь не установлен в Explorer (или если он есть, обходной путь находится в каком-то другом месте), в конечном итоге вызов выполнился в пустое пространство.

Ни инъецирование кода, ни создание обходных путей официально не поддерживаются. Возможно, кто-то добавил в деинсталлятор обходной путь, не понимая, что деинсталлятор инъецирует вызов обходного пути в Explorer. А может, обходной путь был инъецирован антивирусом. Или, возможно, обходной путь инъецирован собственным слоем совместимости приложений Windows. Как бы то ни было, результатом становился вылет Explorer.

Из-за этого люди вроде меня тратят много времени на изучение этих вылетов лишь для того, чтобы прийти к выводу, что их причиной стали другие люди, неправильно использующие систему.

Если вам нужно создать самоудаляющийся двоичный файл, пожалуйста, не используйте инъецирование кода в чей-то чужой процесс. Вот как можно удалить двоичный файл, не оставляя следов:

Создайте временный файл cleanup.js со следующим содержимым:

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);
}

Этот скрипт удаляет себя, а затем в течение двадцати секунд пытается удалить contoso_update.exe. Запустите его командой wscript cleanup.js и позвольте сделать свою работу. Никакого инъецирования кода, никаких обходных путей, всё задокументировано.

Комментарии (21)


  1. AndreyDmitriev
    14.09.2023 11:04
    +10

    Создайте временный файл cleanup.js со следующим содержимым:

    Путь сей может оказаться несколько тернист, ибо " в лоб" мы рискуем получить вот это

    А всё потому, что сначала может быть необходимым сделать вот это

    assoc .js=JSFILE

    Причём сделать это из под админа (деинсталляшка обычно так и запускается, но тем не менее), иначе мы рискуем получить вот это

    Но даже сделав это, мы рискуем получить вот это

    Потому что надо как-то вот так (на моём компе по крайней мере):

    var fso = new ActiveXObject("Scripting.FileSystemObject");
    fso.DeleteFile("D:\\AutoDel\\AutoDel.js");
    var path = "То\\что\\мы\\тут\\удаляем";
    for (var count = 0; fso.FileExists(path) && count < 40; count++) {
    try { fso.DeleteFile(path); break; } catch (e) { }
    WSH.Sleep(500);
    }

    Как-то так.


    1. vesper-bot
      14.09.2023 11:04

      А можно из JS-движка на винде получить имя выполняемого скрипта? А то хардкодить путь к скрипту для fso.DeleteFile как-то некошерно.


      1. cartonworld
        14.09.2023 11:04
        +1

        var scriptFullName = WScript.ScriptFullName;


    1. borisdenis
      14.09.2023 11:04
      +2

      assoc .js=JSFILE

      Ага, тоесть если я специально настроил другую ассоцияцию с этим типом файла, то всё. делай заново? Не хорошо такие изменения без запроса согласия пользователя делать.


      1. AndreyDmitriev
        14.09.2023 11:04
        +3

        Безусловно нехорошо, просто без этого wscript cleanup.js не отрабатывал. Вообще весь этот костыль мне элегантным не кажется, здесь зависимость от js, я б с этим в продакшен не пошёл. В принципе можно либо тривиальным bat файлом обойтись, либо отложенное удаление сделать, я у Руссиновича вроде читал, что запущенный файл можно пометить для удаления и винда сама его прибьёт при перезагрузке, которую из деинсталлятора запросить можно.

        Вроде как-то так (но боюсь ошибиться)

        MoveFileEx(FileName, null, MoveFileFlags.MOVEFILE_DELAY_UNTIL_REBOOT)

        А посмотреть что там запланировано для удаления вроде как через PendMoves можно. Но я это не проверял, так что это неточно.


        1. geher
          14.09.2023 11:04
          +1

          запущенный файл можно пометить для удаления и винда сама его прибьёт при перезагрузке, которую из деинсталлятора запросить можно.

          Я вообще полагал, что оно всегда именно так и делается. Причем не только для запущенных файлов, но и для заблокированных по иным причинам. И не только для удаления, но и для перемещения.

          В nsis, например, для команд копирования и удаления файлов есть ключ, приводящий к постановке задачи на выполнение операции после перезагрузки в случае невозможности сделать это сразу.

          Также можно это сделать через ключ реестра PendingFileRenameOperations. Похоже, что вызов MoveFileEx с соответствующим флагом как раз помещает необходимые данные в этот ключ.

          Кстати, еще в древних Win 95/98/me был механизм для таких же действий через WININIT.INI.


          1. qw1
            14.09.2023 11:04

            Инсталляторы, которые так делают, требуют перезагрузки.
            То есть, снёс старую версию программы, хочешь поставить новую — а она тебе "сначала перезагрузись".


            В 2023 у пользователя открыто 100500 окон и 200 вкладок в браузере. Перезагружаться ради обновления программы? Да ну, варварство какое-то.


            1. geher
              14.09.2023 11:04

              Если речь о программе деинсталляции, которая убивает сама себя, то это решается просто. Достаточно записать убиватель программы деинсталляции в подкаталог для временных файлов, дав ему уникальное имя (UUID), запустить его и выйти, а уже тот после удаления программы деинсталляции может добавить себя для удаления при перезагрузке. Без перезагрузки будет лишь копиться мусор в подкаталоге для временных файлов (а он там и без того копится).

              Впрочем, это не довод против других методов. Просто предполагал, что всегда используется что-то подобное, использующее стандартный документированный системный вызов.


    1. qw1
      14.09.2023 11:04

      Путь сей может оказаться несколько тернист, ибо " в лоб" мы рискуем получить вот это

      В лоб, это
      ShellExecute("D:\\AutoDel.js", ...);
      ?


      Я бы запускал сразу
      wscript.exe D:\Autodel.js
      и не зависел бы от ассоциаций.


  1. vilgeforce
    14.09.2023 11:04

    Писать в чужой процесс - плохо, не надо так делать! АВ вообще должен блочить подобные попытки на корню


    1. Kenya-West
      14.09.2023 11:04
      -1

      Вы хотите что-то запретить в админских правах Windows?

      Если плохо представляете себе их полномочия, то тогда попробуйте запретить нечто подобное root'у в Linux.


      1. red75prim
        14.09.2023 11:04

        Вы хотите что-то запретить в админских правах Windows?

        Права процесса в Windows определяются привязанным к нему (процессу) токеном доступа. По-умолчанию этот токен содержит права пользователя, но при создании процесса эти права можно урезать. В данном случае запретить PROCESS_VM_WRITE.

        Но, видимо, это создаст проблемы с совместимостью. Так что так не делают.


        1. qw1
          14.09.2023 11:04

          Защита уровня
          chmod 000 filename
          и "ой, рут не может удалить этот файл!!!"


      1. mvv-rus
        14.09.2023 11:04
        +2

        Во-первых, Administrator в Windows не является полным аналогом root в *nix: он имеет меньше разрешений и привилегий. Например, привилегией «Act as a part of the operating system» («seTcbPrivilege») — позволяющей создать маркер доступа (аналог совокупности uid и gid для процесса в *nix) от имени любого пользователя, и содержащий любые группы — администратор по умолчанию не обладает. Аналогом root является, скорее, учетная запись самой операционной системы. Разные программы называют ее по разному (полноценного имени у нее нет): System, LocalSystem и т.п., ее SID — S-1-5-18.
        Во-вторых, начиная с Vista в Windows реализован дополнительный механизм ограничени доступа — Mandatory integrity control (контроль целостности на основе полномочий, или, как перевела сама MS — «обязательный контроль целостности»), который ограничивает доступ процесса к объекту(в том числе — к другому процессу) на основе назначенных для них полномочий. Политика доступа по умолчанию запрещает изменение объекта с более высокими полномчиями из процесса с меньшими полоночиями, но возможны и другие варианты политики.
        PS А еще есть специальная учетная запись, которая, начиная с Win 7(или Vista, не помню точно) является владельцем большинства исполняемых файлов системы: NT_SERVICE\Trusted Installer ( это — учетная запись одноименной службы). И администратору, чтобы изменить такие файлы, требуется использовать привилегию «Стать владельцем».


        1. qw1
          14.09.2023 11:04

          («seTcbPrivilege») — позволяющей создать маркер доступа (аналог совокупности uid и gid для процесса в *nix) от имени любого пользователя, и содержащий любые группы — администратор по умолчанию не обладает

          Я не знаю, от кого эта защита. Админ также не может открывать на чтение/запись адресное пространство системных процессов. Но стоит ему вызвать AdjustTokenPrivileges(...SeDebugPrivilege...) — и уже может. В итоге, каждая системная низкоуровневая программа начинается с этого заклинания. С SeTcbPrivilege не проверял, но судя по всему, ситуация с ней ничем не отличается.


          1. red75prim
            14.09.2023 11:04

            Я не знаю, от кого эта защита.

            Вопрос неправильно поставлен. Не "от кого", а "для кого". От пользователя с правами доменного администратора защититься в принципе невозможно. А вот помочь такому пользователю защитить систему от непреднамеренного запуска вредоносных программ можно. Например, запуская программы со списком привилегий, который не включает SeDebugPrivilege. В результате AdjustTokenPrivileges вернёт ERROR_NOT_ALL_ASSIGNED, и в системных процессах ничего прочитать не получится.


    1. includedlibrary
      14.09.2023 11:04

      А как тогда отладкой заниматься? Процессу, запущенному с правами администратора должно быть можно всё. Если процесс пользовательский, то по умолчанию разрешить ему отлаживать только дочерние процессы. Плюсом можно дать возможность запрещать пользовательским процессам отладку вовсе


  1. pvvv
    14.09.2023 11:04
    +5

    Создайте временный файл cleanup.js

    а что не питон? или VBA?

    .bat файл может сделать себе del "%~f0"


    1. qw1
      14.09.2023 11:04

      Питона нет в стандартной инсталляции винды.
      А bat-интерпретатор покажет богомерзкое чёрное окошко, которое может напугать пользователей.


      1. pvvv
        14.09.2023 11:04

        хабр съел тэг <sarcasm>

        у меня вот местами есть W7 и яваскрипта там тоже нет.

        а окошко можно и спрятать

        @start /b "" cmd /c del "%~f0" & exit /b


        1. qw1
          14.09.2023 11:04

          Пишут, что в 7-ке он уже есть: https://www.techwalla.com/articles/how-to-enable-windows-script-host-in-windows-7


          Я помню, видел его даже в XP