Запуск файла из памяти (Ассемблер Под Windows)
Введение
Существует довольно много способов запуска файлов на исполнение. Чаще всего используют CreateProcess, WinExec и прочие апи. Но что делать, если ЕХЕ-файл находится не в виде файла, а в памяти нашего процесса?.. Можно конечно сохранить файл на диск, после чего запустить. Но это недостаточно извращенный метод. В этой статье я попытаюсь в общих чертах рассказать, как стартовать прямо из памяти.
Я опишу те шаги, которые делал при создании криптора, в котором использовал данную технологию. Я уверен, что многие уже делали что-нибуть подобное. Данный материал не является пересказыванием чьих-либо идей. Это просто описание работы кода, написанного за 2 дня. Цель, которую я ставил перед собой — не написание крутого аналога загрузчика компании Майкрософт, а написание процедуры, которая просто получает указатель на образ файла и запускает его.
Если использовать эту технологию в вирусах, то пропадает необходимость поиска АПИ, ведь теперь не к файлу цепляется вирь, а файл к вирю. Впринципе это будет продемонстрированно на примере кода, который довольно легко превратить в вирус 🙂 Но лучше используйте его в мирных целях.
Техника запуска
Итак, для того, чтобы понять эту статью вам необходимо:
- знать формат исполняемого файла (Portable-Executable);
- ассемблер (синтаксис Fasm).
Вся техника заключается в том, чтобы привести файл из того вида, в котором он лежит в файле, в тот вид, в котором он должен быть в памяти, используя информацию, полученную из образа файла (заголовки, структуры). После чего просто передать управление на точку входа. Я не опишу обработку экзотики типа TLS, релоки, потому что я с этим не разбирался.
Начинаем работать
Так как файл, который мы запускаем, может оказатся базозависимым, то желательно как-нибудь заразервировать ему участок памяти, который ему нужен. Делается это вызовом VirtualAlloc. При вызове следует указать адрес и размер области. Но память будет выделена только в том случае, если она свободна. Тут следует вспомнить, что пользовательскому коду можно делать что угодно с памятью в диапазоне от 10000h до 7FFEFFFFh (32-разрядная Windows 2000). Воспользуемся этим и перед тем как резервировать память, будем её освобождать.
Память в нашем случае может быть в таких состояниях:
- Свободная
- Занятая
- Меппинг (загруженный образ исполняемого файла, например)
В первом случае ничего с этой памятью делать не надо, можно просто резервировать. Во втором — следует сначала вызвать VirtualFree с флагом MEM_RELEASE. В третьем — UnMapViewOfFile.
Обладая этими знаниями довольно просто написать процедуру резервирования области памяти:
proc Allock_Region pRegion,Size pusha mov edi,[pRegion] mov esi,[Size] add esi,edi ;esi - указатель на конец выделяемой области ;edi - указатель на начало @@: xinvoke UnmapViewOfFile,edi xinvoke VirtualFree,edi,0,MEM_RELEASE ;освободить память xinvoke VirtualAlloc,edi,10000h,MEM_RESERVE+MEM_COMMIT,PAGE_EXECUTE_READWRITE ;зарезервировать test eax,eax jz @f ;без этой проверки функция работает если параметры в "разумных" пределах add edi,10000h ;память резервируется по 10000h за шаг cmp edi,esi jl @b @@: popa ret endp
Симпатичная процедурка.. Но все-таки возникают некоторые вопросы
- Когда и с какими параметрами её использовать
- Что за xinvoke
Память нужна для того, чтобы загрузить туда файл. А размер файла в памяти и его смещение указанны в заголовке (ImageSize(PE+50h),ImageBase(PE+34h)).
Допустим, наш файл А хочет запустить из памяти файл Б. Может возникнуть такая ситуация, что их ImageBase совпадут или области памяти «пересекутся». Тогда, если мы сделаем вызов UnmapViewOfFile в файле А, то при возврате из процедуры попадем на кусок свободной памяти, что приведет к исключению. Для того, чтобы этого избежать, перед тем как выделять область под файл Б, следует перенести процедуру загрузки из А куда-нибуть подальше (я использовал память за ImageBase+ImageSize файла Б). А так как процедура загрузки переносится, она должна быть базонезависимой. xinvoke это макрос вида
macro xinvoke proc,[arg] { common if ~ arg eq reverse pushd arg common end if call [ebx+_#proc-_delta] }
Он просто вызывает АПИ с именем _ОригинальнаяАПИ, учитывая дельту смещения.
Теперь мы должны делать следующие шаги, учитывая что загрузчик вместе с образом файла Б перенесен куда-то в другую область памяти.
Не мешало бы скопировать заголовок Б в выделенную область памяти (дальше просто «память»). Для этого esi ставим на начало образа Б, edi — на «память», ecx=HeaderSize(PE+54h)+18h(sizeof.IMAGE_FILE_HEADER).
Когда заголовок скопирован, следует разместить все секции в памяти по своим местам, учитывая выравнивание.
proc process_sections num, pStable,pImageBase,pImage ;num - кол-во секций, берем Number Of Sections из IMAGE_FILE_HEADER ;pStable - указатель на таблицу секций (сразу за массивом DataDirectory) ;pImageBase - указатель на выделенную "память" ;pImage - указатель на образ Б pusha mov ecx,[num] mov edx,[pStable] @@: push ecx mov edi,[edx+0ch] ;Section RVA add edi,[pImageBase] ;Section VA mov ecx,[edx+10h] ;Physical Size mov esi,[pImage] add esi,[edx+14h] ;Physical Offset rep movsb ;копируем секцию на её законное место pop ecx add edx,28h ;следующая dec ecx jnz @b popa ret endp
Осталасось заполнить таблицу импортов файла Б.
proc process_imports pTab,pImageBase ;pTab - RVA таблицы импортов (берем из массива DataDirectory) ;pImageBase - указатель на "память" pusha mov edx,[pTab] add edx,[pImageBase] ;VA таблицы импортов .loop: push edx mov edi,[edx+4*4] ;import address table (её и будем заполнять) mov esi,[edx] ;lookup table (можно сделать просто = import address table, они идентичны) test edi,edi jz .ends ;последний IMAGE_IMPORT_DESCRIPTOR нулевой test esi,esi jnz .ok mov esi,edi .ok: mov ecx,[pImageBase] add esi,ecx add edi,ecx ;VA соответствующих таблиц mov eax,[edx+4*3] add eax,ecx ;VA имени DLL @@: cmp byte[eax],0 jnz @f inc eax jmp @b ;могут быть нули для выравнивания @@: xinvoke LoadLibrary,eax mov edx,eax ;загрузить DLL .po_1_dll: lodsd ;RVA IMAGE_IMPORT_BY_NAME test eax,eax jz .exit_it ;таблица заканчивается нулевым элементом bt eax,31 ;если установлен 31 бит, то импорт по ординалу. jnc .no_ord and eax,0ffffh jmp .getproc .no_ord: add eax,[pImageBase] ;VA IMAGE_IMPORT_BY_NAME add eax,2 ;VA IMAGE_IMPORT_BY_NAME.Name .getproc: push edx xinvoke GetProcAddress,edx,eax ;получить адрес АПИ pop edx stosd ;поместить его на свое место jmp .po_1_dll .exit_it: pop edx add edx,5*4 ;перейти к следующему IMAGE_IMPORT_DESCRIPTOR jmp .loop .ends: pop edx popa ret endp
Теперь можно передавать управление точке входа, но перед этим освободить память, где находится загрузчик.
push MEM_RELEASE push 0 mov eax,ebx and eax,0fffff000h push eax ;адрес mov ecx,dword[edx+28h] ;RVA точки входа add ecx,[pVA] ;VA push ecx ;адрес возврата из VirtualFree jmp [_VirtualFree+ebx-_delta] ;освобождаем память
После выполнения этого кода мы уже не возвращаемся в загрузчик, а попадаем прямо на Entry Point загружаемой программы, где и продолжается выполнение.
Кому спасибо:
- Ct757 за постоянную поддержку и сотрудничество.
- Bill Prisoner. Как всегда за слова «так пиши статью».
Литература:
- MSDN: Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
- Джеффри Рихтер «Windows для профессионалов»
Скачать исходники к статье memfile.rar
Источник WASM.RU /22.10.2006/