写点什么

从 0 开始快速上手 WebAssembly:打造基于 WASM 的高性能安全沙盒

  • 2020 年 11 月 16 日
  • 本文字数:5299 字

    阅读完需:约 17 分钟

从0开始快速上手WebAssembly:打造基于WASM的高性能安全沙盒

通过上一篇文章对 Emscripten的使用介绍,大家应该具备一定的使用 Emscripten 工具链开发相关 WebAssembly 项目的基础能力了。在本文中,我们将继续通过具体案例更深入地了解 Emscripten 的使用技巧,同时加强对 WebAssembly 二进制格式、Low-Level 编译目标及接近 Native 执行效率的理解。


《WebAssembly 技术基础到实践》专题文章回顾:


WebAssembly如何演进成为“浏览器第二编程语言”?


从0开始快速上手WebAssembly:Emscripten使用入门


前端核心数据加密


Web 技术的开放以及便捷带来了其极高速的发展,但自作者从事 Web 前端相关开发工作以来,并没有听到太多关于前端核心数据加密的方案,因此“前端数据无加密”慢慢的也变成了业界的一个共识。但在某些日常开发的场景中,我们又会涉及到相当强度的前端核心数据加密的问题,特别是需要在于后端的数据通信上面(包括 HTTP、HTTPS 以及 WebSocket 的数据交换),例如前端日志数据加密就是一个典型的场景。


针对于此类需求,在以往的实践中我们通常使用纯 JavaScript 实现的 Crypto 库来对相关数据加密,同时使用 UglifyJS、Google Closure Compiler 等混淆器进行相关加密代码的混淆,以增加对应的破译难度。但在目前的实践之中由于浏览器自身的调试工具逐渐方便且强大,因此此类方法的安全性变得越来越脆弱。


考虑到这种情况,越来越多的公司开始将前端核心数据加密的相关逻辑迁移至 WebAssembly,依靠 WebAssembly 在整个过程中的编译及二进制特性来提高对应数据加密逻辑的安全性,以此保障业务安全。对于这种方案而言,由于我们可以将 WebAssembly 视作一个全新的 VM,其他语言通过相关工具链(例如 Emscripten)来产出此 VM 可执行的代码,其安全性相比于单纯的 Crypto 库加 JavaScript 混淆器而言具有比较明显的优势。


现在让我们就着手实现一个非常简单的 WebAssembly 签名加密模块来进行一下对应的体验。首先,创建相关的项目,同时下载 Github 上的某 MD5 实现到本项目的 vendor 文件夹中,整体目录结构如下所示:

├main.c├CMakeList.txt├vendor    ├─base64    │    └─CMakeList.txt    │    └─base64.c    │    └─base64.h    └─md5        └─CMakeList.txt        └─md5.c        └─md5.h
复制代码


接着我们依照上一章的 CMake 文件进行稍加修改,增加对此 MD5 库的编译链接,如下所示:


cmake_minimum_required(VERSION 3.15) # 根据你的需求进行修改project(sample C)
set(CMAKE_C_STANDARD 11) # 根据你的C编译器支持情况进行修改set(CMAKE_EXECUTABLE_SUFFIX ".html") # 编译生成.html
include_directories(${PROJECT_SOURCE_DIR}/vendor) # 使得我们能引用第三方库的头文件add_subdirectory(${PROJECT_SOURCE_DIR}/vendor/md5)add_subdirectory(${PROJECT_SOURCE_DIR}/vendor/base64)
add_executable(sample main.c)
# 设置Emscripten的编译链接参数,我们等等会讲到一些常用参数set_target_properties(sample PROPERTIES LINK_FLAGS "\ -s EXIT_RUNTIME=1 \ -s EXPORTED_FUNCTIONS=\"['_sign']\"")
target_link_libraries(sample md5 base64) # 将第三方库与主程序进行链接
复制代码


最后实现我们的数据签名逻辑即可,代码如下:


#include <stdio.h>#include "md5/md5.h"#include "base64/base64.h"
const char* salt_key = "md5 salt key";
#ifdef __EMSCRIPTEN__int sign(const char* msg, int len, char *output){ int data_len = strlen(salt_key) + len + 1; char *data = malloc(data_len); memset(data, 0, data_len); memcpy(data, msg, len); memcpy(data + len, salt_key, strlen(salt_key));
uint8_t result[16] = {0}; md5((uint8_t *)data, strlen(data), result);
char *encode_out = malloc(BASE64_ENCODE_OUT_SIZE(16)); base64_encode(result, 16, encode_out); memcpy(output, encode_out, BASE64_ENCODE_OUT_SIZE(16)); free(encode_out);
return 0;}#endif
复制代码


