Запуск файла из памяти

Дата публикации 22 окт 2006

Запуск файла из памяти — Архив WASM.RU

Введение

Существует довольно много способов запуска файлов на исполнение. Чаще всего  используют CreateProcess, WinExec и прочие апи. Но что делать, если ЕХЕ-файл  находится не в виде файла, а в памяти нашего процесса?.. Можно  конечно сохранить файл на диск, после чего запустить. Но это недостаточно извращенный  метод. В этой статье я попытаюсь в общих чертах рассказать, как стартовать прямо из  памяти.

Я опишу те шаги, которые делал при создании криптора, в котором использовал данную технологию. Я уверен, что многие уже делали что-нибуть подобное. Данный материал не  является пересказыванием чьих-либо идей. Это просто описание работы кода, написанного за 2 дня. Цель, которую я ставил перед собой - не написание крутого аналога  загрузчика компании Майкрософт, а написание процедуры, которая просто получает  указатель на образ файла и запускает его.

Если использовать эту технологию в вирусах, то пропадает необходимость поиска АПИ,  ведь теперь не к файлу цепляется вирь, а файл к вирю. Впринципе это будет продемонстрированно на примере кода, который довольно легко превратить в вирус :smile3: Но лучше используйте его в мирных целях.

Техника запуска

Итак, для того, чтобы понять эту статью вам необходимо:

  1. знать формат исполняемого  файла (Portable-Executable);
  2. ассемблер (синтаксис Fasm).

Вся техника заключается в том, чтобы привести файл из того вида, в котором он лежит в  файле, в тот вид, в котором он должен быть в памяти, используя информацию, полученную  из образа файла (заголовки, структуры). После чего просто передать управление на точку входа. Я не опишу обработку экзотики типа TLS, релоки, потому что я с этим не разбирался.

Начинаем работать

Так как файл, который мы запускаем, может оказатся базозависимым, то желательно  как-нибудь заразервировать ему участок памяти, который ему нужен. Делается это  вызовом VirtualAlloc. При вызове следует указать адрес и размер области. Но память будет выделена только в том случае, если она свободна. Тут следует вспомнить, что  пользовательскому коду можно делать что угодно с памятью в диапазоне от 10000h до  7FFEFFFFh (32-разрядная Windows 2000). Воспользуемся этим и перед тем как резервировать память, будем её освобождать.

Память в нашем случае может быть в таких состояниях:

  1. Свободная
  2. Занятая
  3. Меппинг (загруженный образ исполняемого файла, например)

В первом случае ничего с этой памятью делать не надо, можно просто резервировать. Во  втором - следует сначала вызвать VirtualFree с флагом MEM_RELEASE. В третьем -  UnMapViewOfFile.

Обладая этими знаниями довольно просто написать процедуру резервирования области памяти:

Код (Text):
  1.  
  2. proc    Allock_Region pRegion,Size
  3.         pusha
  4.         mov     edi,[pRegion]
  5.         mov     esi,[Size]
  6.         add     esi,edi
  7.     ;esi - указатель на конец выделяемой области
  8.     ;edi - указатель на начало
  9.      @@:
  10.         xinvoke UnmapViewOfFile,edi
  11.         xinvoke VirtualFree,edi,0,MEM_RELEASE
  12.     ;освободить память
  13.         xinvoke VirtualAlloc,edi,10000h,MEM_RESERVE+MEM_COMMIT,PAGE_EXECUTE_READWRITE
  14.     ;зарезервировать
  15.         test    eax,eax
  16.         jz      @f
  17.     ;без этой проверки функция работает если параметры в "разумных" пределах
  18.         add     edi,10000h
  19.     ;память резервируется по 10000h за шаг
  20.         cmp     edi,esi
  21.         jl      @b
  22.      @@:
  23.         popa
  24.         ret
  25. endp

Симпатичная процедурка.. Но все-таки возникают некоторые вопросы

  1. Когда и с какими параметрами её использовать
  2. Что за xinvoke

Память нужна для того, чтобы загрузить туда файл. А размер файла в памяти и его  смещение указанны в заголовке (ImageSize(PE+50h),ImageBase(PE+34h)).

Допустим, наш файл А хочет запустить из памяти файл Б. Может возникнуть такая  ситуация, что их ImageBase совпадут или области памяти "пересекутся". Тогда, если мы сделаем вызов UnmapViewOfFile в файле А, то при возврате из процедуры попадем на  кусок свободной памяти, что приведет к исключению. Для того, чтобы этого избежать,  перед тем как выделять область под файл Б, следует перенести процедуру загрузки из А куда-нибуть подальше (я использовал память за ImageBase+ImageSize файла Б). А так как  процедура загрузки переносится, она должна быть базонезависимой. xinvoke это макрос вида

