Обратите внимание: Пусть вас не смущает такое вступление. Эта часть статьи, вне всякого сомнения, рассказывает о ptrace, а не об ELF. Но знание формата ELF определенно необходимо, чтобы уметь обращаться к памяти трассируемого процесса. Итак, приступим.
ELF -- это Executable and Linking Format (Формат Исполняемых и Связываемых файлов). Он определяет формат двоичных исполняемых файлов, объектных файлов, разделяемых объектов (библиотек), а так же файлов core dump. Формат ELF используется как компоновщиками (linkers), так и загрузчиками программ, хотя каждый из них интерпретирует ELF-файлы по-своему.
ELF-файл состоит из множества секций и сегментов. Объектные файлы имеют таблицу заголовков секций, исполняемые -- таблицу программных заголовков, а разделяемые библиотеки -- и то и другое. В следующем разделе я познакомлю вас с этими заголовками поближе.
Любой ELF-файл имеет ELF-заголовок. Заголовок всегда размещается в самом начале файла. Он содержит описание двоичного файла, определяя таким образом порядок интерпретации файла.
Структура заголовка приведена ниже (см. /usr/src/include/linux/elf.h) (путь меняется в зависимости от дистрибутива -- прим.ред.)
#define EI_NIDENT 16 typedef struct elf32_hdr{ unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; /* Точка входа */ Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; } Elf32_Ehdr;
Кратко опишу поля структуры
e_ident : Сигнатура и прочая информация. Зависит от аппаратной платформы.
e_type : Содержит информацию о типе файла. Тип может быть одним из следующих: "объектный", "исполняемый", "разделяемый" (shared object) и "core".
e_machine : Вы наверняка уже догадались, что это поле определяет аппаратную архитектуру -- Intel 386, Alpha, Sparc и т.п.
e_version : Версия объектного файла.
e_phoff : Смещение до первого программного заголовка.
e_shoff : Смещение до первого заголовка секции.
e_flags : Флаги процессора. Не используется для i386
e_ehsize : Размер ELF-заголовка в байтах.
e_phentsize & e_shentsize : Размер программного заголовка и заголовка секции, в таблицах программных заголовков и заголовков секций соответственно.
e_phnum & e_shnum : Количество программных заголовков и заголовков секций в соответствующих таблицах.
e_shstrndx : В таблице заголовков секций есть секция, которая содержит имена других секций. Это индекс такой секции в таблице. (см. ниже)
Как я уже упоминал выше, компоновщики интерпретируют файл как множество секций, описанных в таблице заголовков секций, а загрузчик -- как множество сегментов, описанных в таблице программных заголовков. В этом разделе приводится более или менее подробное описание заголовков секций и сегментов.
Двоичный файл выглядит как набор секций, каждую из которых можно представить в виде массива байт. Даже при наличии дополнительной информации, помогающей корректно интерпретировать содержимое секции, приложения могут выполнять интерпретацию по-своему.
Таблица заголовков секций представляет из себя массив заголовков. Нулевой элемент массива всегда пуст и не соответствует ни одной из секций. Каждый заголовок секции имеет следующий формат (см. /usr/src/include/linux/elf.h):
typedef struct elf32_shdr { Elf32_Word sh_name; /* Имя секции, индекс в таблице строк (Elf32) */ Elf32_Word sh_type; /* Тип секции (Elf32) */ Elf32_Word sh_flags; /* Различные атрибуты секции */ Elf32_Addr sh_addr; /* Виртуальный адрес секции */ Elf32_Off sh_offset; /* Смещение от начала файла */ Elf32_Word sh_size; /* Размер секции в байтах */ Elf32_Word sh_link; /* Индекс следующей секции (Elf32) */ Elf32_Word sh_info; /* Дополнительные сведения о секции (Elf32) */ Elf32_Word sh_addralign; /* Выравнивание секции */ Elf32_Word sh_entsize; /* Размер записи в таблице */ } Elf32_Shdr;
Теперь о полях структуры более подробно.
sh_name : Индекс строки в секции, содержащей таблицу строк e_shstrndx. Указывает на начало строки, завершающейся нулевым (0x00) символом, которая используется в качестве имени секции.
sh_type : Тип секции, например, данные, таблица символов, таблица строк и т.п..
sh_flags : Содержит вспомогательную информацию, определяющую порядок интерпретации содержимого секции.
sh_addralign : Содержит размер выравнивания для секции, обычно 0, 1 (оба означают отсутствие выравнивания) или 4.
Смысл назначения остальных полей структуры достаточно прозрачно следует из их названий.
Сегменты используются в процессе загрузки программы, т.е. при создании образа процесса в памяти. Каждый сегмент описывается соответствующим программным заголовком. Таблица представляет из себя массив заголовков. Каждый программный заголовок имеет следующий формат:
typedef struct { Elf32_Word p_type; /* Тип сегмента */ Elf32_Off p_offset; /* Смещение от начала файла */ Elf32_Addr p_vaddr; /* Виртуальный адрес сегмента */ Elf32_Addr p_paddr; /* Физический адрес сегмента */ Elf32_Word p_filesz; /* Размер сегмента в файле */ Elf32_Word p_memsz; /* Размер сегмента в памяти */ Elf32_Word p_flags; /* Флаги сегмента */ Elf32_Word p_align; /* Выравнивание сегмента */ } Elf32_Phdr;
p_type : Определяет тип сегмента, т.е. задает порядок его интерпретации, например:
и т.п..
p_vaddr : относительный виртуальный адрес загрузки сегмента.
p_paddr : физический адрес загрузки сегмента.
p_flags : Содержит флаги прав доступа -- чтение/запись/исполнение
p_align : Выравнивание сегмента в памяти. Если сегмент имеет тип "загружаемый" (loadable), то он выравнивается по границе страницы памяти.
Смысл назначения остальных полей структуры понятен из их названий.
Мы уже имеем представление о структуре ELF-файла. Теперь перейдем к рассмотрению порядка загрузки файла. Обычно, для того чтобы запустить программу мы набираем ее имя в командной строке. На самом деле, после того как мы нажмем на клавишу RETURN (или, если хотите -- ENTER), происходит масса интересных вещей.
Прежде всего командная оболочка вызывает стандартную функцию из libc, которая в свою очередь обращается к ядру. Теперь в игру вступает ядро. Ядро открывает файл и определяет тип файла как исполняемый. Затем загружает файл и все необходимые библиотеки, инициализирует стек и передает управление загруженной программе.
Программа загружается по адресу 0x08048000 (см. /proc/pid/maps), а стек начинается с адреса 0xBFFFFFFF (стек "растет" в сторону меньших адресов).
Теперь, когда процесс загружен в память и нам известно его адресное пространство, мы можем выполнять трассировку этого процесса (при наличии прав доступа) и просматривать/изменять данные в памяти процесса. Однако сказать легко, сделать -- сложнее. Тем не менее, почему бы не попробовать?
Прежде всего, давайте попробуем создать программу, которая могла
бы читать/писать в регистры процессора другой программы. Для
аргумента request
мы будем использовать следующие
значения.
Важно : Не следует забывать о необходимости этого вызова, иначе процесс останется в режиме останова.
struct user_regs_struct
)
определена в файле asm/user.h.
struct user_regs_struct { long ebx, ecx, edx, esi, edi, ebp, eax; unsigned short ds, __ds, es, __es; unsigned short fs, __fs, gs, __gs; long orig_eax, eip; unsigned short cs, __cs; long eflags, esp; unsigned short ss, __ss; };
Теперь вставим некоторый код в тело трассируемого процесса и заставим процесс исполнить его, изменив содержимое регистра eip (instruction pointer).
Делается это довольно просто. Для начала мы запускаем трассировку процесса, затем считываем содержимое регистров. Куда-нибудь в стек вставляем наш код, который мы хотим исполнить и изменяем содержимое регистра eip таким образом, чтобы он указывал на первую инструкцию нашего кода. После чего завершаем трассировку процесса. После этих действий трассируемый процесс приступит к исполнению нашего внедренного кода.
Пример состоит из трех файлов с исходными текстами. Первый -- это сам трассировщик. Второй -- это код на языке Ассемблера, который будет вставляться в тело трассируемого процесса. И третий -- небольшая программка, которая будет подвержена нашим экспериментам.
Файлы с исходными текстами
Скомпилируем файлы.
#cc Sample.c -o loop #cc Tracer.c Code.S -o catch
Перейдите в другую консоль и запустите программу loop:
#./loop
Вернитесь обратно и запустите трассировщик:
#./catch `ps ax | grep "loop" | cut -f 2 -d ' '`
Теперь вернитесь в консоль, в которой была запущена программа 'loop' и увидите что произошло! Итак! Ваши игры с ptrace начались!
От переводчика: В программу Tracer.c мною были внесены изменения. В оригинальном варианте программа loop при исполнении внедренного кода выводила сообщение "Oh, Caught!". Я взял на себя смелость заменить его текстом "Во! Поймали!". Однако текст "зашит" в кодировке koi8-r, поэтому, если у вас локаль настроена на иную кодировку, то вы увидите это сообщение в искаженном виде. Оригинальный вариант файла Tracer.c находится здесь .
В первой части статьи мы подсчитали количество ассемблерных инструкций, выполненных программой. В этой части мы рассмотрели структуру исполняемого файла и попробовали вставить свой код в тело "подопытного" процесса. В следующей части я покажу как получить доступ к памяти трассируемого процесса. До скорых встреч! Sandeep S.