16/09/2021

Маленький взлом системы (2): заставим процессор выполнить переменную

В прошлой статье (Маленький взлом системы (1): наконец-то вы сможете изменять строки типа char* String) мы рассмотрели как можно писать в область памяти, в которую запись запрещена. Сегодня мы разовьём эту тему и попробуем выполнить переменную. Начнём с подготовительных работ. Как и в прошлый раз, нам придётся изменить режим доступа к памяти, так чтобы её можно было исполнять, то есть, чтобы выполнить call или jmp на неё. Для этого объявим переменную следующим образом:

uint8_t lMagicCode [] = { };

Содержание её мы рассмотрим ниже. А сейчас выполним установку необходимых нам режимов доступа к этой переменной. Здесь повторим действия из примера в предыдущей статьи:

void* lPageBoundary = (void*) ((long) lMagicCode & ~(getpagesize () - 1));
mprotect (lPageBoundary, 1, PROT_EXEC | PROT_WRITE);

Мы добавили режим доступа PROT_EXEC, который и позволит нам выполнить переменную. Как вы заметили, я оставил режим PROT_WRITE. Это сделано потому что MMU работает со страницами. На наших машинах её размер, вероятнее всего будет 4kB, что довольно много и помеченная страница скорее всего заденет область, следующую за интересующей нас переменной. А нам нужен режим чтения и записи для обвязки нашего эксперимента. Поэтому чтобы не схватить SIGSEGV после вызова mprotect, мы выставляем совмещённый режим — запись (которая на x86 MMU не бывает без режима чтения) и выполнение.

Далее нам нужно как-то указать машине, как и когда выполнить эту переменную. На всякий случай помечу: эта переменная сама по себе никогда не выполнится, функция mprotect не запустит её, а лишь пометит, как доступную для выполнения. Перейдём к последнему этапу подготовительных работ на высоком уровне (на уровне C), который заключается в том, чтобы объявить функцию, ссылающуюся на адрес этой переменной, что в последствии, позволит нам выполнять эту переменную.

unsigned int (*NewExit)(unsigned long _ExitCode) = (void*)lMagicCode;

Здесь мы объявили указатель на функцию с названием NewExit, принимающую один параметр _ExitCode и возвращающую значение. Теперь мы можем выполнить нашу переменную и получить возвращаемое ею значение простой строчкой:

unsigned int lResult = NewExit (1);

Пока не надо этого делать — пытаясь выполнить пустую переменную, то есть, выполняя call на адрес пустой переменной, вы по сути дела, «провалитесь» дальше (как было в примере предыдущей статьи с выводом строки), а там может быть что угодно и, скорее всего, данные, даже не похожие на последовательность байт, представляющую собой корректную машинную команду с операндами. Что, вероятнее всего, приведёт к ошибке Illegal instruction.

Далее нам нужно заполнить нашу переменную корректным кодом. Начнём, допустим с возврата, то есть исключим «проваливание» потока выполнения при вызове этой функции. По справочникам найдём код операции ret, который очень простой и равен C3h.

uint8_t lMagicCode [] = { 0xC3 };

На самом деле это retn — return near, то есть ближний возврат или внутрисегментный возврат — возврат в пределах одного сегмента кода.

Теперь можно выполнить нашу функцию, не боясь неопределённых последствий, так как теперь произойдёт следующее: команда call сохранит в стэке адрес возврата равный адресу операнда следующему сразу за ним (адрес call + длинна инструкции call и его операнда), затем перейдёт по указанному адресу (адресу нашей переменной), в которой лежит код команды ret, которая, в свою очередь извлечёт адрес возврата из стэка и выполнит переход по нему.

printf ("The result of NewExit is: %d.\n", lResult);

Сейчас мы будем видеть «мусор» на выходе из функции. Для получения какого-то осмысленного результата, вернём значение. Значение из функции по правилам x86_64 ABI возвращается через регистр «семейства» AX. Мы объявили нашу функцию как возвращающую значение unsigned int, то есть 32 бита. Значит нам нужно записать результат в EAX — 32-битный регистр. Найдём код загрузки константы в EAX — это B8h. При заполнении команды нужно учитывать размерность операнда. Здесь мы не можем написать 0xB8, 0x04. Нам нужно указать всё значение полностью.

