Секреты покорения эльфов

Имплантация чужеродного кода в ELF-файл


Для экспериментов по имплантации нам потребуется живой исполняемый файл, который при помощи компилятора и текстового редактора мы сможем изготовить и самостоятельно. Нажмем <Shift-F4> в Midnight Commaner'е, наберем программу следующего содержания (см. листинг1), затем <F2>/"имя-файла.c" и откомпилируем ее своим любимым gcc с настойками по умолчанию (gcc имя-файла.c -o имя-файла).

#include <stdio.h>

main()

{

       printf("LORDI - the best group in the world!\n"\

       "(www.lordi.org)\nmonsters, bondage and sado-maso\n");

}

Листинг 1 демонстрационная программа, в которую мы будем внедрять посторонний код

Рисунок 5 создание демонстрационной программы для внедрения

Образовавшийся файл загрузим в hex-редактор ("#./ht-0.7.5-linux-i386 имя-файла"), а затем нажмем <F6> (mode) и выберем "elf/image". Редактор перейдет в режим отображения образа исполняемого файла, автоматически перенося нас в окрестности точки входа, отмеченной меткой "entrypoint". Если этого не произойдет, нажмем <F5> (goto), и введем "entrypoint" (без кавычек).

Экран должен выглядеть приблизительно так:



Рисунок 6 исполняемый файл в hex-редакторе hte

Давайте для разминки просто поменяем первые две команды местами: xor ebp,ebp/pop esi на pop esi/xor ebp,ebp. Подведем курсор к первой машинной команде (она расположена по адресу 80482C2h) и нажмем <Ctrl-A> (Assemble), вводим "pop esi". Редактор предложит несколько вариантов ассемблирования на наш выбор: 5Eh и 8Fh C6h. Выбираем 5Eh, как самый короткий (8Fh C6h просто не влезет в отведенное место!), затем точно так ассемблируем команду xor ebp,ebp.

Измененные байты редактор выделят красным цветом (см. рис. 7), что наглядно и очень красиво, но при нажатии на <F2> (save) они вновь зеленеют, подтверждая, что все исправления успешно сохранены. Полей контрольной суммы в ELF-заголовке нет и потому заботиться о ее пересчете не нужно. Линух контрольную сумму файла не считает! А не считает он ее потому, что проектировался головой. Это же не Windows! Такое впечатление, что PE-файл проектировала толпа народу с трудом взаимодействующая между собой. Судите сами: и Линух, и Widows поддерживают механизм отложенной загрузки по требованию. Страницы образа проецируются в память тогда и только тогда, когда к ним происходит обращение, в результате чего немедленно после запуска файл уже готов к работе, а все недостающие страницы дозагружаются уже потом (или не загружаются вообще, например, часть программы, ответственная за печать, вообще не будет загружена, если ни разу не был выбран пункт "print"). Процесс загрузки как бы "размазывается" во времени, не нервируя никакими песочными часами, которые так любит демонстрировать Windows. Но! Ведь при подсчете контрольной суммы происходит неизбежное обращение ко всем страницам и все они загружаются в память, даже если не нужны. Получается, что у нас есть два механизма — один оптимизирует загрузку, другой ее "пессимизирует", съедая весь выигрыш. Где логика?!


А вот разработчики Линуха переложили подсчет контрольной суммы на устройства ввода/вывода, которые ее действительно считают. Конечно, это не страхует от искажений. В частности, жесткие диски контролируют только физические дефекты, но не обращают внимания на логические искажения (типа вируса). Тем не менее, особого смысла в контрольной сумме, хранящейся непосредственно в самой файле, все равно нет. Если вирус может модифицировать файл, он модифицирует и контрольную сумму. По науке, контрольные суммы нужно хранить в отдельном "защищенном хранилище" и их подсчетом должна заниматься файловая система или антивирусные ревизоры. Ни того, ни другого в мире Линуха не наблюдается. То есть, они как бы есть, но ни у кого реально не установлены.

Единственную проблему представляют протекторы и упаковщики исполняемых файлов, контролирующее собственную целостность. С каждым годом их становится все больше и больше. UPX, протектор от shiv'ы… В них на первых порах лучше не внедряться!



Рисунок 7 модификация исполняемого файла в редакторе hte, измененные байты выделяются красным цветом

Но мы отвлеклись. Выходим из hex-редактора, нажав <F10> (где мой привычный выход по Escape?!), и запускаем пропатченный файл. Он запускается, подтверждая свою работоспособность. Значит, модификация прошла успешно! (Ну еще бы! Под моим чутким руководством!)



