Skip to content

Commit

Permalink
Merge pull request #28 from gdesmar/terminal_block
Browse files Browse the repository at this point in the history
Adding support for Unknown Extra block and appended data after Terminal block
  • Loading branch information
Matmaus authored Mar 12, 2024
2 parents 3db3f9e + f541c81 commit ffb9dd7
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 29 deletions.
3 changes: 1 addition & 2 deletions LnkParse3/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
class LnkParserError(Exception):
...
class LnkParserError(Exception): ...
37 changes: 37 additions & 0 deletions LnkParse3/extra/terminal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import hashlib
from LnkParse3.extra.lnk_extra_base import LnkExtraBase

"""
TerminalBlock (4 bytes): A 32-bit, unsigned integer that indicates the end of the extra data section.
This value MUST be less than 0x00000004.
No data should be expected or found after the terminal block, but in the rare case where it
does, this class will fulfill the undocumented feature of keeping track of it.
This can be the case with malicious shortcut files trying to hide their payload.
------------------------------------------------------------------
| 0-7b | 8-15b | 16-23b | 24-31b |
------------------------------------------------------------------
| <u_int32> BlockSignature == 0x00000000 - 0x00000003 |
------------------------------------------------------------------
| appended data |
------------------------------------------------------------------
"""


class Terminal(LnkExtraBase):
def name(self):
return "TERMINAL_BLOCK"

def appended_data(self):
start = 4
return self._raw[start:]

# Overwrite the usual size with the real appended data length
def size(self):
return 4 + len(self.appended_data())

def as_dict(self):
tmp = super().as_dict()
tmp["appended_data_sha256"] = hashlib.sha256(self.appended_data()).hexdigest()
return tmp
22 changes: 22 additions & 0 deletions LnkParse3/extra/unknown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import hashlib
from LnkParse3.extra.lnk_extra_base import LnkExtraBase

"""
This class does not represent a specific extra block defined in the [MS-SHLLINK] documentation.
It aims to cover cases where malicious shortcut files tries to hide their payload in an
undocumented block that still uses the right format and a valid length.
"""


class Unknown(LnkExtraBase):
def name(self):
return "UNKNOWN_BLOCK"

def extra_data(self):
start = 4
return self._raw[start:]

def as_dict(self):
tmp = super().as_dict()
tmp["extra_data_sha256"] = hashlib.sha256(self.extra_data()).hexdigest()
return tmp
16 changes: 14 additions & 2 deletions LnkParse3/extra_data.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import warnings
from struct import unpack
from struct import error as StructError

from LnkParse3.extra_factory import ExtraFactory
from LnkParse3.extra.unknown import Unknown
from LnkParse3.extra.terminal import Terminal

"""
EXTRA_DATA:
Zero or more ExtraData structures (section 2.5).
A structure consisting of zero or more property data blocks followed by a terminal block (section 2.5).
"""


Expand Down Expand Up @@ -36,11 +39,20 @@ def _iter(self):
if cls:
yield cls(indata=data, cp=self.cp)

# If there is data following the Terminal Block, we should take note of it and tell the user.
if len(rest) > 4 and unpack("<I", rest[:4])[0] < 0x00000004:
yield Terminal(indata=rest, cp=self.cp)

def as_dict(self):
res = {}
for extra in self:
try:
res[extra.name()] = extra.as_dict()
if isinstance(extra, Unknown):
if extra.name() not in res:
res[extra.name()] = []
res[extra.name()].append(extra.as_dict())
else:
res[extra.name()] = extra.as_dict()
except (StructError, ValueError) as e:
msg = "Error while parsing `%s` (%s)" % (extra.name(), e)
warnings.warn(msg)
Expand Down
3 changes: 2 additions & 1 deletion LnkParse3/extra_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from LnkParse3.extra.metadata import Metadata
from LnkParse3.extra.known_folder import KnownFolder
from LnkParse3.extra.shell_item import ShellItem
from LnkParse3.extra.unknown import Unknown

"""
------------------------------------------------------------------
Expand Down Expand Up @@ -57,7 +58,7 @@ def extra_class(self):
# Allow for no accompanying data for a reported size, observed in malicious files
try:
sig = str(hex(self._rsig()))[2:] # huh?
return self.EXTRA_SIGS.get(sig)
return self.EXTRA_SIGS.get(sig, Unknown)
except struct.error as e:
warnings.warn(f"Error while parsing extra's signature {e}")
return None
44 changes: 28 additions & 16 deletions LnkParse3/lnk_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,22 +256,34 @@ def nice_id(identifier):
cprint("EXTRA BLOCKS:", 1)
for extra_key, extra_value in self.extras.as_dict().items():
cprint(f"{extra_key}", 2)
for key, value in extra_value.items():
if extra_key == "METADATA_PROPERTIES_BLOCK" and isinstance(value, list):
cprint(f"{nice_id(key)}:", 3)
for storage in value:
cprint("Storage:", 4)
for storage_key, storage_value in storage.items():
if isinstance(storage_value, list):
cprint(f"{nice_id(storage_key)}:", 5)
for item in storage_value:
cprint("Property:", 6)
for item_key, item_value in item.items():
cprint(f"{nice_id(item_key)}: {item_value}", 7)
else:
cprint(f"{nice_id(storage_key)}: {storage_value}", 5)
else:
cprint(f"{nice_id(key)}: {value}", 3)
if extra_key == "UNKNOWN_BLOCK":
cprint("Block:", 3)
for list_value in extra_value:
for key, value in list_value.items():
cprint(f"{nice_id(key)}: {value}", 4)
else:
for key, value in extra_value.items():
if extra_key == "METADATA_PROPERTIES_BLOCK" and isinstance(
value, list
):
cprint(f"{nice_id(key)}:", 3)
for storage in value:
cprint("Storage:", 4)
for storage_key, storage_value in storage.items():
if isinstance(storage_value, list):
cprint(f"{nice_id(storage_key)}:", 5)
for item in storage_value:
cprint("Property:", 6)
for item_key, item_value in item.items():
cprint(
f"{nice_id(item_key)}: {item_value}", 7
)
else:
cprint(
f"{nice_id(storage_key)}: {storage_value}", 5
)
else:
cprint(f"{nice_id(key)}: {value}", 3)

def format_linkFlags(self):
return " | ".join(self.header.link_flags())
Expand Down
1 change: 1 addition & 0 deletions LnkParse3/text_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
the terminating null character are undefined and can have any value. The
undefined bytes MUST NOT be used.
"""

