-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
Introduces support for LZNT1 decompression, commonly leveraged by malware through RtlDecompressBuffer (closes gchq#534). The decompression logic is ported from go-ntfs, the test data is similar to malduck's. from: gchq#1675
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/** | ||
Check failure on line 1 in src/core/lib/LZNT1.mjs GitHub Actions / build (16.x)
Check failure on line 1 in src/core/lib/LZNT1.mjs GitHub Actions / main
Check failure on line 1 in src/core/lib/LZNT1.mjs GitHub Actions / build (18.x)
|
||
* | ||
Check failure on line 2 in src/core/lib/LZNT1.mjs GitHub Actions / build (16.x)
Check failure on line 2 in src/core/lib/LZNT1.mjs GitHub Actions / main
Check failure on line 2 in src/core/lib/LZNT1.mjs GitHub Actions / build (18.x)
|
||
* LZNT1 Decompress. | ||
Check failure on line 3 in src/core/lib/LZNT1.mjs GitHub Actions / build (16.x)
Check failure on line 3 in src/core/lib/LZNT1.mjs GitHub Actions / main
Check failure on line 3 in src/core/lib/LZNT1.mjs GitHub Actions / build (18.x)
|
||
* | ||
Check failure on line 4 in src/core/lib/LZNT1.mjs GitHub Actions / build (16.x)
Check failure on line 4 in src/core/lib/LZNT1.mjs GitHub Actions / main
Check failure on line 4 in src/core/lib/LZNT1.mjs GitHub Actions / build (18.x)
|
||
* @author 0xThiebaut [thiebaut.dev] | ||
Check failure on line 5 in src/core/lib/LZNT1.mjs GitHub Actions / build (16.x)
Check failure on line 5 in src/core/lib/LZNT1.mjs GitHub Actions / main
Check failure on line 5 in src/core/lib/LZNT1.mjs GitHub Actions / build (18.x)
|
||
* @copyright Crown Copyright 2023 | ||
Check failure on line 6 in src/core/lib/LZNT1.mjs GitHub Actions / build (16.x)
Check failure on line 6 in src/core/lib/LZNT1.mjs GitHub Actions / main
Check failure on line 6 in src/core/lib/LZNT1.mjs GitHub Actions / build (18.x)
|
||
* @license Apache-2.0 | ||
Check failure on line 7 in src/core/lib/LZNT1.mjs GitHub Actions / build (16.x)
Check failure on line 7 in src/core/lib/LZNT1.mjs GitHub Actions / main
Check failure on line 7 in src/core/lib/LZNT1.mjs GitHub Actions / build (18.x)
|
||
* | ||
Check failure on line 8 in src/core/lib/LZNT1.mjs GitHub Actions / build (16.x)
Check failure on line 8 in src/core/lib/LZNT1.mjs GitHub Actions / main
Check failure on line 8 in src/core/lib/LZNT1.mjs GitHub Actions / build (18.x)
|
||
* https://github.com/Velocidex/go-ntfs/blob/master/parser%2Flznt1.go | ||
Check failure on line 9 in src/core/lib/LZNT1.mjs GitHub Actions / build (16.x)
Check failure on line 9 in src/core/lib/LZNT1.mjs GitHub Actions / main
Check failure on line 9 in src/core/lib/LZNT1.mjs GitHub Actions / build (18.x)
|
||
*/ | ||
Check failure on line 10 in src/core/lib/LZNT1.mjs GitHub Actions / build (16.x)
Check failure on line 10 in src/core/lib/LZNT1.mjs GitHub Actions / main
Check failure on line 10 in src/core/lib/LZNT1.mjs GitHub Actions / build (18.x)
|
||
|
||
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 blockHeader = Utils.byteArrayToInt(compressed.slice(coffset, coffset + 2), "little"); | ||
coffset += 2; | ||
|
||
const size = blockHeader & SIZE_MASK; | ||
const blockEnd = coffset + size + 1; | ||
|
||
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 ((blockHeader & COMPRESSED_MASK) !== 0) { | ||
while (coffset < blockEnd) { | ||
let header = compressed[coffset++]; | ||
|
||
for (let i = 0; i < 8 && coffset < blockEnd; i++) { | ||
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 symbolOffset = (pointer >> (12 - displacement)) + 1; | ||
const symbolLength = (pointer & (0xFFF >> displacement)) + 2; | ||
const shiftOffset = decompressed.length - symbolOffset; | ||
|
||
for (let shiftDelta = 0; shiftDelta < symbolLength + 1; shiftDelta++) { | ||
const shift = shiftOffset + shiftDelta; | ||
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; | ||
} |
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; |
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: [] | ||
} | ||
], | ||
} | ||
]); |