Skip to content

Commit

Permalink
feat: implement the hash module.
Browse files Browse the repository at this point in the history
The `hash` module implements hashing functions `md5`, `sha1` and `sha256`.
  • Loading branch information
plusvic committed Sep 26, 2023
1 parent 89e785b commit 861e8fc
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 0 deletions.
50 changes: 50 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ lazy_static = "1.4.0"
line-span = "0.1.3"
linkme = "0.3"
log = "0.4"
md5 = "0.7.0"
sha1 = "0.10.6"
sha256 = "1.4.0"
memchr = "2.6.3"
memx = "0.1.28"
num = "0.4.0"
Expand Down
10 changes: 10 additions & 0 deletions yara-x/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,18 @@ text-module = [
# The Time module allows you to retrieve epoch in seconds that can
# be used in conditions of a rule to check againts other epoch time.
time-module = []
# The hash module provides functions for computing md5, sha1 and sha-256 hashes
hash-module = [
"dep:md5",
"dep:sha1",
"dep:sha256",
]

# Features that are enabled by default.
default = [
"constant-folding",
"fast-regexp",
"hash-module",
"time-module",
"test_proto2-module",
"test_proto3-module",
Expand All @@ -67,6 +74,9 @@ itertools = { workspace = true }
lazy_static = { workspace = true }
linkme = { workspace = true }
log = { workspace = true, optional = true }
md5 = { workspace = true, optional = true }
sha1 = { workspace = true, optional = true }
sha256 = { workspace = true, optional = true }
memchr = { workspace = true }
memx = { workspace = true }
num = { workspace = true }
Expand Down
149 changes: 149 additions & 0 deletions yara-x/src/modules/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use std::cell::RefCell;

use md5 as md5_hash;
use rustc_hash::FxHashMap;
use sha1 as sha1_hash;
use sha1::Digest;
use sha256::digest as sha256_digest;

use crate::modules::prelude::*;
use crate::modules::protos::hash::*;

thread_local!(
static SHA256_CACHE: RefCell<FxHashMap<(i64, i64), String>> =
RefCell::new(FxHashMap::default());

static SHA1_CACHE: RefCell<FxHashMap<(i64, i64), String>> =
RefCell::new(FxHashMap::default());

static MD5_CACHE: RefCell<FxHashMap<(i64, i64), String>> =
RefCell::new(FxHashMap::default());
);

#[module_main]
fn main(_ctx: &ScanContext) -> Hash {
// With every scanned file the cache must be cleared.
SHA256_CACHE.with(|cache| cache.borrow_mut().clear());
SHA1_CACHE.with(|cache| cache.borrow_mut().clear());
MD5_CACHE.with(|cache| cache.borrow_mut().clear());

Hash::new()
}

#[module_export(name = "md5")]
fn md5(
ctx: &mut ScanContext,
offset: i64,
size: i64,
) -> Option<RuntimeString> {
let cached = MD5_CACHE.with(|cache| -> Option<RuntimeString> {
Some(RuntimeString::from_bytes(
ctx,
cache.borrow().get(&(offset, size))?,
))
});

if cached.is_some() {
return cached;
}

let range = offset.try_into().ok()?..(offset + size).try_into().ok()?;
let data = ctx.scanned_data().get(range)?;
let digest = format!("{:x}", md5_hash::compute(data));
let result = RuntimeString::from_bytes(ctx, digest.as_bytes());

MD5_CACHE.with(|cache| {
cache.borrow_mut().insert((offset, size), digest);
});

Some(result)
}

#[module_export(name = "md5")]
fn md5_str(ctx: &mut ScanContext, s: RuntimeString) -> Option<RuntimeString> {
Some(RuntimeString::from_bytes(
ctx,
format!("{:x}", md5_hash::compute(s.as_bstr(ctx))),
))
}

#[module_export(name = "sha1")]
fn sha1(
ctx: &mut ScanContext,
offset: i64,
size: i64,
) -> Option<RuntimeString> {
let cached = SHA1_CACHE.with(|cache| -> Option<RuntimeString> {
Some(RuntimeString::from_bytes(
ctx,
cache.borrow().get(&(offset, size))?,
))
});

if cached.is_some() {
return cached;
}

let range = offset.try_into().ok()?..(offset + size).try_into().ok()?;
let data = ctx.scanned_data().get(range)?;
let mut hasher = sha1_hash::Sha1::new();

hasher.update(data);

let digest = format!("{:x}", hasher.finalize());
let result = RuntimeString::from_bytes(ctx, digest.as_bytes());

SHA1_CACHE.with(|cache| {
cache.borrow_mut().insert((offset, size), digest);
});

Some(result)
}

#[module_export(name = "sha1")]
fn sha1_str(ctx: &mut ScanContext, s: RuntimeString) -> Option<RuntimeString> {
let mut hasher = sha1_hash::Sha1::new();
hasher.update(s.as_bstr(ctx));

Some(RuntimeString::from_bytes(ctx, format!("{:x}", hasher.finalize())))
}

#[module_export(name = "sha256")]
fn sha256(
ctx: &mut ScanContext,
offset: i64,
size: i64,
) -> Option<RuntimeString> {
let cached = SHA256_CACHE.with(|cache| -> Option<RuntimeString> {
Some(RuntimeString::from_bytes(
ctx,
cache.borrow().get(&(offset, size))?,
))
});

if cached.is_some() {
return cached;
}

let range = offset.try_into().ok()?..(offset + size).try_into().ok()?;
let data = ctx.scanned_data().get(range)?;
let digest = sha256_digest(data);
let result = RuntimeString::from_bytes(ctx, digest.as_bytes());

SHA256_CACHE.with(|cache| {
cache.borrow_mut().insert((offset, size), digest);
});

Some(result)
}

#[module_export(name = "sha256")]
fn sha256_str(
ctx: &mut ScanContext,
s: RuntimeString,
) -> Option<RuntimeString> {
Some(RuntimeString::from_bytes(
ctx,
sha256_digest(s.as_bstr(ctx).as_bytes()),
))
}
2 changes: 2 additions & 0 deletions yara-x/src/modules/modules.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// File generated automatically by build.rs. Do not edit.
#[cfg(feature = "text-module")]
pub mod text;
#[cfg(feature = "hash-module")]
pub mod hash;
#[cfg(feature = "test_proto2-module")]
pub mod test_proto2;
#[cfg(feature = "time-module")]
Expand Down
13 changes: 13 additions & 0 deletions yara-x/src/modules/protos/hash.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
syntax = "proto2";

import "yara.proto";

option (yara.module_options) = {
name : "hash"
root_message: "Hash"
rust_module: "hash"
};

message Hash {
// This module contains only exported functions, and doesn't return any data
}
46 changes: 46 additions & 0 deletions yara-x/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2801,6 +2801,52 @@ fn test_defined_1() {
condition_false!(r#"defined true and false"#);
}

#[test]
#[cfg(feature = "hash-module")]
fn test_hash_module() {
rule_true!(
r#"
import "hash"
rule test {
condition:
hash.md5(0, filesize) == "6df23dc03f9b54cc38a0fc1483df6e21" and
hash.md5(3, 3) == "37b51d194a7513e45b56f6524f2d51f2" and
hash.md5(0, filesize) == hash.md5("foobarbaz") and
hash.md5(3, 3) == hash.md5("bar")
}
"#,
b"foobarbaz"
);

rule_true!(
r#"
import "hash"
rule test {
condition:
hash.sha1(0, filesize) == "5f5513f8822fdbe5145af33b64d8d970dcf95c6e" and
hash.sha1(3, 3) == "62cdb7020ff920e5aa642c3d4066950dd1f01f4d" and
hash.sha1(0, filesize) == hash.sha1("foobarbaz") and
hash.sha1(3, 3) == hash.sha1("bar")
}
"#,
b"foobarbaz"
);

rule_true!(
r#"
import "hash"
rule test {
condition:
hash.sha256(0, filesize) == "97df3588b5a3f24babc3851b372f0ba71a9dcdded43b14b9d06961bfc1707d9d" and
hash.sha256(3, 3) == "fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9" and
hash.sha256(0, filesize) == hash.sha256("foobarbaz") and
hash.sha256(3, 3) == hash.sha256("bar")
}
"#,
b"foobarbaz"
);
}

#[test]
#[cfg(feature = "test_proto2-module")]
fn test_defined_2() {
Expand Down

0 comments on commit 861e8fc

Please # to comment.