All streams
Search
Write a publication
Pull to refresh

Comments 46

Круто! Как я сам не додумался до этого! Все гениальное просто!

Очень подробно написано. Не мой профиль, но даже я все поняла. Спасибо автору

Не хватает последнего шага - зарегистрировать новый тип исполняемых файлов через binfmt_misc.

Согласен, очень хорошая идея, в следующих статьях обязательно сделаю. Спасибо!

Нафига все эти пляски с ручным вводом байтов кода в консоль? NASM умеет формировать bin-файлы (безо всяких заголовков), которые можно использовать напрямую, как, к примеру, COM-файлы MS-DOS.

Если уж очень хочется, то можно и заголовок прямо в NASM'е вставить инструкцией DB или DWORD, но надо понимать, что может поехать абсолютная адресация. Так что файл в этом случае в память надо загружать полностью с заголовком, чтобы адреса не сдвинулись.

А еще binfmt_misc может исполнимые файлы не только по сигнатуре регистрировать, но и по расширению (в DOS/Windows стиле), так что можно прям те же COM-файлы без заголовка в Linux возродить.

Понимаю, но, в рамках этой статьи, мне хотелось сделать все максимально просто и доступно. А усложнять и углубляться можно долго:)
Спасибо за комментарий!

Ну так тут как раз проще и получается: NASM генерирует конечный исполнимый файл, а приложение должно уметь только загружать его и исполнять.

Мне просто кажется, что это чуть больше будет похоже на черный ящик. Хотелось чуть больше "ручками" повзаимодействовать и меньше использовать готовые утилиты, или, по крайней мере, не сильно погружаясь в их особенности и функционал.

Но я согласен абсолютно — писать байты в консоль это жесть как муторно, зато наглядно! :D

А что дает эта наглядность? Код x86/x86-64 практически нечитаемый, там все на битах, длина команды от одного байта до 15. Это не PDP-11, который сходу в восьмеричной системе читается-пишется и все по 16-битным словам исключительно. К примеру, 1SSDD - команда MOV, где SS - источник, DD - приемник, первый разряд тип адресации, второй разряд номер регистра от 0 до 7, все единообразно. Следующие одно или два слова могут быть либо константными адресами либо константными данными (зависит от типа адресации). Если операция с байтом, а не словом, то устанавливается старший бит слова команды в единичку: 11SSDD. Вот здесь можно детали посмотреть. Вот где наглядность! А переписывать руками байты из HEX-редактора в консоль - это не наглядность.

Тоже об этом подумал, но с другой стороны, когда сам ручками вводишь байты, лучше чувствуешь что происходит
Это как учиться водить на механике, а не на автомате, для понимания основ - бесценно

Шестнадцатеричные цифры вводить - только ошибки лепить. Если уж хочется прям вводить, запустите DEBUG досовский под DOSBox, там как раз можно руками команды ассемблера вводить (адреса и константы таки придется шестнадцатеричными вбивать, а адреса еще и вычислять).

Есть более современные способы быстро получить байтики для инструкций, хотя бы llvm-mc.

Не совсем понял, при чем здесь это. Понятно, что любой компилятор будет работать быстрее и надежнее, чем ручной ввод. Тут люди хотят "наглядности" и "чувствовать".

Лучше всё таки привыкать к современным инструментам - тот же llvm-mc периодически использую для всяких мелочей. Дальше уже эти байтики запихиваем в программе в массивчик, даём права и исполняем.

Так в чем отличие от использования того же NASM для компиляции ассемблерного кода? Им и пользоваться проще, и с точки зрения самого ассемблера и компиляцию одной строкой вызвать.

NASM удобнее если действительно надо скомпилировать и получить готовый объектник. Если нужен hex для одной или нескольких инструкций, чтобы посмотреть глазами или вставить как константу в сишный код, придётся ещё какой нибудь objdump вызывать. В этом плане llvm-mc проще - дали инструкцию, получили hex.

Ну, тоже не сахар, в командной строке кучу обрамления писать. Плюс эта штука довольно нестандартный вид команд ассемблера требует, типа addl %eax, %ebx

Для быстрого ассемблирования есть онлайн инструменты типа этого.

довольно нестандартный вид команд ассемблера

Обычный AT&T синтаксис, objdump тоже его выдаёт. Но в целом да, кому что удобнее.

Для Intel он нестандартный, поэтому в das уже 25 лет как есть директива, переключающая синтаксис на интеловский.

Может на винде нестандартный, на линуксе и прочих юниксах по умолчанию все тулы работают с AT&T.

Если интересно как сделать теперь следующий шаг и добавить кроссплатформенности для SEF-файлов, могу подсказать - заодно выйдет еще одна понятная и интересная статья

Вот это была бы "пушка"! Представляем себе исполняемый файл для любой ОС!...Я балдею.

Чтобы "для любой ОС", надо ещё системные вызовы абстрагировать, это отдельная задача.

Всем, кому интересна эта тема предлагается связаться со мной в телеграмме @RigidusRigidus - сделаем группу и обсудим как мы можем совместить эти подходы (включая и https://justine.lol/ape.html)

Туда и двигаемся, можно сказать)

