Программа, удаляющая сама себя (Self-Deleting Executables) v1.1

Замечание

Этот документ представляет собой перевод статьи "Self-Deleting Executables" Джеймса Брауна (с его разрешения). Я не стал переводить пару описываемых Джеймсом методов. Если вам все же интересно узнать о них, то зайдите на страничку оригинала статьи.

Исправлены обнаруженные ошибки и неточности в оригинале. В оригинале, примеры приводятся на С, я перевел их на Delphi.
Download source (C)

В версии статьи 1.1 я исправил найденные орфографические ошибки, а также переработал текст статьи.

Введение

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

Зачем вам может понадобиться программа удаляющая сама себя? Единственная стоящая причина, которую я знаю - это программа деинсталляции. Она должна удалить файлы приложения и затем удалить с диска и себя. Я уверен, что существуют и другие хорошие причины, но большинство других причин - вирусы или трояны, пытающиеся скрывать себя от посторонних глаз.

В целом я знаю пять различных методов, каждый из которых я кратко опишу далее. Должен заметить, что лишь один из этих методов разработан мною. Все, кроме последнего метода, представляют собой перепечатку материалов из колонки Джефри Рихтера "Win32 Q&A" в журнале January 1996 MSJ. Щелкните здесь, если хотите прочитать оригинал статьи. Итак, я надеюсь теперь никто не подумает, что я присвоил себе труды и идеи других людей.

Так в чем же проблема?

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

var
  szEXEPathname: array[0..MAX_PATH] of Char;
begin
  GetModuleFileName(nil, szEXEPathname, MAX_PATH);
  DeleteFile(szEXEPathname);
end;

Этот код получает полный путь к текущему исполняемому модулю и затем пытается удалить файл исполняемого модуля. Данный код не дает ожидаемого результата, потому что все 32-х битные версии операционной системы Windows (95, 98, ME, NT, 2000, XP и т.д.) используют механизм, называемый "файлы проецируемые в память", для загрузки образа исполняемого модуля в память.

Когда загрузчик Windows запускает исполняемый модуль, он открывает файл исполняемого модуля на диске и проецирует его в память. Этот файл остается открытым все время жизни процесса, и закрывается только после того, как процесс будет завершен. Из-за этого невозможно удаление файла исполняемого модуля обычными методами. Просто запустите notepad.exe, и попробуйте его удалить.

Метод MoveFileEx

Я собираюсь описать этот метод, хотя он решает поставленную задачу не до конца, потому что он очень полезен и удобен в других ситуациях.

function MoveFileEx(lpExistingFileName: PChar;
  lpNewFileName: PChar; dwFlags: DWORD):  LongBool;  

Этот вызов API функции перемещает файл в новое место. Когда вы передаете nil в качестве второго параметра, файл перемещается в "никуда", что равнозначно его удалению. Без дополнильных усилий, этот вызов закончится неудачей, если вы укажете в качестве файла - файл текущего исполняемого модуля. Однако, если мы передадим MOVEFILE_DELAY_UNTIL_REBOOT в качестве параметра dwFlags, то мы попросим систему отложить операцию перемещения (удаления) до перезагрузки системы.

У данного метода есть пара недостатков. Во-первых, мы не можем удалить папку в которой находится исполняемый модуль. Во-вторых, файл не удаляется тотчас же. Поэтому, если система не перезагружается часто (например, вместо выключения ее отправляют в ждущий режим) файл так и будет оставаться на диске до тех пор, пока систему не перезагрузят. Но самая главная проблема заключается в том, что функция MoveFileEx не реализована в Windows 95/98/ME.

Метод WININIT.INI

В Windows 95/98/ME, приложение называемое WININIT.EXE запускается при каждой загрузке системы. Это приложение ищет файл WININIT.INI. Если этот файл существует, WININIT.EXE ищет в нем секцию [Rename]. Каждая запись в секции [Rename] описывает операцию переименования файла, которая произойдет единственный раз. Этот способ очень похож на метод MoveFileEx описанный ранее.

[Rename]
NUL=c:\dir\myapp.exe
c:\dir\new.exe=c:\dir\old.exe

Имя файла, слева от знака равенства, задает новое имя файла, указанного справа от равенства. Когда в качестве нового имени файла указан NUL, файл удаляется. Таким образом, приложение может добавить в секцию [Rename] новую запись, указав NUL в качестве нового имени и полный путь к своему файлу.

При добавлении записи в секцию [Rename] следует проявить осторожность. Вы не можете пользоваться API функцией WritePrivateProfileString, потому что эта функция удалит часть остальных записей из секции [Rename]. Потому что, она считает, что в каждой секции не может быть несколько записей с одинаковым именем, в нашем случае NUL.

Метод с использованием самоудаляющегося пакетного файла (.bat, .cmd)

Этот способ довольно известен и был документирован в MSDN. Данный способ работает как на Windows линейки так и на NT. Он работает из-за того, что MS-DOS bat файлы способны удалять сами себя. Для теста, создайте пустой bat файл, скопируйте в него следующую строку и запустите его.

del %0

Этот пакетный файл удаляет себя при запуске и вызывает ошибку "Не удается найти пакетный файл". Эта ошибка - простое сообщение и может быть проигнорировано.

В таком виде от него мало толку. Модифицируем его немного:

