Історія про те, як я знайшов бекдор в одному з найпопулярніших розширень для VS Code

... або як ніколи не потрібно робити . Або через що видалили розширення майже на 5,5 мільйонів людей.

Хоча в мене вже є дворічний досвід у програмуванні, моя спеціальність — фарбувати кнопки та перекладати JSON-и. У кібербезпеці я розумію майже нічого, лише те, що вивчив на парах у рідному ХНУ

Я часто так роблю. Якось чув, що тестове завдання в JetBrains — це створити власну IDE. Ну, можливо, колись і зроблю.

Зайшов у список плагінів VS Code, переглянув — нічого цікавого: приблизно те саме, що є в моїх пет-проєктах, оку немає за що зачепитися. І тут — бінго! Побачив дуже цікавий плагін для іконок: код обфускований і виконує якісь незрозумілі дії. Ну, це ж JavaScript, тож маю розібратися...

Дуже дивна ситуація — зустріти обфускований код у розширеннях VS Code. Це велика рідкість, адже сама обфускація знижує швидкість виконання десь на 70–80%. Тож мені це одразу здалося підозрілим.

Заварив собі каву, зрозумів, що це надовго, і почав потроху перебирати цей код.

Знайшов деобфускатор obf-io.deobfuscate.io, прогнав код. Спочатку нічого не зрозумів. Подивився ще раз — знову нічого. І лише з третьої спроби, коли нарешті розібрався, мені стало моторошно.

Це ж те саме, про що я читав у книзі Девіда Фленегана, коли ще був трейні? Виклик функції зі строки... Але навіщо? І чому таким неявним чином? Щось намагаються приховати?

Деобфускував, почав аналізувати й зрозумів: «Йой, якби я хотів зробити бекдор, я би зробив саме так». (Хоча тоді я формулював це простіше, адже код був надзвичайно складним для розуміння.)

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

var callback = function () {
    var toggler = true;
    return function (globalThis, anonymousFunction) {
        var deeperCallback = toggler ? function () {
            if (anonymousFunction) {
                var appliedFunctionInstance = anonymousFunction.apply(globalThis, arguments);
                anonymousFunction = null;
                return appliedFunctionInstance;
            }
        } : function () {};
        toggler = false;
        return deeperCallback;
    };
}();
var appliedCallback = function () {
    return callback.toString().search(/((((.+)+)+)+$)/).toString().constructor(callback).search(/(((.+)+)+)+$/);
};
appliedCallback();

Взагалі, у недеобфускованому варіанті виглядає ось так :

var a0_0x3ad204 = a0_0x232f;
(function (_0x8891f2, _0xad3e14) {
    var _0x510787 = a0_0x232f, _0x4419d5 = _0x8891f2();
    while (!![]) {
        try {
            var _0x53ad36 = -parseInt(_0x510787(0x288)) / 0x1 * (parseInt(_0x510787(0x225)) / 0x2) + -parseInt(_0x510787(0x2bf)) / 0x3 + parseInt(_0x510787(0x2e7)) / 0x4 * (-parseInt(_0x510787(0x213)) / 0x5) + parseInt(_0x510787(0x23c)) / 0x6 * (parseInt(_0x510787(0x231)) / 0x7) + -parseInt(_0x510787(0x2b0)) / 0x8 + parseInt(_0x510787(0x1f0)) / 0x9 * (parseInt(_0x510787(0x296)) / 0xa) + parseInt(_0x510787(0x289)) / 0xb;
            if (_0x53ad36 === _0xad3e14) break; else _0x4419d5['push'](_0x4419d5['shift']());
        } catch (_0x3cabe0) {
            _0x4419d5['push'](_0x4419d5['shift']());
        }
    }
}(a0_0x38d2, 0xe954b));
var a0_0x297bcb = (function () {
    var _0xf226e = !![];
    return function (_0x1329aa, _0x15f4f7) {
        var _0x195b68 = _0xf226e ? function () {
            var _0x52051e = a0_0x232f;
            if (_0x15f4f7) {
                var _0x3840b0 = _0x15f4f7[_0x52051e(0x2d2)](_0x1329aa, arguments);
                return _0x15f4f7 = null, _0x3840b0;
            }
        } : function () {
        };
        return _0xf226e = ![], _0x195b68;
    };
}()), a0_0x402e19 = a0_0x297bcb(this, function () {
    var _0x233d78 = a0_0x232f, _0x415c2e = {'PIbAv': '(((.+)+)+)' + '+$'};
    return a0_0x402e19[_0x233d78(0x275)]()[_0x233d78(0x214)](_0x415c2e['PIbAv'])[_0x233d78(0x275)]()[_0x233d78(0x2d6) + 'r'](a0_0x402e19)[_0x233d78(0x214)]('(((.+)+)+)' + '+$');
});
function a0_0x232f(_0x4dd6bf, _0x35c2ea) {
    var _0x23460c = a0_0x38d2();
    return a0_0x232f = function (_0x402e19, _0x297bcb) {
        _0x402e19 = _0x402e19 - 0x1d4;
        var _0x38d2cc = _0x23460c[_0x402e19];
        return _0x38d2cc;
    }, a0_0x232f(_0x4dd6bf, _0x35c2ea);
}
a0_0x402e19();