在这里我们使用了上一章介绍的Memory Copy的方式来进行结果数据的传递,因此我们的 JavaScript 代码调用应如下所示:

const str = intArrayFromString("your data here");const ptr = allocate(str, "i8", ALLOC_NORMAL);const output = Module._malloc(1024);Module._sign(ptr, str.length, output);console.log(UTF8ToString(output)); // O6VFgoqQiF52FYyH4VmpPQ==
复制代码


sign的具体实现中我们可以看到,为了增加整体的安全性,我们对其中的内容进行了“加盐”处理。这种操作看起来万无一失,但是实际上我们稍加分析是可以拿到对应的“盐”值的。现在我们将生成的 sample.wasm 文件拖入到编辑器中,然后搜索md5 salt key,我们可以很顺利的得到对应的“盐”值。


WebAssembly 无法隐藏所有信息


针对于这种情况,我们可以进一步使用异或等算法对“盐”值进行相关的处理来达到更深度的处理,但对于需要更高强度的核心数据保护的应用而言这也不过是叠加的障眼法,在这里我们需要更可靠的方式来达到我们的需求。

沙盒保护


沙盒保护的思路比较直接:为了更好的达到代码整体保护的目的,我们会自行创建一个完全独立的代码执行环境。在这个独立环境中,我们根据我们的需求限制内部代码使用的 built-in API 从而达到完全可控的状态。与此同时,只要我们符合执行代码的相关规范,那么我们可以自行设计相关的 Opcode 及 IR,来达到防逆向的目的。针对于前端环境而言,其整体流程如下所示:


沙盒保护的编译/运行过程


由于整个前端环境大部分都依靠于 JavaScript,因此在沙盒环境上我们有非常多的选择,在本章中我们采用 QuickJS 来进行相关的介绍和演示。如果有其他的沙盒环境需求,可以参考本章的相关思路来进行适当的修改。


QuickJS 想必大家都不会陌生,其作者 Fabrice Bellard 编写了 TinyCC/TinyGL/FFmpeg/JSLinux 等多个知名项目。QuickJS 实现精小,几乎完整支持 ECMA2019,同时具有极低的启动速度有优秀的解释执行性能。在使用 QuickJS 完成我们的沙盒之前,我们先简单的了解一下 QuickJS 的相关 API,然后再使用 Emscripten 来对其进行编译完成我们的整个示例。


首先我们对 CMake 进行相关修改,增加对 QuickJS 的编译链接支持,如下所示:


cmake_minimum_required(VERSION 3.15)project(sample C)
set(CMAKE_C_STANDARD 11)include_directories(${PROJECT_SOURCE_DIR}/vendor)add_subdirectory(${PROJECT_SOURCE_DIR}/vendor/quickjs)
add_executable(sample main.c)
target_link_libraries(sample quickjs)
复制代码


然后我们先通过"Hello World"的输出示例来完成整个项目的初始化,代码如下:

