Skip to content

Commit 30b1456

Browse files
committed
Add support for LZNT1 decompression.
1 parent 6ed9d45 commit 30b1456

File tree

8 files changed

+161
-2
lines changed

8 files changed

+161
-2
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ npm-debug.log
33
travis.log
44
build
55
.vscode
6+
.idea
67
.*.swp
78
src/core/config/modules/*
89
src/core/config/OperationConfig.json

src/core/config/Categories.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,8 @@
351351
"LZMA Decompress",
352352
"LZMA Compress",
353353
"LZ4 Decompress",
354-
"LZ4 Compress"
354+
"LZ4 Compress",
355+
"LZNT1 Decompress"
355356
]
356357
},
357358
{

src/core/lib/LZNT1.mjs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
*
3+
* LZNT1 Decompress.
4+
*
5+
* @author 0xThiebaut [thiebaut.dev]
6+
* @copyright Crown Copyright 2023
7+
* @license Apache-2.0
8+
*
9+
* https://github.com/Velocidex/go-ntfs/blob/master/parser%2Flznt1.go
10+
*/
11+
12+
import Utils from "../Utils.mjs";
13+
import OperationError from "../errors/OperationError.mjs";
14+
15+
const COMPRESSED_MASK = 1 << 15,
16+
SIZE_MASK = (1 << 12) - 1;
17+
18+
/**
19+
* @param {number} offset
20+
* @returns {number}
21+
*/
22+
function getDisplacement(offset) {
23+
let result = 0;
24+
while (offset >= 0x10) {
25+
offset >>= 1;
26+
result += 1;
27+
}
28+
return result;
29+
}
30+
31+
/**
32+
* @param {byteArray} compressed
33+
* @returns {byteArray}
34+
*/
35+
export function decompress(compressed) {
36+
const decompressed = Array();
37+
let coffset = 0;
38+
39+
while (coffset + 2 <= compressed.length) {
40+
const doffset = decompressed.length;
41+
42+
const block_header = Utils.byteArrayToInt(compressed.slice(coffset, coffset + 2), "little");
43+
coffset += 2;
44+
45+
const size = block_header & SIZE_MASK;
46+
const block_end = coffset + size + 1;
47+
48+
if (size === 0) {
49+
break;
50+
} else if (compressed.length < coffset + size) {
51+
throw new OperationError("Malformed LZNT1 stream: Block too small! Has the stream been truncated?");
52+
}
53+
54+
if ((block_header & COMPRESSED_MASK) !== 0) {
55+
while (coffset < block_end) {
56+
let header = compressed[coffset++];
57+
58+
for (let mask_idx = 0; mask_idx < 8 && coffset < block_end; mask_idx++) {
59+
if ((header & 1) === 0) {
60+
decompressed.push(compressed[coffset++]);
61+
} else {
62+
const pointer = Utils.byteArrayToInt(compressed.slice(coffset, coffset + 2), "little");
63+
coffset += 2;
64+
65+
const displacement = getDisplacement(decompressed.length - doffset - 1);
66+
const symbol_offset = (pointer >> (12 - displacement)) + 1;
67+
const symbol_length = (pointer & (0xFFF >> displacement)) + 2;
68+
const shift_offset = decompressed.length - symbol_offset;
69+
70+
for (let shift_delta = 0; shift_delta < symbol_length + 1; shift_delta++) {
71+
const shift = shift_offset + shift_delta;
72+
if (shift < 0 || decompressed.length <= shift) {
73+
throw new OperationError("Malformed LZNT1 stream: Invalid shift!");
74+
}
75+
decompressed.push(decompressed[shift]);
76+
}
77+
}
78+
header >>= 1;
79+
}
80+
}
81+
} else {
82+
decompressed.push(...compressed.slice(coffset, coffset + size + 1));
83+
coffset += size + 1;
84+
}
85+
}
86+
87+
return decompressed;
88+
}
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @author 0xThiebaut [thiebaut.dev]
3+
* @copyright Crown Copyright 2023
4+
* @license Apache-2.0
5+
*/
6+
7+
import Operation from "../Operation.mjs";
8+
import {decompress} from "../lib/LZNT1.mjs";
9+
10+
/**
11+
* LZNT1 Decompress operation
12+
*/
13+
class LZNT1Decompress extends Operation {
14+
15+
/**
16+
* LZNT1 Decompress constructor
17+
*/
18+
constructor() {
19+
super();
20+
21+
this.name = "LZNT1 Decompress";
22+
this.module = "Compression";
23+
this.description = "Decompresses data using the LZNT1 algorithm.<br><br>Similar to the Windows API <code>RtlDecompressBuffer</code>.";
24+
this.infoURL = "https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xca/5655f4a3-6ba4-489b-959f-e1f407c52f15";
25+
this.inputType = "byteArray";
26+
this.outputType = "byteArray";
27+
this.args = [];
28+
}
29+
30+
/**
31+
* @param {byteArray} input
32+
* @param {Object[]} args
33+
* @returns {byteArray}
34+
*/
35+
run(input, args) {
36+
return decompress(input);
37+
}
38+
39+
}
40+
41+
export default LZNT1Decompress;

src/web/html/index.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@
6262
"Training branch predictor...",
6363
"Timing cache hits...",
6464
"Speculatively executing recipes...",
65-
"Adding LLM hallucinations..."
65+
"Adding LLM hallucinations...",
66+
"Decompressing malware..."
6667
];
6768

6869
// Shuffle array using Durstenfeld algorithm

tests/node/tests/operations.mjs

+4
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,10 @@ WWFkYSBZYWRh\r
635635
assert.strictEqual(chef.keccak("Flea Market").toString(), "c2a06880b19e453ee5440e8bd4c2024bedc15a6630096aa3f609acfd2b8f15f27cd293e1cc73933e81432269129ce954a6138889ce87831179d55dcff1cc7587");
636636
}),
637637

638+
it("LZNT1 Decompress", () => {
639+
assert.strictEqual(chef.LZNT1Decompress("\x1a\xb0\x00compress\x00edtestda\x04ta\x07\x88alot").toString(), "compressedtestdatacompressedalot");
640+
}),
641+
638642
it("MD6", () => {
639643
assert.strictEqual(chef.MD6("Head Over Heels", {key: "arty"}).toString(), "d8f7fe4931fbaa37316f76283d5f615f50ddd54afdc794b61da522556aee99ad");
640644
}),

tests/operations/index.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import "./tests/JSONtoCSV.mjs";
6262
import "./tests/JWTDecode.mjs";
6363
import "./tests/JWTSign.mjs";
6464
import "./tests/JWTVerify.mjs";
65+
import "./tests/LZNT1Decompress.mjs";
6566
import "./tests/MS.mjs";
6667
import "./tests/Magic.mjs";
6768
import "./tests/MorseCode.mjs";
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* LZNT1 Decompress tests.
3+
*
4+
* @author 0xThiebaut [thiebaut.dev]
5+
* @copyright Crown Copyright 2023
6+
* @license Apache-2.0
7+
*/
8+
import TestRegister from "../../lib/TestRegister.mjs";
9+
10+
TestRegister.addTests([
11+
{
12+
name: "LZNT1 Decompress",
13+
input: "\x1a\xb0\x00compress\x00edtestda\x04ta\x07\x88alot",
14+
expectedOutput: "compressedtestdatacompressedalot",
15+
recipeConfig: [
16+
{
17+
op: "LZNT1 Decompress",
18+
args: []
19+
}
20+
],
21+
}
22+
]);

0 commit comments

Comments
 (0)