kvs/elfreader.py

589 lines
25 KiB
Python
Raw Normal View History

2026-05-02 23:04:46 +03:00
#!/usr/bin/env python3
"""
===============================================================================
ELF Reader аналог readelf для ELF64 (AMD64/x86-64)
===============================================================================
НАЗНАЧЕНИЕ
----------
Модуль читает и отображает заголовки исполняемых ELF-файлов в стиле утилиты
readelf из пакета GNU Binutils. Вывод на русском языке, формат максимально
приближен к оригинальному readelf -h -l -S.
Поддерживается только архитектура AMD64 (x86-64), little-endian, 64-битный
формат ELF.
ИСПОЛЬЗОВАНИЕ
-------------
python3 elfreader.py <путь_к_elf_файлу>
Пример:
python3 elfreader.py /bin/ls
python3 elfreader.py моя_программа.elf
ФУНКЦИОНАЛЬНОСТЬ
-----------------
Выводит три блока информации:
1. Заголовок ELF (readelf -h):
- Magic-число и класс (ELF64)
- Порядок байт (little-endian)
- Тип файла (EXEC, DYN, REL, CORE)
- Целевая машина (X86-64)
- Адрес точки входа
- Расположение и количество программных/секционных заголовков
- Индекс строковой таблицы секций
2. Заголовки разделов (readelf -S):
- Таблица всех секций с адресами, смещениями, размерами
- Флаги секций (W=запись, A=выделение памяти, X=исполнение, I=инфо)
- Поля ссылок, информации и выравнивания
- Легенда обозначений флагов
3. Заголовки программы/сегментов (readelf -l):
- Таблица сегментов с виртуальными/физическими адресами
- Файловый и памяти'ный размеры
- Флаги доступа (R=чтение, W=запись, E=исполнение)
- Соответствие сегментов секциям
ВЫВОДИМЫЕ ДАННЫЕ
-----------------
Все числовые значения выводятся в шестнадцатеричном виде (адреса, смещения,
размеры) или десятичном (счётчики, индексы). Флаги секций и сегментов
отображаются в сокращённой буквенной нотации.
ОГРАНИЧЕНИЯ
-----------
- Только ELF64 little-endian (архитектура x86-64)
- Не поддерживаются 32-битные ELF-файлы
- Не анализируются таблицы символов (.symtab, .dynsym)
- Не обрабатываются релокации (.rel, .rela)
- Не читаются отладочные секции (.debug_*)
- Не разбираются динамические секции (.dynamic)
- Не интерпретируются NOTE-секции
- Не выводится информация о версиях символов
- Не читается интерпретатор из секции .interp
ТРЕБОВАНИЯ
----------
- Python 3.6 или выше
- Только стандартная библиотека (struct, sys)
- Внешние зависимости отсутствуют
АРХИТЕКТУРА МОДУЛЯ
-------------------
Весь код организован в виде набора чистых функций без классов:
Чтение структур:
read_elf_header() - парсинг основного ELF-заголовка (64 байта)
read_section_headers() - парсинг заголовков секций (по 64 байта каждый)
read_program_headers() - парсинг программных заголовков (по 56 байт каждый)
get_section_name() - извлечение имени секции из строковой таблицы
Форматирование вывода:
format_elf_header() - форматирование ELF-заголовка в строку
format_sections() - форматирование таблицы секций
format_program_headers() - форматирование таблицы сегментов
format_segment_section_map() - форматирование связей сегменты->секции
Вспомогательные:
get_flags_description() - преобразование битовых флагов секции в строку
ФОРМАТ ELF64 (краткая справка)
-------------------------------
ELF-заголовок (Elf64_Ehdr): 64 байта
- e_ident[16]: magic (4 байта), класс (1), порядок байт (1), версия (1),
OS/ABI (1), ABI version (1), padding (7)
- e_type (2 байта): тип файла
- e_machine (2 байта): архитектура (0x3E = x86-64)
- e_version (4 байта): версия формата
- e_entry (8 байт): адрес точки входа
- e_phoff (8 байт): смещение таблицы программных заголовков
- e_shoff (8 байт): смещение таблицы заголовков секций
- e_flags (4 байта): флаги процессора
- e_ehsize (2 байта): размер этого заголовка
- e_phentsize (2 байта): размер одного программного заголовка
- e_phnum (2 байта): количество программных заголовков
- e_shentsize (2 байта): размер одного заголовка секции
- e_shnum (2 байта): количество заголовков секций
- e_shstrndx (2 байта): индекс секции со строковой таблицей имён секций
Заголовок секции (Elf64_Shdr): 64 байта
- sh_name (4 байта): смещение имени в .shstrtab
- sh_type (4 байта): тип секции
- sh_flags (8 байт): флаги (W=1, A=2, X=4, I=0x8)
- sh_addr (8 байт): виртуальный адрес в памяти
- sh_offset (8 байт): смещение данных в файле
- sh_size (8 байт): размер секции в байтах
- sh_link (4 байта): ссылка на другую секцию
- sh_info (4 байта): дополнительная информация
- sh_addralign (8 байт): выравнивание адреса
- sh_entsize (8 байт): размер элемента (для таблиц)
Программный заголовок (Elf64_Phdr): 56 байт
- p_type (4 байта): тип сегмента (LOAD, DYNAMIC, INTERP, ...)
- p_flags (4 байта): флаги доступа (R=4, W=2, E=1)
- p_offset (8 байт): смещение данных в файле
- p_vaddr (8 байт): виртуальный адрес загрузки
- p_paddr (8 байт): физический адрес
- p_filesz (8 байт): размер в файле
- p_memsz (8 байт): размер в памяти
- p_align (8 байт): выравнивание
ПРИМЕР ВЫВОДА
-------------
$ python3 elfreader.py программа.elf
Заголовок ELF:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Класс: ELF64
Данные: дополнение до 2, от младшего к старшему
Version: 1 (current)
OS/ABI: UNIX - System V
Версия ABI: 0
Тип: EXEC (Исполняемый файл)
Машина: Advanced Micro Devices X86-64
...
ИНТЕГРАЦИЯ
----------
Модуль можно использовать как самостоятельную утилиту:
python3 elfreader.py файл.elf
Или импортировать функции для программного анализа ELF-файлов:
from elfreader import read_elf_header, read_section_headers
with open('программа.elf', 'rb') as f:
hdr = read_elf_header(f)
print(f"Точка входа: 0x{hdr['e_entry']:x}")
СРАВНЕНИЕ С readelf
-------------------
Вывод elfreader.py максимально приближен к выводу readelf из GNU Binutils.
Основные отличия:
- Все заголовки на русском языке (Заголовок ELF, Заголовки разделов, ...)
- Незначительные различия в форматировании пробелов
- Отсутствует часть расширенной информации (символы, релокации, dynamic)
- Не отображается интерпретатор из секции .interp
Для полного анализа ELF-файлов рекомендуется использовать оригинальный
readelf или библиотеку pyelftools для программного доступа.
АВТОР И ЛИЦЕНЗИЯ
-----------------
Создано в образовательных целях для изучения формата ELF.
Свободное использование, модификация и распространение.
"""
import struct
import sys
# ---------- константы ELF64 ----------
ELFMAG = b'\x7fELF'
ELFCLASS64 = 2
ELFDATA2LSB = 1
EM_X86_64 = 0x3E
# Типы ELF (e_type)
ET_NONE = 0
ET_REL = 1
ET_EXEC = 2
ET_DYN = 3
ET_CORE = 4
ETYPE_NAMES = {
ET_NONE: 'NONE (Нет типа файла)',
ET_REL: 'REL (Перемещаемый файл)',
ET_EXEC: 'EXEC (Исполняемый файл)',
ET_DYN: 'DYN (Совместно используемый объектный файл)',
ET_CORE: 'CORE (Файл ядра)'
}
# Типы секций (sh_type)
SHT_NULL = 0
SHT_PROGBITS = 1
SHT_SYMTAB = 2
SHT_STRTAB = 3
SHT_RELA = 4
SHT_HASH = 5
SHT_DYNAMIC = 6
SHT_NOTE = 7
SHT_NOBITS = 8
SHT_REL = 9
SHT_SHLIB = 10
SHT_DYNSYM = 11
SHT_INIT_ARRAY = 14
SHT_FINI_ARRAY = 15
SHT_PREINIT_ARRAY = 16
SHT_GROUP = 17
SHT_SYMTAB_SHNDX = 18
SHT_NUM = 19
SHT_GNU_HASH = 0x6ffffff6
SHT_VERSYM = 0x6fffffff
SHT_VERNEED = 0x6ffffffe
SHT_VERDEF = 0x6ffffffd
SHT_NAMES = {
SHT_NULL: 'NULL',
SHT_PROGBITS: 'PROGBITS',
SHT_SYMTAB: 'SYMTAB',
SHT_STRTAB: 'STRTAB',
SHT_RELA: 'RELA',
SHT_HASH: 'HASH',
SHT_DYNAMIC: 'DYNAMIC',
SHT_NOTE: 'NOTE',
SHT_NOBITS: 'NOBITS',
SHT_REL: 'REL',
SHT_SHLIB: 'SHLIB',
SHT_DYNSYM: 'DYNSYM',
SHT_INIT_ARRAY: 'INIT_ARRAY',
SHT_FINI_ARRAY: 'FINI_ARRAY',
SHT_PREINIT_ARRAY: 'PREINIT_ARRAY',
SHT_GROUP: 'GROUP',
SHT_SYMTAB_SHNDX: 'SYMTAB_SHNDX',
SHT_NUM: 'NUM',
SHT_GNU_HASH: 'GNU_HASH',
SHT_VERSYM: 'VERSYM',
SHT_VERNEED: 'VERNEED',
SHT_VERDEF: 'VERDEF'
}
# Типы программных заголовков (p_type)
PT_NULL = 0
PT_LOAD = 1
PT_DYNAMIC = 2
PT_INTERP = 3
PT_NOTE = 4
PT_SHLIB = 5
PT_PHDR = 6
PT_TLS = 7
PT_GNU_EH_FRAME = 0x6474e550
PT_GNU_STACK = 0x6474e551
PT_GNU_RELRO = 0x6474e552
PT_GNU_PROPERTY = 0x6474e553
PT_NAMES = {
PT_NULL: 'NULL',
PT_LOAD: 'LOAD',
PT_DYNAMIC: 'DYNAMIC',
PT_INTERP: 'INTERP',
PT_NOTE: 'NOTE',
PT_SHLIB: 'SHLIB',
PT_PHDR: 'PHDR',
PT_TLS: 'TLS',
PT_GNU_EH_FRAME: 'GNU_EH_FRAME',
PT_GNU_STACK: 'GNU_STACK',
PT_GNU_RELRO: 'GNU_RELRO',
PT_GNU_PROPERTY: 'GNU_PROPERTY'
}
# OS/ABI идентификаторы
OSABI_NAMES = {
0: 'UNIX - System V',
1: 'HP-UX',
2: 'NetBSD',
3: 'GNU/Linux',
6: 'Solaris',
9: 'FreeBSD',
12: 'OpenBSD'
}
# ---------- функции чтения структур ----------
def read_elf_header(f):
"""Читает и возвращает словарь с полями ELF-заголовка (64-бит)."""
data = f.read(64)
if len(data) < 64:
raise ValueError("Слишком маленький файл для ELF64 заголовка")
fmt = '16s H H I Q Q Q I H H H H H H'
(e_ident, e_type, e_machine, e_version,
e_entry, e_phoff, e_shoff, e_flags,
e_ehsize, e_phentsize, e_phnum,
e_shentsize, e_shnum, e_shstrndx) = struct.unpack(fmt, data)
if e_ident[:4] != ELFMAG:
raise ValueError("Не ELF файл")
if e_ident[4] != ELFCLASS64:
raise ValueError("Не 64-битный ELF")
if e_ident[5] != ELFDATA2LSB:
raise ValueError("Не little-endian (ожидался x86-64)")
return {
'e_ident': e_ident,
'e_type': e_type,
'e_machine': e_machine,
'e_version': e_version,
'e_entry': e_entry,
'e_phoff': e_phoff,
'e_shoff': e_shoff,
'e_flags': e_flags,
'e_ehsize': e_ehsize,
'e_phentsize': e_phentsize,
'e_phnum': e_phnum,
'e_shentsize': e_shentsize,
'e_shnum': e_shnum,
'e_shstrndx': e_shstrndx
}
def read_section_headers(f, shoff, shnum):
"""Возвращает список словарей с заголовками секций."""
f.seek(shoff)
sections = []
fmt = 'I I Q Q Q Q I I Q Q'
for _ in range(shnum):
data = f.read(64)
vals = struct.unpack(fmt, data)
sections.append({
'sh_name': vals[0],
'sh_type': vals[1],
'sh_flags': vals[2],
'sh_addr': vals[3],
'sh_offset': vals[4],
'sh_size': vals[5],
'sh_link': vals[6],
'sh_info': vals[7],
'sh_addralign': vals[8],
'sh_entsize': vals[9]
})
return sections
def read_program_headers(f, phoff, phnum):
"""Возвращает список словарей с программными заголовками."""
f.seek(phoff)
headers = []
fmt = 'I I Q Q Q Q Q Q'
for _ in range(phnum):
data = f.read(56)
vals = struct.unpack(fmt, data)
headers.append({
'p_type': vals[0],
'p_flags': vals[1],
'p_offset': vals[2],
'p_vaddr': vals[3],
'p_paddr': vals[4],
'p_filesz': vals[5],
'p_memsz': vals[6],
'p_align': vals[7]
})
return headers
def get_section_name(f, shstrtab_offset, shstrtab_size, sh_name_offset):
"""Извлекает имя секции из таблицы строк."""
f.seek(shstrtab_offset + sh_name_offset)
name = b''
while True:
ch = f.read(1)
if ch == b'\0' or ch == b'':
break
name += ch
return name.decode('latin-1', errors='replace')
def get_flags_description(sh_flags, sh_type=0, sh_info=0):
"""
Расшифровка флагов секции: W (запись), A (назнач), X (исполняемый), I (инфо).
Для секций REL/RELA флаг 'I' устанавливается, если sh_info указывает
на целевую секцию (sh_info > 0), как это делает readelf.
"""
flags = []
if sh_flags & 0x1: flags.append('W') # WRITE
if sh_flags & 0x2: flags.append('A') # ALLOC
if sh_flags & 0x4: flags.append('X') # EXECINSTR
if sh_flags & 0x8: flags.append('I') # INFO (GNU extension)
# Дополнительно: REL/RELA секции с sh_info > 0 получают флаг I
if (sh_type == SHT_REL or sh_type == SHT_RELA) and sh_info > 0:
if 'I' not in flags:
flags.append('I')
return ''.join(flags)
def is_section_in_segment(sec, ph):
"""
Проверяет, принадлежит ли секция сегменту.
Исключает секции с нулевым адресом (не загружаемые в память).
"""
if sec['sh_size'] == 0:
return False
if sec['sh_addr'] == 0:
return False
if ph['p_memsz'] == 0:
return False
# Проверяем, что секция находится внутри сегмента
return (sec['sh_addr'] >= ph['p_vaddr'] and
sec['sh_addr'] + sec['sh_size'] <= ph['p_vaddr'] + ph['p_memsz'])
# ---------- функции форматирования (на русском) ----------
def format_elf_header(hdr):
"""Возвращает строку с ELF-заголовком в стиле readelf -h."""
lines = []
lines.append("Заголовок ELF:")
lines.append(f" Magic: {hdr['e_ident'].hex(' ')}")
lines.append(f" Класс: ELF64")
if hdr['e_ident'][5] == 1:
lines.append(f" Данные: дополнение до 2, от младшего к старшему")
else:
lines.append(f" Данные: дополнение до 2, от старшего к младшему")
lines.append(f" Version: {hdr['e_ident'][6]} (current)")
lines.append(f" OS/ABI: {OSABI_NAMES.get(hdr['e_ident'][7], 'Неизвестно')}")
lines.append(f" Версия ABI: {hdr['e_ident'][8]}")
etype = ETYPE_NAMES.get(hdr['e_type'], 'UNKNOWN')
lines.append(f" Тип: {etype}")
if hdr['e_machine'] == EM_X86_64:
lines.append(f" Машина: Advanced Micro Devices X86-64")
else:
lines.append(f" Машина: 0x{hdr['e_machine']:x}")
lines.append(f" Версия: 0x{hdr['e_version']:x}")
lines.append(f" Адрес точки входа: 0x{hdr['e_entry']:x}")
lines.append(f" Начало заголовков программы: {hdr['e_phoff']} (байт в файле)")
lines.append(f" Начало заголовков раздела: {hdr['e_shoff']} (байт в файле)")
lines.append(f" Флаги: 0x{hdr['e_flags']:x}")
lines.append(f" Size of this header: {hdr['e_ehsize']} (bytes)")
lines.append(f" Size of program headers: {hdr['e_phentsize']} (bytes)")
lines.append(f" Number of program headers: {hdr['e_phnum']}")
lines.append(f" Size of section headers: {hdr['e_shentsize']} (bytes)")
lines.append(f" Number of section headers: {hdr['e_shnum']}")
lines.append(f" Section header string table index: {hdr['e_shstrndx']}")
return '\n'.join(lines)
def format_sections(f, sections, shstrtab_offset, shstrtab_size):
"""Возвращает строку с таблицей секций в стиле readelf -S."""
lines = []
lines.append("\nЗаголовки разделов:")
lines.append(" [Нм] Имя Тип Адрес Смещение Размер Разм.Ent Флаги Ссылк Инфо Выравн")
for idx, sec in enumerate(sections):
name = get_section_name(f, shstrtab_offset, shstrtab_size, sec['sh_name'])
stype = SHT_NAMES.get(sec['sh_type'], f"0x{sec['sh_type']:x}")
flags = get_flags_description(sec['sh_flags'], sec['sh_type'], sec['sh_info'])
if idx == 0:
# Пустая секция - специальный формат
lines.append(f" [{idx:2d}] {name:20s} {stype:16s} {sec['sh_addr']:016x} {sec['sh_offset']:08x}")
lines.append(f" {sec['sh_size']:016x} {sec['sh_entsize']:016x} {sec['sh_link']:<6d}{sec['sh_info']:<6d}{sec['sh_addralign']:<8d}")
else:
flags_str = flags if flags else " "
lines.append(f" [{idx:2d}] {name:20s} {stype:16s} {sec['sh_addr']:016x} {sec['sh_offset']:08x}")
lines.append(f" {sec['sh_size']:016x} {sec['sh_entsize']:016x} {flags_str:4s} {sec['sh_link']:<6d}{sec['sh_info']:<6d}{sec['sh_addralign']:<8d}")
# Добавляем легенду флагов
lines.append("Обозначения флагов:")
lines.append(" W (запись), A (назнач), X (исполняемый), M (слияние), S (строки),")
lines.append(" I (инфо), L (порядок ссылок), O (требуется дополнительная работа ОС),")
lines.append(" G (группа), T (TLS), C (сжат), x (неизвестно), o (специфич. для ОС),")
lines.append(" E (исключён), D (mbind), l (большой), p (processor specific)")
return '\n'.join(lines)
def format_program_headers(phdrs):
"""Возвращает строку с программными заголовками в стиле readelf -l."""
lines = []
lines.append("\nЗаголовки программы:")
lines.append(" Тип Смещ. Вирт.адр Физ.адр")
lines.append(" Рзм.фйл Рзм.пм Флаги Выравн")
for ph in phdrs:
ptype = PT_NAMES.get(ph['p_type'], f"0x{ph['p_type']:x}")
# Формируем флаги
flags = ''
if ph['p_flags'] & 4: flags += 'R'
if ph['p_flags'] & 2: flags += 'W'
if ph['p_flags'] & 1: flags += 'E'
if not flags: flags = ' '
lines.append(f" {ptype:14s} 0x{ph['p_offset']:016x} 0x{ph['p_vaddr']:016x} 0x{ph['p_paddr']:016x}")
lines.append(f" 0x{ph['p_filesz']:016x} 0x{ph['p_memsz']:016x} {flags:4s} 0x{ph['p_align']:x}")
return '\n'.join(lines)
def format_segment_section_map(sections, phdrs, get_name_func):
"""Форматирует соответствие сегментов и секций."""
lines = []
lines.append("\n Соответствие раздел-сегмент:")
lines.append(" Сегмент Разделы...")
for pidx, ph in enumerate(phdrs):
seg_sections = []
for sidx, sec in enumerate(sections):
if is_section_in_segment(sec, ph):
name = get_name_func(sidx)
if name:
seg_sections.append(name)
if seg_sections:
section_names = ' '.join(seg_sections)
lines.append(f" {pidx:02d} {section_names}")
else:
lines.append(f" {pidx:02d} ")
return '\n'.join(lines)
# ---------- главная функция ----------
def main():
if len(sys.argv) != 2:
print(f"Использование: {sys.argv[0]} <elf-файл>", flush=True)
sys.exit(1)
filename = sys.argv[1]
try:
with open(filename, 'rb') as f:
# 1. ELF заголовок
hdr = read_elf_header(f)
if hdr['e_machine'] != EM_X86_64:
print("Предупреждение: обнаружена не x86-64 архитектура, вывод может быть некорректным.", flush=True)
print(format_elf_header(hdr), flush=True)
# Читаем секции для использования в других блоках
sections = []
if hdr['e_shnum'] > 0:
sections = read_section_headers(f, hdr['e_shoff'], hdr['e_shnum'])
# Функция для получения имён секций (для маппинга)
def get_sec_name(sidx):
if sidx < len(sections):
shstrtab = sections[hdr['e_shstrndx']]
return get_section_name(f, shstrtab['sh_offset'], shstrtab['sh_size'], sections[sidx]['sh_name'])
return '???'
# Вывод секций
if hdr['e_shnum'] > 0:
shstrtab = sections[hdr['e_shstrndx']]
print(format_sections(f, sections, shstrtab['sh_offset'], shstrtab['sh_size']), flush=True)
else:
print("\nФайл не содержит таблицы разделов.", flush=True)
# Вывод программных заголовков
if hdr['e_phnum'] > 0:
phdrs = read_program_headers(f, hdr['e_phoff'], hdr['e_phnum'])
print(format_program_headers(phdrs), flush=True)
# Соответствие сегментов и секций
print(format_segment_section_map(sections, phdrs, get_sec_name), flush=True)
else:
print("\nФайл не содержит программных заголовков.", flush=True)
except Exception as e:
print(f"Ошибка: {e}", file=sys.stderr, flush=True)
sys.exit(1)
if __name__ == '__main__':
main()