#include "quickjs/quickjs.h"
int main() { JSRuntime *rt = JS_NewRuntime(); JSContext *ctx = JS_NewContext(rt); JSValue value = JS_Eval(ctx, "'Hello World!'", 12, "<sandbox>", 0); printf("%s\n", JS_ToCString(ctx, value)); // Hello World! JS_FreeContext(ctx); JS_FreeRuntime(rt); return 0;}
复制代码


现在,对于沙盒内部的 JavaScript 代码而言,我们将会暴露出一个名为 crypto 的全局函数,此函数会对传入的字符串进行 MD5 的相关加密,并且返回给外部环境,其内部 JavaScript 代码如下所示:


const MD5_SALT_KEY = 'md5 salt key';function md5(str) {  // MD5的相关算法实现  // 此处逻辑你可以引入npm上的相关库  // 然后使用Webpack/Parcel等工具进行编译}
function crypto(str) { str = `${str}${MD5_SALT_KEY}`; return md5(str);}
复制代码


QuickJS 要调用对应的内部全局 crypto 函数很简单,我们使用 JS_Eval 即可完成:

#include <string.h>#include "quickjs/quickjs.h"
const char* JS_CODE = "const MD5_SALT_KEY = 'md5 salt key';\n" "function md5(str) { return str; }\n" "function crypto(str) {\n" " return md5(`${str} ${MD5_SALT_KEY}`);\n" "};";
int main() { JSRuntime *rt = JS_NewRuntime(); JSContext *ctx = JS_NewContext(rt); JS_Eval(ctx, JS_CODE, strlen(JS_CODE), "<sandbox>", 0); JSValue value = JS_Eval(ctx, "crypto('data')", 14, "<sandbox>", 0);
printf("%s\n", JS_ToCString(ctx, value)); // data md5 salt key JS_FreeContext(ctx); JS_FreeRuntime(rt); return 0;}
复制代码


运行我们的代码,我们可以看到,当调用执行结束后其正确的输出了相关内容。接下来我们对目前的实现加入部分 Emscripten 的胶水代码并进行 WebAssembly 的编译,从而使得我们能从 Web 端或 NodeJS 进行相关的执行,调整后的 CMake 如下所示:

cmake_minimum_required(VERSION 3.15)project(sample C)
set(CMAKE_C_STANDARD 11)set(CMAKE_EXECUTABLE_SUFFIX ".html") # 编译生成.html
include_directories(${PROJECT_SOURCE_DIR}/vendor)add_subdirectory(${PROJECT_SOURCE_DIR}/vendor/quickjs)
add_executable(sample main.c)
set_target_properties(sample PROPERTIES LINK_FLAGS "\ -s EXIT_RUNTIME=0 \ -s EXPORTED_FUNCTIONS=\"['_init', '_eval', '_dispose']\"")
target_link_libraries(sample quickjs)
复制代码


接着,我们调整我们的 C++部分代码如下:

#include <string.h>#include "quickjs/quickjs.h"
static JSRuntime *rt;static JSContext *ctx;
const char* JS_CODE = "const MD5_SALT_KEY = 'md5 salt key';\n" "function md5(str) { return str; }\n" "function crypto(str) {\n" " return md5(`${str} ${MD5_SALT_KEY}`);\n" "};";
int init(){ rt = JS_NewRuntime(); ctx = JS_NewContext(rt); JS_Eval(ctx, JS_CODE, strlen(JS_CODE), "<sandbox>", 0); return 0;}
int eval(const char *str, int len, char *output){ JSValue value = JS_Eval(ctx, str, len, "<sandbox>", 0); const char *retstr = JS_ToCString(ctx, value); memcpy(output, retstr, strlen(retstr)); JS_FreeValue(ctx, value); return 0;}
int dispose(){ JS_FreeContext(ctx); JS_FreeRuntime(rt); return 0;}
复制代码


最后我们进行相关编译,然后使用如下代码进行相关的调用:

const datastr = "data";const buffer = intArrayFromString(datastr);const ptr = allocate(buffer, 'i8', ALLOC_NORMAL);
const output = Module._malloc(1024);Module._init();Module._eval(ptr, buffer.length, output);Module._dispose();console.log(UTF8ToString(output)); // output: data md5 salt key
复制代码


根据上一章的实践我们知道,由于我们的内部 JavaScript 代码是以字符串的方式直接进行呈现和编译的,因此如果我们对 Emscripten 编译生成 WASM 文件进行二进制查看的话,我们仍然能还原出我们的实际相关实现。同时根据我们的流程来看,我们首先需要将其 JavaScript 内容编译为 Opcode,然后再进行相关的嵌入才较为合理。要想得到 QuickJS 的 Opcode ByteStream 比较简单,直接通过 qjc 即可获得:

> cd quickjs> make all> ./qjsc -c index.js> cat out.c
复制代码


通过查找 out.c 我们可以看到 qjsc 为我们生成了如下的代码:

/* File generated automatically by the QuickJS compiler. */
#include <inttypes.h>
const uint32_t qjsc_index_size = 209;
const uint8_t qjsc_index[209] = { 0x02, 0x06, 0x18, 0x4d, 0x44, 0x35, 0x5f, 0x53, 0x41, 0x4c, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x06, 0x6d, 0x64, 0x35, 0x0c, 0x63, 0x72, 0x79, 0x70, // ......};
复制代码

其中的 qjsc_index 数组就是我们 JavaScript 代码对应的 Opcode ByteStream。接着我们修改我们的代码,然后调整执行的方法,其代码如下:

const uint8_t JS_CODE[209] = { 0x02, 0x06, 0x18, 0x4d, 0x44, 0x35, 0x5f, 0x53, 0x41, 0x4c, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x06, 0x6d, 0x64, 0x35, 0x0c, 0x63, 0x72, 0x79, 0x70, // ......};
int init(){ rt = JS_NewRuntime(); ctx = JS_NewContext(rt); JS_Eval(ctx, JS_CODE, 209, "<sandbox>", JS_EVAL_TYPE_MODULE); return 0;}
复制代码


最后我们同样进行整体编译,然后尝试相同调用,可以看到其执行正常。同时查看 WASM 文件我们已经无法顺利查看到内部执行 JavaScript 代码内容了(已变为 QuickJS 的 Opcode ByteStream)。

总结

在本章我们较为详细的介绍了前端加密及代码保护的困境,以及如何使用 WebAssembly 并结合 QuickJS 打造高性能安全沙盒的相关介绍,关于 QuickJS 的更进一步的内容可以参考其官方网站。在下一章中,我们将使用 WebAssembly 结合 WebGL 来进行一些图形相关的实践,同时对比其在此领域相比 JavaScript 的优劣势。


《WebAssembly 技术基础到实践》专题文章推荐:


WebAssembly如何演进成为“浏览器第二编程语言”?


从0开始快速上手WebAssembly:Emscripten使用入门

2020 年 11 月 16 日 15:253883

评论 4 条评论

发布
用户头像
emmm... quickjs 还得搞 CMakeLists 呀,没看到文档里有;另外 opcode uint8_t 写成了 uint8;调用方法好像也不是使用 JS_Eval... 真的跑通过么
2020 年 12 月 20 日 11:01
回复
uint8手误已改正,JS_Eval是存在的方法,头文件可以看到定义,CMakeList作者觉得比较简单就没提供
2020 年 12 月 21 日 14:08
回复
2020 年 12 月 21 日 14:09
回复
嗯嗯,这个方法的确存在,但是使用的时候发现其无法直接用于 opcode,而是需要使用 JS_ReadObject 和 JS_EvalFunction 才能执行 opcode。和文档有些出入,不知道是不是因为quickjs 版本的问题,我用的是 2020-11-08
2021 年 02 月 03 日 15:12
回复
没有更多了
发现更多内容

深度详解企业CRM系统,体验软件快速开发平台

Marilyn

敏捷开发 快速开发 CRM

第12周学习总结

Vincent

极客时间 极客大学

关注你自己,如同篮球巨星一样,让身体最佳化,持续投入最爱的事情。

叶小鍵

健康 科普 王立铭 肥胖

MySQL-技术专题-聚集索引和慢查询

浩宇天尚

第12周作业

Vincent

极客时间 极客大学

架构师的成长之路

华章IT

CTO 架构师 架构师之道

Servlet-技术专题-Servlet3异步原理与实践

浩宇天尚

最新版MySQL在MacOS上的安装与使用

王磊

MySQL

java安全编码指南之:ThreadPool的使用

程序那些事

java安全编码 java编码指南 java安全编码指南 java代码规范

分布式系统设计理念这么难学?

架构师修行之路

分布式 微服务

LAXCUS大数据集群操作系统:一个分布式分时共享E级系统软件(四)

陈泽云

人工智能 大数据 数据结构 操作系统 数据存储

APP 莫名崩溃,开始以为是 Header 中 name 大小写的锅,最后发现原来是容器的错!

程序员小航

Java bug Header携带签名 工作笔记 问题排查

sync-player:使用websocket实现异地同步播放视频

GoEasy消息推送

websocket 数据同步 实时通信

甲方日常 33

句子

工作 随笔杂谈 日常

spring-boot-route(二十)Spring Task实现简单定时任务

Java旅途

Java Spring Boot Spring Task

十八、深入Python函数

刘润森

Python

忘记MySQL密码怎么办?一招教你搞定!

王磊

MySQL

据说99.99%的人都会答错的类加载的问题

AI乔治

Java 架构 JVM 类加载 性能调优

计算机网络基础知识总结

cxuan

计算机网络 计算机

手把手带你玩转 openEuler | openEuler 的使用

openEuler

操作系统 openEuler

go-zero 如何应对海量定时/延迟任务?

万俊峰Kevin

定时任务 时间轮 microservice 延迟任务 Go 语言

MySQL-技术专题-联合索引最左前缀匹配原则

浩宇天尚

第19届亚运会门票采用区块链技术防伪

CECBC

区块链技术 防伪 溯源

微服务架构:基于微服务和Docker容器技术的PaaS云平台架构设计(微服务架构实施原理)

AI乔治

Java 架构 微服务 ,docker

黄金圈法则:成功者必备的深度思考方法

陆陆通通

黄金圈法则 厉害 牛逼

速度(Velocity)不背这个锅

BY林子

敏捷开发 估算与计划

iOS底层原理之—dyld与objc的关联

iOSer

ios开发 iOS Developer dyld objc

Java Reference核心原理分析

AI乔治

Java 架构 JVM 性能调优

帆软授权失效处理

Flychen

架构师第一期作业(第5周)

Cheer

作业

云原生在京东丨基于 Tekton 打造下一代云原生 CI 平台

京东科技开发者

ci 云原生 Tekton

从0开始快速上手WebAssembly:打造基于WASM的高性能安全沙盒-InfoQ