rizin/librz/hash/algorithms/ssdeep/ssdeep.c

456 lines
13 KiB
C

// SPDX-FileCopyrightText: 2022 deroad <wargio@libero.it>
// SPDX-License-Identifier: LGPL-3.0-only
#include "ssdeep.h"
#include <rz_types.h>
#include <rz_endian.h>
#include <rz_util.h>
#include <rz_diff.h>
#include "fnv_hash.h"
#define SSDEEP_BASE64_CHARSET "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
#define SSDEEP_ROLL_SLIDE_WINDOW_SIZE 7u
#define SSDEEP_BLOCK_HASHES_SIZE 31u
#define SSDEEP_SPAMSUM_LENGTH 64u
#define SSDEEP_SPAMSUM_HALF_LENGTH (SSDEEP_SPAMSUM_LENGTH / 2)
#define SSDEEP_MIN_BLOCK_SIZE 3ull
#define SSDEEP_N_BLOCK_HASHES 31u
#define SSDEEP_BLOCK_SIZE(i) (SSDEEP_MIN_BLOCK_SIZE << (i))
#define SSDEEP_TOTAL_SIZE(i) (SSDEEP_BLOCK_SIZE(i) * SSDEEP_SPAMSUM_LENGTH)
#define SSDEEP_LIMIT_SIZE_INIT SSDEEP_TOTAL_SIZE(0)
#define SSDEEP_LIMIT_MAX_SIZE SSDEEP_TOTAL_SIZE(SSDEEP_N_BLOCK_HASHES - 1)
#define SSDEEP_DIGEST_SIZE (SSDEEP_SPAMSUM_LENGTH + 2)
#define SSDEEP_ROLL_HASHES_SIZE (SSDEEP_SPAMSUM_LENGTH - (SSDEEP_ROLL_SLIDE_WINDOW_SIZE - 1))
#define SSDEEP_SMALL_BLOCK_LIMIT ((((99 + SSDEEP_ROLL_SLIDE_WINDOW_SIZE) / (SSDEEP_ROLL_SLIDE_WINDOW_SIZE)) * SSDEEP_MIN_BLOCK_SIZE))
typedef struct roll_hash_t {
ut8 window[SSDEEP_ROLL_SLIDE_WINDOW_SIZE];
ut32 current;
ut32 state[3];
} RollHash;
typedef ut8 SumHash;
typedef struct digest_block_t {
ut32 length; // digest length
char digest[SSDEEP_DIGEST_SIZE];
char prev_half;
SumHash complete;
SumHash half;
} DigestBlock;
struct rz_ssdeep_t {
RollHash roll;
DigestBlock blocks[SSDEEP_N_BLOCK_HASHES];
SumHash lasth;
bool last_iteration;
ut64 block_limit;
ut64 total_size;
ut32 start;
ut32 end;
ut32 roll_mask;
};
static const char *base64_charset = SSDEEP_BASE64_CHARSET;
static const char fnv_b64_table[64][64] = {
FNV_B64_00, FNV_B64_01, FNV_B64_02, FNV_B64_03, FNV_B64_04, FNV_B64_05, FNV_B64_06, FNV_B64_07,
FNV_B64_08, FNV_B64_09, FNV_B64_0A, FNV_B64_0B, FNV_B64_0C, FNV_B64_0D, FNV_B64_0E, FNV_B64_0F,
FNV_B64_10, FNV_B64_11, FNV_B64_12, FNV_B64_13, FNV_B64_14, FNV_B64_15, FNV_B64_16, FNV_B64_17,
FNV_B64_18, FNV_B64_19, FNV_B64_1A, FNV_B64_1B, FNV_B64_1C, FNV_B64_1D, FNV_B64_1E, FNV_B64_1F,
FNV_B64_20, FNV_B64_21, FNV_B64_22, FNV_B64_23, FNV_B64_24, FNV_B64_25, FNV_B64_26, FNV_B64_27,
FNV_B64_28, FNV_B64_29, FNV_B64_2A, FNV_B64_2B, FNV_B64_2C, FNV_B64_2D, FNV_B64_2E, FNV_B64_2F,
FNV_B64_30, FNV_B64_31, FNV_B64_32, FNV_B64_33, FNV_B64_34, FNV_B64_35, FNV_B64_36, FNV_B64_37,
FNV_B64_38, FNV_B64_39, FNV_B64_3A, FNV_B64_3B, FNV_B64_3C, FNV_B64_3D, FNV_B64_3E, FNV_B64_3F
};
#define sum_hash_reset(state) \
(state = FNV_INIT);
#define sum_hash_update(state, value) \
(state = fnv_b64_table[state][value & 0x3f]);
#define roll_hash_reset(hash) \
memset(hash, 0, sizeof(RollHash))
static void roll_hash_update(RollHash *hash, ut8 value8) {
ut32 value32 = value8;
ut32 current = hash->window[hash->current];
hash->state[1] = hash->state[1] - hash->state[0] + (SSDEEP_ROLL_SLIDE_WINDOW_SIZE * value32);
hash->state[0] = hash->state[0] + value32 - current;
hash->state[2] = (hash->state[2] << 5) ^ value32;
hash->window[hash->current] = value8;
hash->current++;
if (hash->current >= SSDEEP_ROLL_SLIDE_WINDOW_SIZE) {
hash->current = 0;
}
}
static ut32 roll_hash_sum(RollHash *hash) {
return hash->state[0] + hash->state[1] + hash->state[2];
}
static void block_hash_reset(DigestBlock *block) {
sum_hash_reset(block->complete);
sum_hash_reset(block->half);
memset(block->digest, 0, SSDEEP_DIGEST_SIZE);
block->prev_half = 0;
block->length = 0;
}
static void block_hash_update(DigestBlock *block) {
block->length++;
block->digest[block->length] = 0;
sum_hash_reset(block->complete);
if (block->length >= SSDEEP_SPAMSUM_HALF_LENGTH) {
return;
}
sum_hash_reset(block->half);
block->prev_half = 0;
}
static void ssdeep_update_next_block(RzSSDeep *context) {
DigestBlock *last = &context->blocks[context->end - 1];
if (context->end <= (SSDEEP_N_BLOCK_HASHES - 1)) {
DigestBlock *next = &last[1]; // get the next block
block_hash_reset(next);
next->complete = last->complete;
next->half = last->half;
context->end++;
} else if (context->end == SSDEEP_N_BLOCK_HASHES && !context->last_iteration) {
context->last_iteration = true;
context->lasth = last->complete;
}
}
static void ssdeep_update_start_block(RzSSDeep *context) {
if ((context->end - context->start) < 2 ||
context->total_size < context->block_limit ||
context->blocks[context->start + 1].length < SSDEEP_SPAMSUM_HALF_LENGTH) {
return;
}
context->start++;
context->block_limit <<= 1;
context->roll_mask <<= 1;
context->roll_mask |= 1;
}
static void ssdeep_update(RzSSDeep *context, ut8 c) {
roll_hash_update(&context->roll, c);
ut32 hash = roll_hash_sum(&context->roll) + 1;
ut32 roll_size = hash / (ut32)SSDEEP_MIN_BLOCK_SIZE;
for (ut32 i = context->start; i < context->end; ++i) {
sum_hash_update(context->blocks[i].complete, c);
sum_hash_update(context->blocks[i].half, c);
}
if (context->last_iteration) {
sum_hash_update(context->lasth, c);
}
if (!hash ||
(roll_size & context->roll_mask) ||
(hash % (ut32)SSDEEP_MIN_BLOCK_SIZE)) {
return;
}
roll_size >>= context->start;
for (ut32 i = context->start; i < context->end; i++) {
DigestBlock *block = &context->blocks[i];
if (!block->length) {
ssdeep_update_next_block(context);
}
block->digest[block->length] = base64_charset[block->complete];
block->prev_half = base64_charset[block->half];
if (block->length < (SSDEEP_SPAMSUM_LENGTH - 1)) {
block_hash_update(block);
} else {
ssdeep_update_start_block(context);
}
if (roll_size & 1) {
break;
}
roll_size >>= 1;
};
}
RzSSDeep *rz_ssdeep_new(void) {
RzSSDeep *context = RZ_NEW0(RzSSDeep);
if (context) {
rz_ssdeep_init(context);
}
return context;
}
void rz_ssdeep_init(RzSSDeep *context) {
rz_return_if_fail(context);
roll_hash_reset(&context->roll);
block_hash_reset(&context->blocks[0]);
context->total_size = 0;
context->block_limit = SSDEEP_LIMIT_SIZE_INIT;
context->last_iteration = false;
context->start = 0;
context->end = 1;
context->roll_mask = 0;
}
bool rz_ssdeep_update(RzSSDeep *context, const ut8 *buf, ut64 len) {
rz_return_val_if_fail(context && buf, false);
context->total_size += len;
for (ut64 i = 0; i < len; i++) {
ssdeep_update(context, buf[i]);
}
return true;
}
void rz_ssdeep_fini(RzSSDeep *context, char *result) {
rz_return_if_fail(context && result);
memset(result, 0, RZ_HASH_SSDEEP_DIGEST_SIZE);
DigestBlock tmp = { 0 };
DigestBlock *block_0 = NULL, *block_1 = NULL;
ut32 start = context->start;
ut32 roll_hash = roll_hash_sum(&context->roll);
if (context->total_size > SSDEEP_LIMIT_MAX_SIZE) {
RZ_LOG_ERROR("ssdeep: total size exeeds max size\n");
return;
}
while (SSDEEP_TOTAL_SIZE(start) < context->total_size) {
++start;
}
if (start >= context->end) {
start = context->end - 1;
}
while (start > context->start && context->blocks[start].length < SSDEEP_SPAMSUM_HALF_LENGTH) {
--start;
}
ut64 block_size = SSDEEP_BLOCK_SIZE(start);
block_0 = &context->blocks[start];
block_1 = &context->blocks[start + 1];
if (roll_hash) {
block_0->digest[block_0->length++] = base64_charset[block_0->complete];
} else if (block_0->digest[block_0->length]) {
// has a tail char, we must include it
block_0->length++;
}
// ensure the last char is always the delimiter
block_0->digest[block_0->length] = 0;
if (start < context->end - 1) {
if (block_1->length > SSDEEP_SPAMSUM_HALF_LENGTH - 1) {
block_1->length = SSDEEP_SPAMSUM_HALF_LENGTH - 1;
}
if (roll_hash) {
block_1->digest[block_1->length++] = base64_charset[block_1->half];
} else if (block_1->prev_half) {
block_1->digest[block_1->length++] = block_1->prev_half;
}
} else if (roll_hash) {
tmp.length++;
tmp.digest[0] = start ? base64_charset[context->lasth] : base64_charset[block_0->complete];
block_1 = &tmp;
}
// ensure the last char is always the delimiter
block_1->digest[block_1->length] = 0;
snprintf(result, RZ_HASH_SSDEEP_DIGEST_SIZE, "%" PFMT64u ":%s:%s", block_size, block_0->digest, block_1->digest);
}
// removes char sequences longer than 3.
static char *remove_triplets(const char *old) {
size_t len = strlen(old);
char *new_str = RZ_NEWS0(char, len + 1);
if (len < 3 || !new_str) {
free(new_str);
return NULL;
}
new_str[0] = old[0];
new_str[1] = old[1];
new_str[2] = old[2];
for (size_t i = 3, j = 3; i < len; ++i) {
if (old[i] == old[i - 1] && old[i] == old[i - 2] && old[i] == old[i - 3]) {
continue;
}
new_str[j++] = old[i];
}
return new_str;
}
static bool has_same_roll_hashes(const char *hash_a, size_t len_a, const char *hash_b, size_t len_b) {
RollHash hash = { 0 };
ut32 hashes[SSDEEP_ROLL_HASHES_SIZE] = { 0 };
const ut32 window_end = SSDEEP_ROLL_SLIDE_WINDOW_SIZE - 1;
for (ut32 i = 0; i < window_end; i++) {
roll_hash_update(&hash, hash_a[i]);
}
for (ut32 i = window_end; i < len_a; i++) {
roll_hash_update(&hash, hash_a[i]);
hashes[i - window_end] = roll_hash_sum(&hash);
}
len_a -= window_end;
roll_hash_reset(&hash);
for (ut32 i = 0; i < window_end; i++) {
roll_hash_update(&hash, hash_b[i]);
}
len_b -= window_end;
for (ut32 j = 0; j < len_b; j++) {
roll_hash_update(&hash, hash_b[j + window_end]);
ut32 sum = roll_hash_sum(&hash);
for (ut32 i = 0; i < len_a; i++) {
if (sum != hashes[i] || memcmp(hash_a + i, hash_b + j, SSDEEP_ROLL_SLIDE_WINDOW_SIZE)) {
continue;
}
return true;
}
}
return false;
}
double calculate_hashes_distance(const char *hash_a, size_t len_a, const char *hash_b, size_t len_b) {
size_t max_len = RZ_MAX(len_a, len_b) + 1;
float *dist1 = RZ_NEWS0(float, max_len);
float *dist2 = RZ_NEWS0(float, max_len);
if (!dist1 || !dist2) {
RZ_LOG_ERROR("ssdeep: failed to allocate buffer for distance calculation\n");
free(dist1);
free(dist2);
return 0;
}
for (size_t i = 0; i <= len_b; i++) {
dist1[i] = (float)i;
}
for (size_t i = 0; i < len_a; i++) {
dist2[0] = 1.0f + i;
for (size_t j = 0; j < len_b; j++) {
float rpl_cost = dist1[j];
if (hash_a[i] != hash_b[j]) {
// replace 2 chars
rpl_cost += 2.0f;
}
float rem_cost = dist2[j] + 1.0f; // remove 1 char
float add_cost = dist1[j + 1] + 1.0f; // add 1 char
dist2[j + 1] = RZ_MIN(RZ_MIN(rem_cost, rpl_cost), add_cost);
}
// swap the two distances
float *tmp = dist1;
dist1 = dist2;
dist2 = tmp;
}
double result = (double)dist1[len_b];
free(dist1);
free(dist2);
result /= (double)(len_a + len_b);
return 1.0 - result;
}
static double calculate_similarity(const char *hash_a, const char *hash_b, ut32 block_size) {
size_t len_a = strlen(hash_a);
size_t len_b = strlen(hash_b);
if (len_a < SSDEEP_ROLL_SLIDE_WINDOW_SIZE || len_b < SSDEEP_ROLL_SLIDE_WINDOW_SIZE) {
return 0.0;
}
if (!has_same_roll_hashes(hash_a, len_a, hash_b, len_b)) {
return 0.0;
}
double similarity = calculate_hashes_distance(hash_a, len_a, hash_b, len_b);
if (block_size < SSDEEP_SMALL_BLOCK_LIMIT) {
double max_score = block_size;
max_score /= (double)SSDEEP_MIN_BLOCK_SIZE;
max_score *= (double)RZ_MIN(len_a, len_b);
if ((similarity * 100.0) > max_score) {
similarity = max_score / 100.0;
}
}
return similarity;
}
double rz_ssdeep_compare(const char *hash_a, const char *hash_b) {
double similarity = 0.0;
ut32 block_a, block_b;
char *digest_a0 = NULL, *digest_a1 = NULL;
char *digest_b0 = NULL, *digest_b1 = NULL;
RzList *token_a = rz_str_split_duplist(hash_a, ":", true);
RzList *token_b = rz_str_split_duplist(hash_b, ":", true);
if (rz_list_length(token_a) != 3 || rz_list_length(token_b) != 3) {
RZ_LOG_ERROR("diff: the expected hashes are not in ssdeep format\n");
similarity = -1.0;
goto end;
}
block_a = strtol(rz_list_first_val(token_a), NULL, 10);
digest_a0 = remove_triplets(rz_list_get_n(token_a, 1));
digest_a1 = remove_triplets(rz_list_last_val(token_a));
block_b = strtol(rz_list_first_val(token_b), NULL, 10);
digest_b0 = remove_triplets(rz_list_get_n(token_b, 1));
digest_b1 = remove_triplets(rz_list_last_val(token_b));
if (!block_a || !block_b || !digest_a0 || !digest_a1 || !digest_b0 || !digest_b1) {
RZ_LOG_ERROR("diff: the expected hashes are not in ssdeep format\n");
similarity = -1.0;
goto end;
}
if (block_a != block_b && block_a != (block_b << 1) && block_b != (block_a << 1)) {
similarity = 0.0;
} else if (block_a == block_b && !strcmp(digest_a1, digest_b1)) {
similarity = 1.0;
} else if (block_a == block_b) {
double score1 = calculate_similarity(digest_a0, digest_b0, block_a);
double score2 = calculate_similarity(digest_a1, digest_b1, block_a << 1);
similarity = RZ_MAX(score1, score2);
} else if (block_a == (block_b << 1)) {
similarity = calculate_similarity(digest_a0, digest_b1, block_a);
} else /* if (block_b == (block_a << 1)) */ {
similarity = calculate_similarity(digest_a1, digest_b0, block_b);
}
end:
free(digest_a0);
free(digest_a1);
free(digest_b0);
free(digest_b1);
rz_list_free(token_a);
rz_list_free(token_b);
return similarity;
}