百趣云 百趣云的博客

字体指纹与音频指纹:风控采集清单里的冷门杀器

字体指纹:你装了什么字体,你就是谁

设备上安装的字体列表是高度个性化的指纹:设计狮的电脑装满字体,普通用户是系统默认,虚拟机/云手机的字体列表则"干净"得可疑。

检测原理(侧面测量法):JS 不能直接枚举字体,但能"测量"——用不同字体渲染同一文本,测渲染尺寸。字体存在则尺寸变化,不存在则回退默认字体尺寸:

function detectFont(fontName) {
    var baseFonts = ['monospace', 'sans-serif', 'serif'];
    var testString = 'mmmmmmmmmmlli';
    var span = document.createElement('span');
    span.style.fontSize = '72px';
    span.textContent = testString;
    document.body.appendChild(span);

    var defaultWidth = {};
    baseFonts.forEach(function(base) {
        span.style.fontFamily = base;
        defaultWidth[base] = span.offsetWidth;
    });

    var detected = baseFonts.some(function(base) {
        span.style.fontFamily = "'" + fontName + "'," + base;
        return span.offsetWidth !== defaultWidth[base];  // 尺寸变了 = 字体存在
    });
    document.body.removeChild(span);
    return detected;
}

// 遍历几百个候选字体名,得到本机字体清单
var fontList = ['PingFang SC', 'Microsoft YaHei', 'SimSun', ...].filter(detectFont);

几百个候选字体测下来,字体清单的 hash 就是字体指纹。

音频指纹:AudioContext 的渲染特征

音频指纹利用 AudioContext 的离线渲染:同一段音频信号处理代码,不同设备的声卡/音频栈渲染结果有微小浮点差异:

function getAudioFingerprint(callback) {
    var ctx = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 44100, 44100);
    var oscillator = ctx.createOscillator();
    oscillator.type = 'triangle';
    oscillator.frequency.value = 10000;

    var compressor = ctx.createDynamicsCompressor();  // 压缩器参数放大差异
    oscillator.connect(compressor);
    compressor.connect(ctx.destination);
    oscillator.start(0);
    ctx.startRendering();

    ctx.oncomplete = function(e) {
        var buffer = e.renderedBuffer.getChannelData(0);
        // 取一段采样值求和,浮点尾数差异即指纹
        var sum = buffer.slice(4500, 5000).reduce(function(a, b) { return a + Math.abs(b); }, 0);
        callback(sum.toString());
    };
}

对抗方案

字体指纹

  1. 补环境场景:mock 掉测量相关的 DOM API。offsetWidth 返回什么由你控制——从真实浏览器采集一套"字体-尺寸"映射表,按表返回
  2. 浏览器自动化场景:字体列表由系统决定,多开实例要差异化就得真的装不同字体(虚拟机镜像预装不同字体包),或 hook 测量函数按种子返回不同尺寸

音频指纹

// hook OfflineAudioContext 的渲染结果,加噪声
const origStartRendering = OfflineAudioContext.prototype.startRendering;
OfflineAudioContext.prototype.startRendering = function() {
    return origStartRendering.apply(this, arguments).then(function(buffer) {
        var data = buffer.getChannelData(0);
        for (var i = 4500; i < 4600; i++) {
            data[i] += (Math.random() - 0.5) * 1e-7;  // 微噪声,人耳不可闻级
        }
        return buffer;
    });
};

噪声量级讲究:真实设备的音频指纹差异就在小数点后 6-7 位,噪声加在 1e-7 量级,既改变指纹又不至于被"差异过大"检测命中。

精髓:冷门指纹的真实价值

  1. 它们是"环境真实性"的交叉验证项。canvas/webgl 你伪造了,但字体列表为空(Node 环境没 DOM 测量能力)、音频指纹缺失——风控的评分模型里"缺失"本身就是负分
  2. 移动端 vs 桌面端的字体差异是系统级特征。Android 没有 PingFang SC,iOS 没有 Microsoft YaHei——字体列表和 ua 声称的平台矛盾,直接露馅。伪造时字体清单必须和 ua 平台匹配
  3. 检测成本决定使用频率。字体测量要操作 DOM 几百次(有性能开销),音频指纹是异步的——所以这两样通常在"风控等级升高时"才启用,作为二次验证。平时不采不代表不采,行为异常触发深度检测时它们就上场了

总结

  1. 字体指纹用侧面测量法(渲染尺寸差异),无法直接枚举
  2. 音频指纹用 OfflineAudioContext 渲染的浮点差异
  3. 对抗:补环境 mock 测量 API,自动化加微噪声(1e-7 量级)
  4. 字体清单必须与 ua 平台匹配,缺失本身是负分

交流微信:run1255

By 百趣云 阅读量:6 On