Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Operation: YARA Rules #468

Merged
merged 14 commits into from
Jan 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 78 additions & 77 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"jsqr": "^1.1.1",
"jsrsasign": "8.0.12",
"kbpgp": "^2.0.82",
"libyara-wasm": "0.0.11",
"lodash": "^4.17.11",
"loglevel": "^1.6.1",
"loglevel-message-prefix": "^3.0.0",
Expand Down
2 changes: 2 additions & 0 deletions src/core/Ingredient.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Ingredient {
this._value = null;
this.disabled = false;
this.hint = "";
this.rows = 0;
this.toggleValues = [];
this.target = null;
this.defaultIndex = 0;
Expand All @@ -45,6 +46,7 @@ class Ingredient {
this.defaultValue = ingredientConfig.value;
this.disabled = !!ingredientConfig.disabled;
this.hint = ingredientConfig.hint || false;
this.rows = ingredientConfig.rows || false;
this.toggleValues = ingredientConfig.toggleValues;
this.target = typeof ingredientConfig.target !== "undefined" ? ingredientConfig.target : null;
this.defaultIndex = typeof ingredientConfig.defaultIndex !== "undefined" ? ingredientConfig.defaultIndex : 0;
Expand Down
1 change: 1 addition & 0 deletions src/core/Operation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ class Operation {

if (ing.toggleValues) conf.toggleValues = ing.toggleValues;
if (ing.hint) conf.hint = ing.hint;
if (ing.rows) conf.rows = ing.rows;
if (ing.disabled) conf.disabled = ing.disabled;
if (ing.target) conf.target = ing.target;
if (ing.defaultIndex) conf.defaultIndex = ing.defaultIndex;
Expand Down
126 changes: 126 additions & 0 deletions src/core/operations/YARARules.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* @author Matt C [matt@artemisbot.uk]
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/

import Operation from "../Operation";
import OperationError from "../errors/OperationError";
import Yara from "libyara-wasm";

/**
* YARA Rules operation
*/
class YARARules extends Operation {

/**
* YARARules constructor
*/
constructor() {
super();

this.name = "YARA Rules";
this.module = "Yara";
this.description = "YARA is a tool developed at VirusTotal, primarily aimed at helping malware researchers to identify and classify malware samples. It matches based on rules specified by the user containing textual or binary patterns and a boolean expression. For help on writing rules, see the <a href='https://yara.readthedocs.io/en/latest/writingrules.html'>YARA documentation.</a>";
this.infoURL = "https://wikipedia.org/wiki/YARA";
this.inputType = "ArrayBuffer";
this.outputType = "string";
this.args = [
{
name: "Rules",
type: "text",
value: "",
rows: 5
},
{
name: "Show strings",
type: "boolean",
hint: "Show each match's data",
value: false
},
{
name: "Show string lengths",
type: "boolean",
hint: "Show the length of each match's data",
value: false
},
{
name: "Show metadata",
type: "boolean",
hint: "Show the metadata of each rule",
value: false
},
{
name: "Show counts",
type: "boolean",
hint: "Show the number of matches per rule",
value: true
}
];
}

/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
if (ENVIRONMENT_IS_WORKER())
self.sendStatusMessage("Instantiating YARA.");
const [rules, showStrings, showLengths, showMeta, showCounts] = args;
return new Promise((resolve, reject) => {
Yara().then(yara => {
if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Converting data for YARA.");
let matchString = "";

const inpArr = new Uint8Array(input); // Turns out embind knows that JS uint8array <==> C++ std::string

if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Running YARA matching.");

const resp = yara.run(inpArr, rules);

if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Processing data.");

if (resp.compileErrors.size() > 0) {
for (let i = 0; i < resp.compileErrors.size(); i++) {
const compileError = resp.compileErrors.get(i);
if (!compileError.warning) {
reject(new OperationError(`Error on line ${compileError.lineNumber}: ${compileError.message}`));
} else {
matchString += `Warning on line ${compileError.lineNumber}: ${compileError.message}`;
}
}
}
const matchedRules = resp.matchedRules;
for (let i = 0; i < matchedRules.size(); i++) {
const rule = matchedRules.get(i);
const matches = rule.resolvedMatches;
let meta = "";
if (showMeta && rule.metadata.size() > 0) {
meta += " [";
for (let j = 0; j < rule.metadata.size(); j++) {
meta += `${rule.metadata.get(j).identifier}: ${rule.metadata.get(j).data}, `;
}
meta = meta.slice(0, -2) + "]";
}
const countString = showCounts ? `${matches.size()} time${matches.size() > 1 ? "s" : ""}` : "";
if (matches.size() === 0 || !(showStrings || showLengths)) {
matchString += `Input matches rule "${rule.ruleName}"${meta}${countString.length > 0 ? ` ${countString}`: ""}.\n`;
} else {
matchString += `Rule "${rule.ruleName}"${meta} matches (${countString}):\n`;
for (let j = 0; j < matches.size(); j++) {
const match = matches.get(j);
if (showStrings || showLengths) {
matchString += `Pos ${match.location}, ${showLengths ? `length ${match.matchLength}, ` : ""}identifier ${match.stringIdentifier}${showStrings ? `, data: "${match.data}"` : ""}\n`;
}
}
}
}
resolve(matchString);
});
});
}

}

export default YARARules;
2 changes: 2 additions & 0 deletions src/web/HTMLIngredient.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class HTMLIngredient {
this.value = config.value;
this.disabled = config.disabled || false;
this.hint = config.hint || false;
this.rows = config.rows || false;
this.target = config.target;
this.defaultIndex = config.defaultIndex || 0;
this.toggleValues = config.toggleValues;
Expand Down Expand Up @@ -229,6 +230,7 @@ class HTMLIngredient {
class="form-control arg"
id="${this.id}"
arg-name="${this.name}"
rows="${this.rows ? this.rows : 3}"
${this.disabled ? "disabled" : ""}>${this.value}</textarea>
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
</div>`;
Expand Down
3 changes: 3 additions & 0 deletions src/web/Manager.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ class Manager {
this.addDynamicListener("#rec-list li.operation > div", "dblclick", this.recipe.operationChildDblclick, this.recipe);
this.addDynamicListener("#rec-list .dropdown-menu.toggle-dropdown a", "click", this.recipe.dropdownToggleClick, this.recipe);
this.addDynamicListener("#rec-list", "operationremove", this.recipe.opRemove.bind(this.recipe));
this.addDynamicListener("textarea.arg", "dragover", this.recipe.textArgDragover, this.recipe);
this.addDynamicListener("textarea.arg", "dragleave", this.recipe.textArgDragLeave, this.recipe);
this.addDynamicListener("textarea.arg", "drop", this.recipe.textArgDrop, this.recipe);

// Input
this.addMultiEventListener("#input-text", "keyup", this.input.inputChange, this.input);
Expand Down
63 changes: 63 additions & 0 deletions src/web/RecipeWaiter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,69 @@ class RecipeWaiter {
window.dispatchEvent(this.manager.statechange);
}

/**
* Handler for text argument dragover events.
* Gives the user a visual cue to show that items can be dropped here.
*
* @param {event} e
*/
textArgDragover (e) {
// This will be set if we're dragging an operation
if (e.dataTransfer.effectAllowed === "move")
return false;

e.stopPropagation();
e.preventDefault();
e.target.closest("textarea.arg").classList.add("dropping-file");
}

/**
* Handler for text argument dragleave events.
* Removes the visual cue.
*
* @param {event} e
*/
textArgDragLeave (e) {
e.stopPropagation();
e.preventDefault();
e.target.classList.remove("dropping-file");
}

/**
* Handler for text argument drop events.
* Loads the dragged data into the argument textarea.
*
* @param {event} e
*/
textArgDrop(e) {
// This will be set if we're dragging an operation
if (e.dataTransfer.effectAllowed === "move")
return false;

e.stopPropagation();
e.preventDefault();
const targ = e.target;
const file = e.dataTransfer.files[0];
const text = e.dataTransfer.getData("Text");

targ.classList.remove("dropping-file");

if (text) {
targ.value = text;
return;
}

if (file) {
const reader = new FileReader();
const self = this;
reader.onload = function (e) {
targ.value = e.target.result;
self.ingChange();
};
reader.readAsText(file);
}
}


/**
* Sets register values.
Expand Down
1 change: 1 addition & 0 deletions tests/operations/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import "./tests/Magic";
import "./tests/ParseTLV";
import "./tests/Media";
import "./tests/ToFromInsensitiveRegex";
import "./tests/YARA.mjs";

// Cannot test operations that use the File type yet
//import "./tests/SplitColourChannels";
Expand Down
24 changes: 24 additions & 0 deletions tests/operations/tests/YARA.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* YARA Rules tests.
*
* @author Matt C [matt@artemisbot.uk]
*
* @copyright Crown Copyright 2019
* @license Apache-2.0
*/
import TestRegister from "../TestRegister";

TestRegister.addTests([
{
name: "YARA Match: simple foobar",
input: "foobar foobar bar foo foobar",
expectedOutput: "Rule \"foo\" matches (4 times):\nPos 0, length 3, identifier $re1, data: \"foo\"\nPos 7, length 3, identifier $re1, data: \"foo\"\nPos 18, length 3, identifier $re1, data: \"foo\"\nPos 22, length 3, identifier $re1, data: \"foo\"\nRule \"bar\" matches (4 times):\nPos 3, length 3, identifier $re1, data: \"bar\"\nPos 10, length 3, identifier $re1, data: \"bar\"\nPos 14, length 3, identifier $re1, data: \"bar\"\nPos 25, length 3, identifier $re1, data: \"bar\"\n",
recipeConfig: [
{
"op": "YARA Rules",
"args": ["rule foo {strings: $re1 = /foo/ condition: $re1} rule bar {strings: $re1 = /bar/ condition: $re1}", true, true, true, true],
}
],
},
]);