Skip to content

Commit fe89efb

Browse files
committed
feat: implement console module
1 parent 1c64437 commit fe89efb

File tree

10 files changed

+251
-6
lines changed

10 files changed

+251
-6
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ object = { git = 'https://github.com/vthib/boreal-object', branch = "version-0.3
2323

2424
# yara-rust:
2525
# - Add ScanFlags::PROCESS_MEMORY: https://github.com/Hugal31/yara-rust/pull/130
26+
# - Add CallbackMsg::ConsoleLog: https://github.com/Hugal31/yara-rust/pull/139
2627
# yara:
2728
# - Add base address to elf entrypoint: https://github.com/VirusTotal/yara/pull/1989
2829
# - Fixes for macho module and process memory: https://github.com/VirusTotal/yara/pull/1995

boreal-cli/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ fn main() -> ExitCode {
136136
#[cfg(not(feature = "authenticode"))]
137137
let mut compiler = Compiler::new();
138138

139+
let _r = compiler.add_module(boreal::module::Console::with_callback(Box::new(|log| {
140+
println!("{log}");
141+
})));
142+
139143
compiler.set_params(
140144
boreal::compiler::CompilerParams::default()
141145
.fail_on_warnings(args.get_flag("fail_on_warnings"))

boreal-cli/tests/cli.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ rule a {
594594
.stderr("")
595595
.success();
596596
}
597+
597598
// Test when some inputs in a dir cannot be read
598599
#[test]
599600
#[cfg(unix)]
@@ -675,3 +676,28 @@ rule second {
675676
.stderr("")
676677
.success();
677678
}
679+
680+
#[test]
681+
fn test_console_log() {
682+
let rule_file = test_file(
683+
r#"
684+
import "console"
685+
686+
rule logger {
687+
condition:
688+
console.log("this is ", "a log")
689+
}"#,
690+
);
691+
692+
let input = test_file("");
693+
cmd()
694+
.arg(rule_file.path())
695+
.arg(input.path())
696+
.assert()
697+
.stdout(predicate::eq(format!(
698+
"this is a log\nlogger {}\n",
699+
input.path().display()
700+
)))
701+
.stderr("")
702+
.success();
703+
}

boreal/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,14 @@ free. If however someone can provide a valid use-case, this difference can be re
123123
- `pe.imphash()` is behind the _hash_ feature
124124
- [x] string
125125
- [x] time
126+
- [x] console
126127

127128
Modules not yet supported:
128129

129130
- [ ] cuckoo
130131
- [ ] dex
131132
- [ ] dotnet
132133
- [ ] magic
133-
- [ ] console
134134

135135
## Missing Features
136136

boreal/src/compiler/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,16 @@ struct ImportedModule {
8686
impl Compiler {
8787
/// Create a new object to compile YARA rules.
8888
///
89-
/// Almost available modules are enabled by default:
89+
/// Modules enabled by default:
9090
/// - `time`
9191
/// - `math`
9292
/// - `string`
9393
/// - `hash` if the `hash` feature is enabled
9494
/// - `elf`, `macho` and `pe` if the `object` feature is enabled
9595
///
96+
/// Modules disabled by default:
97+
/// - `console`
98+
///
9699
/// However, the pe module does not include signatures handling. To include it, you should have
97100
/// the `authenticode` feature enabled, and use [`Compiler::new_with_pe_signatures`]
98101
///

boreal/src/module/console.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use std::fmt::Write;
2+
use std::{collections::HashMap, sync::Arc};
3+
4+
use super::{EvalContext, Module, ModuleData, ModuleDataMap, StaticValue, Type, Value};
5+
6+
/// `console` module.
7+
pub struct Console {
8+
callback: Arc<Box<LogCallback>>,
9+
}
10+
11+
/// Type of callback called when a message is logged.
12+
pub type LogCallback = dyn Fn(String) + Send + Sync;
13+
14+
impl Module for Console {
15+
fn get_name(&self) -> &'static str {
16+
"console"
17+
}
18+
19+
fn get_static_values(&self) -> HashMap<&'static str, StaticValue> {
20+
[
21+
(
22+
"log",
23+
StaticValue::function(
24+
Self::log,
25+
vec![
26+
vec![Type::Bytes],
27+
vec![Type::Bytes, Type::Bytes],
28+
vec![Type::Integer],
29+
vec![Type::Bytes, Type::Integer],
30+
vec![Type::Float],
31+
vec![Type::Bytes, Type::Float],
32+
],
33+
Type::Integer,
34+
),
35+
),
36+
(
37+
"hex",
38+
StaticValue::function(
39+
Self::hex,
40+
vec![vec![Type::Integer], vec![Type::Bytes, Type::Integer]],
41+
Type::Integer,
42+
),
43+
),
44+
]
45+
.into()
46+
}
47+
48+
fn setup_new_scan(&self, data_map: &mut ModuleDataMap) {
49+
data_map.insert::<Self>(Data {
50+
callback: Arc::clone(&self.callback),
51+
});
52+
}
53+
}
54+
55+
pub struct Data {
56+
callback: Arc<Box<LogCallback>>,
57+
}
58+
59+
impl ModuleData for Console {
60+
type Data = Data;
61+
}
62+
63+
impl Console {
64+
/// Create a new console module with a callback.
65+
///
66+
/// The callback will be called when expressions using this module
67+
/// are used.
68+
#[must_use]
69+
pub fn with_callback(callback: Box<LogCallback>) -> Self {
70+
Self {
71+
callback: Arc::new(callback),
72+
}
73+
}
74+
75+
fn log(ctx: &mut EvalContext, args: Vec<Value>) -> Option<Value> {
76+
let mut args = args.into_iter();
77+
let mut res = String::new();
78+
add_value(args.next()?, &mut res)?;
79+
if let Some(arg) = args.next() {
80+
add_value(arg, &mut res)?;
81+
}
82+
83+
let data = ctx.module_data.get::<Console>()?;
84+
(data.callback)(res);
85+
86+
Some(Value::Integer(1))
87+
}
88+
89+
fn hex(ctx: &mut EvalContext, args: Vec<Value>) -> Option<Value> {
90+
let mut args = args.into_iter();
91+
let res = match args.next()? {
92+
Value::Integer(v) => format!("0x{v:x}"),
93+
value => {
94+
let mut res = String::new();
95+
add_value(value, &mut res)?;
96+
let v: i64 = args.next()?.try_into().ok()?;
97+
write!(&mut res, "0x{v:x}").ok()?;
98+
res
99+
}
100+
};
101+
102+
let data = ctx.module_data.get::<Console>()?;
103+
(data.callback)(res);
104+
105+
Some(Value::Integer(1))
106+
}
107+
}
108+
109+
fn add_value(value: Value, out: &mut String) -> Option<()> {
110+
match value {
111+
Value::Integer(v) => write!(out, "{v}").ok(),
112+
Value::Float(v) => write!(out, "{v}").ok(),
113+
Value::Bytes(v) => {
114+
for byte in v {
115+
for b in std::ascii::escape_default(byte) {
116+
out.push(char::from(b));
117+
}
118+
}
119+
Some(())
120+
}
121+
_ => None,
122+
}
123+
}

boreal/src/module/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ use std::sync::Arc;
4444
use crate::memory::{Memory, Region};
4545
use crate::regex::Regex;
4646

47+
mod console;
48+
pub use console::{Console, LogCallback};
49+
4750
mod time;
4851
pub use time::Time;
4952

boreal/tests/it/modules.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::utils::{check, check_boreal, check_err};
1+
use std::sync::Mutex;
2+
3+
use crate::utils::{check, check_boreal, check_err, Compiler};
24

35
#[track_caller]
46
fn check_tests_err(condition: &str, expected_err: &str) {
@@ -441,6 +443,71 @@ fn test_module_hash() {
441443
test("not defined hash.crc32(100, 2)");
442444
}
443445

446+
#[test]
447+
fn test_module_console() {
448+
static LOGS: Mutex<Vec<String>> = Mutex::new(Vec::new());
449+
let mut compiler = Compiler::new();
450+
let res = compiler
451+
.compiler
452+
.add_module(boreal::module::Console::with_callback(Box::new(|log| {
453+
LOGS.lock().unwrap().push(log);
454+
})));
455+
assert!(res);
456+
457+
compiler.add_rules(
458+
r#"import "console"
459+
rule a {
460+
condition:
461+
console.log("a\n\xBAc\x00-") and
462+
console.log("bytes: ", "bo\tr") and
463+
console.log(15) and
464+
console.log("value", 15) and
465+
console.log(3.59231) and
466+
console.log("float: ", 015.340) and
467+
console.log("", "bar") and
468+
console.hex(12872) and
469+
console.hex("val: ", -12872) and
470+
false and
471+
console.log("missing")
472+
473+
}"#,
474+
);
475+
476+
let mut checker = compiler.into_checker();
477+
checker.check(b"", false);
478+
479+
assert_eq!(
480+
&*LOGS.lock().unwrap(),
481+
&[
482+
r"a\n\xbac\x00-",
483+
r"bytes: bo\tr",
484+
"15",
485+
"value15",
486+
"3.59231",
487+
"float: 15.34",
488+
"bar",
489+
"0x3248",
490+
"val: 0xffffffffffffcdb8",
491+
]
492+
);
493+
494+
let yara_logs = checker.capture_yara_logs(b"");
495+
assert_eq!(
496+
yara_logs,
497+
&[
498+
r"a\x0a\xbac\x00-",
499+
r"bytes: bo\x09r",
500+
"15",
501+
"value15",
502+
"3.592310",
503+
"float: 15.340000",
504+
"bar",
505+
"0x3248",
506+
"val: 0xffffffffffffcdb8",
507+
]
508+
);
509+
}
510+
444511
#[test]
445512
fn test_module_iterable_imbricated() {
446513
check_ok(

boreal/tests/it/utils.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub struct Checker {
1515
}
1616

1717
pub struct Compiler {
18-
compiler: boreal::Compiler,
18+
pub compiler: boreal::Compiler,
1919
yara_compiler: Option<yara::Compiler>,
2020
}
2121

@@ -473,6 +473,24 @@ impl Checker {
473473
yara_scanner: self.yara_rules.as_ref().map(|v| v.scanner().unwrap()),
474474
}
475475
}
476+
477+
pub fn capture_yara_logs(&mut self, mem: &[u8]) -> Vec<String> {
478+
let mut logs = Vec::new();
479+
480+
let Some(rules) = &mut self.yara_rules else {
481+
return logs;
482+
};
483+
484+
rules
485+
.scan_mem_callback(mem, 0, |msg| {
486+
if let yara::CallbackMsg::ConsoleLog(log) = msg {
487+
logs.push(log.to_string_lossy().to_string());
488+
}
489+
yara::CallbackReturn::Continue
490+
})
491+
.unwrap();
492+
logs
493+
}
476494
}
477495

478496
#[derive(Debug)]

0 commit comments

Comments
 (0)