В языках высокого уровня это за нас делает компилятор. GAS так же подставляет дополненные значения в зависимости от суффикса мнемонического представления команды, например movl $0x04, %eax запишет в реальный код B804000000h, дополнив нашу четвёрку нулями до длинны long. Здесь это не тот long, о котором мы говорили в моделях памяти (LP64), это long с точки зрения x86-ой машины длинной 32 бита. Некоторые трансляторы даже подбирают код конкретной операции из обобщённого мнемонического представления и дополняют размерность операнда в зависимости от указанных размерностей приёмника и источника.

В противном случае, если мы не распишем все байты составляющие 32-битное значение, последующий код, который мы запишем как следующую инструкцию или операнд, в процессе выборки команды процессором будет воспринят как данные для загрузки в EAX. Поэтому соберём код 0xB8, 0x07, 0x00, 0x00, 0x00. И наша переменная приобретёт следующий вид:

uint8_t lMagicCode [] =
{
  0xB8, 0x07, 0x00, 0x00, 0x00, // movl $0x7, %eax
  0xC3,                         // retn
};

Теперь, вызвав эту функцию как

printf ("The result of NewExit is: %d.\n", NewExit (0));

Мы получим:

The result of NewExit is: 7

Функция названа NewExit. Давайте придадим ей этот смысл. Для этого мы воспользуемся системной функцией под номером 60/3Ch и вызовем её при помощи syscall, которая имеет код операции 0F05h. Операционная система, при вызове системных функций от пользовательских процессов, принимает номер функции в регистре EAX. Писать туда мы уже умеем. Наша переменная с машинным кодом приобретёт такой вид (возврат из этой функции нам уже не нужен):

uint8_t lMagicCode [] =
{
   0xB8, 0x3C, 0, 0, 0, // movl $0x3C, %eax, 3Ch/60 - system call exit()
   0x0F, 0x05,          // syscall
};

У нашей функции есть один параметр. Этот параметр попадёт в возвращаемое значение функции main, то есть код нашей переменной аналогичен функции exit. Вы можете это проверить запустив программу на исполнение и проверив код выхода при помощи echo $?. Вы увидите число переданное вами в функцию NewExit.

Хоть мы и передаём int в качестве параметра, возвращаемое из функции main значение всегда снижается системой до одного байта, поэтому не имеет смысла задавать значения более 255.

Здесь может возникнуть вопрос — «А почему так? Мы так много кода писали для простейших действий, а возвращаемое значение попадает в систему без каких либо операций вообще». Дело в том, что по правилам x86_64 ABI первый параметр, указанный в функции, записывается в регистр RDI. А системная функция exit, 60/3Ch возвращает в систему в качестве кода выхода значение RDI. Так совпало — наше значение «провалилось» насквозь и попало в оболочку, в переменную $? и писать нам для этого действительно ничего не пришлось.

P.S.
Интересно отметить тот факт, что отлаживать участки программы, представленные в виде переменных и сформированные в качестве их содержания, будет проблематично. Это связано с тем, что этот код попадает в секцию .data, которая отладчиком не рассматривается как код. Даже если посмотреть ассемблерный вывод после компилятора, то мы увидим просто переменную, в которой будут числовые значения — перечень наших байт в объявленном массиве.

        .size   lMagicCode, 7
lMagicCode:
        .byte   -72
        .byte   60
        .byte   0
        .byte   0
        .byte   0
        .byte   15
        .byte   5

Ниже приведу всю программу:

#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/mman.h>

int main (void)
{
  uint8_t lMagicCode [] =
  {
//     0xB8, 0x3C, 0, 0, 0,          // movl $0x3C, %eax # 3Ch/60 - system call exit()
//     0x0F, 0x05,                   // syscall
    0xB8, 0x07, 0x00, 0x00, 0x00, // movl $0x7, %eax
    0xC3,                         // retn
  };
  
  void* lPageBoundary = (void*) ((long) lMagicCode & ~(getpagesize () - 1));
  
  mprotect (lPageBoundary, 1, PROT_EXEC | PROT_WRITE);
  
  unsigned int (*NewExit)(unsigned long _ExitCode) = (void*)lMagicCode;
  
  unsigned long lResult = NewExit (2);
  
  printf ("The result of NewExit is: %d. Or will never be printed...\n", NewExit (0));
  
  return 0;
}

No comments:

Post a Comment