1. Введение
Открытость и удобство Сети обусловили чрезвычайно высокую скорость разработки, но в то же время принесли и множество скрытых опасностей, особенно для защиты кодов ядер, с этим связано множество решений. «нет секретов во внешнем коде», кажется, является отраслевым консенсусом, который обычно распространяется в области внешнего интерфейса. Тем не менее, в ежедневном процессе разработки мы будем задействовать и требовать шифрования основного кода внешнего интерфейса со значительной степенью надежности, особенно при обмене данными с серверной частью (включая запросы HTTP, HTTPS и обмен данными через WebSocket).
Рассмотрим сценарий.В продуктах, связанных с видео, нам обычно нужно добавить соответствующую логику безопасности, чтобы предотвратить прямую потоковую передачу или пиратство. Специально для прямых трансляций наши файлы потокового видео обычно делятся на сегменты, а затем соответствующие параметры URL-адреса генерируются с помощью согласованного алгоритма и запрашиваются один за другим. Шардинг обычно выполняется с интервалом от 5 до 10 секунд.Если URL-адрес шардинга полностью размещен на бэкенде в качестве интерфейса, это не только окажет большое давление на бэкэнд, но и приведет к задержкам в запросах прямой трансляции.Поэтому мы обычно поместите части реализации на переднюю часть, чтобы уменьшить нагрузку на заднюю часть и улучшить работу. Для iOS или Android мы можем написать соответствующий алгоритм на C/C++, затем скомпилировать его в dylib или около того и запутать, чтобы увеличить сложность взлома, но для внешнего интерфейса аналогичная технология не может быть использована. Конечно, после полного продвижения asm.js и WebAssembly мы можем использовать его для дальнейшего повышения безопасности нашего основного кода, но из-за открытости стандартов asm.js и WebAssembly уровень безопасности не так хорош, как предполагалось. .
В этой статье сначала должным образом рассматриваются текущие популярные технические идеи и краткие реализации, связанные с защитой основного кода внешнего интерфейса, а затем специально описывается более безопасная и надежная идея защиты основного кода внешнего интерфейса (SecurityWorker) для справки и улучшения. Конечно, автор не является профессиональным специалистом по фронтенд-безопасности, и его понимание некоторых технических аспектов безопасности может быть несколько однобоким и недостаточным. Добро пожаловать, чтобы оставить сообщение для обсуждения.
2. Obfuscator с использованием JavaScript
В нашем ежедневном процессе разработки мы знакомы с обфускатором JavaScript, мы часто используем его для сжатия и обфускации кода, чтобы уменьшить размер кода и повысить сложность кода для чтения человеком. К часто используемым элементам относятся:
Принцип обфускатора JavaScript не сложен. Его ядром является выполнение преобразования AST (переписывание абстрактного синтаксического дерева) над целевым кодом. Мы полагаемся на существующую библиотеку JavaScript AST Parser, чтобы легко реализовать собственный обфускатор Javascript. Ниже мы используемacornДля реализации перезаписи фрагмента оператора if.
Предположим, у нас есть такой фрагмент кода:
for(var i = 0; i < 100; i++){
if(i % 2 == 0){
console.log("foo");
}else{
console.log("bar");
}
}
Используя UglifyJS для запутывания кода, мы можем получить следующие результаты:
for(var i=0;i<100;i++)i%2==0?console.log("foo"):console.log("bar");
Теперь давайте попробуем написать собственный обфускатор, чтобы запутать фрагмент кода для достижения эффекта UglifyJS:
const {Parser} = require("acorn")
const MyUglify = Parser.extend();
const codeStr = `
for(var i = 0; i < 100; i++){
if(i % 2 == 0){
console.log("foo");
}else{
console.log("bar");
}
}
`;
function transform(node){
const { type } = node;
switch(type){
case 'Program':
case 'BlockStatement':{
const { body } = node;
return body.map(transform).join('');
}
case 'ForStatement':{
const results = ['for', '('];
const { init, test, update, body } = node;
results.push(transform(init), ';');
results.push(transform(test), ';');
results.push(transform(update), ')');
results.push(transform(body));
return results.join('');
}
case 'VariableDeclaration': {
const results = [];
const { kind, declarations } = node;
results.push(kind, ' ', declarations.map(transform));
return results.join('');
}
case 'VariableDeclarator':{
const {id, init} = node;
return id.name + '=' + init.raw;
}
case 'UpdateExpression': {
const {argument, operator} = node;
return argument.name + operator;
}
case 'BinaryExpression': {
const {left, operator, right} = node;
return transform(left) + operator + transform(right);
}
case 'IfStatement': {
const results = [];
const { test, consequent, alternate } = node;
results.push(transform(test), '?');
results.push(transform(consequent), ":");
results.push(transform(alternate));
return results.join('');
}
case 'MemberExpression':{
const {object, property} = node;
return object.name + '.' + property.name;
}
case 'CallExpression': {
const results = [];
const { callee, arguments } = node;
results.push(transform(callee), '(');
results.push(arguments.map(transform).join(','), ')');
return results.join('');
}
case 'ExpressionStatement':{
return transform(node.expression);
}
case 'Literal':
return node.raw;
case 'Identifier':
return node.name;
default:
throw new Error('unimplemented operations');
}
}
const ast = MyUglify.parse(codeStr);
console.log(transform(ast)); // 与UglifyJS输出一致
Конечно, мы понимаем, что это всего лишь простой пример, на самом деле путаница будет намного сложнее, чем текущая реализация, необходимо учитывать множество деталей синтаксиса, здесь для справки только инициируйте обучение.
Из приведенной выше реализации мы видим, что обфускатор JavaScript просто изменяет код JavaScript в другую форму, менее удобочитаемую, чтобы усложнить человеческий анализ и достичь цели повышения безопасности. Этот подход хорошо работал давным-давно, но по мере того, как инструменты разработчика становятся все более и более мощными, на самом деле очень легко изменить исходный базовый алгоритм Javascript с помощью одношаговой отладки. Конечно, есть довольно много библиотек, которые сделали больше улучшений в последующем.JavaScript Obfuscator Tool Это представительный проект среди них, который добавляет такие функции, как защита от отладки, префикс переменных, обфускация переменных и т. д. для повышения безопасности. Но то же самое остается прежним.Поскольку запутанный код все еще находится в открытом тексте, мы все еще можем попытаться восстановить его, если у нас будет достаточно терпения и с помощью инструментов разработчика, поэтому безопасность все еще сильно снижается.
3. Метод расширения C/C++ с использованием Flash
В период, когда Flash был еще популярен, чтобы облегчить разработчикам движков использование C/C++ для повышения производительности движков, связанных с Flash-играми, Adobe открыла исходный код.CrossBridgeэта технология. В этом процессе исходный код C/C++ преобразуется в целевой код, необходимый среде выполнения Flash, с помощью LLVM IR, что значительно повышает как эффективность, так и безопасность. Для текущих декомпиляторов с открытым исходным кодом сложно декомпилировать код C/C++, скомпилированный CorssBridge, а поскольку отладка отключена в производственной среде среды выполнения Flash, также сложно выполнить соответствующую одноэтапную отладку.
Использование метода расширения Flash C/C++ для защиты основного кода внешнего интерфейса кажется идеальным методом, но на мобильной стороне для Flash нет места, и Adobe объявила, что Flash больше не будет поддерживаться в 2020 году. у нас нет абсолютно никаких причин использовать этот метод для защиты основного кода нашего внешнего интерфейса.
Конечно, поскольку Flash по-прежнему имеет большую долю на ПК, а браузеры ниже IE10 по-прежнему имеют большую долю, мы все еще можем рассматривать это как решение, совместимое со стороной ПК.
4. Используйте asm.js или WebAssembly
Чтобы решить проблемы с производительностью Javascript, Mozilla предложила новое подмножество базового синтаксиса Javascript —asm.js, который с точки зрения JIT-дружественности значительно повышает общую производительность Javascript. Последующая стандартизация Mozilla с другими производителями привела кWebAssemblyстандарт.
Будь то asm.js или WebAssembly, мы можем думать о нем как о совершенно новой виртуальной машине, а другие языки производят исполняемый код этой виртуальной машины через соответствующую цепочку инструментов. С точки зрения безопасности, его сила значительно выше по сравнению с чистым обфускатором Javascript, и по сравнению с методом расширения C/C++ для Flash, это будущее направление развития, и оно было принято основными реализациями браузеров.
Существует много языков и наборов инструментов, которые могут писать и генерировать WebAssembly.Мы используем C/C++ и его Emscripten в качестве демонстрации, чтобы написать простой модуль подписи для опыта.
#include <string>
#include <emscripten.h>
#include <emscripten/bind.h>
#include "md5.h"
#define SALTKEY "md5 salt key"
std::string sign(std::string str){
return md5(str + string(SALTKEY));
}
// 此处导出sign方法供Javascript外部环境使用
EMSCRIPTEN_BIND(my_module){
emscripten::function("sign", &sign);
}
Затем мы используем emscripten для компиляции нашего кода C++ и получения соответствующего сгенерированного файла.
em++ -std=c++11 -Oz --bind \
-I ./md5 ./md5/md5.cpp ./sign.cpp \
-o ./sign.js
Наконец, мы вводим создание файла sign.js, а затем вызываем его.
<body>
<script src="./sign.js"></script>
<script>
// output: 0b57e921e8f28593d1c8290abed09ab2
Module.sign("This is a test string");
</script>
</body>
В настоящее время кажется, что WebAssembly является наиболее идеальным решением для защиты основного кода переднего плана.Мы можем использовать C/C++ для написания соответствующего кода и использовать цепочку инструментов, связанную с Emscripten, для его компиляции в asm.js и wasm, и выбрать в зависимости от поддержки различных браузеров.Используйте asm.js или wasm. А для браузеров ниже IE10 на стороне ПК мы также можем повторно использовать его код C/C++ через CrossBridge для создания соответствующего объектного кода Flash, чтобы добиться очень хорошей совместимости с браузерами.
Однако можно ли гарантировать защиту внешнего кода ядра после использования asm.js/wasm? Поскольку стандартные спецификации asm.js и wasm полностью открыты, для декомпилятора, который хорошо реализует стандарт asm.js/wasm, можно получить максимально читаемый код для анализа кода ядра алгоритма. Но, к счастью, автор пока не нашел хорошо реализованного декомпилятора asm.js/wasm, поэтому я временно думаю, что этот метод можно повторно использовать для защиты безопасности кода ядра фронтенда.
5. SecurityWorker — идея получше и ее реализация
В своей работе автор часто пишет код, относящийся к внешнему ядру, и большинство этих кодов связаны с коммуникацией, например, с шифрованием и дешифрованием данных запроса AJAX, а также с шифрованием и дешифрованием данных протокола WebSocket. Для этой части работы автор обычно использует представленное выше техническое решение asm.js/wasm plus CrossBridge для ее решения. Это решение кажется неплохим на данный момент, но есть еще несколько больших проблем:
- Фронтенд недружелюбен, и большинство фронтенд-инженеров не знакомы с C/C++, Rust и другими связанными техническими системами.
- Невозможно использовать огромную библиотеку npm, что увеличивает стоимость работы.
- В долгосрочной перспективе затраты на взлом не будут большими, и необходимы дальнейшие улучшения безопасности.
Итак, мы потратили две недели на написание лучшего решения для защиты основного кода внешнего интерфейса на основе asm.js/wasm:SecurityWorker.
5.1 Цели
Цель SecurityWorker довольно проста: иметь возможность писать базовые модули алгоритмов с чрезвычайно сильными сторонами безопасности настолько удобно, насколько это возможно. Его разделение на самом деле должно соответствовать следующим 8 пунктам:
- Код написан на Javascript, без использования C/C++, Rust и других технических систем.
- Уметь беспрепятственно использовать библиотеки, связанные с npm, и интегрироваться с интерфейсной экосистемой.
- Окончательный код как можно меньше
- Защита достаточно сильная, а логика выполнения целевого кода и основной алгоритм полностью скрыты.
- Поддержка нескольких сред браузера/апплета/NodeJS (Node по умолчанию не разрешен для предотвращения крупномасштабных вызовов черного ящика)
- Хорошая совместимость, полная совместимость с основными браузерами
- Простота использования и возможность повторного использования технических концепций в стандарте
- Простота отладки, исходный код не запутан, а информация об ошибках является точной и конкретной.
Далее мы постепенно объясним, как SecurityWorker достигает этих целей, и подробно представим его принципы для ознакомления и улучшения.
5.2 Принцип реализации
Как повысить безопасность на основе WebAssembly? Вспоминая наше предыдущее введение, относительно уязвимый момент в безопасности WebAssembly заключается в раскрытии стандартной спецификации WebAssembly.Если мы создадим частную и независимую виртуальную машину поверх WebAssembly, можно ли решить эту проблему? Ответ положительный, поэтому первая проблема, которую мы решаем, заключается в том, как построить независимую от Javascript виртуальную машину поверх WebAssembly. Это легко для WebAssembly, есть много проектов, которые предоставляют ссылки, например, скомпилированные на основе SpiderMonkey.js.jsпроект. Но мы не рассматривали возможность использования SpiderMonkey, потому что создаваемый им код wasm достигает 50M, что в принципе не имеет практической ценности в такой чувствительной к размеру кода среде, как Интернет. Но, к счастью, существует так много встроенных движков, связанных с ECMAScirpt:
- JerryScript
- V7
- duktape
- Espruino
- ...
После сравнения и выбора мы выбрали duktape в качестве нашей базовой виртуальной машины, и наш процесс выполнения стал таким, как показано на следующем рисунке:
Конечно, из рисунка видно, что весь процесс на самом деле имеет относительно большую точку риска.Поскольку наш код компилируется в C/C++ с помощью строкового шифрования, в процессе выполнения мы можем получить основной код после ожидания расшифровки кода в течение определенного времени работы в памяти, как показано на следующем рисунке:
Как решить эту проблему? Наше решение состоит в том, чтобы превратить Javascript в другую форму выражения, которая является нашим общим кодом операции.Например, предположим, что у нас есть этот код:
1 + 2;
Мы превратим его во что-то вроде инструкции по сборке:
SWVM_PUSH_L 1 # 将1值压入栈中
SWVM_PUSH_L 2 # 将2值压入栈中
SWVM_ADD # 对值进行相加,并将结果压入栈中
Наконец, мы внедряем скомпилированные байты кода операции в C/C++ в виде массива uint8, а затем выполняем общую компиляцию, как показано на рисунке:
В течение всего процесса, поскольку наш дизайн кода операции является закрытым, а не общедоступным, и нет открытого кода Javascript, безопасность была значительно улучшена. Таким образом, мы решили № 1, № 2, № 4 в мишени. Но Javascript был реорганизован в опкод, так как же гарантировать целевому номеру 8? Решение очень простое: мы прикрепляем соответствующую информацию к ключевым шагам компиляции Javascript в код операции, чтобы после ошибок выполнения кода мы могли точно сообщать об ошибках на основе соответствующей информации. В то же время мы упростили дизайн опкода, так что размер сгенерированного опкода меньше исходного кода Javascript.
Помимо языковой реализации и некоторых стандартных библиотек, duktape не имеет некоторых периферийных API, таких как AJAX/WebSocket и т. д. Учитывая удобство использования и более легкое принятие и использование фронтенд-разработчиками, мы реализовали часть WebWorker среда для duktape API, включая Websocket/Console/Ajax и т. д., в сочетании с реализацией Fetch/WebSocket, предоставляемой Emscripten, позволяет получить виртуальную машину SecurityWorker.
Итак, последний вопрос: как мы можем уменьшить размер конечного сгенерированного кода asm.js/wasm? Без какой-либо обработки наш сгенерированный код содержит duktape и реализацию многих периферийных API, даже код Hello World будет иметь размер около 340кб после gzip. Чтобы решить эту проблему, мы написали SecurityWorker Loader, который будет скомпилирован с реализацией SecurityWorker Loader для получения конечного файла после обработки сгенерированного кода. Когда код выполняется, загрузчик SecurityWorker освобождает код, который необходимо запустить, а затем выполняет его динамически. Таким образом, мы уменьшили исходный размер кода с исходного размера gzip примерно 340 КБ до примерно 180 КБ.
5.3 Ограничения
SecurityWorker решает многие проблемы предыдущего решения, но также не является самым совершенным решением.Поскольку мы создали ВМ на WebAssembly, когда ваше приложение чувствительно к размеру или требует предельно высокой эффективности выполнения, SecurityWorker не подойдет.Удовлетворите вашу просьбу . Конечно, SecurityWorker может использовать различные методы оптимизации, чтобы значительно уменьшить размер и повысить эффективность выполнения на текущей основе, однако, поскольку он уже удовлетворил наши существующие потребности и цели, в настоящее время нет соответствующих планов по улучшению.
6. Заключение
Анализируя текущие основные решения для защиты ядра переднего плана и представляя SecurityWorker, улучшенное решение, основанное на предыдущем решении, я полагаю, что у всех есть четкое представление о техническом решении защиты алгоритма ядра переднего плана в целом. Конечно, стремлению к безопасности нет конца, и SecurityWorker не является окончательным и идеальным решением.Я надеюсь, что соответствующее введение в этой статье позволит большему количеству людей участвовать в области WebAssembly и безопасности интерфейса, и сделать Интернет лучше.
Фан Лаоши, ты научился этому?