У нас возник резкий рост количества вылетов 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)
vilgeforce
14.09.2023 11:04Писать в чужой процесс - плохо, не надо так делать! АВ вообще должен блочить подобные попытки на корню
Kenya-West
14.09.2023 11:04-1Вы хотите что-то запретить в админских правах Windows?
Если плохо представляете себе их полномочия, то тогда попробуйте запретить нечто подобное root'у в Linux.
red75prim
14.09.2023 11:04Вы хотите что-то запретить в админских правах Windows?
Права процесса в Windows определяются привязанным к нему (процессу) токеном доступа. По-умолчанию этот токен содержит права пользователя, но при создании процесса эти права можно урезать. В данном случае запретить PROCESS_VM_WRITE.
Но, видимо, это создаст проблемы с совместимостью. Так что так не делают.
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 ( это — учетная запись одноименной службы). И администратору, чтобы изменить такие файлы, требуется использовать привилегию «Стать владельцем».qw1
14.09.2023 11:04(«seTcbPrivilege») — позволяющей создать маркер доступа (аналог совокупности uid и gid для процесса в *nix) от имени любого пользователя, и содержащий любые группы — администратор по умолчанию не обладает
Я не знаю, от кого эта защита. Админ также не может открывать на чтение/запись адресное пространство системных процессов. Но стоит ему вызвать AdjustTokenPrivileges(...SeDebugPrivilege...) — и уже может. В итоге, каждая системная низкоуровневая программа начинается с этого заклинания. С SeTcbPrivilege не проверял, но судя по всему, ситуация с ней ничем не отличается.
red75prim
14.09.2023 11:04Я не знаю, от кого эта защита.
Вопрос неправильно поставлен. Не "от кого", а "для кого". От пользователя с правами доменного администратора защититься в принципе невозможно. А вот помочь такому пользователю защитить систему от непреднамеренного запуска вредоносных программ можно. Например, запуская программы со списком привилегий, который не включает SeDebugPrivilege. В результате AdjustTokenPrivileges вернёт ERROR_NOT_ALL_ASSIGNED, и в системных процессах ничего прочитать не получится.
includedlibrary
14.09.2023 11:04А как тогда отладкой заниматься? Процессу, запущенному с правами администратора должно быть можно всё. Если процесс пользовательский, то по умолчанию разрешить ему отлаживать только дочерние процессы. Плюсом можно дать возможность запрещать пользовательским процессам отладку вовсе
pvvv
14.09.2023 11:04+5Создайте временный файл
cleanup.js
а что не питон? или VBA?
.bat файл может сделать себе
del "%~f0"
qw1
14.09.2023 11:04Питона нет в стандартной инсталляции винды.
А bat-интерпретатор покажет богомерзкое чёрное окошко, которое может напугать пользователей.pvvv
14.09.2023 11:04хабр съел тэг <sarcasm>
у меня вот местами есть W7 и яваскрипта там тоже нет.
а окошко можно и спрятать
@start /b "" cmd /c del "%~f0" & exit /b
qw1
14.09.2023 11:04Пишут, что в 7-ке он уже есть: https://www.techwalla.com/articles/how-to-enable-windows-script-host-in-windows-7
Я помню, видел его даже в XP
AndreyDmitriev
Путь сей может оказаться несколько тернист, ибо " в лоб" мы рискуем получить вот это
А всё потому, что сначала может быть необходимым сделать вот это
Причём сделать это из под админа (деинсталляшка обычно так и запускается, но тем не менее), иначе мы рискуем получить вот это
Но даже сделав это, мы рискуем получить вот это
Потому что надо как-то вот так (на моём компе по крайней мере):
Как-то так.
vesper-bot
А можно из JS-движка на винде получить имя выполняемого скрипта? А то хардкодить путь к скрипту для fso.DeleteFile как-то некошерно.
cartonworld
borisdenis
Ага, тоесть если я специально настроил другую ассоцияцию с этим типом файла, то всё. делай заново? Не хорошо такие изменения без запроса согласия пользователя делать.
AndreyDmitriev
Безусловно нехорошо, просто без этого wscript cleanup.js не отрабатывал. Вообще весь этот костыль мне элегантным не кажется, здесь зависимость от js, я б с этим в продакшен не пошёл. В принципе можно либо тривиальным bat файлом обойтись, либо отложенное удаление сделать, я у Руссиновича вроде читал, что запущенный файл можно пометить для удаления и винда сама его прибьёт при перезагрузке, которую из деинсталлятора запросить можно.
Вроде как-то так (но боюсь ошибиться)
А посмотреть что там запланировано для удаления вроде как через PendMoves можно. Но я это не проверял, так что это неточно.
geher
Я вообще полагал, что оно всегда именно так и делается. Причем не только для запущенных файлов, но и для заблокированных по иным причинам. И не только для удаления, но и для перемещения.
В nsis, например, для команд копирования и удаления файлов есть ключ, приводящий к постановке задачи на выполнение операции после перезагрузки в случае невозможности сделать это сразу.
Также можно это сделать через ключ реестра PendingFileRenameOperations. Похоже, что вызов MoveFileEx с соответствующим флагом как раз помещает необходимые данные в этот ключ.
Кстати, еще в древних Win 95/98/me был механизм для таких же действий через WININIT.INI.
qw1
Инсталляторы, которые так делают, требуют перезагрузки.
То есть, снёс старую версию программы, хочешь поставить новую — а она тебе "сначала перезагрузись".
В 2023 у пользователя открыто 100500 окон и 200 вкладок в браузере. Перезагружаться ради обновления программы? Да ну, варварство какое-то.
geher
Если речь о программе деинсталляции, которая убивает сама себя, то это решается просто. Достаточно записать убиватель программы деинсталляции в подкаталог для временных файлов, дав ему уникальное имя (UUID), запустить его и выйти, а уже тот после удаления программы деинсталляции может добавить себя для удаления при перезагрузке. Без перезагрузки будет лишь копиться мусор в подкаталоге для временных файлов (а он там и без того копится).
Впрочем, это не довод против других методов. Просто предполагал, что всегда используется что-то подобное, использующее стандартный документированный системный вызов.
qw1
В лоб, это
ShellExecute("D:\\AutoDel.js", ...);
?
Я бы запускал сразу
wscript.exe D:\Autodel.js
и не зависел бы от ассоциаций.