Explicación detallada de las características de ByteEntropyHistogram extraídas de archivos PE en el conjunto de datos EMBER

1. Importar

Cuando extraemos características de archivos PE, a menudo vemos el siguiente código en proyectos de ingeniería de características PE

class ByteEntropyHistogram(FeatureType):
    ''' 2d byte/entropy histogram based loosely on (Saxe and Berlin, 2015).
    This roughly approximates the joint probability of byte value and local entropy.
    See Section 2.1.1 in https://arxiv.org/pdf/1508.03096.pdf for more info.
    '''

    name = 'byteentropy'
    dim = 256

    def __init__(self, step=1024, window=2048):
        super(FeatureType, self).__init__()
        self.window = window
        self.step = step

    def _entropy_bin_counts(self, block):
        # coarse histogram, 16 bytes per bin
        c = np.bincount(block >> 4, minlength=16)  # 16-bin histogram
        p = c.astype(np.float32) / self.window
        wh = np.where(c)[0]
        H = np.sum(-p[wh] * np.log2(
            p[wh])) * 2  # * x2 b.c. we reduced information by half: 256 bins (8 bits) to 16 bins (4 bits)

        Hbin = int(H * 2)  # up to 16 bins (max entropy is 8 bits)
        if Hbin == 16:  # handle entropy = 8.0 bits
            Hbin = 15

        return Hbin, c

    def raw_features(self, bytez, lief_binary):
        output = np.zeros((16, 16), dtype=np.int)
        a = np.frombuffer(bytez, dtype=np.uint8)
        if a.shape[0] < self.window:
            Hbin, c = self._entropy_bin_counts(a)
            output[Hbin, :] += c
        else:
            # strided trick from here: http://www.rigtorp.se/2011/01/01/rolling-statistics-numpy.html
            shape = a.shape[:-1] + (a.shape[-1] - self.window + 1, self.window)
            strides = a.strides + (a.strides[-1],)
            blocks = np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)[::self.step, :]

            # from the blocks, compute histogram
            for block in blocks:
                Hbin, c = self._entropy_bin_counts(block)
                output[Hbin, :] += c

        return output.flatten().tolist()

    def process_raw_features(self, raw_obj):
        counts = np.array(raw_obj, dtype=np.float32)
        sum = counts.sum()
        normalized = counts / sum
        return normalized

Este código es del conocido proyecto EMBER (referencia 1). EMBER extrae muchas características de archivos PE. EMBER también es un conjunto de datos y un punto de referencia.

ByteEntropyHistogram(FeatureType)Busque esta cadena clave en github o busque en Google, y podrá encontrar muchos proyectos y blogs, incluido el aprendizaje profundo y el aprendizaje automático tradicional. Algunos blogs también escriben que esto es calcular el valor de entropía Entropy . Por lo tanto, puede ver que en muchos lugares se hace referencia directamente a este código para realizar ingeniería de funciones en archivos PE (o archivos binarios arbitrarios), y los resultados reales de las pruebas del autor son realmente buenos.

Pero este código no es fácil de entender, ¿cómo es su proceso de cálculo? ¿Se trata realmente sólo de calcular la entropía? ¿Qué hace block >> 4en el código?

Comprendamos este código para la ingeniería de funciones paso a paso.

2. Depuración en ejecución dinámica

Deje que el código se ejecute y eche un vistazo primero; este es un método común para la depuración de ingeniería. Deje que el código se ejecute, puede saber cuál es el proceso en ejecución, cuál es la entrada y salida de cada función, puede obtener los resultados intermedios en diferentes posiciones del código y puede analizar el problema paso a paso.

Para que ByteEntropyHistogramel código de esta clase se ejecute, es necesario modificarlo ligeramente, como eliminar su clase principal, eliminar algunos parámetros no utilizados en la función y eliminar la parte de la función de inicialización relacionada con la inicialización de la clase principal.

import numpy as np


