习时间:2020.06.06
学习章节:《你不知道的 Blob》
原文对 Blob 的知识点介绍得非常完整清晰,本文通过四个问题来总结本文核心知识:
Blob(Binary Large Object)表示二进制类型的大对象,通常是影像、声音或多媒体文件。MySql/Oracle数据库中,就有一种Blob类型,专门存放二进制数据。在 JavaScript 中 Blob 对象表示一个不可变、原始数据的类文件对象,它不一定非得是大量数据,也可以表示一个小型文件的内容。另外,JavaScript 中的 File 接口是基于 Blob,继承 Blob 的功能并将其扩展使其支持用户系统上的文件。
Blob 由一个可选字符串 type 和 blobParts 组成,其中, type 通常为 MIME 类型。
MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型,常见有:超文本标记语言文本 .html text/html 、PNG图像 .png image/png 、普通文本 .txt text/plain 等。
Blob 构造函数语法为:
const myBlob=new Blob(blobParts[, options])
入参:
出参:返回一个新创建的 Blob 对象,其内容由参数中给定的数组串联组成。
Blob 对象拥有 2 个属性:
注意:** Blob 对象是不可改变的**,但是可以进行分割,并创建出新的 Blob 对象,将它们混合到一个新的 Blob 中。类似于 JavaScript 字符串:我们无法更改字符串中的字符,但可以创建新的更正后的字符串。
let myBlobParts=['<html><h2>Hello Leo</h2></html>']; // 一个包含DOMString的数组
let myBlob=new Blob(myBlobParts, {type : 'text/html', endings: "transparent"}); // 得到 blob
console.log(myBlob.size + " bytes size");
// Output: 31 bytes size
console.log(myBlob.type + " is the type");
// Output: text/html is the type
JavaScript类型化数组是一种类似数组的对象,并提供了一种用于 访问原始二进制数据的机制 。并且在类型数组上调用 Array.isArray() 会返回 false 。详细可参考MDN《JavaScript 类型化数组》章节。
let hello=new Uint8Array([72, 101, 108, 108, 111]); // 二进制格式的 "hello"
let blob=new Blob([hello, ' ', 'leo'], {type: 'text/plain'});
// Output: "Hello leo"
由于 Blob 对象是不可改变的,但我们可以进行分割,并组装成一个新的 Blob 对象:
let blob1=new Blob(['<html><h2>Hello Leo</h2></html>'],
{type : 'text/html', endings: "transparent"});
let blob2=new Blob(['<html><h2>Happy Boy!</h2></html>'],
{type : 'text/html', endings: "transparent"});
let slice1=blob1.slice(16);
let slice2=blob2.slice(0, 16);
await slice1.text();
// currtent slice1 value: "Leo</h2></html>"
await slice2.text();
// currtent slice2 value: "<html><h2>Happy "
let newBlob=new Blob([slice2, slice1],
{type : 'text/html', endings: "transparent"});
await newBlob.text();
// currtent newBlob value: "<html><h2>Happy Leo</h2></html>"
这里整理 2 种图片本地预览的方式:
<body>
<h1>1.DataURL方式:</h1>
<input type="file" accept="image/*" onchange="selectFileForDataURL(event)">
<img id="output1">
<h1>2.Blob方式:</h1>
<input type="file" accept="image/*" onchange="selectFileForBlob(event)">
<img id="output2">
<script>
// 1.DataURL方式:
async function selectFileForDataURL() {
const reader=new FileReader();
reader.onload=function () {
const output=document.querySelector("#output1")
output.src=reader.result;
}
reader.readAsDataURL(event.target.files[0]);
}
//2.Blob方式:
async function selectFileForBlob(){
const reader=new FileReader();
const output=document.querySelector("#output2");
const imgUrl=window.URL.createObjectURL(event.target.files[0]);
output.src=imgUrl;
reader.onload=function(event){
window.URL.revokeObjectURL(imgUrl);
}
}
</script>
</body>
上面主要介绍 Blob URL 和 Data URL 两种方式实现图片本地预览,这两个类型的区别在**《五、拓展》**中介绍。
实现本地预览:将 input 获取到的 file 对象,通过实例化 FileReader ,赋值给变量 reader ,调用reader 的 readAsDataURL 方法,将 file 对象转换为 dataURL ,然后监听 reader 的 onload 属性,获取到读取结果 result ,然后设置为图片的 src 值。 实现分片上传:由于 File 是特殊类型的 Blob,可用于任意 Blob 类型的上下文,所以针对大文件传输,我们可以使用 slice 方法进行文件切割,分片上传。
<body>
<input type="file" accept="image/*" onchange="selectFile(event)">
<button onclick="upload()">上传</button>
<img id="output">
<script>
const chunkSize=10000;
const url="https://httpbin.org/post";
async function selectFile(){
// 本地预览
const reader=new FileReader();
reader.onload=function(){
const output=document.querySelector("#output")
output.src=reader.result;
}
reader.readAsDataURL(event.target.files[0]);
// 分片上传
await upload(event.target.files[0]);
}
async function upload(files){
const file=files;
for(let start=0; start < file.size; start +=chunkSize){
const chunk=file.slice(start, start + chunkSize + 1);
const fd=new FormData();
fd.append("data", chunk);
await fetch(url, { method: "post", body: fd }).then((res)=>{
console.log(res)
res.text();
});
}
}
</script>
</body>
<body>
<input type="file" accept="image/*" onchange="selectFile(event)">
<button onclick="upload()">上传</button>
<button onclick="pause()">暂停</button>
<button onclick="continues()">继续</button>
<img id="output" src="" alt="">
<script>
const chunkSize=30000;
let start=0, curFile, isPause=false;
const url="https://httpbin.org/post";
async function selectFile(){
const reader=new FileReader();
reader.onload=function(){
const output=document.querySelector("#output")
output.src=reader.result;
}
reader.readAsDataURL(event.target.files[0]);
curFile=event.target.files[0];
}
async function upload(){
const file=curFile;
for(start; start < file.size; start +=chunkSize){
if(isPause) return;
const chunk=file.slice(start, start + chunkSize + 1);
const fd=new FormData();
fd.append("data", chunk);
await fetch(url, { method: "post", body: fd }).then((res)=>{
res.text()
}
);
if(chunk.size < chunkSize){
uploadSuccess();
return;
}
}
}
function pause(){
isPause=true;
}
function continues(){
isPause=false;
upload();
}
function uploadSuccess(){
isPause=false;
start=0;
}
</script>
</body>
在实现“从互联网下载数据”方法时,我们使用 createObjectURL 显示图片,在请求互联网图片时,我们有两种方式:
<body>
<button onclick="download1()">使用 XMLHttpRequest 下载</button>
<button onclick="download2()">使用 fetch 下载</button>
<img id="pingan">
<script>
const url="http://images.pingan8787.com/TinyCompiler/111.png";
const pingan=document.querySelector('#pingan');
function download1 (){
const xhr=new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType='blob';
xhr.onload=()=> {
renderImage(xhr.response);
}
xhr.send(null);
}
function download2 (){
fetch(url).then(res=> {
return res.blob();
}).then(myBlob=> {
renderImage(myBlob);
})
}
function renderImage (data){
let objectURL=URL.createObjectURL(data);
pingan.src=objectURL;
// 根据业务需要手动调用 URL.revokeObjectURL(imgUrl)
}
</script>
</body>
通过调用 Blob 的构造函数来创建类型为 "text/plain" 的 Blob 对象,然后通过动态创建 a 标签来实现文件的下载。
<body>
<button onclick="download()">Blob 文件下载</button>
<script>
function download(){
const fileName="Blob文件.txt";
const myBlob=new Blob(["一文彻底掌握 Blob Web API"], { type: "text/plain" });
downloadFun(fileName, myBlob);
}
function downloadFun(fileName, blob){
const link=document.createElement("a");
link.href=URL.createObjectURL(blob);
link.download=fileName;
link.click();
link.remove();
URL.revokeObjectURL(link.href);
}
</script>
</body>
当我们希望本地图片在上传之前,先进行一定压缩,再提交,从而减少传输的数据量。在前端我们可以使用 Canvas 提供的 toDataURL() 方法来实现,该方法接收 type 和 encoderOptions 两个可选参数:
<body>
<input type="file" accept="image/*" onchange="loadFile(event)" />
<script>
// compress.js
const MAX_WIDTH=800; // 图片最大宽度
// 图片压缩方法
function compress(base64, quality, mimeType) {
let canvas=document.createElement("canvas");
let img=document.createElement("img");
img.crossOrigin="anonymous";
return new Promise((resolve, reject)=> {
img.src=base64;
img.onload=()=> {
let targetWidth, targetHeight;
if (img.width > MAX_WIDTH) {
targetWidth=MAX_WIDTH;
targetHeight=(img.height * MAX_WIDTH) / img.width;
} else {
targetWidth=img.width;
targetHeight=img.height;
}
canvas.width=targetWidth;
canvas.height=targetHeight;
let ctx=canvas.getContext("2d");
ctx.clearRect(0, 0, targetWidth, targetHeight); // 清除画布
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
let imageData=canvas.toDataURL(mimeType, quality / 100); // 设置图片质量
resolve(imageData);
};
});
}
// 为了进一步减少传输的数据量,我们可以把它转换为 Blob 对象
function dataUrlToBlob(base64, mimeType) {
let bytes=window.atob(base64.split(",")[1]);
let ab=new ArrayBuffer(bytes.length);
let ia=new Uint8Array(ab);
for (let i=0; i < bytes.length; i++) {
ia[i]=bytes.charCodeAt(i);
}
return new Blob([ab], { type: mimeType });
}
// 通过 AJAX 提交到服务器
function uploadFile(url, blob) {
let formData=new FormData();
let request=new XMLHttpRequest();
formData.append("image", blob);
request.open("POST", url, true);
request.send(formData);
}
function loadFile(event) {
const reader=new FileReader();
reader.onload=async function () {
let compressedDataURL=await compress(
reader.result,
90,
"image/jpeg"
);
let compressedImageBlob=dataUrlToBlob(compressedDataURL);
uploadFile("https://httpbin.org/post", compressedImageBlob);
};
reader.readAsDataURL(event.target.files[0]);
};
</script>
</body>
其实 Canvas 对象除了提供 toDataURL() 方法之外,它还提供了一个 toBlob() 方法,该方法的语法如下:
canvas.toBlob(callback, mimeType, qualityArgument)
和 toDataURL() 方法相比,toBlob() 方法是异步的,因此多了个 callback 参数,这个 callback 回调方法默认的第一个参数就是转换好的 blob文件信息。
在浏览器端,利用一些现成的开源库,比如 jsPDF,我们也可以方便地生成 PDF 文档。
<body>
<h3>客户端生成 PDF 示例</h3>
<script src="https://unpkg.com/jspdf@latest/dist/jspdf.min.js"></script>
<script>
(function generatePdf() {
const doc=new jsPDF();
doc.text("Hello semlinker!", 66, 88);
const blob=new Blob([doc.output()], { type: "application/pdf" });
blob.text().then((blobAsText)=> {
console.log(blobAsText);
});
})();
</script>
</body>
其实 jsPDF 除了支持纯文本之外,它也可以生成带图片的 PDF 文档,比如:
let imgData='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/...'
let doc=new jsPDF();
doc.setFontSize(40);
doc.text(35, 25, 'Paranyan loves jsPDF');
doc.addImage(imgData, 'JPEG', 15, 40, 180, 160);
ArrayBuffer 对象用于表示通用的,固定长度的原始二进制数据缓冲区。且不能直接操纵 ArrayBuffer 的内容,需要创建一个类型化数组对象或 DataView 对象,该对象以特定格式表示缓冲区,并使用该对象读取和写入缓冲区的内容。 Blob 类型的对象表示不可变的类似文件对象的原始数据。Blob 表示的不一定是 JavaScript 原生格式的数据。File 接口基于 Blob,继承了Blob 功能并将其扩展为支持用户系统上的文件。 Blob 类型只有 slice 方法,用于返回一个新的 Blob 对象,包含了源 Blob 对象中指定范围内的数据。 对比发现,ArrayBuffer 的数据,是可以按照字节去操作的,而 Blob 只能作为一个完整对象去处理。所以说,ArrayBuffer 相比 Blob 更接近真实的二进制,更底层。
只需将 ArrayBuffer 作为参数传入即可:
const buffer=new ArrayBuffer(16);
const blob=new Blob([buffer]);
需要借助 FileReader 对象:
const blob=new Blob([1,2,3,4,5]);
const reader=new FileReader();
reader.onload=function() {
console.log(this.result);
}
reader.readAsArrayBuffer(blob);
function GET(url, callback) {
let xhr=new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType='arraybuffer'; // or xhr.responseType="blob";
xhr.send();
xhr.onload=function(e) {
if (xhr.status !=200) {
alert("Unexpected status code " + xhr.status + " for " + url);
return false;
}
callback(new Uint8Array(xhr.response)); // or new Blob([xhr.response]);
};
}
Blob URL 格式如 blob:域名/uuid , Data URL 格式如: data:[<mediatype>][;base64],<data> 。mediatype 是个 MIME 类型的字符串,例如 "image/jpeg" 表示 JPEG 图像文件。如果被省略,则默认值为 text/plain;charset=US-ASCII。
Blob URL 一般长度较短,而 Data URL 因为直接存储图片 base64 编码后的数据,往往比较长。
Blob URL 可以很方便使用 XMLHttpRequest 获取源数据( xhr.responseType='blob' ),而 Data URL 并不是所有浏览器都支持通过 XMLHttpRequest 获取源数据的。
Blob URL 只能在当前应用内使用,把 Blob URL 复制到浏览器地址栏是无法获取数据,而 Data URL 则可以在任意浏览器中使用。
本文中我们主要通过 4 个问题来复习了 Blob 知识点:“Blob 是什么”、“Blob 怎么用”、“Blob 使用场景”和“Blob 与 ArrayBuffer 区别”,在“Blob 使用场景”部分中,也主要介绍了我们实际开发中非常常见的“图片预览”、“图片下载”和“生成文件”的场景。在文章最后,也通过和大家复习了“Blob URL 和 Data URL 区别”,让我们对 Blob 有更深的认识。
轮播图已经是一个很常见的东西,尤其是在各大App的首页顶部栏,经常会轮番显示不同的图片。
一提到轮播图如何实现时,很多人的第一反应就是使用Javascript的定时器,当然这种方法是可以实现的。不过就是有些繁琐,今天这篇文章我们来看看如何不用Javascript,而使用纯CSS代码去实现轮播图吧。
这篇文章的所有代码我都放在了github上,感兴趣的同学可以去看看。
https://github.com/zhouxiongking/article-pages/blob/master/articles/carousel/carousel.html
CSS
首先我们来看看只使用CSS实现的轮播图效果。
实现效果图
看到上述的实现效果后,我们来具体分析下页面的构成。
页面在布局上首先要有5张图片,图片固定宽度。
每张图片对应一个标题,标题通过ul>li实现,事先算好宽度,跟随图片一起滚动。
下边有个1,2,3,4,5表示图片顺序的索引,鼠标放上去后会显示对应的图片。
接下来我们通过代码层面去看看整个效果是如何实现的。
首先来看看HTML页面的实现,代码中都有每个区域的描述。
HTML页面
实现这个效果主要是通过CSS代码的,其代码量比较大,我们分开来看。
外层容器
对于最外层容器我们设置绝对定位,方便图片标题子元素的定位。
外层容器
图片标题
对于图片的标题我们也采用绝对定位,并且让标题横向一行展示,方便在动画的时候直接横向滚动。
得到的代码如下所示。
图片标题
图片与图片容器
接下来是设置图片容器属性以及图片的基本大小。
图片容器也采用绝对定位,宽度可以动态设置,根据图片数量计算。每张图片设置宽度与高度,得到的代码如下。
图片与图片容器
图片动画效果
然后设置图片的动画效果,对于任意的图片都有进入和静止两个状态,中间的效果可以任意定制。
在这里,中间效果设置成5%的间隔,其他时间在进行位置的切换,因为图片是处于水平分布,通过设置margin-left的值为负数进行偏移即可。
图片动画效果
数字索引的基本属性
对于下面的数字图标也是通过基本的CSS属性进行设置的,包括宽高,行高,透明度等等。
在鼠标移动到对应的数字上后,数字会显示不同的颜色。而且在鼠标停留在数字上后,动画效果会暂停。
数字索引基本属性
数字索引的偏移量
因为数字是水平方向展示的,因此要设定每个数字的水平偏移量。
数字水平偏移量
鼠标停在数字上的动画效果
然后就是处理鼠标停留在数字上的动画效果,因为每张图片对应特定的数字,需要计算出每次的动画开始位置和结束位置。
鼠标停在数字上的动画效果
动画效果赋予指定的数字
最后一步就是将定义的动画效果赋予指定的数字上,每个数字都有特定的id。得到的代码如下。
动画效果赋予指定的数字
至此所有步骤完成了,就可以得到文章开始的动画效果了。
这篇文章完全通过CSS实现了一个轮播图的效果,相比于使用JS来说减少页面阻塞程度,效果更好。
今天介绍一种走私技术,HTML Smuggling,这不是一种特别新的攻击方法,但是它确实在越来越多的攻击场景中出现,其中就包括网络钓鱼。接下来我们就来了解一下HTML走私的来龙去脉以及它在钓鱼中是怎样被利用的吧。
什么是HTML走私?HTML走私是指,浏览器根据HTML文件的内容在主机上创建恶意文件,而不是直接转发/下载恶意软件的一种技术。HTML走私可以通过在HTML页面中将恶意文件隐藏为编码后的“string”来绕过外围安全性检查和在线检测。
大多数周边/在线安全检测方法都是通过匹配文件中的某些特定模式。但在HTML走私的情况下,恶意文件被嵌在浏览器中,所以检测这样隐藏的代码是困难的。而且由于代码中的混淆,甚至检测HTML走私这种情况本身也很困难。
在去年也就是2020年的网络钓鱼调查中,我遇到了两种本质上不同的HTML走私方式。这两种方案都将数据存储在HTML文件中,并且提供了无需向服务器发送额外请求就可以“下载”数据的方法。此外,这两种方案都是基于HTML5的下载属性(虽然不是强制性的但是有好处)。
先介绍第一种方案,基于JavaScript Blob,也是我经常看到的方案。Blob对象表示一个不可变、原始数据的类文件对象。它的数据可以按照文本或二进制的格式进行读取,也可以转换为ReadableStream来读取。借助Blob,我们可以将我们的恶意文件存储在HTML代码中,然后将其置入浏览器中,而不是直接向Web服务器发送文件上传请求。
我们可以通过下面两个链接来创建我们的HTML页面:
https://gist.github.com/darmie/e39373ee0a0f62715f3d2381bc1f0974
https://developer.mozilla.org/en-US/docs/Web/API/Blob
要存储文件,首先,需要将文件编码为base64,可以在PowerShell中使用一下代码来实现:
$base64string=[Convert]::ToBase64String([IO.File]::ReadAllBytes($FileName))
然后替换下面html文件中<>所指的值。fileName变量表示你希望在默认情况下下载文件的名称。base64_encoded_file变量存储base64编码后的文件。
<html>
<body>
<script>
var fileName=<>
var base64_encoded_file=<>
function _base64ToArrayBuffer(base64,mimeType) {
var binary_string=window.atob(base64);
var len=binary_string.length;
var bytes=new Uint8Array( len );
for (var i=0; i < len; i++) {
bytes[i]=binary_string.charCodeAt(i);
}
return URL.createObjectURL(new Blob([bytes], {type: mimeType}))
}
var url=_base64ToArrayBuffer(base64_encoded_file,'octet/stream')
const link=document.createElement('a');
document.body.appendChild(link);
link.href=url;
link.download=fileName;
link.innerText='Download';
</script>
</body>
</html>
现在只要我们点击上面的链接,就会开始下载步骤。
实现HTML走私的另一种方案是使用DataURL。这种解决方案就不需要使用JavaScript了。使用DataURL进行HTML走私的目标就是将较小的文件嵌入HTML文件中。
虽然上面强调了小文件,但是实际上,限制也不是很严格。DataURL的最大长度由浏览器中的最大URL长度决定。比如,在Opera中,这个大小是65535个字符,虽然这个字节数对于转移1080p的电影来说是远远不够的,但是用来反弹shell已经足够了。
这种方法比第一种基于JS Blob的要简单得多。先看一下语法:
data::前缀;
[<mediatype>]:可选项,是一个MIME类型的字符串,比如我们最常见的image/jpeg表示JPEG图像,如果缺省,默认值为text/plain;charset=US-ASCII;
[;base64]:可选项,如果数据是base64编码的,那么就用;base64标记
,:将数据与控制信息分开;
<data>:数据本身。
<html>
<body>
<a href="data:text/x-powershell;base64,aXBjb25maWcNCmhvc3RuYW1l"
download="test.ps1">
Download
</a>
</body>
</html>
这是一个简单的HTML例子,加载该页面的时候会显示一个download按钮。我们点击下载,浏览器就会将test.ps1文件保存到我们的电脑上。而写入的内容自然就是aXBjb25maWcNCmhvc3RuYW1lbase64解码后的内容。
发现了一个比较有意思的事情。当讲将<a>标签中的download去掉后,也就是变成了下面的样子:
<html>
<body>
<a href="data:text/x-powershell;base64,aXBjb25maWcNCmhvc3RuYW1l" >
Download
</a>
</body>
</html>
我们重新在Chrome浏览器中打开并尝试下载,这个时候发现,这个按钮已经失效了,点击不会有任何反应。但是用Firefox打开,依旧可以下载文件,只不过firefox会创建一个随机的文件名,并且保存的文件不会有后缀:
综上,我们可以将DataURL和JS Blob相结合来添加一个函数,当然我们可以不适用任何脚本,但是和上面的例子中的问题一样,显然会更到更多的限制。
HTML走私是钓鱼攻击一个很好的补充。在HTML走私的帮助下,攻击者可以逃避一些检测和记录机制。在这种方式下,文件被“注入”到HTML文档中,而绝大多数的安全解决方案根本不会去检测。
当然HTML走私也有一些缺点。首先,它需要用户交互,虽然浏览器会自动处理恶意文件,但是下载到用户主机还是需要用户同意,除非配置了未经用户确认的自动下载。而且即使文件被下载了,还是需要将其执行。
上面介绍了两种常见的HTML走私方法,回到文章的主题,如何利用HTML走私来进行钓鱼呢?这里介绍两种比较常见的方法。首先,第一种是让电子邮件包含一个指向外部网页的链接,而该指向的网页使用了HTML走私技术。这是比较常见的邮件钓鱼方式。另一种方法是,在电子邮件中添加一个HTML附件,该HTML文件走私了恶意代码,这种情况下,用户打开的就是本地的HTML文件,不需要访问外部站点。
在这两种情况下,好处是恶意内容很难被检测到,因为恶意代码隐藏在HTML内容中。通过这种方式,可以绕过很多道安全防御。
首先,我们来看一下正常的文件下载步骤是怎样的。在下图的例子中,用户访问一个网站,然后下载一个exe文件:
1 . 首先,用户打开一个正常的网站。这个网站可以是用户自己输入的,也可以从包含链接的钓鱼邮件中获得。
2 . 浏览器向网站发送HTTP请求。如果存在代理,那么这个流量就会走代理。
3 . 代理会检查浏览器请求的URL。因为许多的代理都有检查域信誉的能力,如果代理认为请求的域是恶意的,那么Proxy不会进行后续的请求,通信将在这一步就停止;反之,代理会将请求转发给Web服务器。
4 . Web服务器返回请求的资源给代理,在本例中,Web服务器返回的就是一个普通的HTML页面。
5 . 代理检查Web服务器的响应。在这一步会进行各种过滤。代理可以阻止用户下载一些类型的文件。此外,代理可以有一个内置的沙箱来测试文件是否安全,而HTML文件通常不会再沙箱中进行测试就被允许通过代理转发给用户。
6 . 页面通过代理转发给浏览器。此时判断文件可信度的角色就变成了用户,用户可以决定浏览器中显示的某个文件是否安全,如果用户信任,就可以进行文件下载,在本例中,该文件是一个exe文件。
7 . 浏览器向Web服务器发送文件下载请求。
8 . 代理的处理和之前相同,这一步没什么区别。
9 . Web服务器将请求的exe文件发送给代理。
10 . 当代理获得该exe文件时候,它会对其进行综合性评估。如果设置了网络中禁止下载exe文件,那么显然,exe下载请求在这一步就结束了,文件将无法到达用户的机器。代理还会检查文件是否是已知的恶意文件。此外,它还可以决定是不是要将文件放到沙箱中进行测试。这里有一个关键点,当代理知道这个文件是一个可执行文件时候,它可以决定是不是要继续转发。而且,网络分路器(Network Taps)可以检测到这个exe文件,如果它们认为该文件是恶意的,也可以阻止用户下载该文件。
11 . 如果一切顺利,那么文件就到达了它的最终目的地——用户的电脑。
在上面的步骤中,最重要的一步就是第10步。这一步有普通文件下载和HTML走私下载的最大区别。在正常下载的情况下,exe文件易于检测和评估(比如检查散列,沙箱执行检查,收集头文件信息来判断等等)。而在HTML走私的情况下,文件不会以一种未编码的方式在网络上传播,它总是会被嵌入到另一个HTML文件中。
首先,先介绍一种很常见的走私技巧。在这种情况下,攻击者会将事先准备好的链接放入电子邮件中。该链接会指向包含走私文件的外部链接。在这种情况下,恶意的内容不会通过电子邮件的网关,而是以隐藏的形式,通过代理被用户访问。
1 . 有一个带着外部链接的e-mail向目标用户发送。
2 . 邮件网关对邮件进行检查。在这一步有很多种检查方法,但在本例的场景下,我们就认为邮件网关并不会阻拦该邮件,因为这里没有包含什么恶意文件。
3 . 用户点击邮件中的链接。
4 . 默认浏览器启动。
5 . 浏览器向存储(或是生成)HTML文档的外部站点发送HTTP请求。
6 . 代理会进行一些必要的检查,但是如果该URL并没有被加入黑名单,那么该请求就会被转发到该站点所对应的服务器。
7 . 服务器返回HTML页面以及隐藏在其中的exe文件。exe文件是以字符串的形式存储在文档中,此时它已经变成了HTML内容的一部分。
8 . 代理会检查响应内容。在本例中,响应只是一个HTML文件,代理不会去拦截HTML文件。而且,HTML文件在绝大多数情况下不会被转发到沙箱中执行检查。因而,隐藏在HTML文档中的恶意内容几乎不会在这里被检测到。当然通过其他的检测技术,它们也可能能识别到HTML走私技术(但是这里指的也只是HTML走私这件事本身,而不是隐藏在HTML中的恶意文件)。现在我们就认为代理没有识别出这个情况,将响应HTML页面转发给浏览器。
9 . 浏览器重新拼接嵌入在HTML页面中的字符串,然后启动下载进程。
10 . 最后恶意exe文件被存储到用户的电脑上。
我们可以看到,在这种情况下,e-mail的安全检查机制并不能起到太大的作用。因为电子邮件本身不是恶意的,其中的链接可以是合法的链接。
代理当然可以查看HTML文件,但是很少有安全性检测方案会对HTML文件进行彻底地处理。大多数情况下,这些文件都是被允许通过代理的。IDS解决方案还倾向于依赖检测文件扩展名或是magic bytes(主要是通过分析文件的第一个字节来判断文件的类型),因此文件中的编码字符串很难被它们捕获。但是其实我们可以创建一个模式匹配解决方案来检测一些隐藏在HTML中的东西。这在后面会提到。
这种方法是指包含走私的HTML文件被附加到了电子邮件中。
1 . 邮件网关收到一份带有HTML附件的电子邮件。
2 . 邮件网关会对电子邮件中附带的HTML进行检查。如果检查未通过,那么邮件网关会阻击电子邮件的后续转发或者选择删除附件。但是,现在还没有听说过什么电子邮件安全解决方案能够通过静态分析或者动态分析来检测HTML走私(即使是在HTML走私的是恶意代码。)
3 . 用户打开HTML附件。
4 . 浏览器打开并加载HTML文档。
5 . 浏览器处理HTML文件的内容,处理被走私的文件并将其提供给用户下载。
6 . 文件被保存到计算机上。当然,在这一步,要么需要用户的批准,要么需要浏览器设置了自动下载,就是未经确认的下载。
这种方法相比上一种的好处就在于,它不需要通过任何代理。而且,网络上的IDS/IPS安全检查通常不会去检查电子邮件,所以,如果攻击者能够成功欺骗电子邮件网关,那么在网络上就没有其他的难处了。
在这一小节中我们来探讨一下如何防御与检测HTML走私。
在发生了HTML走私的情况下,我们可以有两种检测方式,我们要么就关注HTML走私技术本身,要么可以尝试捕获隐藏在HTML文档中的恶意代码。
这里介绍几种防御方法。
虽然对于HTML走私没有什么万能的防御办法。禁用JavaScript看起来是一种可行的方案,毕竟一种HTML走私技术就是基于JavaScript Blob的,但是,在企业环境中禁用JS肯定会带来很多问题。当然,如果在你遇到的显示场景中,禁用JS是可行的,那么也可以尝试这种方法,那么在这个时候,你只需要用模式匹配去识别DataURL是不是存在HTML走私情况就好。
HTML走私的一种类型是使用HTML文件作为附件。不幸的是,这在很多场景中都是常见的,实际上,现在有很多带有HTML附件的合法的、与业务相关的电子邮件在增加。另一方面,不法分子也利用HTML附件来进行钓鱼。
如果你能在不影响业务的情况下阻止带有HTML附件的邮件,那么对于利用邮件中带有的HTML来进行钓鱼的路就被堵住了。那此时我们只需要关注邮件中附带的链接了。
我们可以看到,前面这两种防御方法都无法同时覆盖所有的HTML走私情况。在最好的情况下,我们可以使用这两种方案来防住部分HTML走私;在最坏的情况下,我们甚至都不能使用这些防御解决方案,因为这些方案在大多数现在企业环境中是不适用的。
Microsoft Defender Application Guard是Windows提供的一种安全解决方案,主要用于隔离各种试图使用来自不可信源资源的应用程序,后面简称MDAG。MDAG有两种不同的形式。第一种是Application Guard for Edge,它主要是用来隔离企业定义的不受信任(或不特别受信任)的站点,在企业的员工浏览Internet时保护公司。Application Guard通过在一个启用了Hyper-V的隔离容器中打开站点来实现这一点。而且,它还与Firefox和Chrome浏览器兼容。
另一个版本是Application Guard for Office。它能够保护可信资源不受不可信资源的攻击。它也是通过在一个容器中打开不受信任的Word、Excel、PowerPoint文档来实现。当然,这也和HTML走私无关,所以就不继续讨论了。
我们已经知道,Application Guard for Edge主要是通过在隔离的虚拟环境中打开不受信任的链接来保护我们的系统。如此一来,一个未知的URL,或者说是一个恶意的URL就不会对主机造成危害了。下载的恶意文件会被隔离存储在隔离的环境中,而不会直接与宿主机接触。
如果一个链接在电子邮件中到达,并且给定的URL并不在Application Guard的白名单中,那么它就会在一个隔离的Edge中被打开。因为对于该URL的访问是在隔离的环境中进行的,那么每个文件下载行为也都是在这个隔离的环境中进行的,因此被走私的恶意文件不会对主机造成真实伤害。
当然,这个解决方案也并不是十全十美的。它也有一些缺点:
1 . 为了使Edge能发挥作用,你需要使用白名单和untrust来标记一切网站。这在大公司中,意味着相关的管理员需要承受巨大的负载。因为用户会不断地进行异常请求。
2 . 虽然Application Guard能够防住以链接形式附在电子邮件中的HTML走私请求,但是它无法防御以电子邮件附件形式传递的HTML走私。这些附件到达用户主机后,会作为本地文件在浏览器中打开,因此,这些文件不会被当作不受信任的域来处理,不会在隔离的Edge容器中打开这些HTML文件,也就不会触发Application Guard。因此通过电子邮件附件进行的HTML走私在这种情况下依然可以成功。
3 . 会影响用户访问站点的速度,因为需要启动容器。
如果HTML文件中没有利用JS来走私,而是仅仅使用了DataURL,在这种情况下,我们可以使用模式匹配(比如匹配data:)来判断是不是发生了HTML走私。我们可以实用IDS或是简单的YARA工具来检测,比如github上的开源工具LaikaBOSS就是一款可用的检测工具。
KaikaBOSS对于下载的文件(HTML文件),甚至是电子邮件附带的文件,都非常有效。它可以检测大量的数据,伸缩性很好,所以即使在大型环境中,也同样可以派上用场。
如果使用了JavaScript,那么可能代码是被混淆过的,此时模式匹配可能就不太有效果了。我们只能用其他的方法来检测混淆的代码。
在使用了HTML走私的场景下,被下载的文件是在浏览器中创建的,而在一般的下载场景中,我们可以看到,是需要我们向Web服务器发起请求,然后Web服务器传送给我们的,而且还会经过代理,经由代理检查文件的安全性。因此我们还可以通过关注普通文件下载和基于HTML文件创建下载文件之间的区别来鉴别HTML走私。因此我们可以通过查看代理日志、观察是否有浏览器进行在主机上创建文件等方式来查看是不是发生了HTML走私。
这种方式的缺点在于:
1 . 许多流量在默认情况下不会通过代理。比如,一些公司会配置它们的网络,使其内部网站无法通过代理访问。这个时候,如果通过浏览器生成了一个文件,但是没有代理日志。这种情况经常发生,不应该基于这种情况创建警报。
2 . 在一些情况下,用户可以绕过代理,也不会产生代理日志。
3 . 可以在浏览器中打开本地文件,然后保存为新文件,这也是浏览器创建的文件,同样没有代理日志。
如果用户是在上面的情况下进行合法行为,我们都不应该进行拦截,因此难点就在于我们很难区分这是不是一种走私情况。
您可以将HTML文件发送到沙箱,然后使用沙箱中的浏览器打开它。不是通过互联网访问的HTML文件除了一些临时文件之外,不应该在机器上创建任何文件。理论上,如果在没有网络访问的情况下,HTML文件在沙箱中创建了文件,那么沙箱应该要发出一个警报来提醒用户。
但是,经验表明,HTML文件通常不会放到沙箱中打开。它们可以通过静态分析进行检查(如LaikaBoss),也可以动态分析进行检查,但是动态分析将花费更多的时间,并且会显著降低网络速度。
但是沙箱这个解决方案在大多数的情况下不是特别友好,因为在转发给用户之前打开每个HTML文件负载也挺大的。而且,如果恶意文件是需要用户点击下载的,那么这种方式也行不通,只有在页面配置了自动下载的情况下,我们才能通过沙箱来检测。所以总体来说,这也不是一个特别好的方法。
另一种方式是使用取证分析中可能会使用的技术——Zone.Identifier来区分是不是发生了HTML走私。
由HTML走私创建的文件的Zone.Identifier是这样的(在新版Edge、Chrome和Firefox测试过):
[ZoneTransfer]
ZoneId=3
HostUrl=about:internet
而正常下载的文件的Zone.Identifier则是(这在不同的浏览器之间存在区别):
[ZoneTransfer]
ZoneId=3
ReferrerUrl=https://www.sans.org/security-resources/posters/windows-forensic-analysis/170/download
HostUrl=https://www.sans.org/security-resources/posters/windows-forensic-analysis/170/download
这种方法的局限就在于,它只能在Windows(NTFS)上这样做。因此这种方式也不是完全可靠的。
Chrome会存储下载文件的来源,如果源是JavaScript Blob或是DataURL,那么这些信息就会作为源显示。
当我们打开一个本地的HTML被走私文件:
当然上面的信息不会告诉我们该被走私的恶意文件是从哪里下载的。
当我们访问远程的走私HTML文件时,我们就可以看到相应的恶意文件下载URL了,但是也仅仅是在走私文件是使用了JS Blob时能看到,在使用DataURL时则不会显示远程链接:
这种情况相比前面的几种方法看起来更加实用。在远程访问,并使用了JS Blob的情况下,我们甚至能看到恶意文件的真实来源,如果攻击者使用了DataURL,我们还可以利用这个信息重新生成文件,这在一些场景下可能也是有用的。
总体来说,目前想要完全检测和防御HTML走私是一件比较困难的事情。当然,上面介绍的方法也只是相对简单地介绍了HTML走私下的钓鱼事情,在现实攻击场景中,可能会遇到更复杂的情况。
*请认真填写需求信息,我们会在24小时内与您取得联系。