在 Web 环境下,浏览器提供了许多可供开发者使用的 API 和机制,这些机制在正常开发中发挥着至关重要的作用。但在某些场景下,它们也可能被用来进行隐蔽通信或数据的“暗度陈仓”。本文将从多个角度介绍这些方法,并给出示例代码与详细解释。

1. 基于存储机制

浏览器原生提供了多种数据存储方式,不同方式在数据容量、生命周期、作用域等方面有所差异,常见的包括:localStoragesessionStoragecookieIndexedDBCache API 等。

1.1 localStorage

数据存储在浏览器本地,生命周期默认永久(除非用户手动清理或程序清理)。某tfstk参数就是通过localStorage传递一些链路参数的。

// 写入数据 
localStorage.setItem('secretData', 'aHR0cHM6Ly8xOTk3LnBybw==');// Base64 编码后的秘密数据 

// 读取数据 
const secret = localStorage.getItem('secretData'); console.log(secret);

1.2 sessionStorage

localStorage 类似,但数据只在当前会话生效,关闭页面或浏览器标签后即失效。

sessionStorage.setItem('secretData', 'aHR0cHM6Ly8xOTk3LnBybw=='); 

const secretData= sessionStorage.getItem('secretData'); console.log('Session storage data:', secretData);

传统的跨请求存储机制,可设置过期时间、作用域等,也可用于服务器端通信。某isg、abck等都依赖于上一次的isg、abck。

// 设置 cookie 
document.cookie = "secretData=aHR0cHM6Ly8xOTk3LnBybw==; path=/; max-age=3600"; 

// 读取 cookie 
const cookies = document.cookie; 
console.log('All cookies:', cookies);

1.4 IndexedDB

浏览器内置的非关系型数据库,可存储大容量结构化数据。

// 打开数据库 
const request = indexedDB.open('mySecretDB', 1); request.onupgradeneeded = (event) => { 
	const db = event.target.result; 
	if (!db.objectStoreNames.contains('secrets')) {
		db.createObjectStore('secrets', {keyPath:'id'});
	}
}; 

request.onsuccess = (event) => { 
	const db = event.target.result; 
	const tx = db.transaction('secrets', 'readwrite');
	const store = tx.objectStore('secrets'); 
	store.put({
		id:'secretData',
		value:'aHR0cHM6Ly8xOTk3LnBybw=='
	});
};

1.5 Cache API + 自定义 Response

主要用于缓存静态文件,也可以缓存自定义的 Response 对象。

// 写入
caches.open('myCache').then((cache) => {
	const secretResponse = new Response(
		'aHR0cHM6Ly8xOTk3LnBybw==', 
		{
			headers: {'Content-Type': 'text/plain'},
		}
	);
	cache.put('/secretData', secretResponse); }); 

// 读取
caches.open('myCache').then(
	async (cache) => {
		const response = await cache.match('/secretData');
		if (response) {
			const secretData = await response.text(); 
			console.log('Hidden data in cache:', secretData); 
		}
	}
);

2. 基于线程tcp通信

iframe/Worker(包括各种Worker子类) + MessageChannel/BroadcastChannel/websocket

页面间、worker间、iframe间的通信(也可用于同页面),5s盾、kasada用于通信的主要技术,最近某231小版本也加入了postMessage的功能性检测。

2.1 MessageChannel:

<iframe id="secretData"></iframe>
<script>
  // 主线程
  const channel = new MessageChannel();
  channel.port1.onmessage = (e) => {
    console.log('Message received from iframe:', e.data);
  };
  const iframe = document.getElementById('secretData');
  iframe.contentWindow.postMessage('aHR0cHM6Ly8xOTk3LnBybw==', '*', [channel.port2]);
</script>

2.2 BroadcastChannel:

// 在页面A中
const channelA = new BroadcastChannel('secretData');

// 在页面B中接收
const channelB = new BroadcastChannel('secretData');
channelB.onmessage = (e) => {
  console.log('来自页面 B 的消息:', e.data); 
};

