图标相似度筛选在渗透攻防中的应用:感知哈希算法与直方图对比分析

时间:2024-10-09 16:45:48

在网络安全的渗透攻防过程中,影子资产的收集与分析是信息搜集阶段的关键任务之一。影子资产包括未被公开标记或遗留在互联网上的服务器、网站等,它们常常成为攻击者的突破口。为了提高影子资产的识别效率,安全研究者往往会借助资产测绘平台进行大范围的搜索。但这些搜索结果中,常常包含大量不相关的资产,因此对这些资产进行精细筛选显得尤为重要。本文将结合图标相似度筛选,深入探讨两种常见算法——感知哈希算法(pHash)与直方图相似度算法的原理、应用。

1. 影子资产与图标相似度

在收集影子资产时,网站的图标(favicon)是一项常被利用的特征。通过图标的相似度判断,安全人员可以快速筛选出相关性较高的网站。例如,若目标网站的图标与某些搜索结果中的图标高度相似,可能表明这些资产隶属于同一组织。因此,图标相似度的计算成为了筛选资产中的重要一步。

通常在图标相似度的判断中有两种较为广泛的技术路径:

  • 基于算法的相似度计算:如感知哈希算法(Perceptual Hashing, pHash)或直方图匹配算法,它们直接通过图像本身的特征进行计算。
  • 基于深度学习的神经网络模型:这种方法可以通过大量训练数据构建神经网络来判断图像相似度,但其复杂度和计算资源需求较高,且对训练数据质量依赖较大。

本文聚焦于第一类方法,即纯算法的实现,具体分析感知哈希和直方图算法。

2. 感知哈希算法 (Perceptual Hashing, pHash)

感知哈希算法的核心思想是将图像进行处理,得到一个相对固定长度的“哈希值”,该哈希值可以用于快速对比两张图片的相似度。感知哈希算法不同于传统的加密哈希(如MD5或SHA-1),它不追求唯一性,而是为了保留图片的视觉信息,使得相似的图片会生成相似的哈希值。pHash 具体的步骤如下:

  1. 缩放图片:将图片缩放为固定大小(如32x32),以减少计算量,同时保留关键视觉信息。

  2. 灰度处理:将彩色图像转换为灰度图像,去除颜色对比的干扰。

  3. 离散余弦变换(DCT):对灰度图像进行DCT变换,提取频域信息。低频部分保留了图像的主要结构和特征,而高频部分往往与细节噪声相关。

  4. 生成哈希值:从DCT变换的结果中,取其低频部分的平均值,并将每个像素值与该均值比较,生成一个二进制字符串(哈希值)。

  5. 汉明距离判断相似度:对比两张图片的哈希值,计算汉明距离(即两个二进制字符串不同位的数量)。距离越小,图片的相似度越高。

以下是感知哈希算法的Java实现:

package com.potato.potatotool.content.redTeam.infoGathering.imgSimilarity;

import java.awt.Graphics2D;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;

import javax.imageio.ImageIO;

/**
 * @author Potato
 * @desc 图片感知哈希算法(pHash)通过计算图片的感知哈希值并比较哈希值之间的汉明距离来判断图片相似度
 */