Рисунок 8 результат работы модифицированного файла после перестановки пары команд местами — полет нормальный

А теперь займется более серьезными вещами, попытавшись внедрить в программу реальный код, который делает что-то полезное. Сразу возникает вопрос: куда мы будет внедряться? Между сегментами свободного места нет, между секциями тоже. Можно (теоретически) расширить последний сегмент и внедрится сюда, но во-первых, это будет слишком заметно, а во-вторых, слишком муторно и утомительно.

Но все не так плохо, как кажется! По умолчанию gcc выравнивает стартовые адреса функций по границе 10h, а это, значит, что даже наш демонстрационный файл содержит просто кучу свободного пространства. В среднем 10h/2h = 8h байт на каждую функцию, включая служебные. Сюда и мамонта упрятать можно, если, конечно, его предварительно расчленить. Вот, смотрите, сами:



....... ! main:                          ;xref o80482d7

....... !   push     ebp

8048385 !   mov      ebp, esp

8048387 !   sub      esp, 8

804838a !   and      esp, 0fffffff0h

804838d !   mov      eax, 0

8048392 !   sub      esp, eax

8048394 !   mov      dword ptr [esp], strz_LORDI___the_best_group_in_the_80484e0

804839b !   call     wrapper_8049634_80482b0

80483a0 !   leave

80483a1 !   ret

80483a2     nop

80483a3     nop

80483a4     nop

80483a5     nop

80483a6     nop

80483a7     nop

80483a8     nop

80483a9     nop

80483aa     nop

80483ab     nop

80483ac     nop

Листинг 2  цепочка команд NOP, оставленная компилятором в конце функции main для выравнивания

А вот еще одна лазейка — буфер ввода/вывода, расположенный в сегменте данных, дамп которого приведен ниже. Это целых 28 байт, которые можно использовать по своему усмотрению! Даже если никаких явных файловых манипуляторов в файле нет (как, например, в нашей демонстрационной программе), такой буфер все равно создается при компиляции программы, что наш случай и подтверждает.

 80484c2      db     00h ; ' '

 80484c3      db     00h ; ' '

 80484c4

 .......      ;********************************************************

 .......      ; data object _IO_stdin_used, size 4 (global)

 .......      ;********************************************************

 .......      _IO_stdin_used:

 .......      db     01h ; ' '

 80484c5      db     00h ; ' '

 80484c6      db     02h ; ' '

 80484c7      db     00h ; ' '

 80484c8      db     00h ; ' '

 80484c9      db     00h ; ' '

 80484ca      db     00h ; ' '

 80484cb      db     00h ; ' '

 80484cc      db     00h ; ' '

 80484cd      db     00h ; ' '

 80484ce      db     00h ; ' '

 80484cf      db     00h ; ' '

 80484d0      db     00h ; ' '

 80484d1      db     00h ; ' '

 80484d2      db     00h ; ' '

 80484d3      db     00h ; ' '

 80484d4      db     00h ; ' '

 80484d5      db     00h ; ' '

 80484d6      db     00h ; ' '



 80484d7      db     00h ; ' '

 80484d8      db     00h ; ' '

 80484d9      db     00h ; ' '

 80484da      db     00h ; ' '

 80484db      db     00h ; ' '

 80484dc      db     00h ; ' '

 80484dd      db     00h ; ' '

 80484de      db     00h ; ' '

Листинг 3 stdin-буфер, расположенный в сегменте данных

Остается решить как передать управление на внедренный код. Это можно сделать различными путями: скорректировать точку входа (HTE это умеет) или внедрить в ее окрестности специальный jmp. Вот там мы и поступим!

Запускам редактор, переходим в точку входа и смотрим на нее очень внимательно:

....... ! entrypoint:

....... !   pop      esi

80482c1 !   xor      ebp, ebp

80482c3 !   mov      ecx, esp

80482c5 !   and      esp, 0fffffff0h

80482c8 !   push     eax

80482c9 !   push     esp

80482ca !   push     edx

80482cb !   push     __libc_csu_fini

80482d0 !   push     __libc_csu_init

80482d5 !   push     ecx

80482d6 !   push     esi

80482d7 !   push     main

80482dc !   call     wrapper_8049630_80482a0

80482e1 !   hlt

80482e2 !   nop

80482e3 !   nop