Повний варіант коду не публікую з очевидних міркувань, але ви його можете самостійно знайти в одному із повідмолень в issue у репозиторії vs code

https://github.com/microsoft/vsmarketplace/
Ви можете дослідити його самостійно.

Звичайно, моє представлення коду не працюватиме, адже ми порушуємо дві основні механіки цього коду:

Ми змінили self-defending функцію, тож перевірка через регулярний вираз не пройде.
Функція викликається за допомогою строки, але я знаю, що така практика є дуже неефективною з точки зору оптимізації, а деякі Content Security Policy (CSP) взагалі її забороняють.
Після цього я зрозумів, що тут щось не чисто. Почав переглядати репозиторій автора — можливо, я помилився? (Я постійно тримав у голові думку, що можу бути неправий, і намагався довести собі протилежне.)

Дивлюся: github.com/...​-icons?tab=readme-ov-file (вже видалено)

After 10 years Material Theme and derivatives moved (months ago) to closed source to focus on important things like providing more support with premium versions of our themes, so we don’t have to ask for contributions again.

If you have a brain but disagree with this decision, feel free to explore the 800+ forks that continue to use the old Apache license, including the full history prior to our transition to closed source. Keep in mind that Apache 2.0 has conditions (though some in the open-source community might overlook them for convenience).

Тобто, бозна-скільки часу це розширення було опенсорсним, у нього було безліч контриб’юторів, які вкладали свої зусилля. До того ж, воно поширювалося за ліцензією Apache 2.0. І раптом автор видаляє всю історію комітів, обфускує код, змінює умови ліцензії та каже: «Усе так і було, ви нічого не бачили».

Як на мене, це несправедливо, особливо щодо людей, які брали участь у розробці цього розширення. Автор просто переписав усю історію розробки використовуючи

git push —force , а потім змінює ліцензію нічого про це не сказавши команді.

Але йдемо далі.

У якийсь момент я зрозумів, що такими темпами буду дуже довго його деобфускувати, а ентузіазм почав згасати. Тож вирішив просто завантажити файл на VirusTotal і подивитися, що він покаже.

Нуль детекцій.

www.virustotal.com/...​4eb9a3a70b066a4/detection

( Я з самого початку не знав про те, що аналіз поведінки підтягується трохи пізніше, з часом ). Подумав — марится. Коли стільки працюєш і не таке буде являтися. Відписав свому товарищу. Висловив свої аргументи, той сказав мені — дуже дивна історія, на який ляд хтось буде настільки скривати звичайний код для зміни іконок. Ага, ну можливо. Тут відбувались перевірка поведінки через virusTotal , спробую завантажити детальну інфу з sandBox, просить авторизацію. Зайшов. Побачив ось такі метчі :

1. Чіпає Windows API .
2. Обфускований код
3. Співпадіння по apt_CN_Tetris_JS_advanced_1

Дуже дивно, що створює дочірні процеси та лізе у системні директорії. ( Більш детально у посиланні на virusTotal ).

YARA-правила саме для apt_CN_Tetris_JS_advanced_1 :

1) github.com/...​aster/yara/apt_tetris.yar
2) github.com/...​etris/tetris_advanced.yar (цей автор знайшов його першим).

Почав дивитись, на око знайшов мінімум 4-ри співпадіння з apt_CN_Tetris_JS_advanced_1 . Штука взагалі дуже цікава. Усього кількість виявлень з 2021-го року 28 штук. Дуже штучна історія, але можлива колізія при обфускації.

bazaar.abuse.ch/...​_CN_Tetris_JS_advanced_1

Більш детально про нього — imp0rtp3.wordpress.com/2021/08/12/tetris

На деякий проміжок часу я подумав, що знайшов саме це. Рукам знайшов співпадіння. Але, зрозумів, ну цей китаєць запускаєтся же у браузері, а тут у нас NodeJS . І тим паче ніякої мережевої активності за ним я не знайшов... Про всяк випадок вирішив продивитись його .

Поведінка розширення у sandBox

1) Деталі з capeSandbox — VirusTotal -> Behavior -> Full Report -> CAPE Sandbox, там дуже багато чого цікавого є . ( Не змог прикріпити, бо URL динамічний )