Код (Text):
  1.  
  2. macro xinvoke proc,[arg]                    
  3.   {                                        
  4.     common
  5.       if ~ arg eq
  6.     reverse
  7.       pushd arg
  8.     common
  9.       end if
  10.     call [ebx+_#proc-_delta]
  11.   }

Он просто вызывает АПИ с именем _ОригинальнаяАПИ, учитывая дельту смещения.

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

Не мешало бы скопировать заголовок Б в выделенную область памяти (дальше просто  "память"). Для этого esi ставим на начало образа Б, edi - на "память",  ecx=HeaderSize(PE+54h)+18h(sizeof.IMAGE_FILE_HEADER).

Когда заголовок скопирован, следует разместить все секции в памяти по своим местам,  учитывая выравнивание.

Код (Text):
  1.  
  2. proc    process_sections num, pStable,pImageBase,pImage
  3. ;num - кол-во секций, берем Number Of Sections из IMAGE_FILE_HEADER
  4. ;pStable - указатель на таблицу секций (сразу за массивом DataDirectory)
  5. ;pImageBase - указатель на выделенную "память"
  6. ;pImage - указатель на образ Б
  7.         pusha
  8.         mov     ecx,[num]
  9.         mov     edx,[pStable]
  10.      @@:
  11.         push    ecx
  12.         mov     edi,[edx+0ch]
  13.     ;Section RVA
  14.         add     edi,[pImageBase]
  15.     ;Section VA
  16.         mov     ecx,[edx+10h]
  17.     ;Physical Size
  18.         mov     esi,[pImage]
  19.         add     esi,[edx+14h]
  20.     ;Physical Offset
  21.         rep     movsb
  22.     ;копируем секцию на её законное место
  23.         pop     ecx
  24.         add     edx,28h
  25.     ;следующая
  26.         dec     ecx
  27.         jnz     @b
  28.  
  29.         popa
  30.         ret
  31. endp

Осталасось заполнить таблицу импортов файла Б.

Код (Text):
  1.  
  2. proc    process_imports pTab,pImageBase
  3. ;pTab - RVA таблицы импортов (берем из массива DataDirectory)
  4. ;pImageBase - указатель на "память"
  5.         pusha
  6.         mov     edx,[pTab]
  7.         add     edx,[pImageBase]
  8.     ;VA таблицы импортов
  9.       .loop:
  10.         push    edx
  11.         mov     edi,[edx+4*4]
  12.     ;import address table (её и будем заполнять)
  13.         mov     esi,[edx]
  14.     ;lookup table (можно сделать просто = import address table, они идентичны)
  15.         test    edi,edi
  16.         jz      .ends
  17.     ;последний IMAGE_IMPORT_DESCRIPTOR нулевой
  18.         test    esi,esi
  19.         jnz     .ok
  20.         mov     esi,edi
  21.       .ok:
  22.     mov ecx,[pImageBase]
  23.         add     esi,ecx
  24.         add     edi,ecx
  25.     ;VA соответствующих таблиц
  26.         mov     eax,[edx+4*3]
  27.         add     eax,ecx
  28.     ;VA имени DLL
  29.       @@:
  30.         cmp     byte[eax],0
  31.         jnz     @f
  32.         inc     eax
  33.         jmp     @b
  34.     ;могут быть нули для выравнивания
  35.       @@:
  36.         xinvoke LoadLibrary,eax
  37.         mov     edx,eax
  38.     ;загрузить DLL
  39.       .po_1_dll:
  40.         lodsd
  41.     ;RVA IMAGE_IMPORT_BY_NAME
  42.         test    eax,eax
  43.         jz      .exit_it
  44.     ;таблица заканчивается нулевым элементом
  45.         bt      eax,31
  46.     ;если установлен 31 бит, то импорт по ординалу.
  47.         jnc     .no_ord
  48.         and     eax,0ffffh
  49.         jmp     .getproc
  50.       .no_ord:
  51.         add     eax,[pImageBase]
  52.     ;VA IMAGE_IMPORT_BY_NAME
  53.         add     eax,2
  54.     ;VA IMAGE_IMPORT_BY_NAME.Name
  55.       .getproc:
  56.         push    edx
  57.         xinvoke GetProcAddress,edx,eax
  58.     ;получить адрес АПИ
  59.         pop     edx
  60.         stosd
  61.     ;поместить его на свое место
  62.         jmp     .po_1_dll
  63.       .exit_it:
  64.         pop     edx
  65.         add     edx,5*4
  66.     ;перейти к следующему IMAGE_IMPORT_DESCRIPTOR
  67.         jmp     .loop
  68.       .ends:
  69.         pop     edx
  70.         popa
  71.         ret
  72. endp

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

Код (Text):
  1.  
  2.       push    MEM_RELEASE
  3.         push    0
  4.         mov     eax,ebx
  5.         and     eax,0fffff000h
  6.         push    eax
  7.         ;адрес
  8.         mov     ecx,dword[edx+28h]
  9.         ;RVA точки входа
  10.         add     ecx,[pVA]
  11.         ;VA
  12.         push    ecx
  13.         ;адрес возврата из VirtualFree
  14.         jmp     [_VirtualFree+ebx-_delta]
  15.         ;освобождаем память

После выполнения этого кода мы уже не возвращаемся в загрузчик, а попадаем прямо на Entry Point загружаемой программы, где и продолжается выполнение.

Кому спасибо:

  • Ct757 за постоянную поддержку и сотрудничество.
  • Bill Prisoner. Как всегда за слова "так пиши статью".

Литература:

  • MSDN: Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
  • Джеффри Рихтер "Windows для профессионалов"

Скачать исходники к статье memfile.zip

© Freeman

0 1.927
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532