请问什么叫js逆向?
作者:卡卷网发布时间:2025-01-10 19:07浏览数量:87次评论数量:0次
We页面已经是大家非常熟悉的事物。在We开发过程中,后端主要负责程序架构设计和数据,而前端则负责页面的展示效果和用户交互。有一种不够严谨但通俗易懂的说法:<>“前端代码写给浏览器看,后端代码写给看。”>
对有开发经验的朋友来说,前后端交互的机制理解会更加深入。在当前主流的前后端分离开发模式下,前后端通过接口标准进行对接与整合。为了确保接口调用过程中的数据安全性,以及防止请求参数被篡改,大多数接口都会引入一些安全机制,如<>请求签名、身份认证、动态Cookie>等。此外,一些还会对返回数据进行加密处理,常见的加密方式包括AES、RSA等,或者在传输过程中对数据进行序列化(如Protouf)。这些技术点我们会在后续章节中详细讨论。
在接口设计中,<>请求签名>是一种非常常见的安全措施,如L中的加密参数sign
;<>身份认证>也有很多实例,如通过动态Cookie实现。生成这些参数的逻辑通常由JaScript代码控制。因此,如果我们想绕过浏览器环境,直接从接口获取数据,就需要深入调试并分析JaScript的调用逻辑,弄清楚中加密参数的生成过程。通过研究和还原加密参数的生成规则,这个过程通常被称为<>JaScript逆向>。
目前,我们总结了几种常见的加密参数逆向方法:
<>基于源码还原加密逻辑>:直接分析并实现加密算法;<>补环境复制代码进行模拟>:搭建完整的运行环境,将原有加密代码移植到新环境中运行;<>RPC远程调用>:通过远程调用的方式直接复用目标环境的加密逻辑。
在一些实际案例中,关键步骤是将浏览器运行环境移植到Node.js中。由于Node.js采用了V8引擎,与浏览器运行JaScript的引擎一致,因此可在一定程度上复用浏览器的加密逻辑。但需要注意的是,Node.js没有图形界面支持,因此浏览器中的一些特定API(如window
、nigator
、DOM作等)在Node环境中并不存在。因此,掌握Node.js环境的搭建以及补齐浏览器环境,是进行JaScript逆向的重要技能之一。
此外,<>Chrome浏览器>是JaScript逆向的核心工具。熟练使用Chrome控制台以及编写相关插件,基本可以满足大多数抓包、调试和Hook的需求。这部分内容也会在后续详细讲解。
第一章逆向基础
1.1语法基础
Js调试相对方便,通常只需要chrome或者一些抓包工具、扩展插件,就能顺利的完成逆向分析。但是Js的弱类型和语法多样,各种闭包,逗号表达式等语法让代码可读性变得不如语言顺畅。所以需要学习一下基础语法。
基本数据类型引用数据类型语句标识符算数运算符较运算符
在JaScript中将数字存储为64位浮点数,但所有按位运算都以32位二进制数执行。在执行位运算之前,JaScript将数字转换位32位有符号整数。执行按位作后,结果将转换回64位JaScript数。
Jascript函数
JaScript中的函数是头等公民,不仅可以像变量一样使用它,同时它还具有十分强大的抽象能力
定义函数的2种方式
在JaScript中,定义函数的方式如下:
functionas(x){
if(x>=0){
retnx;
}else{
retn-x;
}
}
上述as()函数的定义如下:
function指出这是一个函数定义;as是函数的名称;(x)括号内列出函数的参数,多个参数以,分隔;{……}之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。
请注意,函数体内部的语句在执行时,一旦执行到retn时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有retn语句,函数执行完毕后也会返回结果,只是结果为undefined。
由于JaScript的函数也是一个对象,上述定义的as()
函数实际上是一个函数对象,而函数名as
可以视为指向该函数的变量。
因此,第二种定义函数的方式如下:
varas=function(x){
if(x>=0){
retnx;
}else{
retn-x;
}
};
在这种方式下,function(x){……}是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量as,所以,通过变量as就可以调用该函数。
注意:上述两种定义完全等价,第二种方式按照完整语法需要在函数体末尾加一个;,表示赋值语句结束。(加不加都一样,如果按照语法完整性要求,需要加上)
调用函数时,按顺序传入参数即可:
as(10);//返回10
as(-9);//返回9
由于JaScript允许传入任意个参数而不影响调用,因此传入的参数定义的参数多也没有问题,虽然函数内部并不需要这些参数:
as(10,'lalala');//返回10
as(-9,'haha','hehe',null);//返回9
传入的参数定义的少也没有问题:
此时as(s)函数的参数x将收到undefined,计算结果为NaN。要避免收到undefined,可以对参数进行检查:
functionas(x){
if(typeofx!=='numer'){
throw'Notanumer';//停止并抛出错误信息
}
if(x>=0){
retnx;
}else{
retn-x;
}
}
Js中有一个被称为作用域的特性。作用域是在运行时代码中的某些特定部分中变量、函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和资源的可见性。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
Js的作用域分为三种:全局作用域、函数作用域、块级作用域。全局作用域可以让用户在任何位置进行调用,需要注意的是最外层函数和在最外层函数外面定义的变量拥有全局作用域,所有未定义直接赋值的变量会自动声明为拥有全局作用域,所有window对象的属性也拥有全局作用域。函数作用域也就是说只有在函数内部可以被访问,当然函数内部是可以访问全局作用域的。块级作用域则是在if和switch的条件语句或for和while的循环语句中,块级作用域可通过新增命令let和const声明,所声明的变量在指定的作用域外无法被访问。
<li>
<divonclick="xxx()">点击</div>
</li>动态绑定是指先获取到dom元素,然后在元素上绑定事件
<script>
varxx=document.getElementyId("lx");
xx.onclick=function(){}
</script>事件主要通过addEventListener()方法来实现
<script>
varxx=document.getElementyId("lx");
xx.addEventListener("click",function(){})
</script>ind()和on()绑定都是属于JQuery的事件绑定方法,ind()的事件函数只能针对已经存在的元素进行事件的设置
$("utton").ind("click",function(){
$("p").slideToggle();
});
$(document).ready(function(){
$("p").on("click",function(){});
});还有live()和delegate()等事件绑定方法,目前并不常用。
第二章浏览器控制台
首先介绍一下浏览器控制台的使用,以开发者使用最多的chrome为例。Windows作下的F12键可以打开控制台,mac作下用Fn+F12键打开。我们选择平时使用较多的模块进行介绍
2.1Network
Network是Js调试的重点,面板上由、过滤器、数据流概览、请求列表、数据计这五部分组成。
:PresserveLog是保留请求志的作用,在跳转页面的时候勾选上可以看到跳转前的请求。Disalecache是禁止缓存的作用,Offline是离线模拟。过滤器:根据规则过滤器请求列表的内容,可以选择XHR,JS,S,WS等。数据流概览:显示请求、响应的时间轴。请求列表:默认是按时间排序,可以看到浏览器所有的请求,主要用于网络请求的查看和分析,可以查看请求头、响应状态和内容、Form表单等。数据计:请求总数、总数据量、总花费时间等。
浏览器控制台Network下各项属性的含义
作用:
可以查看调取接口是否正确,后台返回的数据;查看请求状态、请求类型、请求
2.1.1Network-Headers
首先打开控制台,找到Network.刷新页面可以看到Name
Name对应的是资源的名称及路径,StatusCode是请求返回的状态码,一般情况下当状态码为200时,则表示接口匹配成功。点击任一文件名,右侧会出现Header选项。
RequestL:资源请求的lRequestMethod:请求方法(方法)StatusCode:状态码200(状态码)OK301-资源(网页等)被永久转移到其它L404-请求的资源(网页等)不存在500-内部错误(后台问题)RemoteAddress:远程;ReferrerPolicy:控制请求头中refrrer的内容包含值的情况:当一个用户点击页面中的一个链接,然后跳转到目标页面时,本变页面会收到一个信息,即用户是从哪个源链接跳转过来的。也就是说当你发起一个请求,请求头中的referrer字段就说明了你是从哪个页面发起该请求的;“”,空串默认按照浏览器的机制设置referrer的内容,默认情况下是和no-referrer-when-downgrade设置得一样“no-referrer”,不显示referrer的任何信息在请求头中“no-referrer-when-downgrade”,默认值。当从s跳转到或者请求其资源时(安全降级S→),不显示referrer的信息,情况(安全同级S→S,或者→)则在referrer中显示完整的源的L信息“same-origin”,表示浏览器只会显示referrer信息给同源,并且是完整的L信息。所谓同源,是协议、域名、端口都相同的“origin”,表示浏览器在referrer字段中只显示源的源(即协议、域名、端口),而不包括完整的路径“strict-origin”,该策略更为安全些,和origin策略相似,只是不允许referrer信息显示在从s到的请求中(安全降级)“origin-when-cross-origin”,当发请求给同源时,浏览器会在referrer中显示完整的L信息,发个非同源时,则只显示源(协议、域名、端口)“strict-origin-when-cross-origin”,和origin-when-cross-origin相似,只是不允许referrer信息显示在从s到的请求中(安全降级)“unsafe-l”浏览器总是会将完整的L信息显示在referrer字段中,无论请求发给任何补充:什么是referrer?Access-Control-Allow-Origin:请求头中允许设置的请求方法Connection:连接方式content-length:响应数据的数据长度,单位是ytecontent-type:客户端发送的类型及采用的编码方式Date:客户端请求服务端的时间Vary:用来指示缓存(例如squid)根据什么条件去缓存一个请求Last-Modified:服务端对该资源最后修改的时间:服务端的we服务端名Content-Encoding:gzip压缩编码类型Traner-Encoding:chunked:分块传递数据到客户端Accept:客户端能接收的资源类型Accept-Encoding:客户端能接收的压缩数据的类型Accept-Language:客户端接收的语言类型Cache-Control:no-cache服务端禁止客户端缓存页面数据Connection:keep-alive客户端和服务端的连接关系Cookie:客户端暂存服务端的信息Host:连接的目标主机和端口号Praa:no-cache服务端禁止客户端缓存页面数据Referer:来于哪里(即从哪个页面跳转过来的)User-Agent:客户端版本号的名字
2.2Soces
Soces按列分为三列,从左至右分别是文件列表区、当前文件区、断点调试区。
文件列表区中有Page、Snippets、FileSytem等。Page可以看到当前所在的文件位置,在Snippets中单击NewSnippets可以添加自定义的Js代码,FileSytem可以把本地的文件导入到chrome中。
当前文件区是需要重点作的区域,单击下方的{}来格式化代码,就能看到美观的Js代码,然后可以根据指定行数进行断点调试。
断点调试区也非常重要,每个作点都需要了解是什么作用。最上方的功能区分别是暂停、跳过、进入、跳出、步骤进入、禁用断点、异常断点。
Watch:变量,对加入列表的变量进行。
CallStack:断点的调用堆栈列表,完整地显示了导致代码被暂停的执行路径。
Scope:当前断点所在函数执行的作用域内容。
reakpoints:断点列表,将每个断点所在文件/行数/改成简略内容进行展示。
DOMreakpoints:DOM断点列表。
XHR/fetchreakpoints:对达到满足过滤条件的请求进行断点拦截。
EventListenerreakpoints:打开可的事件列表,可以在事件并且触发该事件时进入断点,调试器会停留在触发事件代码行。
2.3Application
Application是应用部分,主要记录加载的所有资源信息。包括存储数据(LocalStorage、SessionStorage、InDexedD、WeSQL、Cookies)、缓存数据、字体、图片、脚本、样式表等。LocalStorage(本地存储)和SessionStorage中可以查看和其存储的键值对。这里使用最多的是对Cookies的了,有时候调试需要清除Cookies,可以在Application的Cookies位置单击鼠标右键,选择Clear进行清除,或者根据Cookies中指定的Name和Value来进行清除,便于进一步调试。
注意:我们辨别Cookie来源时,可以看Only这一栏,有√的是来自于服务端,没有√的则是本地生成的。
2.4Console
谷歌控制台中的Console区域用于DOM元素、调试JaScript代码、查看HTML解析,一般是通过Console.log()来输出调试信息。在Console中也可以输出window、document、location等关键字查看浏览器环境,如果对某函数使用了断点,也可以在Console中调用该函数。
如果你平时只是用console.log()来输出一些变量的值,那你肯定还没有用过console的强大的功能。下面带你用console玩玩花式调试。
来看下主要的调试函数及用法:
console.log(),console.error(),console.warn(),console.()最基本也是最常用的用法了,分别表示输出普通信息、错误信息、警示信息和提示性信息,且error和warn方法有特定的图标和颜色标识。
以下是优化和美化后的内容,提升了排版清晰度、语法规范性和易读性:
示例:console.assert(expression,message)
functiongreaterThan(a,){console.assert(a>,{message:"aisnotgreaterthan",a,});}greaterThan(5,6);
console.count(lael)
functionlogin(name){console.count(name+"loggedin");}login("User1");login("User2");login("User1");
console.dir(oject)
console.dir(document.ody);
console.dirxml(oject)
console.dirxml(document.ody);
console.group([lael])
和console.groupEnd([lael])
console.log("Thisistheouterlevel");console.group("Group1");console.log("Level2");console.group("Group2");console.log("Level3");console.warn("Moreoflevel3");console.groupEnd();console.log("acktolevel2");console.groupEnd();console.log("acktotheouterlevel");
console.groupCollapsed(lael)
console.groupCollapsed("CollapsedGroup");console.log("Thisgroupiscollapsedydefault");console.groupEnd();
console.time([lael])
和console.timeEnd([lael])
console.time("ProcessTimer");for(leti=0;i<1000000;i++){//模拟耗时作}console.timeEnd("ProcessTimer");console.assert(expression,message)
console.time()
开始一个计时器。console.timeEnd()
结束计时器并输出总耗时。两个函数的lael
参数需要一致。lael
:计时器的名称,默认为"default"
(可选)。
<>参数>:<>功能>:<>示例>:与console.group()
相同,但默认分组是折叠状态。可以手动展开查看分组内容。
<>功能>:<>示例>:<>输出>ThisistheouterlevelLevel2Level3Moreoflevel3(warning)acktolevel2acktotheouterlevel
在控制台创建一个新的分组。所有输出的内容会添加一个缩进,表示属于当前分组。调用console.groupEnd()
后,结束当前分组。lael
:分组的标识符(可选)。
<>参数>:<>功能>:<>示例>:打印输出XML元素及其子元素。对HTML和XML元素调用console.dirxml()
和console.log()
是等价的。
<>功能>:<>示例>:控制台会显示document.ody
的DOM表达式及其所有属性和方法。
<>输出>:打印出对象的详细属性、函数和表达式信息。如果对象是HTML元素,则会打印其DOM属性。
oject
:要打印的对象。<>参数>:<>功能>:<>示例>:
<>输出>User1loggedin:1User2loggedin:1User1loggedin:2该函数用于计算并输出以特定标识符为参数的console.count
函数被调用的次数。
lael
:计算数量的标识符。<>参数>:<>功能>:<>示例>:
如果表达式a>
为false
,控制台会输出错误信息。错误信息包含提供的自定义message
和变量值。<>说明>
参数:expression:条件语句,语句会被解析成oolean,且为false的时候会触发message语句输出message:输出语句,可以是任意类型该函数会在expression为false的时候,在控制台输出一段语句,输出的内容就是传入的第二个参数message的内容。当我们在只需要在特定的情况下才输出语句的时候,可以使用console.assert再举几个例子:
console.time();
vararr=newArray(10000);
for(vari=0;i<arr.length;i++){
arr[i]=newOject();
}
console.timeEnd();//default:3.696044921875ms对console.time(lael)设置一个自定义的lael字段,并使用console.timeEnd(lael)设置相同的lael字段来结束计时器。
console.time('total');
vararr=newArray(10000);
for(vari=0;i<arr.length;i++){
arr[i]=newOject();
}
console.timeEnd('total');//total:3.696044921875ms
console.time('total');
console.time('initarr');
vararr=newArray(10000);
console.timeEnd('initarr');
for(vari=0;i<arr.length;i++){
arr[i]=newOject();
}
console.timeEnd('total');
//initarr:0.0546875ms
//total:2.5419921875ms
console.trace(oject)
第三章加密参数的定位方法
想要找到Js加密参数的生成过程,就必须要找到参数的位置,然后通过deug来进行观察调试。我们总结了目前通用的调试方式。每种方法都有其特的运用之道,大家只有灵活运用这些参数定位方法,才能更好地提高逆向效率。
3.1巧用搜索
搜索作较简单,打开控制台,通过快捷键Ctrl+F打开搜索框。在Network中的不同位置使用Ctrl+F会打开不同的搜索区域,有全局搜索、页面搜索。
另外关于搜索也有一定的技巧,如果加密参数的是signate,可以直接全局搜索signate,搜索不到可以尝试搜索sign或者搜索接口名。如果还没有找到位置,则可以使用下面几种方法。
3.2堆栈调试
控制台的Initiator堆栈调试是我们较喜欢的调试方式之一,不过新版本的谷歌浏览器才有,如果没有Initiator需要更新Chrome版本。Initiator主要是为了请求是怎样发起的,通过它可以快速定位到调用栈中。
具体使用方法是先确定请求的接口,然后进入Initiator,单击第一个Requestcallstack参数,进入Js文件后,在跳转行上打上断点,然后刷新页面等待调试。
3.3控制台调试
控制台的Console中可以由console.log()方法来执行某些函数,该方法对于开发调试很有帮助,有时通过输出会找起来更便捷。在断点到某一处时,可以通过console.log()输出此时的参数来查看状态和属性,console.log()方法在后面的参数还原中也很重要。
3.4XHR
XHR是XMLHttpRequest的简称,通过XHR的断点,可以匹配l中params参数的触发点和调用堆栈,另外post请求中FromData的参数也可以用XHR来拦截。
使用方法:打开控制台,单击Soces,右侧有一个XHR/fetchreakpoints,单击+号即可添加事件。像一些L中的_signate参数就很适合使用XHR断点。
3.5事件
这里其实和XHR有些相似,为了方便记忆,我们将其单放在一个小节中。
有的时候找不到参数位置,但是知道它的触发条件,此时可以使用事件进行断点,在Soces中有
DOMreakpoints、GloalListeners、EventListenerreakpoints都可以进行DOM事件。
如需要对Canvas进行断点,就在EventListenerreakpoints中选择Canvas,勾选Createcanvascontext时就是对创建canvas时的事件进行了断点。
3.6添加代码片
在控制台中添加代码片来完成Js代码注入,也是一种不错的方式。
使用方法:打开控制台,单击Soces,然后单击左侧的snippets,新建一个ScriptSnippet,就可以在空白区域编辑Js代码了。
3.7Hook
在Js中也需要用到Hook技术,例如当想分析某个cookie是如何生成时,如果想通过直接从代码里搜索该cookie的名称来找到生成逻辑,可能会需要审核非常多的代码。这个时候,如果能够用hookdocument.cookie的set方法,那么就可以通过打印当时的调用方法堆栈或者直接下断点来定位到该cookie的生成代码位置。
什么是hook?
在JS逆向中,我们通常把替换原函数的过程都称为Hook。一般使用Oject.defineProperty()来进行hook。
以下先用一段简单的代码理解Hook的过程:
functiona(){
console.log("I'ma.");
}
a=function(){
console.log("I'm.");
};
a()//I'm.直接覆盖原函数是以最简单的做法,以上代码将a函数进行了重写,再次调用a函数将会输出I’m.
如果还想执行原来a函数的内容,可以使用中间变量进行存储:
functiona(){
console.log("I'ma.");
}
varc=a;
a=function(){
console.log("I'm.");
};
a()//I'm.c()//I'ma.此时,调用a函数会输出I’m.,调用c函数会输出I’ma.
这种原函数直接覆盖的方法通常只用来进行临时调试,实用性不大,但是它能够帮助我们理解Hook的过程,在实际JS逆向过程中,我们会用到更加高级一点的方法,如Oject.defineProperty()。
Oject.defineProperty()
Oject.defineProperty(oj,prop,descriptor)oj:需要定义属性的当前对象prop:当前需要定义的属性名descriptor:属性描述符,可以取以下值;
属性描述符的取值通常为以下:
通常情况下,对象的定义与赋值是这样的:
varpeople={}
people.name="o"
people["age"]="18"
console.log(people)//{name:'o',age:'18'}使用defineProperty()方法:
varpeople={}
Oject.defineProperty(people,'name',{value:'o',writale:true//是否可以被重写})
console.log(people.name)//'o'people.name="Tom"console.log(people.name)//'Tom'我们一般hook使用的是get和set方法:
varpeople={name:'o',};
varcount=18;//定义一个age获取值时返回定义好的变量count
Oject.defineProperty(people,'age',{get:function(){console.log('获取值!');retncount;},
set:function(val){console.log('设置值!');count=val+1;},});console.log(people.age);
people.age=20;
console.log(people.age);输出:
通过这样的方法,我们就可以在设置某个值的时候,添加一些代码,如deugger;
让其断下,然后利用调用栈进行调试,找到参数加密、或者参数生成的地方,需要注意的是,加载时首先要运行我们的Hook代码,再运行自己的代码,才能够成功断下,这个过程我们可以称之为Hook代码的注入。
TamperMonkey注入
TamperMonkey俗称油猴插件,是一款免费的浏览器扩展和最为流行的用户脚本器,支持很多主流的浏览器,包括Chrome、MicrosoftEdge、Safari、Opera、Firefox、UC浏览器、360浏览器、QQ浏览器等等,基本上实现了脚本的一次编写,所有平台都能运行,可以说是基于浏览器的应用算是正的跨平台了。用户可以在GreasyFork、OpenUserJS等平台直接获取别人发布的脚本,功能众多且强大,如视频解析、去广告等。
来到谋奇异首页,可以看到cookie里面有个__dfp值:
我们想通过Hook的方式,让在生成__dfp的地方断下,就可以编写如下函数:
我们以某奇艺的cookie为例来演示如何编写TamperMonkey脚本,首先去应用商店安装TamperMonkey,安装过程不再赘述,然后点击图标,添加新脚本,或者点击面板,再点击加号新建脚本,写入以下代码:
(function(){
'usestrict';
varcookieTemp='';
Oject.defineProperty(document,'cookie',{
set:function(val){
if(val.indexOf('__dfp')!==-1){
deugger;//调试点
}
console.log('Hook捕获到cookie设置->',val);
cookieTemp=val;
retnval;
},
get:function(){
retncookieTemp;
},
});
})();
if(val.indexOf('__dfp')!=-1){deugger;}
的意思是检索__dfp
在字符串中首次出现的位置,等于-1表示这个字符串值没有出现,反之则出现。如果出现了,那么就deugger断下,这里要注意的是不能写成if(val=='__dfp'){deugger}
,因为val传过来的值类似于__dfp=xxxxxxxxxx
,这样写是无法断下的。
写入后进行保存
主体的JaScript自执行函数和前面的都是一样的,这里需要注意的是最前面的注释,每个选项都是有意义的,所有的选项参考TamperMonkey文档,以下列出了较常用、较重要的部分选项(其中需要特别注意@match、@include、@run-at)
清除cookie,开启TamperMonkey插件,再次来到某奇艺首页,可以成功被断下,也可以跟进调用栈来进一步分析__dfp值的来源。
第四章常见的压缩和混淆
在We发展早期,Js在We中承担的职责并不多,Js文件较简单,也不需要任何的保护。随着Js文件体积的增大和前后端交互增多,为了加快传输速度并提高接口的安全性,出现了很多的压缩工具和混淆加密工具。
代码混淆的本质是对于代码标识和结构的调整,从而达到不可读不可调用的目的,常用的混淆有字符串、变量名混淆,如把字符串转换成_0x,把变量重命名等,从结构的混淆包括控制流平坦化,虚假控制流和指令替换,代码加密主要有通过veal方法去执行字符串函数,通过escape()等方法编码字符串、通过转义字符加密代码、自定义加解密方法(RSA、ase64、AES、MD5等),或者通过一些开源的工具进行加密。
另外目前市面上较常见的混淆还有o混淆(ofuscator),特征是定义数组,数组位移。不仅Js中的变量名混淆,运行逻辑等也高度混淆,应对这种混淆可以使用已有的工具o-decrypt或者AST解混淆或者使用第三方提供的反混淆接口。大家平时可以多准备一些工具,在遇到无法识别的Js时,可以直接使用工具来反混淆和解密,当然逆向工作本身就很看运气。
例如翻看的Jascript源代码,可以发现很多压缩或者看不太懂的字符,如jascript文件名被编码,文件的内容被压缩成几行,变量被修改成单个字符或者一些十六进制的字符——这些导致我们无法轻易根据Jascript源代码找出某些接口的加密逻辑。
4.1JaScript压缩
这个非常简单,Jascript压缩即去除JaScript代码中不必要的空格、换行等内容或者把一些可能公用的代码进行处理实现共享,最后输出的结果都压缩为几行内容,代码的可读性变得很差,同时也能提高的加载速度。
如果仅仅是去除空格、换行这样的压缩方式,其实几乎是没有任何防护作用的,因为这种压缩方式仅仅是降低了代码的直接可读性。因为我们有一些格式化工具可以轻松将JaScirpt代码变得易读,如利用IDE、在线工具或Chrome浏览器都能还原格式化的代码。
这里举一个最简单的JaScript压缩示例。原来的JaScript代码是这样的:
functionecho(stringA,string){
constname="Germey";
alert("hello"+name);
}
压缩之后就变成这样子:
functionecho(d,c){
conste="Germey";
alert("hello"+e);
}可以看到,这里参数的名称都被简化了,代码中的空格也被去掉了,整个代码也被压缩成了一行,代码的整体可读性降低了。
目前主流的前端开发技术大多都会利用wepack、Rollup等工具进行打包。wepack、Rollup会对源代码进行编译和压缩,输出几个打包好的JaScript文件,其中我们可以看到输出的JaScript文件名带有一些不规则的字符串,同时文件内容可能只有几行,变量名都用一些简单字母表示。这其中就包含JaScript压缩技术,如一些公共的库输出成undle文件,一些调用逻辑压缩和转义成冗长的几行代码,这些都属于JaScript压缩,另外,其中也包含了一些很基础的JaScript混淆技术,如把变量名、方法名替换成一些简单的字符,降低代码的可读性。
但整体来说,JaScript压缩技术只能在很小的程度上起到防护作用,想要正的提高防护效果,还得依JaScript混淆和加密技术。
4.2JaScript混淆
JaScript混淆完全是在JaScript上面进行的处理,它的目的就是使得JaScript变得难以阅读和分析,大大降低代码的可读性,是一种很实用的JaScript保护方案。
JaScript混淆技术主要有以下几种。
变量名混淆:将带有含义的变量名、方法名、常量名随机变为无意义的类乱码字符串,降低代码的可读性,如转换成单个字符或十六进制字符串。字符串混淆:将字符串阵列化集中并可进行MD5或ase64加密存储,使代码中不出现明文字符串,这样可以避免使用全局搜索字符串的方式定位到入口。对象键名替换:针对JaScript对象的属性进行加密转化,隐代码之间的调用关系。控制流平坦化:打乱函数原有代码的执行流程及函数调用关系,使代码逻辑变得混乱无序。无用代码注入:随机在代码中不会被执行到的无用代码,进一步使代码看起来更加混乱。调试保护:基于调试器特征,对当前运行环境进行检查,加入一些deugger语句,使其在调试模式下难以顺利执行JaScript代码。多态变异:使JaScript代码每次被调用时,将代码自身立刻自动发生变异,变为与之前完全不同的代码,即功能完全不变,只是代码形式变异,以此杜绝代码被动态分析和调试。域名锁定:使JaScript代码只能在指定域名下执行。代码自我保护:如果对JaScript代码进行格式化,则无法执行,导致浏览器假死。特殊编码:将JaScript完全编码为人不可读的代码,如表情符号、特殊表示内容、等等。
总之,以上方案都是JaScript混淆的实现方式,可以在不同程度上保护JaScript代码。
在前端开发中,现在JaScript混淆的主流实现使jascript-ofuscator和terser这两个库。它们都能提供一些代码混淆功能,也都有对应的wepack和Rollup打包工具的插件。利用它们,我们可以非常方便地实现页面的混淆,最终输出压缩和混淆后的JaScript代码,使得JaScript代码的可读性大大降低。
下面我们以jascript-ofuscator为例来介绍一些代码混淆的实现,了解了实现,那么我们自然就对混淆的机制有了更加深刻的认识。
jascript-ofuscator的介绍内容如下:
链接:s://jascriptofuscator/
它是支持ES8的免费、高效的JaScript混淆库,可以使得JaScript代码经过混淆后难以被复制、盗用、混淆后的代码具有和原来的代码一模一样的功能。
首先我们需要安装好Node.js12.x及以上版本,确保可以正常使用npm命令。
具体的安装方式如下:
简单的说Node.js就是运行在服务端的JaScript。
Node.js是一个基于ChromeJaScript运行时建立的一个平台。
Node.js是一个事件驱动I/O服务端JaScript环境,基于Google的V8引擎,V8引擎执行Jascript的速度非常快,性能非常好。
如果你是一个前端程序员,你不懂得像PHP、Python或Ruy等动态编程语言,然后你想创建自己的服务,那么Node.js是一个非常好的选择。
Node.js是运行在服务端的JaScript,如果你熟悉Jascript,那么你将会很容易的学会Node.js
官网:s://nodejs.org/文档:s://nodejs.org/en/do/中文网:://nodejs/基础教程:s://runoo/nodejs/nodejs-tutorial.html
接着新建一个文件夹,如js-o然后进入该文件夹,初始化工作空间:
这里会提示我们输入一些信息,然后创建package.json文件,这就完成了项目的初始化了。
接下来,我们来安装jascript-ofuscator这个库:
npmi-Djascript-ofuscator稍等片刻,即可看到本地js-o文件下生成了一个node_modules文件夹,里面就包含了jascript-ofuscator这个库,这就说明安装成功了。
接下来,我们就可以编写代码来实现一个混淆样例了。如,新建main.js文件,其内容如下:
constcode=`letx='1'+1console.log('x',x)`
constoptions={compact:false,}
constofuscator=require('jascript-ofuscator')
functionofuscate(code,options){retnofuscator.ofuscate(code,options).getOfuscatedCode()}
console.log(ofuscate(code,options))这里我们定义了两个变量:一个是code,即需要被混淆的代码;另一个是混淆选项options,是一个Oject。接下来,我们引入了jascript-ofuscator这个库,然后定义了一个方法,给其传入code和options来获取混淆之后的代码,最后控制台输出混淆后的代码。
代码逻辑较简单,我们来执行一下代码:
nodemain.js
输出结果如下:
看到了吧,那么简单的代码,被我们混淆成了这个样子,其实这里我们就是设定了“控制流平坦化”选项。整体看来,代码的可读性大大降低了,JaScript调试的难度也大大加强了。
4.3jascript-ofuscator示例
下面的例子代码同上
4.3.1代码压缩
这里jascript-ofuscator也提供了代码压缩的功能,使用其参数compact即可完成JaScript代码的压缩,输出为一行内容。参数compact的默认值是true,如果定义为false,则混淆后的代分行显示。
如果不设置或者把compact设置为true,结果如下:
可以看到,单行显示的时候,对变量名进行了进一步的混淆,这里变量的命名都变成了十六进制形式的字符串,这是因为启用了一些默认压缩和混淆的方式。总之我们可以看到代码的可读性相之前大大降低。
4.3.2变量名混淆
变量名混淆可以通过在jascript-ofuscator中配置identifierNamesGenerator参数来实现。我们通过这个参数可以控制变量名混淆的方式,如将其设置为hexadecimal,则会将变量名替换为十六进制形式的字符串。该参数的取值如下。
hexadecimal:将变量名替换为十六进制形式的字符串,如0xac123。mangled:将变量名替换为普通的简写字符,如a,,c等。
该参数的默认值为:hexadecimal
我们将该参数改为:mangled来试一下:
constcode=`
letx='1'+1;
console.log('x',x);
`;
constoptions={
compact:true,
identifierNamesGenerator:'mangled'
};
constofuscator=require('jascript-ofuscator');
functionofuscate(code,options){
retnofuscator.ofuscate(code,options).getOfuscatedCode();
}
console.log(ofuscate(code,options));
运行结果如下:
另外,我们还可以通过设置identifiersPrefix参数来控制混淆后的变量前缀,示例如下:
constcode=`
letx='1'+1;
console.log('x',x);
`;
constoptions={
identifiersPrefix:'kk'
};
constofuscator=require('jascript-ofuscator');
functionofuscate(code,options){
retnofuscator.ofuscate(code,options).getOfuscatedCode();
}
console.log(ofuscate(code,options));
运行结果如下:
可以看到,混淆后的变量前缀加上了我们自定义的字符串kk。
另外,renameGloals这个参数还可以指定是否混淆全局变量和函数名称,默认值为false。示例如下:
constcode=`
var$=function(id){
retndocument.getElementyId(id);
};
`;
constoptions={
renameGloals:true
};
constofuscator=require('jascript-ofuscator');
functionofuscate(code,options){
retnofuscator.ofuscate(code,options).getOfuscatedCode();
}
console.log(ofuscate(code,options));
运行结果如下:
可以看到,这里我们声明了一个全局变量$,在renameGloals设置为true之后,这个变量也被替换了。如果后文用到了这个变量,可能就会有找不到定义的错误,因此这个参数可能导致代码执行不通。
如果我们不设置renameGloals或者将其设置为false,结果如下:
可以看到,最后还是有$的声明,其全局名称没有被改变。
4.3.3字符串混淆
字符串混淆,就是将一个字符串声明放到一个数组里面,使之无法被直接搜索到。这可以通过stringArray参数来控制,默认为true。
此外,我们还可以通过rotateStringArray参数来控制数组化后结果的元素顺序,默认为true。
示例如下:
constcode=`
vara='helloworld';
`;
constoptions={
stringArray:true,
rotateStringArray:true
};
constofuscator=require('jascript-ofuscator');
functionofuscate(code,options){
retnofuscator.ofuscate(code,options).getOfuscatedCode();
}
console.log(ofuscate(code,options));
运行结果如下:
另外,我们还可以使用unicodeEscapeSequence这个参数对字符串进行Unicode转码,使之更加难以辨认,示例如下:
constcode=`
vara='helloworld';
`;
constoptions={
compact:false,
unicodeEscapeSequence:true
};
constofuscator=require('jascript-ofuscator');
functionofuscate(code,options){
retnofuscator.ofuscate(code,options).getOfuscatedCode();
}
console.log(ofuscate(code,options));
可以看到,这里字符串被数字化和Unicode化,非常难以辨认。
在很多JaScript逆向过程中,一些关键的字符串可能会作为切入点来查找加密入口,用了这种混淆之后,如果有人想通过全局搜索的方式搜索hello这样的字符串找加密入口,也就没法搜到了。
4.3.4代码自我保护
我们可以通过设置selfDefending参数来开启代码自我保护功能。开启之后混淆后的JaScript会强制以一行显示。如果我们将混淆后的代码进行格式化或者重命名,该段代码将无法执行。
示例如下:
constcode=`
console.log('helloworld');
`;
constoptions={
selfDefending:true
};
constofuscator=require('jascript-ofuscator');
functionofuscate(code,options){
retnofuscator.ofuscate(code,options).getOfuscatedCode();
}
console.log(ofuscate(code,options));
如果我们将上述代码放到控制台,它的执行结果和之前是一模一样的,没有任何问题。
如果我们将其进行格式化,然后粘贴到浏览器控制台里面,浏览器会直接卡死无法运行。这样如果有人对代码进行了格式化,就无常对代码进行运行和调试,从而起到了保护作用。
4.3.5控制流平坦化
控制流平坦化其实就是将代码的执行逻辑混淆,使其变得复杂、难度。其基本的思想是将一些逻辑处理块都一加上一个前驱逻辑块,每个逻辑块都由前驱逻辑块进行条件判断个分发,构成一个个闭环逻辑,这导致整个执行逻辑十分复杂、难度。
如说这里有一段示例代码:
console.log(c);
console.log(a);
console.log();代码逻辑一目了然,一次在控制台输出了c,a,三个变量的值。但是如果把这段代码进行控制流平坦化处理,代码就会变成这样:
constcode=`
console.log(c);
console.log(a);
console.log();
`;
constoptions={
compact:false,
controlFlowFlattening:true
};
constofuscator=require('jascript-ofuscator');
functionofuscate(code,options){
retnofuscator.ofuscate(code,options).getOfuscatedCode();
}
console.log(ofuscate(code,options));
使用控制流平坦化可以使得执行逻辑更加复杂、难读,目前非常多的前端混淆都会加上这个选项。但启用控制流平坦化之后,代码的执行时间会变长。
4.3.6无用代码注入
无用代码即不会被执行的代码或对上下文没有任何影响的代码,注入之后可以对现有的JaScript代码的阅读形成干扰。我们可以使用deadCodeInjection参数开启这个选项,其默认值为false。
示例:
constcode=`
console.log(c);
console.log(a);
console.log();
`;
constoptions={
compact:false,
deadCodeInjection:true
};
constofuscator=require('jascript-ofuscator');
functionofuscate(code,options){
retnofuscator.ofuscate(code,options).getOfuscatedCode();
}
console.log(ofuscate(code,options));
这种混淆方式通过混入一些特殊的判断条件并加入一些不会被执行的代码,可以对代码起到一定的干扰作用。
4.3.7对象键名替换
如果是一个对象,可以使用tranormOjectKeys来对对象的键值进行替换,示例如下:
constcode=`
(function(){
varoject={
foo:'1',
ar:{
az:'2'
}
};
})();
`;
constoptions={
compact:false,
tranormOjectKeys:true
};
constofuscator=require('jascript-ofuscator');
functionofuscate(code,options){
retnofuscator.ofuscate(code,options).getOfuscatedCode();
}
可以看到,Oject的变量名被替换为了特殊的变量,代码的可读性变差,这样我们就不好直接通过变量名进行搜寻了,这也可以起到一定的防护作用。
4.3.8禁用控制台输出
我们可以使用disaleConsoleOutput来禁用掉console.log输出功能,加大调试难度,示例如下:
constcode=`
console.log('helloworld');
`;
constoptions={
disaleConsoleOutput:true
};
constofuscator=require('jascript-ofuscator');
functionofuscate(code,options){
retnofuscator.ofuscate(code,options).getOfuscatedCode();
}
console.log(ofuscate(code,options));
此时,我们如果执行这段代码,发现是没有任何输出的,这里实际上就是将console的一些功能禁用了。
4.3.9调试保护
我们知道,如果Jascript代码中加入关键字deugger关键字,那么执行到该位置的时候,就会进入断点调试模式。如果在代码多个位置都加入deugger关键字,或者定义某个逻辑来反复执行deugger,就会不断进入断点调试模式,原本的代码就无法顺畅执行了。这个过程可以称为调试保护,即通过反复执行deugger来使得原来的代码无法顺畅执行。
其效果类似于执行了如下代码:
setInterval(()=>{
deugger;
},3000);
如果我们把这段代码粘贴到控制台,它就会反复执行deugger语句,进入断点调试模式,从而干扰正常的调试流程。在jascript-ofuscator中,我们可以使用deugProtection来启用调试保护机制,还可以使用deugProtectionInterval来启用无限调试(deug),使得代码在调试过程中不断进入断点模式,无法顺畅执行。配置如下:
constoptions={
deugProtection:true,
};混淆后的代跳到deugger代码的位置,使得整个代码无法顺畅执行,对JaScript代码的调试形成干扰。
4.3.10域名锁定
我们还可以通过控制domainLock来控制JaScript代码只能在特定域名下运行,这样就可以降低代码被模拟或者盗用的风险。
示例如下:
constcode=`
console.log('helloworld');
`;
constoptions={
domainLock:['kk']
};
constofuscator=require('jascript-ofuscator');
functionofuscate(code,options){
retnofuscator.ofuscate(code,options).getOfuscatedCode();
}
console.log(ofuscate(code,options));
这里我们使用domainLock指定了一个域名://kk,也就是设置了一个域名白名单,混淆后的代码结果如下:
这段代码就只能在指定的域名://kk下运行,不能在运行。这样的话,如果一些相关JaScript代码被单剥离出来,想在运行或者使用程序模拟运行的话,运行结果只有失败,这样就可以有效降低代码被模拟或盗用的风险。
4.3.11特殊编码
另外,还有一些特殊的工具包(jjencode,aaencode等)他们可以对代码进行混淆和编码。
链接:s://sojson
示例如下:
使用jjencode工具的结果:
使用aaencode工具的结果:
可以看到,通过这些工具,原本非常简单的代码被转化为一些几乎完全不可读的代码,但实际上运行效果还是相同的。这些混淆方式较另类,看起来虽然没有什么头绪,但实际上找到规律是非常好还原的,并没有正达到强力混淆的效果。
关于这种混淆代码的方法,一般直接复制到控制台运行或者用工具进行转换,如果运行失败,就需要按分号分割语句,逐行调试分析源码。
以上便是对JaScript混淆方式的介绍和总结。总的来说经过混淆的JaScript代码的可读性大大降低,同时其防护效果也大大增强。
第五章常见的编码和加密
我们在爬取的时候,会遇到一些需要分析接口或L信息的情况,这时会有各种各样的类似加密的情形。
某个的L带有一些看不太懂的长串加密参数,要抓取就得必须懂得这些参数是怎么构造的,否则我们连完整的L都构造不出来,更不用说爬取了。在分析某个的Ajax接口时,可以看到接口的一些参数也是加密的,RequestHeaders里面也可能带有一些加密参数,如果不知道这些参数的具体构造逻辑,就没法直接用程序来模拟这些Ajax请求。
常见的编码有ase64、unicode、lencode编码,加密有MD5、SHA1、HMAC、DES、RSA等。
本节简单介绍一下常见的编码加密,同时附上Python实现加密的方法。
由于内容太多,剩下2w+字包含高阶和案例讲解请移步到公众号查看。谢谢合作。
://weixin./r/7A2TEtknJrVC793sz(二维码自动识别)
END
免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。
请记住:卡卷网 Www.Kajuan.Net
广告位

你 发表评论:
欢迎