2) Деталі Threat Zone — app.threat.zone/...​amic-scan-report/overview

3) LevelBlue Labs — otx.alienvault.com/...​4b4e406eb3e74b270a1791961

Порадився зі своїми друзями, написав своєму викладачу з університету, перечислив свої аргументи, той каже : «негайно пиши у Microsoft , потрібно його видалити». 11-та ночі. Спробую подзвонити : 10 хвилин розповідь про різні політики, після того дуже бадьорим голосом робот мені каже «передзвоніть у робочій час».

Проходить ще один день. Ранок. Думаю, що потрібно написати Майкрософтам.

Мене переводять на якогось консультанта, який мені десь з годину намагаєтся розповісти про те, як надіслати скаргу. Перекидує мене на сторінку, у якій можна описати свою проблему. Описую . Пише : «у вас немає плану для обслуговування по цьому питанню» ... Вирішую поздвонити. Мені відповідають англійською, я стресую, майже ніч не спав, бо розбирав той код, на ламаному інгліші намагаюсь пояснити підтримці свою проблему, та взагалі не розуміє що таке VS Code . Після того, вони вирішили мене почати футболити по різним відділам, і так я опинився у відділі продажів продуктів Office . Добре, що мені там допомогли, вислухали. Пишу листа ім. Інші докази відправляю наступними листами, усього догнав до кількості 7 листів , у яких описав додаткові спостереження : і про те, що код дуже різко закрили, і про видалену історію розробки, і про можливі співпадіння із цим китайцем, і про цю дірку із функцією, ну коротше дуже цікава історія.

Того ж дня отримую відповідь приблизно такого змісту :

«Дуже дякуємо, але ваше питання буде розглянуто у робочий час по стандартному індійському часовому поясу» ...

Ну добре. Почав робити свою основну роботу, чекаю, що ж там взагалі трапится, час від часу перевіряю, чи доступне це розширення, і в один момент бачу : усі розширення автора заблоковані, хоча я описував проблему саме у тому, що на скріншоті.

Після того думаю : можливо на час дослідження вони його виключили. Ну усіляке буває . Давай почекаю. Я вже навіть декілька разів статтю на DOU про це підготував, але у останній момент подумав : «зараз почекаю, аби не зганьбитись на увесь інтернет».

У цей момент так само аналізую , що робить автор :

— Закриває свій профіль на github
— Користувачі видкривають issue у темі із розширенням, автор видаляє усі повідомлення, після закриває issue. Навіщо ?

— Видаляє репозиторій або робить його приватним і намагаєтся створити нову тему і знову її закинути на VS Code Marketplace . Він намагаєтся обійти блокування і знову той самий код закинути вже для нової теми .

Про себе думаю : чому просто не вийти і сказати «вийшла помилка, ось уся кодова база, вивчайте» ( ну я би так і зробив би ), замість того автор веде себе максимально підозріло.

Чекаю відповідь від Майків. Саме у цей момент я став свідком того, як після моїх дій сталися якісь важливі та масштабні події, відчув це вперше. Люди почали писати на Reddit із питанням, а у репозиторії автора почали так само виникати запитання. У мене дуже сильно горіли руки відписати їм, але чекаю ... чекаю. Проходить ще день , мені прилітає повідомлення

Ага, так і є . Усі розширення автори видалені, про це вже виходять відео на YouTube , пости у профільних групах, і я думаю : люди вже почали на цьому популярність збирати, тому чому я мовчу ? Потрібно, аби про це суспільство знало .

На останок : повідомлення від розробника з офіційної команди VS Code .

Себто, вони знайшли ще штуки, як я не знайшов ( я так розумію питання у залежностях і у тому, що розширення виконує потенційно зловмисницкі дії виходячи за дозволені межі).
news.ycombinator.com/item?id=43178831

Офіційне повідомлення у репозиторії VS Code .

github.com/...​b/main/RemovedPackages.md

Компілюю цю статтю, після, розумію, що такі співпадіння трапляються дуже рідко, тепер про ситуацію, яка взагалі склалась (як я вже розумію) :

— Автор бозна коли створив тему, у неї контриб’ютили інші розробники, опен-сорс, безліч форків, розширення дуже швидко стало успішним. Після того автор захотів коммерціалізувати це і зробив преміум версію, а для того, аби ніхто не зміг використовувати опен-сорс — ПРОСТО ЗАКРИВ ЙОГО, видалив історію комітів і нікому про це не сказав. Історія і вклад у розробку великої кількості людей просто згорів.

— Автор не захотів використовувати стандартні рішення і написав власний велосипед на генераторах, який може залазити у які завгодно системні директорії і робити що завгодно за допомогою функції зі строки у самому початку цього посту

— Обфускував код і усі його проблеми вилізли на світ божий, тепер будь-який анивірус його детектить як malicious