:Repeat
del "C:\MYDIR\MYPROG.EXE"
if exist "MYPROG.EXE" goto Repeat
rmdir "C:\MYDIR"
del %0

Этот пакетный файл в цикле пытается удалить указанный файл, и будет пытаться это сделать, пока не добьется успеха. Когда файл будет удален, он удаляет указанную папку и себя.

Исполняемый модуль должен запустить пакетный файл, используя CreateProcess, и немедленно завершить свою работу. Также хорошая идея - задать исполняемой нити пакетного файла низкий приоритет, чтобы цикл съедал поменьше процессорного времени.

Метод COMSPEC

Этот метод мне прислал Tony Varnas:

function SelfDelete: Boolean;
var
  szFile: array[0..MAX_PATH] of Char;
  szCmd: String;
begin
  if (GetModuleFileName(0, szFile, MAX_PATH) <> 0) and
     (GetShortPathName(szFile, szFile, MAX_PATH) <> 0) then
  begin
    szCmd:= '/c del ' + '"'+szFile+'"' + ' >> NUL';
    if (GetEnvironmentVariable('ComSpec', szFile, MAX_PATH) <> 0) and
       (ShellExecute(0, 0, szFile, PChar(szCmd), 0, SW_HIDE) > 32) then
    begin
      Result:= True;
      Exit;
    end;
  end;
  Result:= False;
end;

Этот способ очень похож на метод с пакетным файлом, но более изящен в реализации. Он работает во всех 32-х битных версиях Windows (95, 98, ME, NT, 2000, XP), так как на всех них определена переменная окружения COMSPEC. По умолчанию в ней содержится полный путь к системному интерпретатору командной строки. Для Windows 95 - "command.exe", для Windows NT - "cmd.exe".

Для работы функции необходимо ее вызвать и тут же завершить работу своего приложения. Она вызывает системный интерпретатор командной строки и просит его выполнить следующую команду:

del <executable-path> >> NUL

Эта команда удаляет указанный файл и перенаправляет операции вывода в NUL (no output). Интерпретатор вызывается как скрытое окно, поэтому пользователь не видит процесса на своем экране.

Метод DELETE_ON_CLOSE

Функция CreateFile принимает несколько флагов, которые влияют на то, как файл будет создаваться или открываться. Один из этих флагов - FILE_FLAG_DELETE_ON_CLOSE, указывает на то, что файл будет удален, когда последний дескриптор (handle) файла будет закрыт. Основа данного метода заключается в том, что исполняемый модуль будет запущен с установленным флагом FILE_FLAG_DELETE_ON_CLOSE, поэтому, когда приложение закончит свою работу, файл будет удален автоматически.

Первый шаг заключается в создании пустого файла, с установленным флагом FILE_FLAG_DELETE_ON_CLOSE. Содержимое текущего исполняемого модуля копируется в новый файл. Затем создается новый процесс из копии исполняемого модуля. Это увеличивает количество дескрипторов на новый файл. Кроме того, новому процессу передается полный путь к файлу текущего процесса и PID в качестве аргументов командной строки.

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

Итак, копия исполняемого модуля запущена, и начинает исполняться. PID, переданный через командную строку, используется для открытия первоначального процесса. Дублирующий процесс дожидается завершения первоначального процесса используя WaitForSingleObject. После этого, дублирующий процесс удаляет файл первоначального процесса, путь к которому так же был передан как аргумент командой строки. Таким образом мы удалили файл исполняемого модуля исходного процесса, как и хотели в самом начале. Теперь дублирующий процесс завершает свою работу, количество дескрипторов на файл с копией модуля становится равным 0. А так как файл был создан с флагом FILE_FLAG_DELETE_ON_CLOSE, он удаляется.

Звучит слишком заумно, но это не так трудно. Давайте повторим еще раз:

[Текущий процесс]
1. Создает новый файл с флагом FILE_FLAG_DELETE_ON_CLOSE.
2. Копирует содержимое исполняемого модуля в новый файл.
3. Создает новый процесс из нового файла.
4. Передает новому процессу полный путь к своему
   исполняемому модулю и свой PID.
5. Засыпает на небольшое время, чтобы дать новому процессу
   время для загрузки.
6. Закрывает новый файл.
7. Завершает свою работу.

[ Дублирующий процесс ]
8. Ждет пока первоначальный процесс завершит свою работу.
9. Удаляет файл первоначального процесса.
10.Завершает свою работу.

Этот метод требует очень аккуратного кодирования, потому что, программа должна быть написана так, чтобы она могла выполнять обе задачи. Т.е. программа должна уметь играть роли "текущего" и "дублирующего" процессов.

Метод немного трудноват, но делает свою работу очень хорошо. Программа uninstall, которую я написал и использую в своих продуктах, реализует именно этот метод.

Можно немного модифицировать данный метод - разделить "текущий" и "дублирующий" процессы на разные модули. Т.е. написать очень маленький отдельный модуль, задача которого, лишь удалять переданный в командной строке файл. Этот маленький модуль можно вшить в ресурсы исполняемого модуля, который на шаге 2) в новый файл будет записывать не свою копию, а маленький модуль. В остальном, все тоже самое, как было описано выше.

Слава Антонов © 2002 — August 13, 2008
Индекс цитирования
Hosted by uCoz