Листинг 4 точка входа и ее окрестности

Почему бы нам не заменить pop esi/xor ebp,ebp на jmp на наш код, откуда мы сможем сделать все, что задумано, выполнить эти команды и вернуться обратно? Но для начала необходимо подготовь код, который мы будем внедрять. Для простоты выведем короткое приветствие на экран. На языке ассемблера это звучит приблизительно так:

mov eax, 4                 ; системный вызов write

mov ebx,1                  ; идентификатор стандартного вывода

mov ecx, offset begin_msg ; указатель на первый символ выводимого сообщения

mov edx, offset end_msg    ; указатель на последний символ выводимого сообщения

int

80h                           ; вывод на экран

pop esi                           ; сохраненные команды

xor ebx,ebp                ;

jmp

80482C3h               ; возврат в программу

Листинг 5 исходная программа, выводящая приветствие на экран



Это не самый оптимальный вариант и его можно здорово оптимизировать, если переписать так:

xor eax,eax

add al, 4

xor ebx,ebx

inc ebx

mov ecx, offset begin_msg

mov edx,ecx

add edx, sizeof(msg)

int 80h

pop esi

xor ebp, ebp

jmp

80482C3h

Листинг 6 оптимизированный вариант

Теперь прокручивая файл в hex-редакторе, найдем и выпишем стартовые адреса всех цепочек NOP'ов, пригодных для внедрения. А какие цепочки пригодны для внедрения? Если две соседние цепочки расположены в пределах досягаемости короткого перехода (грубо — в пределах сотни байт), 3х NOP'ов будет вполне достаточно (2 байта на команду перехода, один — на любую однобайтовую команду полезного кода, например, inc ebx или pop esi). В противном случае нам необходимо иметь цепочку по крайней мере из 6ти NOP'ов — пять на команду близкого перехода и один на полезную команду.

В нашем случае получается:

8048306h                                10 байт

80483a2                  14 байт

8048464                  12 байт

Листинг 7 перечень стартовых адресов цепочек NOP'ов пригодных для внедрения и их длина

Итого — 36 байт. Вполне достойное место для демонстрационной программы! Начинаем заполнять цепочки NOP'ов полезным кодом. С первой попытки у нас получается:

8048306              31 c0                      xor    eax, eax

8048308              04 04                      add    al, 4

804830a              e9 93 00 00 00                    jmp    80483a2h

804830f              90                         nop

Листинг 8 заполняем первую цепочку NOP'ов (предварительный вариант)

При этом один последний NOP остается потерян, но по-другому не получается. Команда XOR EBX,EBX занимает два байта и сюда не лезет. А что, если переставить команды местами? Перенести add al,4 в следующую цепочку NOP, а вместо нее вставить XOR EBX,EBX/INC EBX

8048306              31c0                       xor    eax, eax

8048308              31db                       xor    ebx, ebx



804830a              43                         inc    ebx

804830b              e9 92 00 00 00                    jmp    80483a2h

Листинг 9 заполняем первую цепочку NOP'ов (окончательный вариант)

Тогда следующая цепочка будет заполнена так:

80483a2              0404                        add    al, 4

80483a4              b9 ?? ?? ?? ??                     mov    ecx, offset begin_msg

80483a9              89ca                        mov    edx, ecx

80483ab              e9 b4 00 00 00                    jmp    8048464h

Листинг 10 заполняет вторую цепочку NOP'ов (предварительный вариант)

В третью, последнюю, цепочку NOP'ов остаток кода уже не вмещается, не хватает одного единственного байта! Что ж, попытаемся еще немного ужать наш код. Например, пары инструкций mov edx,ecx/add edx,sizeof(msg), которые занимают 5 байт, можно использовать lea edx,[ecx+sizeof(msg)]. Тогда все влезает! Ну а само сообщение можно разместить в сегменте данных… Поскольку, свободного места там не очень много, ограничимся строкой "hello". Завершающий нуль в конце ставить необязательно, поскольку системный вызов write выводит ровно столько символов, сколько ему приказано вывести и ни на какие знаки "останова" не реагирует.

Если все было сделано правильно (что маловероятно, в первый раз ошибки делают все), наш файл победоносно выведет строку "hello", а следом за ней, ту строку, которая выводит сама подопытная программа и экран будет выглядеть так:



Рисунок 9 результат работы программы после внедрения (строка hello, которую выводит имплантированный код, для наглядности выделена красным цветом и обведена овалом)


Содержание раздела