— Без попередження змінив ліцензію .

Отож-бо, підсумовуючи :

— Я ніяким чином не блокував це розширення, лише повідомив Microsoft про свої спостереження.
— Microsoft САМІ вирішили про те, що цей код має багато червоних прапорців, тому не тільки тимчасово заблокували розширення, але після детального аналізу повністю його видалили (!) . Я чекав із публікацією до останнього у глибині серця сподіваючись, що сталась помилка.
— У коді дійсно існує дірка для того, аби запускати код зі строки, у тому числі парсится через 2 регулярки, що з моєї точки зору дуже сумнівно з точки зору оптимізації.
— Код обфускований
— Автор не може пояснити навіщо він видалив усю історію розробки і повністю видалив працю багатьох розробників, які туди комітили.
— Його подальші дії, коли він намагався обійти захист і повторно опублікувати тему — дуже бентежить .

Уявіть, наскільки би вам було образливо, якщо би ви комітили у опен-сорс, а потім вам би сказали : «Хлопче, то усьо фігня, тепер нічого твого немає тут, йди звідси, то тепер усе моє і ліцензію я відкликаю» , - отож бо.

Автор зробив усе для того, аби його дії виглядали дуже і дуже не гарно .

Ось так я провів позаминулі вихідні. І кожен раз думав про те, як написати так, щоб не помилитися, бо тема дуже чутлива. Але, навіть мені, з моїм невеликим досвідом зрозуміло, що :

— Продукт під ліцензією Apache 2.0 яка за словами самого автора була «irreversible» не потрібно переводити у closed source
— Не потрібно обфускувати код, який має можливості для взаємодії з системою. Взагалі не потрібно обфускувати розширення для зміни іконок у IDE.
— Не потрібно писати велосипеди
— Якщо йде розслідування цього інциденту не потрібно створювати нові теми та намагатися опублікувати цей код знову.
— Уважно дивіться про те, що ви завантажуєте у розширеннях.

Окрему дяку хочу висловити своєму товарищу і пітоністу , який допоміг мені розібратись у цій ситуації : Всеволоду Зайченко .

Ось така історія. Не знаю, чи повчальна, чи ні. Це мій перший пост на DOU =)

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось38
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

А че мягкотелые сами не прогнали это расширение через вирус тотал до того как публиковать ? Четкое совпадение сигнатур или чего там — красный флаг.

шкода, що якість картинок не дозволяє нормально прочитати, що на них написано.
Виникло питання: звідки така інформація, що обфускація впливає на взаємодію аж так, що швидкодія падає на 70-80%. Цьому є якісь підтвердження?

«я витратив забагато часу за останні 4 дні — більше 40 годин — у спробах зрозуміти що тут відбувається» — господі, там практично жодних технічних деталей у відео, а я передивився десь 80% у нормальному режимі (1.5Х) і лише частину (20%) проскіпав.
Якщо хтось піде дивитись відео зі сподіваннями отримати технічні деталі — у мене для вас погані новини)

Дуже класно. Дякую за статтю.

Але я не дуже розумію як це працює. Поясніть будь-ласка хто розуміє.

Тобто проблема в цій строчці коду:

return callback.toString().search(/((((.+)+)+)+$)/).toString().constructor(callback).search(/(((.+)+)+)+$/)

З того що я тут бачу це
1) який-небудь колбек переводиться в стрінгу,
2) потім в цій стрінзі шукається якийсь номер
3) потім цей номер переводиться в стрінгу
4) потім ця стрінга викликає свій конструктор який переводить код колблеку в стрінгу
5) потім в цій стрінзі знов шукається і повертається якийсь індекс.

Чи виглядає це жахливо? Так.
Але як на мене це схоже на результат транспіляції нового джаваскрипта в старий джаваскрипт коли воно гарні класи переводить в ту діч з вкладеними конструкторами і самовиконуючимися функціями.
Якщо цей код був написаний мільйоном девелоперів і мільйон раз проганявся через усякі старі і не старі транспілятори і обфускатори, то чи могло це привести до такого кінцевого вигляду чи зовсім не могло? Ну і якщо не могло, то все одно саме цей кусок не виглядає що він поводить себе як eval(). Можна трошки переписать і буде, але не саме такий.

В open source це хоч знайти можна, а в closed source — ні.

поклади на місце і забудь, бекдор, то кого нада бекдор, нема чого чіпати.

Щодо зміни ліцензії — це не поодинокі випадки. youtu.be/ZFc6jcaM6Ms тут описують один кейс з OS лібою для .net

перекладай на англ, потім зробиш візу талантів кудись, якщо треба буде

та да, могли б для приличия в пресрелизе хоть бы ссылкой указать автора

Підписатись на коментарі