Внедрение кода в ехе-файл в конец секции без добавления API-функций.
Алгоритм внедрения кода
Технология внедрения кода в ехе-файл применяется для изменения исходного функционирования программы при отсутствии исходного текста программы.
При низкой подготовке хакера внедрение кода может вызвать проблемы и ошибки в приложениях. Эту технологию часто используют и вредоносные приложения. Фактически, внедрение кода – это грязный взлом.
Но в учебных целях технологию внедрения знать необходимо хотя бы по тому, что необходимо представлять этот механизм и противостоять ему.
Сначала рассмотрим механизм внедрения кода без использования новых API-функций, т.к. при таком внедрении необходимо представлять все параметры внедряемых функций и способы их передачи. А это сильно усложняет процесс внедрения.
В начале процесса внедрения необходимо подсчитать количество свободных байт в секции кода. Пример такого подсчета был изложен при рассмотрении РЕ-формата.
Для внесения изменений в ехе-файл необходимо найти в программе свободное место. В простейшем случае, когда код внедрения небольшой и простой (т.е. не внедряются дополнительные API-функции) – его всегда можно найти в конце исполняемого ехе-файла (CodeCave). Механизм внедрения состоит в том, что сначала одну или две команды (в зависимости от необходимого количества байтов) необходимо скопировать и перенести в конец ехе-файла. А на их место поставить jmp на новый адрес, куда перенесена команда (команды). После написания в конце ехе-файла нового фрагмента последней командой необходимо также поставить команду jmp на адрес возврата – адрес первой неизмененной в начале ехе-файла команды.
Алгоритм внедрения простейшего кода (без добавления API-функций) в ехе-файл может быть следующий:
- Подсчет числа свободных байтов в секции кода. Пример такого подсчета при помощи программы-анализатора LordPE был изложен при рассмотрении РЕ-формата.
- Открываем в отладчике x64Dbg ранее полученный ехе-файл: Файл/Открыть.
- Выполняем программу, нажимая кнопку F9.Копируем одну (как правило) или две (для того, чтобы вставить команду jmp с адресом перехода) команды с начала программы.
- Копируем одну (как правило) или две (для того, чтобы вставить команду jmp с адресом перехода) команды с начала программы.
- Вставляем скопированные команды с начала программы в конец файла и копируем адрес начала этого нового фрагмента.
- На освободившееся место в начале программы вставляем команду jmp c адресом на начало нового в конце кода фрагмента.
- Изменяем и/или дописываем необходимый код.
- Последней командой в конце нового фрагмента должна быть команда jmp с адресом возврата – адресом первой не измененной в начале программы команды.
- Сохраняем изменения.
Рассмотрим процесс внедрения на примере программы 4.1, в которой решается уравнение ab+c/d в среде masm64 и осуществляется вывод времени ее выполнения. Сначала напишем программу, получим ее ехе-файл и проанализируем его в отладчике x64Dbg.
Программа 4.1. Решение уравнения ab+c/d в среде masm64 и вывод времени ее выполнения:
Код (ASM):
- include win64a.inc
- .data ; директива начала сегмента данных
- a1 dq 1 ; резервирование в памяти 8 байтов для переменной Х
- b1 dq 2 ;
- c1 dq 4 ;
- d1 dq 2
- res1 dq ?
- titl db «Вывод через функцию MessageBox»,0; название упрощенного окна
- st1 dq ?,0 ; буфер выведения сообщения.
- ifmt db «Вывод чисел с памяти через MessageBox:»,10,9,»ab+c/d»,10,
- «a = %d»,10,»b = %d»,10,»c = %d»,10,»d = %d»,10,»res = %d»,10,10,
- «Время выполнения = %d тиков»,10, ;
- «Автор программы: НТУ ХПИ, каф. ВТП»,0 ;
- .code ; директива начала сегментa команд
- WinMain proc
- sub rsp,28h; cтек: 28h=32d+8; 8 — возврат
- mov rbp,rsp
- rdtsc
- xchg rax,r15
- mov r10,a1 ; a:=1
- imul r10,r10,2 ; ab
- xor rdx,rdx ; подготовка к делению
- mov rax,c1 ; c:=4
- mov r11,d1 ; d:=2
- div r11 ; c/d
- add rax,r11 ; ab+c/d
- mov res1,rax
- rdtsc
- sub rax,r15
- invoke wsprintf,ADDR st1,ADDR ifmt,a1,b1,c1,d1,res1,rax
- invoke MessageBox,0,addr st1,addr titl,MB_ICONINFORMATION;
- invoke RtlExitUserProcess,0 ;возвращение упр. ОС и освобожд. ресурсов
- WinMain endp
- end
Результат выполнения программы приведен на рис.
Алгоритм программы – линейный, переходы отсутствуют
При анализе алгоритма видно, что в начале программы располагаются две команды, которые предназначены для резервирования стека и его выравнивания на границу, кратную 16 байтам.
Первая команда программы – это rdtsc. Команда rdtsc – ассемблерная инструкция, которая читает счётчик TSC (Time Stamp Counter) и возвращающая в регистрах RDX:RAX количество тактов с момента последнего сброса процессора.
Команда rdtsc чаще всего используется:
- для измерения времени;
- для точного измерения временных интервалов, в том числе при проведении оптимизации (измерение времени, необходимого для выполнения конкретных инструкций или их набора);
- в антиотладочных целях;
- как источник энтропии для генераторов псевдослучайных чисел.
Содержимое ячеек памяти, которые указаны в программе можно просмотреть, если нажать правую кнопку мыши и выбрать Перейти к дампу/ Константа.
Рассмотрим внедрение нового кода в предыдущий пример. В качестве кода внедрения заменим арифметические операции на логические. Для упрощения программы фукционал и начальные условия оставим прежние.
Первоначальный вариант изменяем на новый, но с использования вместо команд умножения и деления логических команд, который может быть таким: Код (ASM):
- mov r10,1 ;
- sal r10,1 ; сдвиг влево на 1 разряд – умножение на 2
- mov r11,4 ;
- sar r11,1 ; сдвиг вправо на 1 разряд – деление на 2
- add r10,r11 ; результат операции ab + c/d
- mov res1,r10
Необходимо обратить внимание на команду занесения результата в ячейку памяти, например res1: mov res1,r10. Для записи такой команды в отладчик x64Dbg (в ехе-файл) сначала необходимо выяснить адрес ячейки памяти, а потом применить запись в виде:
mov qword ptr ds:[7FF6478B3020],r10
Фрагмент кода с применением логических команд всегда короче, а отсутствие «тяжелых» команд умножения и деления позволяет выполнить этот фрагмент намного быстрее. В рассматриваем примере, если вырезать старый функционал, то новый функционал вместится на его место. Но так бывает не всегда. Для простоты рассмотрения новый функционал разместим в конце ехе-файла. Естественно, что в таком случае необходимо проверить объем свободного места в секции программами-анализаторами РЕ-формата. Но и этот вопрос отложим для последующего рассмотрения. Будем считать, что для нашего простого внедрения места вполне достаточно.
Действия по внедрению простейшего кода (без добавления API-функций) в ехе-файл следующие:
- Открываем в отладчике x64Dbg ранее полученный ехе-файл: Файл/ Открыть.
- Выполняем программу, нажимая кнопку F9.
- Определяем, какие команды удалить (или скопировать, если надо) как старый функционал, чтобы освободить место для команды jmp.
Если необходимо только скопировать дизассемблерный код, то выполняем действия: Shift + правая клавиша мышки (для выбора нескольких команд); «Копировать»/ «Дизассемблерный код».
Сначала устанавливаем курсор на первую команду, - 00007FF69013100B 4C:8B15 EE1F0000 mov r10,qword ptr ds:[7FF690133000]которую хотим переместить и дважды нажимаем на левую клавишу мыши.
Выделенная команда имеет размерность в 7 байт. Вводим на первой выбранной команде пустую команду nop. Команда nop имеет размерность один байт. Для того, чтобы произошло автоматическое заполнение командами nop всех байт уничтожаемой команды необходимо поставить галочку Сохранить размер во всплывающем окне (рис. 4.3). - По очереди выделяем следующие команды, дважды нажимаем левую клавишу мыши, набираем команду nop и нажимаем OK.
- Пропускаем 4-й шаг алгоритма, т.к. по условию задачи не надо оставлять старый функционал.
- Вставляем команду jmp c адресом на адрес начала фрагмента нового в конце кода. Для этого ставим курсор после последней команды
call qword ptr ds:[<&RtlExitUserProcess>]
Копируем адрес свободной ячейки. Для этого нажимаем на ней правую кнопку мыши и выбираем Копировать/ Адрес. - После этого, ставим курсор в начало кода после команды
xchg rcx,rax
на первую пустую команду nop.
Дважды нажимаем правую клавишу мыши и в появившемся окне вводим слово jmp, затем пробел и вставляем адрес на свободную ячейку в конце программы сочетанием клавиш Ctrl + C - Нажимаем OK. Команда jmp 7FF6BC9010A4 вставилась.
- Дописываем необходимый код. Для этого, в конце программы, на свободный адрес (00007FF6BC9010A4), на который будем переходить командой jmp ставим курсор мыши. Дважды нажимаем левую клавишу мыши и вводим сначала первую новую команду с новым функционалом). А затем – и все остальные.
Последней командой в конце нового фрагмента должна быть команда jmp с адресом возврата – адресом первой неизмененной в начале программы команды.
Для того, чтобы не записывать адрес возврата вручную, выделим команду в начале кода, на которую планируем возвратиться. Этой командой будет: - 00007FF7BF051034 0F31 rdtscДля этого нажимаем на ней правую кнопку мыши и выбираем Копировать/ Адрес. Необходимый адрес скопируется в буфер.
В результате, новый блок кода будет иметь вид - Необходимо сохранить внесенные изменения. Это можно сделать двумя способами.
Первый способ: выбираем Файл/ Исправить файл. В новом окне выбрать пункт Исправить файл. Затем – новое имя и расширение ехе. Для того, чтобы иметь возможность вернуться и загрузить наиболее правильную исправленную версию необходимо сохранять изменения и давать каждый раз новые имена или индексы к ним для сохраненных ехе-файлов.
Второй способ: выбрать пиктограмму с названием Исправления, нажать на кнопку Исправить файл, а далее новое имя и расширение ехе.
После проведенных преобразований новый ехе-файл выводит сообщение, которое в дальнейшем можно тоже изменить.
Можно изменить и текстовые сообщения.
Для внесения изменений в упрощенное окно вывода информации можно использовать функцию преобразования wsprintf. Это позволит также внести изменения в вывод информации в самом окне.
При передаче параметров в функции wsprintf
- первым параметром является параметр ADDR st1, который передается функции через регистр RCX.
- Второй параметр – это ADDR ifmt – строка форматирования, который передается через регистр RDX.
- Третий параметр – Z – передается через регистр R8.
- Четвертый параметр – X – передается через регистр R9.
- Остальные параметры передаются через стек. Например, пятый – параметр Y – через [rsp + 20h]. Шестой – параметр rax – через [rsp + 28h] и т.д.
Можно внести изменения и в функцию MessageBox. Эта функция предназначена для вывода сообщений. В ней можно поменять и титул упрощенного окна, и непосредственное сообщение.
Для внесения изменений в вывод информации необходимо воспользоваться таблицей кодировки символов страницы кодировки 1251, например http://foxtools.ru/ASCII#1251
Будем менять второй параметр функции wsprintf – это ADDR ifmt – строка форматирования, который передается через регистр RDX.
Для этого, выделяем правой кнопкой мыши строку с передачей второго параметра
lea rdx,qword ptr ds:[7FF608483057]
и выбираем Перейти к дампу/ Константа.
После того, как в дампе памяти будет отображаться содержимое, начиная с адреса 7FF608483057,необходимо подвести курсор к тем байтам дампа памяти, которые будем менять. Дважды нажать левую клавишу мышки на ячейке дампа памяти и в появившемся окне исправляем кодировку символов
Наиболее часто применяемый символ – это символ пробела. Он имеет кодировку = 20. Этой кодировкой можно затереть всю ненужную информацию. А более практично – это написать любой текст, например, слово Инфицировано, каждая буква которого кодируется соответствующим символом, а все слово пишется как C8 ED F4 E8 F6 E8 F0 EE E2 E0 ED EE. Для изменения нескольких подряд расположенных байтов в дампе памяти удобнее мышкой выделить эти значения, нажать на правую клавишу мышки Двоичные операции/Заполнить и ввести новую текстовую информацию
Окно с окончательным результатом внедрения приведено на рис.
Таким образом, после проведенного внедрения кода можно сделать выводы: