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

Все статьи

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

Введение

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

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

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

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

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

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

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

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

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

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

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

В первом случае ничего с этой памятью делать не надо, можно просто резервировать. Во  втором - следует сначала вызвать 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

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

  1. Когда и с какими параметрами её использовать
  2. Что за 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.zip

2002-2013 (c) wasm.ru