class ByteEntropyHistogram():

    name = 'byteentropy'
    dim = 256

    def __init__(self, step=1024, window=2048):
        self.window = window
        self.step = step

    def _entropy_bin_counts(self, block):
        # coarse histogram, 16 bytes per bin
        c = np.bincount(block >> 4, minlength=16)  # 16-bin histogram
        p = c.astype(np.float32) / self.window
        wh = np.where(c)[0]
        H = np.sum(-p[wh] * np.log2(
            p[wh])) * 2  # * x2 b.c. we reduced information by half: 256 bins (8 bits) to 16 bins (4 bits)

        Hbin = int(H * 2)  # up to 16 bins (max entropy is 8 bits)
        if Hbin == 16:  # handle entropy = 8.0 bits
            Hbin = 15

        return Hbin, c

    def raw_features(self, bytez):
        output = np.zeros((16, 16), dtype=np.int32)
        a = np.frombuffer(bytez, dtype=np.uint8)
        if a.shape[0] < self.window:
            Hbin, c = self._entropy_bin_counts(a)
            output[Hbin, :] += c
        else:
            # strided trick from here: http://www.rigtorp.se/2011/01/01/rolling-statistics-numpy.html
            shape = a.shape[:-1] + (a.shape[-1] - self.window + 1, self.window)
            strides = a.strides + (a.strides[-1],)
            blocks = np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)[::self.step, :]

            # from the blocks, compute histogram
            for block in blocks:
                Hbin, c = self._entropy_bin_counts(block)
                output[Hbin, :] += c

        return output.flatten().tolist()

    def process_raw_features(self, raw_obj):
        counts = np.array(raw_obj, dtype=np.float32)
        sum = counts.sum()
        normalized = counts / sum
        return normalized


with open('cfdbbd60c7dd63db797fb27e1c427077ce1915b8894ef7165d8715304756a7e2', 'rb') as fr:
	bytez = fr.read()# get binary file bytes array
	be = ByteEntropyHistogram()
	raw_obj = be.raw_features(bytez)# get raw feature 
	fea_vec = be.process_raw_features(raw_obj)# get final feature vector
	print('out',fea_vec.shape)# (256,) float 1d-vector

Finalmente, se lee un archivo binario y ByteEntropyHistogramse llaman a su vez las funciones proporcionadas para obtener el vector de características.

El proceso de esta operación es:

  1. raw_features()_entropy_bin_counts(block)En, use la ventana deslizante para llamar y calcular el valor de Hbin yc para los datos binarios (matriz de bytes, que es la variable de bloque en el código) en cada ventana
  2. El valor c calculado se acumula en una matriz bidimensional de 16*16 según la posición especificada por Hbin ( output[Hbin, :] += c)
  3. Esta matriz bidimensional finalmente se aplana (reforma) en un vector de pensamiento ( return output.flatten().tolist()), que es el valor de raw_obj en el código.
  4. Al final, la función utiliza raw_obj process_raw_features()para realizar una normalización simple, que consiste en dividir cada valor de datos por la suma ( normalized = counts / sum)

Los puntos clave en este proceso son dos funciones: _entropy_bin_counts(block)y raw_features(), las dos funciones siguientes se analizan con más detalle

3. _entropy_bin_counts(block)Proceso de cálculo

  1. valor de entrada de la función

Esta función calcula los datos en una ventana, por lo que el bloque de entrada representa una matriz de bytes; la matriz de bytes puede entenderse como una estructura de lista y cada valor de datos que contiene es un número entero entre 0 y 255. Por ejemplo:

La matriz de bytes leída se almacena en el bloque, por ejemplo de la siguiente manera

bytez = b'MZ\x90\x00\x03\x00\x00\x00\x04'

En este ejemplo, MZ es el ID del archivo PE y 0x son los datos hexadecimales. Esta matriz de bytes se puede convertir en una matriz int equivalente de la siguiente manera:

bytez = [77,90,144,0,3,0,0,0,4]

Hay 9 datos en la matriz de bytes, el valor del código ASCII de la letra 'M' es 77 en decimal y el valor del código ASCII de 'Z' es 90. Los valores de las dos matrices de bytes anteriores son los mismos.

El programa eventualmente convertirá bytez al formato np.array como un bloque, es decir block = np.frombuffer(bytez, dtype=np.uint8), el proceso es el siguiente:

import numpy as np
bytez = b'MZ\x90\x00\x03\x00\x00\x00\x04'
block = np.frombuffer(bytez, dtype=np.uint8)
print(block)# array([ 77,  90, 144,   0,   3,   0,   0,   0,   4], dtype=np.uint8)
  1. bloque >> 4

Después de que la función reciba la matriz de bloques, esta operación se realizará primero en el bloque block >> 4. Esta es una operación de desplazamiento a la derecha, que desplazará cada dato en la matriz de bloques hacia la derecha en 4 bits. Debido a que los datos en el bloque son datos int de 8 bits, desplazar 4 bits hacia la derecha equivale a eliminar los 4 bits inferiores y retener los 4 bits superiores. Es decir, el número entero después de dividir cada dato por 16 (2 elevado a la cuarta potencia). Un ejemplo de este proceso es el siguiente:

block = np.array([ 77,  90, 144,   0,   3,   0,   0,   0,   4], dtype=np.uint8)
y = block >> 4
print(y) # array([4, 5, 9, 0, 0, 0, 0, 0, 0], dtype=uint8)

