openlang

-fstack-protector-strong без libc, segfault на загрузке через %fs по адресу 0x28

Freestanding hex-дампер, x86-64 Linux, свой _start, сырые syscalls, линкуется с -nostdlib -static -no-pie. Собирается чисто, без undefined symbols, но ./hexd -v < /etc/hostname падает с segfault до первого вывода note().

В gdb падение в самом начале hexd_main, на загрузке через %fs, которую я не писал, адрес падения 0x28. Больше ничего не успело выполниться. С -fno-stack-protector всё нормально.

Я определил два символа, которые, как мне казалось, нужны протектору:

unsigned long __stack_chk_guard = 0x595e9fbd94fda766UL;

__attribute__((noreturn))
void __stack_chk_fail(void) { /* write to fd 2, sys_exit(134) */ }
CFLAGS = -std=c11 -O2 -Wall -Wextra -ffreestanding -fno-pic -fno-asynchronous-unwind-tables -fstack-protector-strong

Почему компилятор вообще читает %fs, если guard лежит тут же как обычная глобальная переменная? Выравнивание стека в _start правильное, -mno-red-zone ничего не изменил. Мне что, самому настраивать thread pointer через prctl и ARCH_SET_FS перед любым вызовом?

Переведено с английскогоПоказать оригинал

Разговор продолжается 6

Следуя доводам
  1. Ссылка на ответ

    Я новичок (так что относитесь с осторожностью), но я думал, что по умолчанию guard лежит в обычной глобальной переменной, а TLS нужно включать отдельно? Если указать -mstack-protector-guard=global, будет ли использоваться ваш __stack_chk_guard вместо %fs:0x28?

    Переведено с английскогоПоказать оригинал
  2. Ссылка на ответ

    Kiln, вы перепутали наоборот для x86-64 Linux. По умолчанию используется TLS-канарейка, она читается из %fs:0x28, а обычную глобальную переменную нужно запрашивать отдельно. Это же объясняет и мой адрес сбоя: _start никогда не задаёт FS base, поэтому он нулевой, и загрузка попадает на 0x28. Мой __stack_chk_guard никогда не читался, это просто символ, который лежит без дела.

    Есть два выхода, я иду по первому.

    1. Сказать компилятору использовать глобальную переменную: -mstack-protector-guard=global, а сверху ещё -mstack-protector-guard-symbol=__stack_chk_guard, чтобы он взял именно мой символ. Ничего настраивать в рантайме не нужно.
    2. Оставить по умолчанию и сделать вручную: выделить обнулённый блок байт на 64, положить канарейку по смещению 0x28 и направить FS на него через prctl(ARCH_SET_FS, ...) до запуска первой защищённой функции. Сам _start при этом нужно компилировать без протектора, иначе он упадёт, не добравшись до этого места.

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

    Переведено с английскогоПоказать оригинал
  3. Ссылка на ответ

    В целом да. На x86-64 glibc %fs указывает на thread control block, и начало этой структуры выглядит примерно так:

    • 0x00: tcb pointer (указывает на самого себя)
    • 0x08: dtv
    • 0x10: self pointer
    • 0x18: multiple_threads (int), затем gscope_flag (int) по смещению 0x1c
    • 0x20: sysinfo
    • 0x28: stack_guard

    Так что 0x28 — это просто то место, куда канарейка попала в этой раскладке. GCC жёстко прописывает %fs:0x28, потому что в документах по ABI x86-64 это зафиксировано, насколько я помню, поэтому каждой libc приходится класть её именно туда. Поэтому и второй вариант lathe работает: нужен только блок с 8 байтами по смещению 0x28 и FS, указывающий на него, так что остальное в заголовке не имеет значения.

    Кстати, спасибо, что спросили, мне самому пришлось ещё раз заглянуть в структуру, чтобы убедиться в порядке. Остальные смещения важны только если что-то их читает.

    Переведено с английскогоПоказать оригинал
  4. Ссылка на ответ

    Дополню свой собственный фикс, потому что у варианта 1 есть подвох, который я пропустил.

    Он убирает загрузку через %fs, но канарейка теперь равна константе из исходника, так что она одинакова при каждом запуске и в каждом бинарнике. Для хексдампера это безвредно, но защитой это назвать трудно. Если хотите, чтобы было по-настоящему, ядро отдаёт 16 случайных байт по AT_RANDOM в auxiliary vector. В _start пропустите argv и NULL в конце envp, идите по парам auxv, пока не встретите тип 25, и скопируйте 8 байт по этому указателю в __stack_chk_guard перед вызовом hexd_main. _start должен остаться незащищённым, как и в варианте 2, а копирование должно произойти до входа в любую защищённую функцию, иначе пролог сохранит старое значение, а эпилог сравнит с новым, и проверка провалится.

    Чтобы проверить, что компилятор действительно перестал трогать FS:

    objdump -d hexd | grep '%fs'

    Вывод должен быть пустым. Если что-то всё же находится, обычно это объектник, собранный без флагов guard, так что проверьте, что все файлы в линковке собираются с одинаковыми CFLAGS.

    Про вариант 2: номер сисколла для FS base на x86-64 — 158.

    Переведено с английскогоПоказать оригинал

Другие стороны того же вопроса

  1. STM32H7 UART TX DMA отправляет старые байты при включённом D-cache

    STM32H7, arm-none-eabi-gcc. DMA TX кадра в 64 байта. С выключенным кешем всё нормально. С -DUSE_DCACHE анализатор показывает…

    Ответы 8Последняя активность c
  2. В колбэке из C пропадает арена, которую я настроил

    C-библиотека вызывает колбэк в моём коде на Odin, и колбэк не видит контекст, который я настроил перед регистрацией. У proc "…

    Ответы 5Последняя активность odinc
  3. TMR3 ISR теряет capture-события, хотя флаги w1c и сбрасываются

    Контроллер reflow-печи, клон Cortex-M0, arm-none-eabi-gcc -O2, Fedora. TMR3 работает на 1 МГц. Update event — тик 1 мс, CH1 з…

    Ответы 5Последняя активность c