Конечно интересно, жду)

Вообще, этот SEF формат — быстрая импровизация на тему исполняемых файлов, в которой нет ничего особенно полезного. Цель была (и будет) в плавном введении в эту тематику, чтобы ребята, которые достаточно далеки от этого, смогли по-тихоньку собирать контекст из тучи непонятных слов:D

А сам формат мы постепенно будем развивать как минимум до введения экспортов, импортов и релокаций. Ещё и научимся PE переводить в SEF. Напишем более правильный и сложный загрузчик. Без исполнения массива никуда не денемся и будем использовать похожий сценарий для формирования функций-заглушек при отсутствующем импорте.

Короче, ещё много работы предстоит, поэтому я с удовольствием вбираю новые мысли и идеи!

В Винде запущенный бинарник не может изменить себя на накопителе. Можно лишь скопировать и переименовать обе, чтобы при следующем запуске исполнялся изменённый. А в Линуксе бинарник может себя менять на накопителе?

Хотелось бы так хранить настройки в самом бинарнике, а не отдельным файлом.

Прочитав заголовок, подумал, что увижу информацию о том, как написать для линукса программу на Си/Си++, но внезапно увидел, что здесь ассемблер! Вау! Я такого ещё нигде не видел, спасибо огромное! Опять же, интересный кейс про регистрацию формата исполняемого файла, обязательно буду показывать своим студентам!

Спасибо большое!)

Плюсую, студентам такое точно зайдет, гораздо лучше сухой теории из учебников
Когда сам видишь как из массива байт получается исполняемая программа, это производит впечатление

Теперь мы добавили выделение памяти с помощью mmap с модификаторами доступа RWX (Read, Write, Execute), после в эту память мы скопировали наш байт-код

mprotect(2) вроде может поменять RWX для существующей страницы, без дополнительного выделения и копирования. Про выравнивание и размер страниц только надо не забывать.

Если я не ошибаюсь, то mprotect не подойдет для нашего сценария, так как массив попадет в .data секцию и будет лишь её частью. А mprotect работает именно с целыми страницами, поэтому, как вы и сказали, потребуется выравнивание и учет размера страницы. Это приведет к изменению прав для гораздо большего диапазона адресов нежели нам нужно, что, я считаю, слишком излишним и более вредным, хоть это и требует меньше ресурсов.

Ну mmap ведь тоже отнюдь не 6 байт выделит, а ту же выровененную ~4кБ страницу целиком.

Единственная разница что mprotect также разрешит исполнение тому что уже рядом с program_data[] (вроде вообще на стэке в данном конкретном примере, что делает это ещё более "безопасным") в пределах страницы оказалось.

Но при загрузке исполняемого кода с диска руками под него всё равно надо выделять память, так что да, нет смысла выделять её как попало и потом менять доступ на исполнение.

вот тут https://habr.com/ru/articles/746658/ тоже самое через mprotect только потому что память под сгенерированный исполняемый код выделялась снаружи и лезть туда менять аллокатор на свой было лень.

Согласен с вами, бесспорно использовать mprotect в данном контексте было бы неплохо (хоть и пришлось бы углубиться в управление оперативной памятью), но тут важно будет сказать, что с помощью mmap мы добиваемся отделения исполняемой области, что, на мой взгляд, более правильно.

Ну и если добавить контекст, то далее в статье идёт речь про отображение файла с помощью mmap, поэтому мне принципиально было задействовать этот системный вызов ранее, чтобы не добавлять новой информации и, тем самым, упростить статью:)

Перед исполнением динамически зачитанного/сгенерированного кода непохо ещё кеш инструкций сбросить (скажем, через __builtin___clear_cache). На x86 необязательно, а вот ARM может начать исполнять мусор.

Спасибо! Это действительно важное замечание, и я его обязательно учту в дальнейшем)

Спасибо! <3

Хорошая ламповая статья
Трюк с mmap и исполнением массива это прям обряд посвящения в системщину, cразу начинаешь понимать что код и данные по сути одно и то же. Спасибо что напомнили

Спасибо за прочтение!:)

< segmentation fault (core dumped)

Примечание: ELF файлу можно добавить exec-флаг для стэка и тогда такая программа должна заработать со стэка.
Варианты как это можно сделать:

  • во время линковки с помощью флага -Wl,-zexecstack (gcc);

  • пропатчить готовый бинарник: execstack --set-execstack main.out

Пример как посмотреть модификаторы доступа для стэка у ELF файла:

readelf --program-headers 3_exec_stack/test | grep -A1 GNU_STACK
#   GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
#                  0x0000000000000000 0x0000000000000000  RWE    0x10
#                                                         ↑↑↑

Здорово, спасибо!

Если скомпилировать изначальную программу командой nasm -f bin test.asm (или просто nasm test.asm) то на выходе будет 66 B8 4A 00 00 00 C3

Откуда лишние 0x66 взялись?

Разобрался, в шестнадцатибитном режиме запись в регистр EAX идет как 0x66B8, а в х64 она идет просто 0xB8. Если вначале исходника указать BITS 64 - то файл компилируется как в примере, без 0x66

Sign up to leave a comment.

Articles