Los datos en el bloque de la función de entrada son datos int de 8 bits, por lo que el rango de cada valor de datos es [0,255], después de la block >> 4operación, cada valor de datos es [0,15]. Esto equivale a difuminar el rango de valores de los datos. La ventaja es que la dimensión del vector de características final se reduce y la desventaja es que la precisión de los datos es reducida/difusa.

  1. cuenta binaria

bincount es una función en numpy que se usa para contar el número de apariciones de cada dato en la matriz. Nuestro uso aquí es que la c = np.bincount(block >> 4, minlength=16)longitud mínima aquí indica que la dimensión de los datos de salida es al menos 16. Específicamente para nuestros datos, después block >> 4de la operación, el valor de cada dato es [0,15] y la longitud mínima se establece en 16, luego se generará una matriz unidimensional con una dimensión de 16, que representa cada dato en 0 ~15 el número de ocurrencias.

Este c变量es un parámetro más importante en el valor de retorno de la función y es el resultado de las estadísticas del histograma de los datos de cada ventana.

  1. Cálculo de entropía

La siguiente parte de la función es principalmente calcular el valor de la entropía de la información.

p = c.astype(np.float32) / self.window #计算每个数据出现的概率,window默认值为2048
wh = np.where(c)[0]#输出满足条件 (即非0) 元素的坐标
# 下面是计算信息熵H,并放大2倍
H = np.sum(-p[wh] * np.log2(p[wh])) * 2  
# 再对信息熵值放大2倍,最终相当于Hbin是信息熵值的4倍后的整数
Hbin = int(H * 2)  # up to 16 bins (max entropy is 8 bits)
if Hbin == 16:  # handle entropy = 8.0 bits
	Hbin = 15# 该操作让Hbin返回值在[0,15]范围内

Aquí, después de c变量calcular la entropía de la información y multiplicarla por 4, tome un número entero y cambie su valor límite (si Hbin == 16, cambie su valor a 15, Hbin = 15).

c变量El valor en es el número de apariciones de cada valor en [0,15], por lo que hay 16 valores de probabilidad (p) en total. Cuando las probabilidades son iguales, el valor del resultado de la entropía de la información es el mayor (consulte al teorema en 2). Por lo tanto, el valor máximo de la entropía de la información H es log2(16)=4. En el código, expanda este valor en 4 para obtener Hbin, luego el rango de valores de Hbin es [0,1,2,…,16]. En el código, el valor límite finalmente se cambia a 15, por lo que el rango de valores del Hbin finalmente devuelto es [0,1,2,…,15].

Hasta ahora, los dos valores de retorno de la función.

  • c: blockEl número de apariciones de cada valor después de los datos del medio se divide por 4 y se redondea a un número entero. Es una matriz unidimensional de 1 * 16, que representa un vector de histograma
  • Hbin: es un número entero en [0,15], que representa cel valor entero después de calcular y procesar la entropía de la información

4.raw_features()

Esta función es la función general para extraer características de todo el archivo binario. La clave para entenderla radica en las siguientes líneas de lógica central:

output = np.zeros((16, 16), dtype=np.int32)# 16*16的二维数组
# 用滑动窗口把整个二进制文件切割为多个block的字节数组存储到blocks中
for block in blocks:
	# 对每个block的byte数组计算 Hbin(信息熵)和c(直方图向量)
	Hbin, c = self._entropy_bin_counts(block)
	# 在信息熵Hbin相同的维度上,对c累加
	output[Hbin, :] += c
return output.flatten().tolist()# 最终将16*16的二维数组转换为1*256的一维数组 作为返回值
  1. Configuración de ventana deslizante y escalón

En la función de inicio de la clase, se establecen paso = 1024, ventana = 2048. Esto muestra que el tamaño de la ventana deslizante es 2048 (bytes) y la distancia de cada movimiento (deslizamiento) es 1024 bytes.

  1. Si el tamaño de la matriz de bytes del archivo es menor que el ancho de la ventana

Si el tamaño del archivo binario es relativamente pequeño, posiblemente menos de 2048 bytes, utilice directamente la matriz de bytes del archivo binario para calcular Hbin (entropía de información) y c (vector de histograma).

  1. proceso de ventana corredera

En la otra parte de la función, primero use la ventana deslizante para cortar todo el archivo binario en matrices de bytes de múltiples bloques y almacenarlos en bloques.

  1. acumulación de resultados

