Skip to content

Commit

Permalink
Add support for LZNT1 decompression.
Browse files Browse the repository at this point in the history
  • Loading branch information
0xThiebaut committed Dec 25, 2023
1 parent 6ed9d45 commit 30b1456
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ npm-debug.log
travis.log
build
.vscode
.idea
.*.swp
src/core/config/modules/*
src/core/config/OperationConfig.json
Expand Down
3 changes: 2 additions & 1 deletion src/core/config/Categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,8 @@
"LZMA Decompress",
"LZMA Compress",
"LZ4 Decompress",
"LZ4 Compress"
"LZ4 Compress",
"LZNT1 Decompress"
]
},
{
Expand Down
88 changes: 88 additions & 0 deletions src/core/lib/LZNT1.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
*
* LZNT1 Decompress.
*
* @author 0xThiebaut [thiebaut.dev]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*
* https://github.com/Velocidex/go-ntfs/blob/master/parser%2Flznt1.go
*/

import Utils from "../Utils.mjs";
import OperationError from "../errors/OperationError.mjs";

const COMPRESSED_MASK = 1 << 15,
SIZE_MASK = (1 << 12) - 1;

/**
* @param {number} offset
* @returns {number}
*/
function getDisplacement(offset) {
let result = 0;
while (offset >= 0x10) {
offset >>= 1;
result += 1;
}
return result;
}

/**
* @param {byteArray} compressed
* @returns {byteArray}
*/
export function decompress(compressed) {
const decompressed = Array();
let coffset = 0;

while (coffset + 2 <= compressed.length) {
const doffset = decompressed.length;

const block_header = Utils.byteArrayToInt(compressed.slice(coffset, coffset + 2), "little");

Check failure on line 42 in src/core/lib/LZNT1.mjs

View workflow job for this annotation

GitHub Actions / main

Identifier 'block_header' is not in camel case
coffset += 2;

const size = block_header & SIZE_MASK;

Check failure on line 45 in src/core/lib/LZNT1.mjs

View workflow job for this annotation

GitHub Actions / main

Identifier 'block_header' is not in camel case
const block_end = coffset + size + 1;

Check failure on line 46 in src/core/lib/LZNT1.mjs

View workflow job for this annotation

GitHub Actions / main

Identifier 'block_end' is not in camel case

if (size === 0) {
break;
} else if (compressed.length < coffset + size) {
throw new OperationError("Malformed LZNT1 stream: Block too small! Has the stream been truncated?");
}

if ((block_header & COMPRESSED_MASK) !== 0) {

Check failure on line 54 in src/core/lib/LZNT1.mjs

View workflow job for this annotation

GitHub Actions / main

Identifier 'block_header' is not in camel case
while (coffset < block_end) {

Check failure on line 55 in src/core/lib/LZNT1.mjs

View workflow job for this annotation

GitHub Actions / main

Identifier 'block_end' is not in camel case
let header = compressed[coffset++];

for (let mask_idx = 0; mask_idx < 8 && coffset < block_end; mask_idx++) {

Check failure on line 58 in src/core/lib/LZNT1.mjs

View workflow job for this annotation

GitHub Actions / main

Identifier 'mask_idx' is not in camel case

Check failure on line 58 in src/core/lib/LZNT1.mjs

View workflow job for this annotation

GitHub Actions / main

Identifier 'mask_idx' is not in camel case

Check failure on line 58 in src/core/lib/LZNT1.mjs

View workflow job for this annotation

GitHub Actions / main

Identifier 'block_end' is not in camel case

Check failure on line 58 in src/core/lib/LZNT1.mjs

View workflow job for this annotation

GitHub Actions / main

Identifier 'mask_idx' is not in camel case
if ((header & 1) === 0) {
decompressed.push(compressed[coffset++]);
} else {
const pointer = Utils.byteArrayToInt(compressed.slice(coffset, coffset + 2), "little");
coffset += 2;

const displacement = getDisplacement(decompressed.length - doffset - 1);
const symbol_offset = (pointer >> (12 - displacement)) + 1;

Check failure on line 66 in src/core/lib/LZNT1.mjs

View workflow job for this annotation

GitHub Actions / main

Identifier 'symbol_offset' is not in camel case
const symbol_length = (pointer & (0xFFF >> displacement)) + 2;
const shift_offset = decompressed.length - symbol_offset;

for (let shift_delta = 0; shift_delta < symbol_length + 1; shift_delta++) {
const shift = shift_offset + shift_delta;
if (shift < 0 || decompressed.length <= shift) {
throw new OperationError("Malformed LZNT1 stream: Invalid shift!");
}
decompressed.push(decompressed[shift]);
}
}
header >>= 1;
}
}
} else {
decompressed.push(...compressed.slice(coffset, coffset + size + 1));
coffset += size + 1;
}
}

return decompressed;
}
41 changes: 41 additions & 0 deletions src/core/operations/LZNT1Decompress.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @author 0xThiebaut [thiebaut.dev]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/