import warnings


Expand Down
4 changes: 3 additions & 1 deletion tests/json/shortcut_target_only.json
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
{"shortcut_target": ".\\a.txt"}
{
"shortcut_target": ".\\a.txt"
}
7 changes: 6 additions & 1 deletion tests/json/unknown_target.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"data": {},
"extra": {},
"extra": {
"TERMINAL_BLOCK": {
"appended_data_sha256": "d64c62e65398d37cd27e11fd729fa102016a05ba67f5020e17dfbd3b857dd96e",
"size": 67653
}
},
"header": {
"accessed_time": "2012-08-06T11:51:14+00:00",
"creation_time": "2012-08-06T11:51:14+00:00",
Expand Down
33 changes: 33 additions & 0 deletions tests/json/unknown_target.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Windows Shortcut Information:
Header Size: 76
Link CLSID: 00021401-0000-0000-C000-000000000046
Link Flags: HasTargetIDList | IsUnicode - (129)
File Flags: - (0)

Creation Timestamp: 2012-08-06 11:51:14.390625+00:00
Modified Timestamp: 2012-08-06 11:51:14.390625+00:00
Accessed Timestamp: 2012-08-06 11:51:14.390625+00:00

File Size: 0 (r: 68608)
Icon Index: 0
Window Style: SW_SHOWNORMAL
HotKey: UNSET - UNSET {0x0000}
Reserved0: 0
Reserved1: 0
Reserved2: 0

TARGETS:
Size: 877
Index: 78
ITEMS:
Volume Item
Flags: 0xe
Data: None
Unknown

DATA

EXTRA BLOCKS:
TERMINAL_BLOCK
Size: 67653
Appended data sha256: d64c62e65398d37cd27e11fd729fa102016a05ba67f5020e17dfbd3b857dd96e
28 changes: 22 additions & 6 deletions tests/test_samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_unwanted_attributes_are_not_printed_if_not_specified(self):

our = json.loads(mock_stdout.getvalue())

json_path = os.path.join(JSON_DIR, f"microsoft_example_not_all_attributes.json")
json_path = os.path.join(JSON_DIR, "microsoft_example_not_all_attributes.json")
with open(json_path, 'rb') as fp:
their = json.load(fp)

Expand All @@ -60,7 +60,7 @@ def test_readable_with_network_info(self):

our = mock_stdout.getvalue()

txt_path = os.path.join(JSON_DIR, f"readable_with_network_info.txt")
txt_path = os.path.join(JSON_DIR, "readable_with_network_info.txt")
with open(txt_path, 'r') as fp:
their = fp.read()

Expand All @@ -76,7 +76,7 @@ def test_readable_with_local_info(self):

our = mock_stdout.getvalue()

txt_path = os.path.join(JSON_DIR, f"readable_with_local_info.txt")
txt_path = os.path.join(JSON_DIR, "readable_with_local_info.txt")
with open(txt_path, 'r') as fp:
their = fp.read()

Expand All @@ -92,7 +92,7 @@ def test_print_shortcut_target_json(self):

our = json.loads(mock_stdout.getvalue())

json_path = os.path.join(JSON_DIR, f"shortcut_target_only.json")
json_path = os.path.join(JSON_DIR, "shortcut_target_only.json")
with open(json_path, 'rb') as fp:
their = json.load(fp)

Expand All @@ -108,8 +108,24 @@ def test_print_shortcut_target_readable(self):

our = mock_stdout.getvalue()

json_path = os.path.join(JSON_DIR, f"shortcut_target_only.txt")
with open(json_path, 'r') as fp:
txt_path = os.path.join(JSON_DIR, "shortcut_target_only.txt")
with open(txt_path, 'r') as fp:
their = fp.read()

self.assertEqual(our, their)

def test_print_unknown_target(self):
with open('tests/samples/unknown_target', 'rb') as indata:
lnk = LnkParse3.lnk_file(indata)

mock_stdout = StringIO()
with redirect_stdout(mock_stdout):
lnk.print_lnk_file(print_all=True)

our = mock_stdout.getvalue()

txt_path = os.path.join(JSON_DIR, "unknown_target.txt")
with open(txt_path, 'r') as fp:
their = fp.read()

self.assertEqual(our, their)
Expand Down

0 comments on commit ffb9dd7

Please sign in to comment.