在网络安全的渗透攻防过程中,影子资产的收集与分析是信息搜集阶段的关键任务之一。影子资产包括未被公开标记或遗留在互联网上的服务器、网站等,它们常常成为攻击者的突破口。为了提高影子资产的识别效率,安全研究者往往会借助资产测绘平台进行大范围的搜索。但这些搜索结果中,常常包含大量不相关的资产,因此对这些资产进行精细筛选显得尤为重要。本文将结合图标相似度筛选,深入探讨两种常见算法——感知哈希算法(pHash)与直方图相似度算法的原理、应用。
在收集影子资产时,网站的图标(favicon)是一项常被利用的特征。通过图标的相似度判断,安全人员可以快速筛选出相关性较高的网站。例如,若目标网站的图标与某些搜索结果中的图标高度相似,可能表明这些资产隶属于同一组织。因此,图标相似度的计算成为了筛选资产中的重要一步。
通常在图标相似度的判断中有两种较为广泛的技术路径:
本文聚焦于第一类方法,即纯算法的实现,具体分析感知哈希和直方图算法。
感知哈希算法的核心思想是将图像进行处理,得到一个相对固定长度的“哈希值”,该哈希值可以用于快速对比两张图片的相似度。感知哈希算法不同于传统的加密哈希(如MD5或SHA-1),它不追求唯一性,而是为了保留图片的视觉信息,使得相似的图片会生成相似的哈希值。pHash 具体的步骤如下:
缩放图片:将图片缩放为固定大小(如32x32),以减少计算量,同时保留关键视觉信息。
灰度处理:将彩色图像转换为灰度图像,去除颜色对比的干扰。
离散余弦变换(DCT):对灰度图像进行DCT变换,提取频域信息。低频部分保留了图像的主要结构和特征,而高频部分往往与细节噪声相关。
生成哈希值:从DCT变换的结果中,取其低频部分的平均值,并将每个像素值与该均值比较,生成一个二进制字符串(哈希值)。
汉明距离判断相似度:对比两张图片的哈希值,计算汉明距离(即两个二进制字符串不同位的数量)。距离越小,图片的相似度越高。
以下是感知哈希算法的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);
}
private String getHash(byte[] imageBytes) throws Exception {
InputStream is = new ByteArrayInputStream(imageBytes);
BufferedImage img = ImageIO.read(is);
img = resize(img, size, size);
img = grayscale(img);
double[][] dctValues = calculateDCT(img);
double avg = calculateDCTAverage(dctValues);
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();
}
/**
* 计算两个图片之间的相似度(0到1.0)
* @param srcFile 源图像文件
* @param canFile 候选图像文件
* @return 相似度(0到1.0)
* @throws Exception 处理图像时可能抛出的异常
*/
public double match(File srcFile, File canFile) throws Exception {
String srcHash = getHash(new FileInputStream(srcFile));
String canHash = getHash(new FileInputStream(canFile));
int hammingDistance = calculateHammingDistance(srcHash, canHash);
return 1.0 - (double) hammingDistance / (smallerSize * smallerSize - 1);
}
/**
* 计算两个图片之间的相似度(0到1.0)
* @param srcUrl 源图像URL
* @param canUrl 候选图像URL
* @return 相似度(0到1.0)
* @throws Exception 处理图像时可能抛出的异常
*/
public double match(URL srcUrl, URL canUrl) throws Exception {
String srcHash = getHash(srcUrl.openStream());
String canHash = getHash(canUrl.openStream());
int hammingDistance = calculateHammingDistance(srcHash, canHash);
return 1.0 - (double) hammingDistance / (smallerSize * smallerSize - 1);
}
/**
* 计算两个图片之间的相似度(0到1.0)
* @param srcImg 源图像byte[]
* @param canImg 候选图像byte[]
* @return 相似度(0到1.0)
* @throws Exception 处理图像时可能抛出的异常
*/
public double match(byte[] srcImg, byte[] canImg) throws Exception {
String srcHash = getHash(srcImg);
String canHash = getHash(canImg);
int hammingDistance = calculateHammingDistance(srcHash, canHash);
return 1.0 - (double) hammingDistance / (smallerSize * smallerSize - 1);
}
public int calculateImageDistance(byte[] srcImg, byte[] canImg) throws Exception {
String srcHash = getHash(srcImg);
String canHash = getHash(canImg);
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;
}
}
java的ImageIO类默认无法解析ico类型文件(还有部分其他类型、以及经过加工过的图片),需要使用三方库为ImageIO提供支持,
例如,mvn项目为ImageIO支持ico,pom.xml中添加:
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-ico</artifactId>
<version>3.0.2</version>
</dependency>
pHash 在对图片进行相似度判断时具有以下优点:
直方图算法的主要步骤如下:
将图片分为颜色通道:通常将图片拆分为RGB三个通道,分别计算各通道的像素分布。
计算直方图:统计每个通道中每种颜色值的像素数,生成直方图。
归一化:为了使不同大小的图片可以进行对比,通常需要对直方图进行归一化处理。
比较直方图:使用欧氏距离或巴氏距离等方式,计算两张图片直方图之间的差异。距离越小,图片的相似度越高。
下面是一个直方图相似度计算的示例代码:
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 = ImageIO.read(srcFile);
BufferedImage canImage = ImageIO.read(canFile);
return match(srcImage, canImage);
}
/**
* 比较两张图片的相似度(通过URL读取)
* @param srcUrl 源图片URL
* @param canUrl 候选图片URL
* @return 两张图片的相似度
* @throws IOException URL读取异常
*/
public double match(URL srcUrl, URL canUrl) throws IOException {
BufferedImage srcImage = ImageIO.read(srcUrl);
BufferedImage canImage = ImageIO.read(canUrl);
return match(srcImage, canImage);
}
/**
* 比较两张图片的相似度(通过byte[])
* @param srcBytes 源图片的字节数组
* @param canBytes 候选图片的字节数组
* @return 两张图片的相似度
* @throws IOException 字节数组读取异常
*/
public double match(byte[] srcBytes, byte[] canBytes) throws IOException {
BufferedImage srcImage = ImageIO.read(new ByteArrayInputStream(srcBytes));
BufferedImage canImage = ImageIO.read(new ByteArrayInputStream(canBytes));
return match(srcImage, canImage);
}
/**
* 比较两张BufferedImage图像的相似度
* @param srcImage 源图片
* @param canImage 候选图片
* @return 两张图片的相似度
* @throws IOException 如果图像为空,抛出异常
*/
private double match(BufferedImage srcImage, BufferedImage canImage) throws IOException {
if (srcImage == null || canImage == null) {
throw new IllegalArgumentException("Source or candidate image cannot be null.");
}
float[] srcHistogram = calculateHistogram(srcImage);
float[] canHistogram = calculateHistogram(canImage);
return calculateSimilarity(srcHistogram, canHistogram);
}
}
直方图算法的优点在于:
易于扩展:可以应用于不同颜色空间的图像,例如RGB、HSV等。
但是,直方图相似度也有一些局限性:
敏感性:对图片的亮度、对比度变化较为敏感,容易受到颜色变化的影响。
在图标相似度的计算中,感知哈希算法和直方图算法各有优势和劣势,具体选择取决于场景需求。
比较维度 | 感知哈希算法(pHash) | 直方图算法 |
---|---|---|
计算复杂度 | 较高,需进行DCT变换 | 低,直接计算像素分布 |
抗干扰能力 | 较强,能抵抗缩放、旋转等轻微变动 | 较弱,易受亮度、对比度变化影响 |
局部信息保留 | 良好,保留了图像的主要结构信息 | 差,忽略了图像的空间结构 |
实现难度 | 较高,需进行较复杂的图像处理 | 简单,基于基本的像素统计 |
适用场景 | 更适合判断内容相似的图标 | 适用于颜色分布相近的图片 |
两种方法结合在一起使用,提高准确率
除了使用图标相似度来筛选影子资产,还可以借助AI技术进一步分析网站内容,判断其与目标资产的相关性。这通常包括以下几个步骤:
数据提取:提取网站的标题(title)、域名(domain)及页面内容的开头部分(body_start),这些信息通常能够有效反映网站的主题和用途。
AI模型训练:通过大规模训练数据,构建用于分析文本内容的机器学习模型,如自然语言处理(NLP)模型。这些模型能够识别网页内容的主题、关键词,并判断其与目标资产的相关性。
相关性判断:使用训练好的模型,对目标网站和影子资产的网站内容进行对比,分析它们的主题相似度。如果内容相关性高,则可以认为该资产与目标有较大关联。
这种基于AI的分析能够极大提高筛选的准确性,尤其是在应对大量资产时,可以减少人工筛查的工作量。AI的加入使得渗透攻防中的自动化水平进一步提高,有助于更快、更精准地发现潜在的攻击入口。
package com.potato.potatotool.content.redTeam.infoGathering.imgSimilarity;
import com.potato.potatotool.content.redTeam.infoGathering.Utils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Base64;
import java.util.Map;
/**
* @author Potato
* @desc 该类用于结合 ImgPHsh 和 ImgHistogram 两种方法,提高图片相似度判断的准确率
*/
public class ImgSimilarity {
private ImgHistogram imgHistogram;
private ImgPHsh imgPHsh;
// 权重设置,用于综合两种相似度
private double histogramWeight = 0.5; // 直方图相似度的权重
private double phashWeight = 0.5; // pHash相似度的权重
private double histogramThreshold = 0.8; // 直方图相似度的阈值
private double phashThreshold = 0.8; // pHash相似度的阈值
// 默认构造函数,初始化直方图和pHash
public ImgSimilarity() {
this.imgHistogram = new ImgHistogram();
this.imgPHsh = new ImgPHsh();
}
// 可以自定义权重的构造函数
public ImgSimilarity(double histogramWeight, double phashWeight) {
this();
this.histogramWeight = histogramWeight;
this.phashWeight = phashWeight;
}
/**
* 比较两张图片的相似度,结合直方图和pHash
* @param srcFile 源图片文件
* @param canFile 候选图片文件
* @return 综合的相似度
* @throws IOException 如果文件读取异常
*/
public double match(URL srcFile, URL canFile) throws Exception {
// 获取直方图相似度
double histogramSimilarity = imgHistogram.match(srcFile, canFile);
// 获取pHash相似度
double pHshSimilarity = imgPHsh.match(srcFile, canFile);
// 综合相似度
return histogramWeight * histogramSimilarity + phashWeight * pHshSimilarity;
}
public double match(File srcFile, File canFile) throws Exception {
// 获取直方图相似度
double histogramSimilarity = imgHistogram.match(srcFile, canFile);
// 获取pHash相似度
double pHshSimilarity = imgPHsh.match(srcFile, canFile);
// 综合相似度
return histogramWeight * histogramSimilarity + phashWeight * pHshSimilarity;
}
public double match(byte[] srcFile, byte[] canFile) throws Exception {
// 获取直方图相似度
double histogramSimilarity = imgHistogram.match(srcFile, canFile);
// 获取pHash相似度
double pHshSimilarity = imgPHsh.match(srcFile, canFile);
// 综合相似度
return histogramWeight * histogramSimilarity + phashWeight * pHshSimilarity;
}
// 根据阈值判断是否相似
public boolean matchSimilar(byte[] srcFile, byte[] canFile) throws Exception {
// 获取直方图相似度
double histogramSimilarity = imgHistogram.match(srcFile, canFile);
// 获取pHash相似度
double pHshSimilarity = imgPHsh.match(srcFile, canFile);
return (histogramSimilarity > histogramThreshold || pHshSimilarity > phashThreshold);
}
// 设置直方图相似度的权重
public void setHistogramWeight(double histogramWeight) {
this.histogramWeight = histogramWeight;
}
// 设置pHash相似度的权重
public void setPhashWeight(double phashWeight) {
this.phashWeight = phashWeight;
}
public static void main(String[] args) throws Exception {
Map<String, String> webInfoMap = Utils.getWebInfo("https://211.160.72.129");
System.out.println(webInfoMap);
byte[] srcFile = Base64.getDecoder().decode(webInfoMap.get("iconBase64"));
Map<String, String> webInfoMap1 = Utils.getWebInfo("https://43.143.141.199:8443");
System.out.println(webInfoMap1);
byte[] canFile = Base64.getDecoder().decode(webInfoMap1.get("iconBase64"));
boolean similarity = new ImgSimilarity().matchSimilar(srcFile, canFile);
System.out.println("是否相似: " + similarity);
double similarity1 = new ImgSimilarity().match(new URL("https://t8.baidu.com/it/u=3036650915,1842869833&fm=193"), new URL("https://t9.baidu.com/it/u=140484125,2114791292&fm=193"));
System.out.println("综合相似度: " + similarity1);
}
}
在渗透攻防中,影子资产的筛选对提高攻击效率至关重要。图标相似度筛选是快速筛选潜在目标的有效手段,而感知哈希算法和直方图算法各具特点,适用于不同场景,可以两种方法结合在一起使用,提高准确率。结合AI技术进行内容相关性判断,可以进一步提高筛选精度,为渗透测试人员提供更多准确的目标。