// 在页面A中发送
channelA.postMessage('aHR0cHM6Ly8xOTk3LnBybw==');

2.3 websocket

3. 基于 DOM 属性

3.1 dom标签属性修改

修改各种独有标签的valuetextclassName等值,在另一处读取相应值来做消息传递。

// 写入
const container = document.getElementById('secretData');
container.dataset.value = 'aHR0cHM6Ly8xOTk3LnBybw==';

// 读取
console.log('Value:', container.dataset.value);

3.2 img 标签的 src 参数(请求伪装)

某宝、某音日志上传都有用到。

const img = new Image();
img.src = "/upload_log?data=xxxxxxx"

4. 基于文件的数据隐写

4.1 Canvas隐写与动态绘制编码信息

基于LSB的隐写,可以用作数组隐藏传输,也可以用于canvas指纹识别,对抗指纹浏览器对toDataURL的修改来篡改canvas指纹(如adspower)。

        function encodeMessageInCanvas(message = 'aHR0cHM6Ly8xOTk3LnBybw==', width = 200, height = 100) {
            const canvas = document.createElement('canvas');
            canvas.width = width;
            canvas.height = height;
            const ctx = canvas.getContext('2d');
            ctx.fillStyle = '#f0f0f0';
            ctx.fillRect(0, 0, width, height);
            ctx.fillStyle = '#333';
            ctx.font = '30px Arial';
            ctx.fillText('Enjoy Life😊', 10, 40);
            ctx.font = '20px Times New Roman';
            ctx.fillText('It\'s a cavas fingerprint!', 10, 80);
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
            const data = imageData.data;
            const messageLength = message.length;
            for (let i = 0; i < 32; i++) {
                data[i * 4] = (data[i * 4] & 0xFE) | ((messageLength >> (31 - i)) & 1);
            }

            const binaryMessage = message
                .split('')
                .map(char => char.charCodeAt(0).toString(2).padStart(8, '0'))
                .join('');

            for (let i = 0; i < binaryMessage.length; i++) {
                data[(i + 32) * 4] = (data[(i + 32) * 4] & 0xFE) | parseInt(binaryMessage[i]);
            }

            ctx.putImageData(imageData, 0, 0);
            return canvas.toDataURL();
        }

        function decodeMessageFromCanvas(base64Data) {
            const img = new Image();
            img.src = base64Data;

            return new Promise((resolve) => {
                img.onload = () => {
                    const canvas = document.createElement('canvas');
                    canvas.width = 200;
                    canvas.height = 100;
                    const ctx = canvas.getContext('2d');
                    ctx.drawImage(img, 0, 0);

                    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
                    const data = imageData.data;

                    let messageLength = 0;
                    for (let i = 0; i < 32; i++) {
                        messageLength = (messageLength << 1) | (data[i * 4] & 1);
                    }

                    let binaryMessage = '';
                    for (let i = 0; i < messageLength * 8; i++) {
                        binaryMessage += (data[(i + 32) * 4] & 1).toString();
                    }

                    let message = '';
                    for (let i = 0; i < binaryMessage.length; i += 8) {
                        message += String.fromCharCode(parseInt(binaryMessage.slice(i, i + 8), 2));
                    }

                    resolve(message);
                };
            });
        }

    const base64Data = encodeMessageInCanvas();
    decodeMessageFromCanvas(base64Data).then(
        decodedMessage => {
            console.log(decodedMessage)
        }
    );

4.2 其它

以下原理其实都与图像隐写类似,通过细微的修改特定片段、特定帧或者特定区域的大小、颜色等,使肉眼难以分辨的同时可以通过程序解析出隐写的内容,代码略。

多媒体文件隐写(AudioVideo隐写)

SVG隐写与 path数据传输

CSS动画隐写与编码传输

GIF动态帧嵌入数据

Font 根据大小色差等隐写