Y calcule Hbin (entropía de información) y c (vector de histograma) para la matriz de bytes de cada bloque de ventana, y acumule c en la misma dimensión que la entropía de información Hbin. Finalmente, la matriz bidimensional 16 16 se convierte en una matriz unidimensional 1 256 como valor de retorno.

Resumir

EMBER extrae la característica ByteEntropyHistogram ampliamente utilizada para archivos PE, que es un caso de extracción directa de características para archivos binarios. La esencia de esta característica es utilizar el proceso de ventana deslizante para obtener la entropía de información del histograma después de difuminar los datos binarios en cada ventana, y acumular los valores vectoriales del histograma de datos en cada ventana bajo las dimensiones de diferentes valores de entropía de información. consecuencia de.

Finalmente, haga un comentario general sobre esta clase:

class ByteEntropyHistogram():

    name = 'byteentropy'# 该特征名字
    dim = 256# 该特征最终返回数据的维度,即如下output变量被flatten为一维数组(数据类型为np.int32)

    def __init__(self, step=1024, window=2048):
        self.window = window# 滑动窗口的窗口大小,单位是字节,默认是2048字节
        self.step = step# 滑动窗口的移动宽度,单位是字节,默认是1024字节

	# 对输入的block数据处理后,计算信息熵及直方图
	# block为1维的numpy数组,类型是dtype=np.uint8,说明数据值大小为[0,255](即字节数据)
    def _entropy_bin_counts(self, block):
        # block >> 4会让block数组中每个数据值都除以16,最终每个数据值变为[0,15]
        c = np.bincount(block >> 4, minlength=16) # 统计并返回0~15这16个整数的出现次数:直方图向量
		# 计算 直方图向量c的信息熵值,并将信息熵值放大4倍后标记为Hbin
        p = c.astype(np.float32) / self.window#计算每个数据出现的概率,window默认值为2048
        wh = np.where(c)[0]#输出满足条件 (即非0) 元素的坐标
        # 下面是计算信息熵H,并放大2倍
		H = np.sum(-p[wh] * np.log2(
            p[wh])) * 2  # * x2 b.c. we reduced information by half: 256 bins (8 bits) to 16 bins (4 bits)
		# 再对信息熵值放大2倍,最终相当于Hbin是信息熵值的4倍后的整数
        Hbin = int(H * 2)  # up to 16 bins (max entropy is 8 bits)
        if Hbin == 16:  # handle entropy = 8.0 bits
            Hbin = 15# 该操作让Hbin返回值在[0,15]范围内

        return Hbin, c# Hbin为处理后得到的信息熵值,是整数,取值范围[0,15];c是直方图向量

    def raw_features(self, bytez):
		# 结果放到16x16的整数二维数组中
        output = np.zeros((16, 16), dtype=np.int32)
		# 将读入的字节数组bytez,转换为np.array的一维向量,每个数据值大小为[0,255]
        a = np.frombuffer(bytez, dtype=np.uint8)
		# 如果二进制文件字节数小于window,则直接计算Hbin和c后放到output矩阵中
        if a.shape[0] < self.window:
            Hbin, c = self._entropy_bin_counts(a)
            output[Hbin, :] += c
        else:# 如果二进制文件字节数(size)大于window大小,则滑动窗口
            # 用滑动窗口把整个二进制文件切割为多个block的字节数组存储到blocks中
			# strided trick from here: http://www.rigtorp.se/2011/01/01/rolling-statistics-numpy.html
            shape = a.shape[:-1] + (a.shape[-1] - self.window + 1, self.window)
            strides = a.strides + (a.strides[-1],)
            blocks = np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)[::self.step, :]

            # from the blocks, compute histogram
            for block in blocks:
				# 对每个block的byte数组计算 Hbin(信息熵)和c(直方图向量)
                Hbin, c = self._entropy_bin_counts(block)
				# 在信息熵Hbin相同的维度上,对c累加
                output[Hbin, :] += c

        return output.flatten().tolist()# 最终将16*16的二维数组转换为1*256的一维数组 作为返回值

	# 做简单的 normalization: X = X/SUM(X)
    def process_raw_features(self, raw_obj):# 输入的raw_obj是一维向量int32类型(即raw_features()函数返回值)
        counts = np.array(raw_obj, dtype=np.float32)
        sum = counts.sum()# 对一维数组中的值求和
        normalized = counts / sum# 每个数据值求除以总和,即做简单的normalization
        return normalized

referencia

  1. https://github.com/elastic/ember/blob/master/ember/features.py#L71
  2. https://blog.csdn.net/feixi7358/article/details/83861858

Supongo que te gusta

Origin blog.csdn.net/ybdesire/article/details/132113428
Recomendado
Clasificación