import Operation from "../Operation.mjs";
import {decompress} from "../lib/LZNT1.mjs";

/**
* LZNT1 Decompress operation
*/
class LZNT1Decompress extends Operation {

/**
* LZNT1 Decompress constructor
*/
constructor() {
super();

this.name = "LZNT1 Decompress";
this.module = "Compression";
this.description = "Decompresses data using the LZNT1 algorithm.<br><br>Similar to the Windows API <code>RtlDecompressBuffer</code>.";
this.infoURL = "https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xca/5655f4a3-6ba4-489b-959f-e1f407c52f15";
this.inputType = "byteArray";
this.outputType = "byteArray";
this.args = [];
}

/**
* @param {byteArray} input
* @param {Object[]} args
* @returns {byteArray}
*/
run(input, args) {
return decompress(input);
}

}

export default LZNT1Decompress;
3 changes: 2 additions & 1 deletion src/web/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
"Training branch predictor...",
"Timing cache hits...",
"Speculatively executing recipes...",
"Adding LLM hallucinations..."
"Adding LLM hallucinations...",
"Decompressing malware..."
];

// Shuffle array using Durstenfeld algorithm
Expand Down
4 changes: 4 additions & 0 deletions tests/node/tests/operations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,10 @@ WWFkYSBZYWRh\r
assert.strictEqual(chef.keccak("Flea Market").toString(), "c2a06880b19e453ee5440e8bd4c2024bedc15a6630096aa3f609acfd2b8f15f27cd293e1cc73933e81432269129ce954a6138889ce87831179d55dcff1cc7587");
}),

it("LZNT1 Decompress", () => {
assert.strictEqual(chef.LZNT1Decompress("\x1a\xb0\x00compress\x00edtestda\x04ta\x07\x88alot").toString(), "compressedtestdatacompressedalot");
}),

it("MD6", () => {
assert.strictEqual(chef.MD6("Head Over Heels", {key: "arty"}).toString(), "d8f7fe4931fbaa37316f76283d5f615f50ddd54afdc794b61da522556aee99ad");
}),
Expand Down
1 change: 1 addition & 0 deletions tests/operations/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import "./tests/JSONtoCSV.mjs";
import "./tests/JWTDecode.mjs";
import "./tests/JWTSign.mjs";
import "./tests/JWTVerify.mjs";
import "./tests/LZNT1Decompress.mjs";
import "./tests/MS.mjs";
import "./tests/Magic.mjs";
import "./tests/MorseCode.mjs";
Expand Down
22 changes: 22 additions & 0 deletions tests/operations/tests/LZNT1Decompress.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* LZNT1 Decompress tests.
*
* @author 0xThiebaut [thiebaut.dev]
* @copyright Crown Copyright 2023
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";

TestRegister.addTests([
{
name: "LZNT1 Decompress",
input: "\x1a\xb0\x00compress\x00edtestda\x04ta\x07\x88alot",
expectedOutput: "compressedtestdatacompressedalot",
recipeConfig: [
{
op: "LZNT1 Decompress",
args: []
}
],
}
]);

0 comments on commit 30b1456

Please sign in to comment.