kvs/инструменты/elfreader.py

589 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()