在与攻击者对抗的历程中,Web前端一直是非常薄弱的一环。浏览器毫无保留地把所有前端代码拉取到本地并执行、所有前端代码均透明可见。现在JS随着小程序和前端的兴起越来越火,收到了各个厂商的重视。如何保护前端JavaScript代码的安全? 最近自己正好在做JS的加密,分享一下自己的方案。
混淆工具有很多:YUI Compressor、UglifyJS、Google Closure Compiler,其中商业产品 有jscrambler等
2.加密
有些加密工具很有趣,比如aaencode:
加密前
alert("Hello, JavaScript")
加密后
゚ω゚ノ= /`m´)ノ ~┻━┻//*´∇`*/ ['_']; o=(゚ー゚)=_=3; c=(゚Θ゚) =(゚ー゚)-(゚ー゚); (゚Д゚) =(゚Θ゚)= (o^_^o)/ (o^_^o);(゚Д゚)={゚Θ゚:
'_' ,゚ω゚ノ : ((゚ω゚ノ==3) +'_') [゚Θ゚] ,゚ー゚ノ :(゚ω゚ノ+ '_')[o^_^o
-(゚Θ゚)]
,゚Д゚ノ:((゚ー゚==3) +'_')[゚ー゚] }; (゚Д゚) [゚Θ゚] =((゚ω゚ノ==3) +'_') [c^_^o];(゚Д゚) ['c'] =
((゚Д゚.....
再比如jjencode:
加密前
alert("Hello, JavaScript")
加密后
$=~[];$={___:++$,$$$$:(![]+“”)[$],__$:++$,$_$_:(![]+“”)[$],_$_:++$,$_$$:({}+“”)[$],$$_$:($[$]+“”)[$],_$$:++$,$$$_:(!“”+“”)[$],$__:++$,$_$:++$,$$__:({}+“”)[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+“”)[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+“”)[$.__$])+((!$)+“”)[$._$$]+($.__=$.$_[$.$$_])+($.$=(!“”+“”)[$.__$])+($._=(!“”+“”)[$._$_])+$.$_[$.$_$]+$.__…….
但是他们都无一例外膨胀到令人无法接受的程度,只好放弃。
或是使用base64加密
加密前
function a(e){/* ... */console.log(e.title)}a({title:\'buy\'})
加密后
eval(atob("ZnVuY3Rpb24gYShlKXsvKiAuLi4gKi9jb25zb2xlLmxvZyhlLnRpdGxlKX1hKHt0aXRsZTonYnV5J30p"));
使用Packer加密
加密前
function a(e){/* ... */console.log(e.title)}a({title:'buy'})
加密后
eval(function(p,a,c,k,e,r){e=String;if(!''.replace(/^/,String)){while(c--)r[c]=k[c]||c;k=[function(e){return
r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return
p}('3
0(1){4.5(1.2)}0({2:\'6\'})',7,7,'a|e|title|function|console|log|buy'.split('|'),0,{}))
先从uglify开始,它是github上点赞最多的JS解析引擎。它的安装容易出现各种问题,此时您需要google寻求答案(百度上的不是很全)。
刚开始做这件事时,半就被叫停了,因为据说我们组之前有人用uglify做过,出现了很多建荣祥问题,所以弃用。
于是我转战spidermonkey,他是火狐浏览器(firefox)自带的解析引擎。由C语言写成的。考虑到和现有系统的兼容,以及对指针操作的方便,我们选择C语言开发,而spidermonkey恰好是C语言开发的。
还是首先尝试使用它,使用时发现它的代码风格类似JNI,以下是我之前写的测试用例:
学习spidermonkey的过程大量借助google,官方手册是一个很好地学习途径。贴一个参考链接:
https://wiki.mozilla.org/JavaScript:SpiderMonkey:Parser_API
spidermonkey引擎可帮助我们在C语言的环境下执行JacaScrpt代码,我认为在物联网设备中可以发挥很大的用途。代码使用JNI编写,JNI和普通的C开发区别其实不大,用熟悉了总结出模板以后可以直接用。这段脚本还包含了一些我曾经测试过的痕迹,显示出我曾试图去理解构建语法树的过程和各个参数的含义。
当时我分析了JS的解析和执行过程,它先生成tokenstream,我找出了这个的API,并找出了输入tokenstream,输出语法树的API。然后就开始分析语法树的生成过程。但因为这棵树确实比较复杂,看了两三天都没什么头绪,试图修改别人方案实现自己目的的想法也宣告失败了。
这里只简单记录一下spidermonkey的安装和使用容易忘记的步骤:核心文件在src文件夹下,如果编译以后.o文件在Linux_All_DBG.OBJ里,这个文件夹要配个环境变量。然后,手工移动一下一个头文件
mv /opt/js/src/Linux_All_DBG.OBJ/jsautocfg.h /opt/js/src/
这个方面网上的教程写的很清楚,只需要注意环境变量要配置。具体可参考这篇博客
https://www.cnblogs.com/chenfool/p/3840625.html
0x03 自己学习语法树的过程(学习方法的提升)
走投无路之下,只好自己写语法树了。我买了一本专用于考研的《王道数据结构》,写的比较精炼,推荐给大家。
说起数据结构,由于我本人并不是计算机科班出身,之前并没有接收系统的训练,只能从0开始一点点写起来。我最开始是从顺序表开始的,自己实现了它的插入算法和删除算法。顺序表类似数组,但没有指针域的。写完以后就觉得其实还挺简单的,对于刚入门的初学者难度适中。
接着就开始写二叉树了,通过实现一个简答的二叉树,我了解了数据域,指针域怎么分配,树是如何创建的,记得当时只用了不到一天写完了,很有成就感
如何学习新的知识:原来我是看一点,敲一点代码,理解一点,全部都敲完以后,似乎都懂了,其实印象不深,而且再让你写一遍还写不出来。
这里和大家分享我的导师明姐教我的学习方法,我认为是我自个项目最大的收获:先看一遍,然后自己写,不会的再回头去学。也许刚看完有很多你不会的,但是不要慌张,或者认为自己写不出来,必须全理解了才去做事。遇到不清楚的,自己试着写代码,速度,印象成倍提高,说不定回头看,你写的比教材里的还好。
原来就是看一点敲一点理解一点,太慢了,自从使用新方法后效率成倍增加。感谢我的导师,明姐,这个方法就是她教我的。只要她在安全领域保持5年以上的专注,她以后一定会成为安全圈叱咤风云的人物。希望你一直牛逼下去。乘胜追击写了二叉树的生成,写了二叉树的前序遍历,二叉树的层序遍历,队列的层序遍历,可以说理解了递归思想,和它的使用方法。
0x04 方案介绍
现在开始就是本文的重点了,我们的语法树体现了以下点子和技术:
1、使用兄弟孩子表示法,这个从二叉树衍生出的,我们用了这种结构来构建自己的语法树。
2、 结点回溯技术。如果一个根结点开始,树生长完后怎样回到最开始的根结点呢?通过结点回溯。怎么回溯?生成树的时候在指针域里加一个指向前一结点的指针 CBNODE * parent,不停得往回找,直到找到第一个左结点(长子结点)不为空,然后他的右结点(兄弟结点)。挺自豪的,自己的原创
3、 独创 “前序深度遍历”的概念,可以将指定深度的函数一次性全部拿出来,然后执行加密。
4、 修改语法树的方法:采用了结点替换技术,稍后讲到
5、 如何把整个funtion全都拎出来加密呢?根据前序遍历 “根,左,右”的顺序, 只要找到funtion,从那个结点开始做一个前序遍历,函数就全都出来了,
6、 如何将文件切按照一定的规律切成字符串。这里面的切割技术也是我们自己的想法。
我们的数据域是这样的:
结点的数据域包含三个元素,id是反映了节点的创建顺序,data[]存储了单个字符串,depth则存储了函数的深度
结点的指针域中,有三个指针,firstchild是当前结点的长子结点,或叫左结点。nextsibling是当前结点的兄弟结点,或叫右结点。最后一个father指针指向上一个结点,即指向生成当前结点的那个节点。有人问为什么需要它,通过它,我们才实现了一个重要的功能:节点回溯。
如何生成树:
孩子兄弟表示法,是树型数据结构在的实际应用中用途最广的一种,
森林(图侵删)
森林转二叉树-孩子兄弟表示法 (图侵删)
首先设置一个CBNode型节点的指针,这个指针可以指向整棵树里当前我们试图操作的节点,它的作用类似于数组里的下标索引。 通过更新这个CBNode型指针的地址,始终让它指向当前结点。这样就可以:读取当前结点里的数据,或从当前结点的左节点或右结点开始开辟新内存空间,树得以延伸,生长。
语法树的生成规则:每次遇到{就将当前结点给长子结点开辟内存空间,在里面的数据域里填入数据{。每次遇到},就先把}放进当前结点的右结点里,然后通过节点回溯技术回到函数开始的地方。
结点的回溯。按照我们的设想,当一个function运行到最后时,需要回到这个function开始的地方,继续从他的孩子结点(右结点)进一步的延伸和生长。但是怎么回到原来开始的地方呢?我提出了“结点回溯”的概念:首先我们要在结点的指针域中引入“父指针”的指针,它始终要指向上一个生成它的结点。在生成语法树时就要完成这个操作。当我们遇到“}”符号时,我们就要通过这个父指针所形成的的单向链表,递归的往回追溯,直到回到这个function开始的地方。我没往回追溯一个结点,要看他的左结点是不是不为空,右结点是不是为空,因为往往function开头的节点,它的左节点一定不为空,二它的右结点一定为空。然而我们还得小心,当函数的深度增加后,你会发现满足上述条件的节点不止一个,如何避免和其他function的“}”去匹配,只匹配到本属于他的那个}就很重要。我们的回溯算法很好地解决了这个问题,如下图所示
我们之前还尝试过其他解决方法,比如想过要在做一个临时的结点,专门存贮下潜到下一层开始的那个结点,但是由于函数的深度是未知的,如果只开一个临时节点,将导致我们下潜到第k层时,第k-1层的开头结点会被覆盖,然后就回不去k-2,k-3..层了。除非我已经知道自己下潜到第几层,开辟多个临时节点,每当我深入下一层前就将上一层的入口存进来,但我必须知道我要开辟几个临时结点空间,这会带来临时节点的管理问题。不是不能做,也是一条思路,但是会让程序变复杂,不但开辟和释放变得复杂,而且可能还需要引入类似堆栈或者队列的数据结构,出于时间和空间复杂度的考虑,我就没这么做。
那么下一个问题是,
如何确定当前结点是的深度点呢?我提出了前序深度遍历的概念:第一步是先生成语法树嘛,在生成前,就要在结点的数据域添加data->depth,让每一结点都具有它的深度信息。接着
通过设置一个全局变量,
在合适的时候在语法树下潜时自增,语法树上探时自减,同时将这个全局变量赋值给
data->depth,使得每个节点都具有合适的深度。 第二步,整体要做一次前序遍历,将所有特定深度函数开始的结点就是所有以function开头的全都保存在一个CBNode类型的指针所构成的数组内部。由于之前有了深度信息,很方便就能筛选出特定深度的函数结点,并存入一个指针数组内。
第三步,由于我们在学习数的过程中,写过二叉树前序遍历算法,由于前序遍历“根,左,右”的顺序,只要把函数开头节点输入前序遍历,就会发现,整个函数都遍历出来了。得益于前期的训练和积累,我们才发现了这个有趣的现象并加以利用。
节点替换
如果我能把特定深度的某一个函数拎出来,接下来该干什么就不用说了。加密处理后的函数该如何塞回去呢?我们又一次提出了自己的理解:节点替换。这个过程和逆向中JMP到一款空白内存,写下patch代码后,再JMP回原来的代码继续执行的思想理念是一致的。 什么意思呢?对于一个有长子的节点,我们从他的上一节点开始遍历,就能把整个{}里的内容全部遍历出来。这些恰好“可能”是一个完整的函数(或许是一个if语句,但是不是一个function只要判断一下就知道了)。这就为我们加密函数提供了绝佳的条件。因为如果能把这个“根节点”和原来整棵树的联系彻底“斩断”,然后开辟一个新的节点,在新的节点里塞入加密后的内容后,重新和这棵树建立连接,而新节点的内容是整个加密后的内容,就成功完成了节点的“替换”:展示一部分替换过程
有人说:为什么不把旧结点的内容改掉,然后切断它一系列孩子节点的联系呢,这样也替换了啊。我当初的设想是尽可能多的保存原始的节点,只要能完整保存下来,说不定下个版本针对这些被“抛弃”的节点进行优化。
这两种方案,本质上都是是修改语法树的结构来实现加密的。
节点替换后,加密函数节点依然和依附在语法树上,树的完整性得以保留,只要我们从根节点开始前序遍历,就能一次性把所有的文件都展现出来了,接着就能写入文件了。
最后就是字符切割,也就是把整个JS文件按照我们的想法切割成一个一个小的字符串的过程。这部分需要我们写一个简化的此法分析器。
我想,因为ES5里没有引入符号·,就是数字1左边那个按键表示符号,就想用它做切割符号
为了准确切割,不能受正则表达式的干扰,注释干扰,等等。我花了很多时间在这方面,看下代码好了
int readJS(char *path, char *data[]) {
char *reciever = (char*)malloc(sizeof(char)*1024*1024*200);
struct stat StatInfo; //当前文件或目录的信息
if((stat(path,&StatInfo)) == -1)
{
Mylog_ERROR(__FILE__, __LINE__, "Can't Get File Info !!");
}
if(StatInfo.st_size/1024/1024> 1)
{
Mylog_ERROR(__FILE__, __LINE__, "Too Large To Parse !!");
//return 0;
}
FILE *pf = fopen(path, "rt");
if (NULL == pf) {
Mylog_ERROR(__FILE__, __LINE__, "Read JS file falied!!");
return 0;
}
printf("open js ok\n");
int length = 0;
int num = 0;
char ch='K';//需要初始化
reciever[0]='/';
reciever[1]='*';
reciever[2]='*';
reciever[3]='/';
length=4;
while (ch != EOF)
{
ch = fgetc(pf);
level1:
if(ch == '\'')
{
reciever[length] = ch;
length += 1;
eee:
ch = fgetc(pf);
while(ch != '\'')
{
reciever[length] = ch;
length += 1;
ch = fgetc(pf);
}
if (reciever[length - 1] == '\\' ) //上个字符是转译字符,当前字符是/
{
if(reciever[length - 2]== '\\' && reciever[length - 3] != '\\')
{
goto end1;
}
else
{
reciever[length] = ch;
length += 1;
goto eee;
}
}
end1:
reciever[length] = ch;
length += 1;
continue;
}
if(ch == '\"')
{
reciever[length] = ch;
length += 1;
fff:
ch = fgetc(pf);
while(ch != '\"')
{
reciever[length] = ch;
length += 1;
ch = fgetc(pf);
}
if (reciever[length - 1] == '\\' ) //上个字符是转译字符,当前字符是/
{
if(reciever[length - 2]== '\\'&& reciever[length - 3] != '\\')
{
goto end2;
}
else
{
reciever[length] = ch;
length += 1;
goto fff;
}
}
end2:
reciever[length] = ch;
length += 1;
continue;
}
if (ch == ' ')
{
reciever[length] = ch;
length += 1;
ch = fgetc(pf);
while(ch == ' ')
{
ch = fgetc(pf);
}
goto level1;
}
level2:
if (ch == '{') {
if(reciever[length-1]=='\a')
{
reciever[length] = ch;
length += 1;
reciever[length] = '\a';
length += 1;
continue;
}
reciever[length] = '\a';
length += 1;
reciever[length] = ch;
length += 1;
reciever[length] = '\a';
length += 1;
continue;
}
if (ch == '}') {
if(reciever[length-1]=='\a')
{
reciever[length] = ch;
length += 1;
reciever[length] = '\a';
length += 1;
continue;
}
reciever[length] = '\a';
length += 1;
reciever[length] = ch;
length += 1;
reciever[length] = '\a';
length += 1;
continue;
}
if (ch == '\t') //|| ch == '\v' || ch == '\n'|| ch == '\r'
{
reciever[length] = ' ';
length += 1;
continue;
}
// if (ch == '\n'|| ch == '\r')
// {
// int a =1;
// while (reciever[length - a] == ' ' || reciever[length - a] == '\n'|| reciever[length - a] == '\r'|| reciever[length - a] == '\t'||reciever[length - a] == '\a') { a+=1; }
// if(reciever[length - a] == '}')
// {
// reciever[length] = '\n';
// length += 1;
// continue;
// }
// reciever[length] = ' ';
// length += 1;
// continue;
// }
if (ch == '/')
{
ch = fgetc(pf);
int a = 1;
while (reciever[length - a] == ' ' || reciever[length - a] == '\n'|| reciever[length - a] == '\r') { a+=1; }
if ((reciever[length - a] == '=' && ch != '/'&& ch != '*') || (reciever[length - a] == '(' && ch != '/'&& ch != '*') || (reciever[length - a] == '&' && ch != '/'&& ch != '*')|| (reciever[length - a] == '|' && ch != '/'&& ch != '*')|| (reciever[length - a] == '!' && ch != '/'&& ch != '*')|| (reciever[length - a] == ':' && ch != '/'&& ch != '*')|| (reciever[length - a] == '[' && ch != '/'&& ch != '*')|| (reciever[length - a] == ',' && ch != '/'&& ch != '*')|| (reciever[length - a] == ';' && ch != '/'&& ch != '*'))
{
reciever[length] = '/';
length += 1;
ddd:
while (ch != '/')
{
reciever[length] = ch;
length += 1;
ch = fgetc(pf);
}
if (reciever[length-1] == '\\' && reciever[length-2] != '\\')
{
reciever[length] = ch;
length += 1;
ch = fgetc(pf);
goto ddd;
}
reciever[length] = ch;
length += 1;
continue;
}
if(ch == '*')
{
ch = fgetc(pf);
if(ch =='/')
{
continue;
}
else
{
bbb:
if (ch == '*')
{
ch = fgetc(pf);
if (ch == '/')
{
ch = fgetc(pf);
if (ch == EOF) { break; }
goto level1;
} else
{
goto bbb;
}
}
else
{
ch = fgetc(pf);
goto bbb;
}
}
}
if (ch == '=')
{
reciever[length] = '/';
length += 1;
reciever[length] = ch;
length += 1;
continue;
}
if(ch == '/')
{
aaa:
ch = fgetc(pf);
if(ch == '\r' || ch =='\n')
{
ch = fgetc(pf);
goto level1;
}
else if(ch ==EOF)
{
break;
}
{
goto aaa;
}
}
reciever[length] = '/';
length += 1;
goto level1;
}
reciever[length] = ch;
length += 1;
continue;
}
//从length-1开始需要用0填充,不填充的话length-1是一个?。
// printf("HHHHHHHHHHHHH%c\n",reciever[length - 1]);
// printf("HHHHHHHHHHHHH%c\n",reciever[length]);
// printf("HHHHHHHHHHHHH%c\n",reciever[length + 1]);
//加一个不影响语义的结束符,防止最后一个结点不存在
reciever[length - 1] = '/';
reciever[length] = '*';
reciever[length + 1] = '*';
reciever[length + 2] = '/';
char *tmp = NULL;
int i;
for (i = 0; i < strlen(reciever); i++) //i= seqList->length -1 ; i >=index ; i --
{
tmp = &reciever[i];
if (strncmp(tmp, "function", 8) == 0) {
int a = 1;
while (reciever[length - a] == ' ' || reciever[length - a] == '\n'|| reciever[length - a] == '\r') { a+=1; }
if(reciever[length - a] == '(')
{
memmove(&reciever[length - a + 1], &reciever[length - a], strlen(&reciever[length - a]));
reciever[length - a] = '\a';
i = i-a+2;
printf("warning:%c",reciever[i]);
tmp = NULL;
continue;
}
// if(reciever[length - a] == '=')
// {
// char * backwordtoken1 = 0;
// }
memmove(&reciever[i + 1], &reciever[i], strlen(&reciever[i]));
reciever[i] = '\a';
i += 2;
tmp = NULL;
continue;
}
// if(reciever[i]== ' ')
// {
// while(reciever[i+1] ==' ')
// {
// memmove(&reciever[i + 1], &reciever[i+2], strlen(&reciever[i+2]));
// //int end =;
// reciever[ i+strlen(&reciever[i+1])]='\0';
// }
// }
// if (reciever[i] == '\n'|| reciever[i] == '\r'|| reciever[i] == '\t' || reciever[i] == '\v')
// {
// reciever[length] = ' ';
// length += 1;
// }
tmp = NULL;
}
//Mylog_INFO(__FILE__, __LINE__, "Before splite the string is:");
//Mylog_INFO(__FILE__, __LINE__, reciever);
char *result;
result = strtok(reciever, "\a");
while (result != NULL) {
data[num] = result;
result = strtok(NULL, "\a");
num += 1;
}
fclose(pf);
return num;
}
这部分怎么说呢。。。很坑爹。。能不自己写词法分析的童鞋尽量不自写把
0x05 结语
随着版本的迭代,以后也许会把语法树做的更复杂,让加密/混淆的效果更好。愿天下无贼(其实我是研究逆向技术为主的,希望多点时间研究逆向技术)。
我们的下一步计划是
给语法树增加更多的细节和功能。目前我们实现了对某层function全部加密,下一步我们要给语法树加入花指令。
感觉实现一个功能其实是比较简单的,关键是你有没有自己的想法或者解决问题巧妙的途径。
最后感谢我的导师,也是我的战友明姐,感谢她的指导,前期的顶层设计全部是明姐提出并实现的,我是从创建语法树那个函数开始接手的。虽然不知道你会不会看到这个,只要在安全行业保持专注,凭借出众的能力和优秀的性格和人品,她总有一天成为安全圈叱咤风云的人物。
这棵语法树现在还只是一颗小树苗。但在我们的努力下,它会成为一颗参天大数。全部只是刚刚开始。