public class ImgPHsh {
    private int size = 32;            // 默认DCT处理的图像大小为32x32
    private int smallerSize = 8;      // 默认保留的DCT较低频部分为8x8
    private double[] c;               // DCT系数数组
    private ColorConvertOp colorConvert = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);

    /**
     * 构造方法,初始化DCT系数
     */
    public ImgPHsh() {
        initCoefficients();
    }

    /**
     * 构造方法,允许自定义图像大小和保留的低频区域大小
     * @param size 图像大小
     * @param smallerSize 保留的低频区域大小
     */
    public ImgPHsh(int size, int smallerSize) {
        this.size = size;
        this.smallerSize = smallerSize;
        initCoefficients();
    }

    /**
     * 初始化DCT系数
     */
    private void initCoefficients() {
        c = new double[size];
        for (int i = 1; i < size; i++) {
            c[i] = 1;
        }
        c[0] = 1 / Math.sqrt(2.0);
    }

    /**
     * 计算汉明距离,用于比较两个图片的pHash值
     * @param s1 第一个哈希字符串
     * @param s2 第二个哈希字符串
     * @return 汉明距离(值越小,相似度越高)
     */
    private int calculateHammingDistance(String s1, String s2) {
        int distance = 0;
        for (int k = 0; k < s1.length(); k++) {
            if (s1.charAt(k) != s2.charAt(k)) {
                distance++;
            }
        }
        return distance;
    }

    /**
     * 生成图片的pHash值
     * @param is 输入图片流
     * @return 图片的pHash值(二进制字符串)
     * @throws Exception 处理图像时可能抛出的异常
     */
    private String getHash(InputStream is) throws Exception {
        BufferedImage img = ImageIO.read(is);

        // 步骤1:调整图像尺寸为size x size(默认为32x32)
        img = resize(img, size, size);

        // 步骤2:将图像转为灰度图
        img = grayscale(img);

        // 步骤3:获取图像的DCT值
        double[][] dctValues = calculateDCT(img);

        // 步骤4:仅保留左上角的8x8低频DCT值
        // 步骤5:计算均值(排除[0,0]元素)
        double avg = calculateDCTAverage(dctValues);

        // 步骤6:生成二进制哈希字符串
        return generateHash(dctValues, avg);
    }

    /**
     * 调整图片尺寸
     * @param image 原图像
     * @param width 目标宽度
     * @param height 目标高度
     * @return 调整后的图像
     */
    private BufferedImage resize(BufferedImage image, int width, int height) {
        BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = resizedImage.createGraphics();
        g.drawImage(image, 0, 0, width, height, null);
        g.dispose();
        return resizedImage;
    }

    /**
     * 将图像转为灰度图
     * @param img 原图像
     * @return 灰度图像
     */
    private BufferedImage grayscale(BufferedImage img) {
        colorConvert.filter(img, img);
        return img;
    }

    /**
     * 计算DCT转换后的值
     * @param img 灰度图像
     * @return DCT值矩阵
     */
    private double[][] calculateDCT(BufferedImage img) {
        double[][] pixelValues = new double[size][size];

        // 获取图像像素值
        for (int x = 0; x < img.getWidth(); x++) {
            for (int y = 0; y < img.getHeight(); y++) {
                pixelValues[x][y] = getBlue(img, x, y);
            }
        }

        // 应用DCT转换
        return applyDCT(pixelValues);
    }

    /**
     * 获取图像中某像素点的蓝色值(灰度图中只有蓝色通道有值)
     * @param img 图像
     * @param x 像素点x坐标
     * @param y 像素点y坐标
     * @return 该像素点的蓝色值
     */
    private static int getBlue(BufferedImage img, int x, int y) {
        return img.getRGB(x, y) & 0xff;
    }

    /**
     * 计算DCT的平均值,排除[0,0]位置
     * @param dctValues DCT值矩阵
     * @return 平均值
     */
    private double calculateDCTAverage(double[][] dctValues) {
        double total = 0;
        for (int x = 0; x < smallerSize; x++) {
            for (int y = 0; y < smallerSize; y++) {
                total += dctValues[x][y];
            }
        }
        total -= dctValues[0][0]; // 排除DC分量
        return total / ((smallerSize * smallerSize) - 1);
    }

    /**
     * 生成pHash值(二进制字符串)
     * @param dctValues DCT值矩阵
     * @param avg DCT均值
     * @return 二进制哈希字符串
     */
    private String generateHash(double[][] dctValues, double avg) {
        StringBuilder hash = new StringBuilder();
        for (int x = 0; x < smallerSize; x++) {
            for (int y = 0; y < smallerSize; y++) {
                if (x != 0 || y != 0) {
                    hash.append(dctValues[x][y] > avg ? "1" : "0");
                }
            }
        }
        return hash.toString();
    }

    /**
     * 计算两个图片之间的相似度(汉明距离)
     * @param srcFile 源图像文件
     * @param canFile 候选图像文件
     * @return 汉明距离
     * @throws Exception 处理图像时可能抛出的异常
     */
    public int calculateImageDistance(File srcFile, File canFile) throws Exception {
        String srcHash = getHash(new FileInputStream(srcFile));
        String canHash = getHash(new FileInputStream(canFile));
        return calculateHammingDistance(srcHash, canHash);
    }

    /**
     * 计算两个图片之间的相似度(汉明距离)
     * @param srcUrl 源图像URL
     * @param canUrl 候选图像URL
     * @return 汉明距离
     * @throws Exception 处理图像时可能抛出的异常
     */
    public int calculateImageDistance(URL srcUrl, URL canUrl) throws Exception {
        String srcHash = getHash(srcUrl.openStream());
        String canHash = getHash(canUrl.openStream());
        return calculateHammingDistance(srcHash, canHash);
    }

    /**
     * 离散余弦变换(DCT)算法
     * @param pixelValues 图像像素值矩阵
     * @return DCT值矩阵
     */
    private double[][] applyDCT(double[][] pixelValues) {
        int N = size;
        double[][] DCT = new double[N][N];

        for (int u = 0; u < N; u++) {
            for (int v = 0; v < N; v++) {
                double sum = 0.0;
                for (int i = 0; i < N; i++) {
                    for (int j = 0; j < N; j++) {
                        sum += Math.cos(((2 * i + 1) / (2.0 * N)) * u * Math.PI) *
                                Math.cos(((2 * j + 1) / (2.0 * N)) * v * Math.PI) *
                                pixelValues[i][j];
                    }
                }
                sum *= ((c[u] * c[v]) / 4.0);
                DCT[u][v] = sum;
            }
        }
        return DCT;
    }
}

pHash 在对图片进行相似度判断时具有以下优点:

  • 高效:生成和比较哈希值的过程非常快速,适合大量图片的快速筛选。
  • 鲁棒性:pHash 对图片的缩放、旋转等轻微变动具有较强的抗干扰能力。
  • 简洁性:相比于复杂的神经网络,pHash 算法易于理解和实现。

