Трассировка процессов с помощью Ptrace -- Часть 2
Автор: Sandeep S
Перевод: Андрей Киселев


В первой части мы узнали об основных особенностях ptrace и рассмотрели небольшой пример. Как я уже говорил ранее, практически всегда процессы-отладчики обращаются к памяти или регистрам процессора трассируемого приложения. Теперь бы я хотел осветить строение исполняемых файлов, чтобы вы получили представление о том что и где в них находится. Здесь я расскажу о формате исполняемых файлов ELF, используемом в Linux. А в последнем разделе этой части я представлю небольшой пример программы, которая изменяет содержимое памяти и регистров процессора другой программы, и внедряет в ее тело некоторый дополнительный исполняемый код.

Обратите внимание: Пусть вас не смущает такое вступление. Эта часть статьи, вне всякого сомнения, рассказывает о ptrace, а не об ELF. Но знание формата ELF определенно необходимо, чтобы уметь обращаться к памяти трассируемого процесса. Итак, приступим.

1. Что такое ELF?

ELF -- это Executable and Linking Format (Формат Исполняемых и Связываемых файлов). Он определяет формат двоичных исполняемых файлов, объектных файлов, разделяемых объектов (библиотек), а так же файлов core dump. Формат ELF используется как компоновщиками (linkers), так и загрузчиками программ, хотя каждый из них интерпретирует ELF-файлы по-своему.

ELF-файл состоит из множества секций и сегментов. Объектные файлы имеют таблицу заголовков секций, исполняемые -- таблицу программных заголовков, а разделяемые библиотеки -- и то и другое. В следующем разделе я познакомлю вас с этими заголовками поближе.

2. Заголовки 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;


Кратко опишу поля структуры

  1. e_ident : Сигнатура и прочая информация. Зависит от аппаратной платформы.

  2. e_type : Содержит информацию о типе файла. Тип может быть одним из следующих: "объектный", "исполняемый", "разделяемый" (shared object) и "core".

  3. e_machine : Вы наверняка уже догадались, что это поле определяет аппаратную архитектуру -- Intel 386, Alpha, Sparc и т.п.

  4. e_version : Версия объектного файла.

  5. e_phoff : Смещение до первого программного заголовка.

  6. e_shoff : Смещение до первого заголовка секции.

  7. e_flags : Флаги процессора. Не используется для i386

  8. e_ehsize : Размер ELF-заголовка в байтах.

  9. e_phentsize & e_shentsize : Размер программного заголовка и заголовка секции, в таблицах программных заголовков и заголовков секций соответственно.

  10. e_phnum & e_shnum : Количество программных заголовков и заголовков секций в соответствующих таблицах.

  11. e_shstrndx : В таблице заголовков секций есть секция, которая содержит имена других секций. Это индекс такой секции в таблице. (см. ниже)

3. Секции и Сегменты

Как я уже упоминал выше, компоновщики интерпретируют файл как множество секций, описанных в таблице заголовков секций, а загрузчик -- как множество сегментов, описанных в таблице программных заголовков. В этом разделе приводится более или менее подробное описание заголовков секций и сегментов.

3.1 Секции и заголовки секций

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

Таблица заголовков секций представляет из себя массив заголовков. Нулевой элемент массива всегда пуст и не соответствует ни одной из секций. Каждый заголовок секции имеет следующий формат (см. /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;


Теперь о полях структуры более подробно.

  1. sh_name : Индекс строки в секции, содержащей таблицу строк e_shstrndx. Указывает на начало строки, завершающейся нулевым (0x00) символом, которая используется в качестве имени секции.

  2. sh_type : Тип секции, например, данные, таблица символов, таблица строк и т.п..

  3. sh_flags : Содержит вспомогательную информацию, определяющую порядок интерпретации содержимого секции.

  4. sh_addralign : Содержит размер выравнивания для секции, обычно 0, 1 (оба означают отсутствие выравнивания) или 4.

Смысл назначения остальных полей структуры достаточно прозрачно следует из их названий.

3.2 Сегменты и программные заголовки.

Сегменты используются в процессе загрузки программы, т.е. при создании образа процесса в памяти. Каждый сегмент описывается соответствующим программным заголовком. Таблица представляет из себя массив заголовков. Каждый программный заголовок имеет следующий формат:


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;


  1. p_type : Определяет тип сегмента, т.е. задает порядок его интерпретации, например:

    и т.п..

  2. p_vaddr : относительный виртуальный адрес загрузки сегмента.

  3. p_paddr : физический адрес загрузки сегмента.

  4. p_flags : Содержит флаги прав доступа -- чтение/запись/исполнение

  5. p_align : Выравнивание сегмента в памяти. Если сегмент имеет тип "загружаемый" (loadable), то он выравнивается по границе страницы памяти.

Смысл назначения остальных полей структуры понятен из их названий.

4. Загрузка ELF-файла

Мы уже имеем представление о структуре ELF-файла. Теперь перейдем к рассмотрению порядка загрузки файла. Обычно, для того чтобы запустить программу мы набираем ее имя в командной строке. На самом деле, после того как мы нажмем на клавишу RETURN (или, если хотите -- ENTER), происходит масса интересных вещей.

Прежде всего командная оболочка вызывает стандартную функцию из libc, которая в свою очередь обращается к ядру. Теперь в игру вступает ядро. Ядро открывает файл и определяет тип файла как исполняемый. Затем загружает файл и все необходимые библиотеки, инициализирует стек и передает управление загруженной программе.

Программа загружается по адресу 0x08048000 (см. /proc/pid/maps), а стек начинается с адреса 0xBFFFFFFF (стек "растет" в сторону меньших адресов).

5. Внедрение кода

Теперь, когда процесс загружен в память и нам известно его адресное пространство, мы можем выполнять трассировку этого процесса (при наличии прав доступа) и просматривать/изменять данные в памяти процесса. Однако сказать легко, сделать -- сложнее. Тем не менее, почему бы не попробовать?

Прежде всего, давайте попробуем создать программу, которая могла бы читать/писать в регистры процессора другой программы. Для аргумента request мы будем использовать следующие значения.

Теперь вставим некоторый код в тело трассируемого процесса и заставим процесс исполнить его, изменив содержимое регистра 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 находится здесь .

6. Забегая вперед

В первой части статьи мы подсчитали количество ассемблерных инструкций, выполненных программой. В этой части мы рассмотрели структуру исполняемого файла и попробовали вставить свой код в тело "подопытного" процесса. В следующей части я покажу как получить доступ к памяти трассируемого процесса. До скорых встреч! Sandeep S.


Copyright (C) 2002, Sandeep S. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 83 of Linux Gazette, October 2002


Вернуться на главную страницу