Посвящается Валерию Агееву (awa)
Стековые виртуальные машины
Мир все больше и больше захватывают динамические языки программирования. От "честных" нативных языков в строю остался, пожалуй, только Си (с плюсами и без). Это если брать промышленный мейнстрим. Все популярные языки так или иначе имеют прослойку в виде "исполняющей среды" или "виртуальной машины", которая обеспечивает выполнение кода на той или иной архитектуре железа. И подавляющее большинство этих "виртуальных машин" являются стековыми, т.е. реализуют обработку операций с помощью такой известной в компьютерном мире структуры, как "стек".
Ремарка для тех, кто забыл
Стек (Stack) в переводе с английского означает "стопка". Когда мы кладем в стопку (скажем, книг) какую-то новую книжку, то она оказывается сверху. Убирать из стопки книги мы можем только сверху вниз. Т.е. последняя добавленная книжка снимается из стопки самой первой. Это тот самый принцип "LIFO" - last in/first out. Обратной ситуацией является очередь (в магазине). Кто первый встал - того и тапки.
Java, Python, C# и 1С - все они используют стековые машины для выполнения своего кода. Рискну предположить, что node.js - тоже, но это неточно, а гуглить мне лень. 95% вероятности, что это так и есть.
Если совсем спускаться в академические точности, то есть язык, а есть исполняющая среда. Так вот, язык - это лишь спецификация и, вообще-то, текст. Он не выполняется, он в блокноте написан. А вот то, что выполняет написанное - это может быть как стековым, так и нет. Поэтому нельзя сказать что Java - это стековый язык. Стековым бывает то, что выполняет язык. Так, например, для Java есть общепринятая машина JVM - она стековая. А в Андроидах используется (или использовалась) регистровая машина Dalvik. Ходят слухи, что ее оттуда выпилили, но я не проверял. Язык - один, машин может быть несколько. Но, как правило, этим можно пренебречь, поскольку все равно у каждого языка есть всего одна (реже несколько) реализующих машин и почти все они, скорее всего, будут стековыми.
У языка 1С тоже есть несколько реализаций. Первая - сама 1С, вторая - например, 1Script. Есть еще несколько, чуть менее известных.
Стековая машина
Устроена стековая машина невероятно просто. Я разбирал ее устройство на Хабре еще в 2014 году, поэтому здесь просто коротенько напомню.
Итак, вот есть у вас выражение А = 1 + 1; как оно выполняется стековой машиной?
PushConst 1 PushConst 1 Add LoadVar A
Поместить в стек операнд-константу 1 (2 раза), затем выполнить операцию Add.
Операция Add извлекает свои аргументы из стека (2 штуки) и складывает. Результат кладет обратно на стек. Операция LoadVar берет переданную переменную А и загружает в нее то, что лежит на стеке (в данном случае - результат сложения).
Этот простой алгоритм позволяет эффективно вычислять цепочки выражений. Например, операция А = 1+1+2 будет выглядеть вот так:
PushConst 1 ; поместили константу на стек
PushConst 1 ; поместили константу на стек
Add ; забрали 2 аргумента из стека (1 и еще 1), сложили, поместили в стек результат
PushConst 2 ; поместили константу на стек
Add ; забрали 2 аргумента из стека (2 и еще 2), сложили, поместили в стек результат
LoadVar A ; забрали из стека то, что там лежало (4) и загрузили в переменную А.
Каждая машина имеет свой набор операций, не обязательно такой, как здесь, но принцип у всех общий: операции по очереди кладут что-то на стек или извлекают что-то из стека.
Операции, выполняемые виртуальной машиной, принято называть "байт-кодом". Это такой "ассемблер" для стековой машины.
Как увидеть байт-код машины 1С
По умолчанию, если вы сохраняете какой-либо модуль, он сохраняется в чистом виде, так, как вы его написали. Однако, если поставить на модуль пароль или удалить модуль из поставки, то системе потребуется как-то все-таки знать - что выполнять. Поэтому, система при сохранении компилирует ваш код 1С в байт-код и сохраняет уже его. Например, если вы поставите пароль на модуль внешней обработки, то в файл epf ляжет скомпилированный байткод. Его можно посмотреть утилитами семейства v8unpack.
Какой же хакер без подходящих инструментов
Давайте посмотрим на байткод 1С. Права на описанные инструменты принадлежат их авторам, как и всяческие респекты от меня и сообщества.
Для начала нам потребуется распаковщик файлов epf. Самый простой способ, это установить его через chocolatey
choco install v8unpack --version 3.0.41 --source https://www.myget.org/F/onescript/api/v2
Тем, у кого нет chocolatey (эй,чуваки, как вы без него живете?) можно скачать по прямой ссылке https://github.com/e8tools/v8unpack/releases/download/v.3.0.40/v8unpack.exe но не забудьте потом exe прописать в PATH, чтобы было удобнее запускать.
Итак, возьмем любую внешнюю обработку 1С, модуль которой не скрыт паролем, и посмотрим на нее изнутри.
v8unpack -P КакаяТоОбработка.epf content
Будет создан каталог content, а в нем размещено содержимое внутренних файлов контейнера 1С (кому интересно - формат контейнера описан вот здесь: //infostart.ru/public/250142/)
Итак, в этом контейнере нас интересует каталог "<какой-то-GUID>.0", а в нем файлы "info" и "text". Файл "text" это просто текст модуля, а файл инфо это служебный файл, с которым мы еще поработаем. Теперь удалим каталог content и закроем модуль нашей обработки паролем. Откройте редактирование модуля в конфигураторе и в меню "Текст" выберите пункт "Установить пароль". Теперь, при попытке редактирования модуля Конфигуратор будет спрашивать пароль.

Отлично, а что же на уровне внутренних файлов? Сохраним запароленную обработку, удалим каталог content от предыдущего запуска и повторим команду
v8unpack -P КакаяТоОбработка.epf content
посмотрим в каталог с GUID.0 ого, появился файлик image, а в файле text - какая-то абракадабра. Платформа зашифровала содержимое модуля, его действительно не видно, но ей же надо как-то выполнять алгоритмы, верно? Для этого она перед шифрованием скомпилировала код 1С в байт-код виртуальной машины и записала его в файл image. Посмотрим на него:

Это - ассемблер 1С. Именно его выполняет наша любимая платформа, когда считает всем зарплату. И знаете, что самое интересное? Мы можем напрямую писать код на этом ассемблере, не прибегая к услугам компилятора! Слабо? Я же говорил, что будет весело!
А зачем это нужно?
Ну, во-первых, это просто прикольно, это позволит вам лучше понимать устройство вашей системы и понять, как работают современные управляемые языки программирования. Это позволит вам прокачать навык инженера-программиста и просить более высокую зарплату.
Во-вторых, вы все знаете, что байт-код 1С очень легко декомпилируется обратно в скриптовый код, вплоть до сохранения имен переменных и процедур. Существуют даже обфускаторы кода (здесь же на инфостарте), которые портят код, так чтобы он плохо читался после декомпиляции. На "ассемблере 1С" можно написать код таким образом, что он вообще не будет декомпилироваться. Позже я покажу, как написать работоспособный код, который платформа будет выполнять, но для которого просто не существует соответствующих ему синтаксических конструкций в языке 1С. Декомпилятору будет просто нечего выдать на выходе. Здорово, правда?
Оговорка
Я против воровства результатов чужого труда. Если вы пользуетесь декомпилятором, то скорее всего, вы не хотите платить автору, т.е. просто хотите украсть его работу. Это некрасиво. Поэтому, я не буду приводить ссылки на декомпилятор 1С в сети. Более того, каждый раз, когда в сети (чаще на Мисте) вы видите вопрос про "как декомпилировать обработку", "как снять пароль" - обязательно напишите автору вопроса, что он говнюк и мелкий воришка.
Далее, я также против и обфускаторов. Толку от них мало, их результат все равно декомпилируется и восстанавливается. С помощью написания алгоритма на уровне байт-кода можно создать супер-обфускатор, который неподвластен имеющимся на рынке 1С декомпиляторам. Вопрос взлома станет дороже покупки, что и требовалось обеспечить.
И еще: я не буду рассказывать, как именно построить такой обфускатор (я против, помните), но дам пару намеков, которые могут сработать, если вы захотите создать таковой.
Ну че, поехали?
К статье прилагается внешняя обработка "Ассемблер", которая позволяет изучить формат модуля 1С. Создайте внешнюю обработку с зашифрованным модулем, распакуйте ее с помощью v8unpack и в "Ассемблере" откройте файл "image", который мы рассмотрели чуть раньше.

Тут пока все будет не очень понятно, поэтому, давайте разбираться, как работает виртуальная машина 1С.
Раздел "Операции"
Основной код модуля описан в виде потока операций (команд). Каждая команда имеет числовой номер - код операции. Сокращенно его называют ОпКод (OpCode), этот термин можно встретить в специальной литературе. Каждая команда, помимо опкода имеет один числовой аргумент. Трактовка аргумента зависит от опкода. Каждая команда, получая на вход свой аргумент сама принимает решение, что с ним делать. Некоторые команды не имеют аргумента (ничего не делают со своим аргументом). Итак, запомнили, каждая команда - это два числа: опкод и аргумент операции.
Какие же бывают ОпКоды? В машине 1С их 128, но большую часть составляют встроенные функции типа Лев, НачалоКвартала и им подобные. Низкоуровневых операций существенно меньше.
Раздел "Константы"
С константами вообще классно. Любая ЭВМ должна иметь где-то прошитый набор констант, чтобы понимать что один - это один, а ноль - это ноль. В моем военном прошлом я изучал и работал со старой советской ЭВМ и там был даже специальный блок "ЗУ Констант", хранивший побитовые представления основных констант. Т.е. чтобы прибавить к чему-либо единицу, машина должна понимать, а как вообще выглядит единица с точки зрения включенных/выключенных электронных регистров памяти. Помимо единицы и нуля там же хранилась Пи, таблица синусов/косинусов и всякое такое. Полагаю, современные устройства тоже имеют нечто подобное внутри ПЗУ.
Ну это было лирическое отступление. Наша машина хотя и работает поверх железной, тем не менее, тоже нуждается в термине "Константы". Когда вы в коде пишете "А = 2" компилятор 1С записывает эту двойку в специальный раздел модуля. Туда же попадают литералы дат и строк. При выполнении есть специальная команда "Взять константу за номером таким-то и поместить в стек".
Раздел "Переменные"
Операторы языка 1С (как и любого другого языка) оперируют переменными. В переменную можно положить какое-нибудь значение (например константу). Переменные бывают экспортными, локальными и глобальными. Информацию о переменных компилятор также складирует в специальный раздел. Глобальные переменные модуля хранятся в глобальном разделе "Переменные", Каждый метод (процедура или функция) имеет свой отдельный блок переменных, в котором хранятся переменные относящиеся к конкретному методу.

Как кодить-то?
Кодить в такой парадигме довольно увлекательно. По сути - очень похоже на настоящий ассемблер, только вместо регистров - один стек, на который складываютя и из которого вынимаются операнды. Давайте попробуем сложить 2 числа на байткоде 1С.
Запишем в раздел констант числовую константу 2. Она будет иметь номер 0 в списке констант.
Сложение чисел 2 и 2 будет выглядеть следующим образом:
LdConst 0 LdConst 0 Add 0
Готово! Следите за руками: Все опкоды, которые кладут что-либо на стек, я по традиции обозначил префиксом Ld от слова "Load". Этой традиции много лет, даже Терминатор в своем будущем соблюдал канон и пользовался этим сокращением в своей прошивке.
Команда LdConst имеет аргумент, который указывает на номер константы в списке констант (мы помним, что там по адресу 0 лежит двойка). Команда смотрит в свой аргумент, достает двойку по заданному адресу и кладет ее в стек. В стеке один элемент - двойка.
Вторая команда делает то же самое, в стеке 2 элемента (и оба - двойки)
Третья команда - это операция сложения. Она не нуждается в аргументе, так как оба ее операнда должны лежать в стеке, но поскольку совсем без аргумента нельзя, то в байткоде у Add будет просто 0. Операция сложения извлекает 2 элемента из стека и складывает их по правилам языка 1С (с учетом типа значения самого левого аргумента).
Результат сложения кладется обратно на стек, таким образом получается как бы "возврат" из функции сложения.
Таким образом, для выполнения операции нужно заранее наполнить стек нужным количеством операндов, в нужном порядке.
Превед, Мир!
По традиции, изучение нового языка или технологии проще всего начать с демонстрационной программы "Hello World".
Откройте обработку "Ассемблер" и на закладке "Процедуры" введите строку следующего содержания:

В поле "Флаги процедуры" можно нажать на кнопку выбора с "калькулятором" и посмотреть, что означают флаги.
Далее, на закладке "Константы и переменные" заведите строку

А теперь, наберите в поле "Операции" следующий код:
LdConst 0 ArgNum 1 CallLoc 0 End 0
и нажмите кнопку "Запустить". Моргнет, фыркнет-пшикнет и результат будет выведен в окно сообщений
Вступайте в нашу телеграмм-группу Инфостарт