3. 直方图相似度算法

另一种常用的图片相似度计算方法是基于图像直方图的比较。图像的直方图是一个表示不同灰度级或颜色值的像素分布的图像统计信息。其基本思想是,若两幅图片的直方图分布相似,则图片内容也很可能相似。

直方图算法的主要步骤如下:

  1. 将图片分为颜色通道:通常将图片拆分为RGB三个通道,分别计算各通道的像素分布。

  2. 计算直方图:统计每个通道中每种颜色值的像素数,生成直方图。

  3. 归一化:为了使不同大小的图片可以进行对比,通常需要对直方图进行归一化处理。

  4. 比较直方图:使用欧氏距离或巴氏距离等方式,计算两张图片直方图之间的差异。距离越小,图片的相似度越高。

下面是一个直方图相似度计算的示例代码:

package com.potato.potatotool.content.redTeam.infoGathering.imgSimilarity;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;

import javax.imageio.ImageIO;

/**
 * @author Potato
 * @desc 该类用于计算和比较图像的直方图相似度(使用巴氏系数)
 */
public class ImgHistogram {
    // 颜色最大值255,用于归一化计算
    private static final int COLOR_MAX_VALUE = 255;
    // 红、绿、蓝通道的bin数量
    private int redBins;
    private int greenBins;
    private int blueBins;

    // 默认构造函数,初始化bin数量为4
    public ImgHistogram() {
        this(4, 4, 4); // 默认使用4个bin
    }

    // 可以自定义红、绿、蓝bin数量的构造函数
    public ImgHistogram(int redBins, int greenBins, int blueBins) {
        this.redBins = redBins;
        this.greenBins = greenBins;
        this.blueBins = blueBins;
    }

    /**
     * 计算图像的直方图数据
     * @param image 输入的BufferedImage对象
     * @return 归一化的直方图数据
     */
    private float[] calculateHistogram(BufferedImage image) {
        int width = image.getWidth();
        int height = image.getHeight();
        int[] pixels = new int[width * height];
        float[] histogramData = new float[redBins * greenBins * blueBins];

        // 获取图像的RGB像素数据
        getRGB(image, 0, 0, width, height, pixels);
        float totalPixels = 0;

        // 遍历图像中的每个像素,计算直方图
        for (int row = 0; row < height; row++) {
            for (int col = 0; col < width; col++) {
                int index = row * width + col;
                int red = (pixels[index] >> 16) & 0xff;   // 提取红色分量
                int green = (pixels[index] >> 8) & 0xff;  // 提取绿色分量
                int blue = pixels[index] & 0xff;          // 提取蓝色分量

                // 计算每个分量所在的bin索引
                int redIdx = getBinIndex(redBins, red);
                int greenIdx = getBinIndex(greenBins, green);
                int blueIdx = getBinIndex(blueBins, blue);

                // 计算该像素在直方图数组中的位置
                int histogramIndex = redIdx + greenIdx * redBins + blueIdx * redBins * greenBins;
                histogramData[histogramIndex] += 1;
                totalPixels += 1;
            }
        }

        // 将直方图数据归一化
        for (int i = 0; i < histogramData.length; i++) {
            histogramData[i] /= totalPixels;
        }

        return histogramData;
    }

    /**
     * 将颜色值映射到相应的bin上
     * @param binCount bin的数量
     * @param color 当前的颜色值
     * @return 颜色所在的bin索引
     */
    private int getBinIndex(int binCount, int color) {
        int binIndex = (color * binCount) / COLOR_MAX_VALUE;
        return binIndex >= binCount ? binCount - 1 : binIndex;
    }

    /**
     * 获取图像的RGB值数组
     * @param image 输入的BufferedImage对象
     * @param x 开始的x坐标
     * @param y 开始的y坐标
     * @param width 宽度
     * @param height 高度
     * @param pixels 用于存储RGB值的像素数组
     * @return 填充了RGB值的像素数组
     */
    private int[] getRGB(BufferedImage image, int x, int y, int width, int height, int[] pixels) {
        int type = image.getType();
        if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB) {
            return (int[]) image.getRaster().getDataElements(x, y, width, height, pixels);
        }
        return image.getRGB(x, y, width, height, pixels, 0, width);
    }

    /**
     * 计算两张图片直方图的巴氏系数(Bhattacharyya Coefficient)
     * @param histogram1 图像1的直方图
     * @param histogram2 图像2的直方图
     * @return 返回巴氏系数,0到1之间,1表示完全相同
     */
    private double calculateSimilarity(float[] histogram1, float[] histogram2) {
        double similarity = 0;
        for (int i = 0; i < histogram1.length; i++) {
            similarity += Math.sqrt(histogram1[i] * histogram2[i]);
        }
        return similarity;
    }

    /**
     * 比较两张图片文件的相似度
     * @param srcFile 源图片文件
     * @param canFile 候选图片文件
     * @return 两张图片的相似度
     * @throws IOException 文件读取异常
     */
    public double match(File srcFile, File canFile) throws IOException {
        BufferedImage srcImage