Merge branch 'develop' into issues/issue1418

This commit is contained in:
ikelos
2025-01-01 13:15:11 +00:00
committed by GitHub
163 changed files with 2539 additions and 1444 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
name: Black python linter
name: Black python formatter
on: [push, pull_request]
+15
View File
@@ -0,0 +1,15 @@
---
name: Ruff
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v1
with:
args: check
src: "."
+7 -2
View File
@@ -42,8 +42,13 @@ jobs:
- name: Testing...
run: |
pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k test_windows -v
pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k test_linux -v
# VolShell
pytest ./test/test_volatility.py --volatility=volshell.py --image-dir=./test_images -k test_windows_volshell -v
pytest ./test/test_volatility.py --volatility=volshell.py --image-dir=./test_images -k test_linux_volshell -v
# Volatility
pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k "test_windows and not test_windows_volshell" -v
pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k "test_linux and not test_linux_volshell" -v
- name: Clean up post-test
run: |
+31 -14
View File
@@ -28,10 +28,10 @@ class BannerCacheGenerator:
def run(self):
context = contexts.Context()
json_output = {'version': 1}
json_output = {"version": 1}
path = self._path
filename = '*'
filename = "*"
for banner_cache in [linux.LinuxBannerCache, mac.MacBannerCache]:
sub_path = banner_cache.os
@@ -39,37 +39,54 @@ class BannerCacheGenerator:
for extension in constants.ISF_EXTENSIONS:
# Hopefully these will not be large lists, otherwise this might be slow
try:
for found in pathlib.Path(path).joinpath(sub_path).resolve().rglob(filename + extension):
for found in (
pathlib.Path(path)
.joinpath(sub_path)
.resolve()
.rglob(filename + extension)
):
potentials.append(found.as_uri())
except FileNotFoundError:
# If there's no linux symbols, don't cry about it
pass
new_banners = banner_cache.read_new_banners(context, 'BannerServer', potentials, banner_cache.symbol_name,
banner_cache.os, progress_callback = PrintedProgress())
new_banners = banner_cache.read_new_banners(
context,
"BannerServer",
potentials,
banner_cache.symbol_name,
banner_cache.os,
progress_callback=PrintedProgress(),
)
result_banners = {}
for new_banner in new_banners:
# Only accept file schemes
value = [self.convert_url(url) for url in new_banners[new_banner] if
urllib.parse.urlparse(url).scheme == 'file']
value = [
self.convert_url(url)
for url in new_banners[new_banner]
if urllib.parse.urlparse(url).scheme == "file"
]
if value and new_banner:
# Convert files into URLs
result_banners[str(base64.b64encode(new_banner), 'latin-1')] = value
result_banners[str(base64.b64encode(new_banner), "latin-1")] = value
json_output[banner_cache.os] = result_banners
output_path = os.path.join(self._path, 'banners.json')
with open(output_path, 'w') as fp:
output_path = os.path.join(self._path, "banners.json")
with open(output_path, "w") as fp:
vollog.warning(f"Banners file written to {output_path}")
json.dump(json_output, fp)
if __name__ == '__main__':
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--path', default = os.path.dirname(__file__))
parser.add_argument('--urlprefix', help = 'Web prefix that will eventually serve the ISF files',
default = 'http://localhost/symbols')
parser.add_argument("--path", default=os.path.dirname(__file__))
parser.add_argument(
"--urlprefix",
help="Web prefix that will eventually serve the ISF files",
default="http://localhost/symbols",
)
args = parser.parse_args()
+224 -116
View File
@@ -15,17 +15,17 @@ class VolatilityImage:
filepath: str = ""
vol2_profile: str = ""
vol2_imageinfo_time: float = None
vol2_plugin_parameters: Dict[str, List[str]] = field(default_factory = dict)
vol3_plugin_parameters: Dict[str, List[str]] = field(default_factory = dict)
rekall_plugin_parameters: Dict[str, List[str]] = field(default_factory = dict)
vol2_plugin_parameters: Dict[str, List[str]] = field(default_factory=dict)
vol3_plugin_parameters: Dict[str, List[str]] = field(default_factory=dict)
rekall_plugin_parameters: Dict[str, List[str]] = field(default_factory=dict)
@dataclass
class VolatilityPlugin:
name: str = ""
vol2_plugin_parameters: List[str] = field(default_factory = list)
vol3_plugin_parameters: List[str] = field(default_factory = list)
rekall_plugin_parameters: List[str] = field(default_factory = list)
vol2_plugin_parameters: List[str] = field(default_factory=list)
vol3_plugin_parameters: List[str] = field(default_factory=list)
rekall_plugin_parameters: List[str] = field(default_factory=list)
class VolatilityTest:
@@ -39,32 +39,50 @@ class VolatilityTest:
def result_titles(self) -> List[str]:
return [self.long_name]
def create_prerequisites(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str) -> None:
def create_prerequisites(
self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str
) -> None:
pass
def create_results(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str) -> List[float]:
def create_results(
self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str
) -> List[float]:
self.create_prerequisites(plugin, image, image_hash)
# Volatility 2 Test
print(f"[*] Testing {self.short_name} {plugin.name} with image {image.filepath}")
print(
f"[*] Testing {self.short_name} {plugin.name} with image {image.filepath}"
)
os.chdir(self.path)
cmd = self.plugin_cmd(plugin, image)
start_time = time.perf_counter()
try:
completed = subprocess.run(cmd, cwd = self.path, capture_output = True, timeout = 420)
completed = subprocess.run(
cmd, cwd=self.path, capture_output=True, timeout=420
)
except subprocess.TimeoutExpired as excp:
completed = excp
end_time = time.perf_counter()
total_time = end_time - start_time
print(f" Tested {self.short_name} {plugin.name} with image {image.filepath}: {total_time}")
print(
f" Tested {self.short_name} {plugin.name} with image {image.filepath}: {total_time}"
)
with open(
os.path.join(self.output_directory, f'{self.short_name}_{plugin.name}_{image_hash}_stdout'),
"wb") as f:
os.path.join(
self.output_directory,
f"{self.short_name}_{plugin.name}_{image_hash}_stdout",
),
"wb",
) as f:
f.write(completed.stdout)
if completed.stderr:
with open(
os.path.join(self.output_directory, f'{self.short_name}_{plugin.name}_{image_hash}_stderr'),
"wb") as f:
os.path.join(
self.output_directory,
f"{self.short_name}_{plugin.name}_{image_hash}_stderr",
),
"wb",
) as f:
f.write(completed.stderr)
return [total_time]
@@ -77,31 +95,57 @@ class Volatility2Test(VolatilityTest):
long_name = "Volatility 2"
def plugin_cmd(self, plugin: VolatilityPlugin, image: VolatilityImage):
return ["python2", "-u", "vol.py", "-f", image.filepath, "--profile", image.vol2_profile
] + plugin.vol2_plugin_parameters + image.vol2_plugin_parameters.get(plugin.name, [])
return (
[
"python2",
"-u",
"vol.py",
"-f",
image.filepath,
"--profile",
image.vol2_profile,
]
+ plugin.vol2_plugin_parameters
+ image.vol2_plugin_parameters.get(plugin.name, [])
)
def result_titles(self):
return [self.long_name, "Imageinfo", f"{self.long_name} + Imageinfo"]
def create_results(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash) -> List[float]:
def create_results(
self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash
) -> List[float]:
result = super().create_results(plugin, image, image_hash)
result += [image.vol2_imageinfo_time, result[0] + image.vol2_imageinfo_time]
return result
def create_prerequisites(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash):
def create_prerequisites(
self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash
):
# Volatility 2 image info
if not image.vol2_profile:
print(f"[*] Testing {self.short_name} imageinfo with image {image.filepath}")
print(
f"[*] Testing {self.short_name} imageinfo with image {image.filepath}"
)
os.chdir(self.path)
cmd = ["python2", "-u", "vol.py", "-f", image.filepath, "imageinfo"]
start_time = time.perf_counter()
vol2_completed = subprocess.run(cmd, cwd = self.path, capture_output = True)
vol2_completed = subprocess.run(cmd, cwd=self.path, capture_output=True)
end_time = time.perf_counter()
image.vol2_imageinfo_time = end_time - start_time
print(f" Tested volatility2 imageinfo with image {image.filepath}: {end_time - start_time}")
with open(os.path.join(self.output_directory, f'vol2_imageinfo_{image_hash}_stdout'), "wb") as f:
print(
f" Tested volatility2 imageinfo with image {image.filepath}: {end_time - start_time}"
)
with open(
os.path.join(
self.output_directory, f"vol2_imageinfo_{image_hash}_stdout"
),
"wb",
) as f:
f.write(vol2_completed.stdout)
image.vol2_profile = re.search(b"Suggested Profile\(s\) : ([^,]+)", vol2_completed.stdout)[1]
image.vol2_profile = re.search(
rb"Suggested Profile\(s\) : ([^,]+)", vol2_completed.stdout
)[1]
class RekallTest(VolatilityTest):
@@ -113,11 +157,16 @@ class RekallTest(VolatilityTest):
plugin.rekall_plugin_parameters = plugin.vol2_plugin_parameters
if not image.rekall_plugin_parameters:
image.rekall_plugin_parameters = image.vol2_plugin_parameters
return ["rekall", "-f", image.filepath] + plugin.rekall_plugin_parameters + image.rekall_plugin_parameters.get(
plugin.name, [])
return (
["rekall", "-f", image.filepath]
+ plugin.rekall_plugin_parameters
+ image.rekall_plugin_parameters.get(plugin.name, [])
)
def create_prerequisites(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str) -> None:
shutil.rmtree('/home/mike/.rekall_cache/sessions')
def create_prerequisites(
self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str
) -> None:
shutil.rmtree("/home/mike/.rekall_cache/sessions")
class Volatility3Test(VolatilityTest):
@@ -125,14 +174,18 @@ class Volatility3Test(VolatilityTest):
long_name = "Volatility 3"
def plugin_cmd(self, plugin: VolatilityPlugin, image: VolatilityImage) -> List[str]:
return [
"python",
"-u",
"vol.py",
"-q",
"-f",
image.filepath,
] + plugin.vol3_plugin_parameters + image.vol3_plugin_parameters.get(plugin.name, [])
return (
[
"python",
"-u",
"vol.py",
"-q",
"-f",
image.filepath,
]
+ plugin.vol3_plugin_parameters
+ image.vol3_plugin_parameters.get(plugin.name, [])
)
class Volatility3PyPyTest(VolatilityTest):
@@ -140,26 +193,32 @@ class Volatility3PyPyTest(VolatilityTest):
long_name = "Volatility 3 (PyPy)"
def plugin_cmd(self, plugin: VolatilityPlugin, image: VolatilityImage) -> List[str]:
return [
"pypy3",
"-u",
"vol.py",
"-q",
"-f",
image.filepath,
] + plugin.vol3_plugin_parameters + image.vol3_plugin_parameters.get(plugin.name, [])
return (
[
"pypy3",
"-u",
"vol.py",
"-q",
"-f",
image.filepath,
]
+ plugin.vol3_plugin_parameters
+ image.vol3_plugin_parameters.get(plugin.name, [])
)
class VolatilityTester:
def __init__(self,
images: List[VolatilityImage],
plugins: List[VolatilityPlugin],
frameworks: List[str],
output_dir: str,
vol2_path: str = None,
vol3_path: str = None,
rekall_path = None):
def __init__(
self,
images: List[VolatilityImage],
plugins: List[VolatilityPlugin],
frameworks: List[str],
output_dir: str,
vol2_path: str = None,
vol3_path: str = None,
rekall_path=None,
):
self.images = images
self.plugins = plugins
if not vol2_path:
@@ -172,7 +231,7 @@ class VolatilityTester:
Volatility3Test(vol3_path, output_dir),
Volatility3PyPyTest(vol3_path, output_dir),
Volatility2Test(vol2_path, output_dir),
RekallTest(rekall_path, output_dir)
RekallTest(rekall_path, output_dir),
]
self.tests = [x for x in available_tests if x.short_name.lower() in frameworks]
self.csv_writer = None
@@ -183,7 +242,7 @@ class VolatilityTester:
print(f"[?] Frameworks: {[x.long_name for x in self.tests]}")
def run_tests(self):
with open("volatility-timings.csv", 'w') as csvfile:
with open("volatility-timings.csv", "w") as csvfile:
self.csv_writer = csv.writer(csvfile)
titles = ["Image Hash", "Image Path", "Plugin Name"]
for test in self.tests:
@@ -203,72 +262,121 @@ class VolatilityTester:
self.csv_writer.writerow([image_hash, image.filepath, plugin.name] + results)
if __name__ == '__main__':
if __name__ == "__main__":
plugins = [
VolatilityPlugin(name = "pslist",
vol2_plugin_parameters = ["pslist"],
vol3_plugin_parameters = ["windows.pslist"]),
VolatilityPlugin(name = "psscan",
vol2_plugin_parameters = ["psscan"],
vol3_plugin_parameters = ["windows.psscan"],
rekall_plugin_parameters = ["psscan", "--scan_kernel"]),
VolatilityPlugin(name = "driverscan",
vol2_plugin_parameters = ["driverscan"],
vol3_plugin_parameters = ["windows.driverscan"],
rekall_plugin_parameters = ["driverscan", "--scan_kernel"]),
VolatilityPlugin(name = "handles",
vol2_plugin_parameters = ["handles"],
vol3_plugin_parameters = ["windows.handles"]),
VolatilityPlugin(name = "modules",
vol2_plugin_parameters = ["modules"],
vol3_plugin_parameters = ["windows.modules"]),
VolatilityPlugin(name = "hivelist",
vol2_plugin_parameters = ["hivelist"],
vol3_plugin_parameters = ["registry.hivelist"],
rekall_plugin_parameters = ["hives"]),
VolatilityPlugin(name = "vadinfo",
vol2_plugin_parameters = ["vadinfo"],
vol3_plugin_parameters = ["windows.vadinfo"],
rekall_plugin_parameters = ["vad"]),
VolatilityPlugin(name = "modscan",
vol2_plugin_parameters = ["modscan"],
vol3_plugin_parameters = ["windows.modscan"],
rekall_plugin_parameters = ["modscan", "--scan_kernel"]),
VolatilityPlugin(name = "svcscan",
vol2_plugin_parameters = ["svcscan"],
vol3_plugin_parameters = ["windows.svcscan"],
rekall_plugin_parameters = ["svcscan"]),
VolatilityPlugin(name = "ssdt", vol2_plugin_parameters = ["ssdt"], vol3_plugin_parameters = ["windows.ssdt"]),
VolatilityPlugin(name = "printkey",
vol2_plugin_parameters = ["printkey", "-K", "Classes"],
vol3_plugin_parameters = ["registry.printkey", "--key", "Classes"],
rekall_plugin_parameters = ["printkey", "--key", "Classes"])
VolatilityPlugin(
name="pslist",
vol2_plugin_parameters=["pslist"],
vol3_plugin_parameters=["windows.pslist"],
),
VolatilityPlugin(
name="psscan",
vol2_plugin_parameters=["psscan"],
vol3_plugin_parameters=["windows.psscan"],
rekall_plugin_parameters=["psscan", "--scan_kernel"],
),
VolatilityPlugin(
name="driverscan",
vol2_plugin_parameters=["driverscan"],
vol3_plugin_parameters=["windows.driverscan"],
rekall_plugin_parameters=["driverscan", "--scan_kernel"],
),
VolatilityPlugin(
name="handles",
vol2_plugin_parameters=["handles"],
vol3_plugin_parameters=["windows.handles"],
),
VolatilityPlugin(
name="modules",
vol2_plugin_parameters=["modules"],
vol3_plugin_parameters=["windows.modules"],
),
VolatilityPlugin(
name="hivelist",
vol2_plugin_parameters=["hivelist"],
vol3_plugin_parameters=["registry.hivelist"],
rekall_plugin_parameters=["hives"],
),
VolatilityPlugin(
name="vadinfo",
vol2_plugin_parameters=["vadinfo"],
vol3_plugin_parameters=["windows.vadinfo"],
rekall_plugin_parameters=["vad"],
),
VolatilityPlugin(
name="modscan",
vol2_plugin_parameters=["modscan"],
vol3_plugin_parameters=["windows.modscan"],
rekall_plugin_parameters=["modscan", "--scan_kernel"],
),
VolatilityPlugin(
name="svcscan",
vol2_plugin_parameters=["svcscan"],
vol3_plugin_parameters=["windows.svcscan"],
rekall_plugin_parameters=["svcscan"],
),
VolatilityPlugin(
name="ssdt",
vol2_plugin_parameters=["ssdt"],
vol3_plugin_parameters=["windows.ssdt"],
),
VolatilityPlugin(
name="printkey",
vol2_plugin_parameters=["printkey", "-K", "Classes"],
vol3_plugin_parameters=["registry.printkey", "--key", "Classes"],
rekall_plugin_parameters=["printkey", "--key", "Classes"],
),
]
parser = argparse.ArgumentParser()
parser.add_argument("--output-dir", type = str, default = os.getcwd(), help = "Directory to store all results")
parser.add_argument("--vol3path",
type = str,
default = os.path.join(os.getcwd(), 'volatility3'),
help = "Path ot the volatility 3 directory")
parser.add_argument("--vol2path",
type = str,
default = os.path.join(os.getcwd(), 'volatility'),
help = "Path to the volatility 2 directory")
parser.add_argument("--rekallpath",
type = str,
default = os.path.join(os.getcwd(), 'rekall'),
help = "Path to the rekall directory")
parser.add_argument("--frameworks",
nargs = "+",
type = str,
choices = [x.short_name.lower() for x in VolatilityTest.__subclasses__()],
default = [x.short_name.lower() for x in VolatilityTest.__subclasses__()],
help = "A comma separated list of frameworks to test")
parser.add_argument('images', metavar = 'IMAGE', type = str, nargs = '+', help = 'The list of images to compare')
parser.add_argument(
"--output-dir",
type=str,
default=os.getcwd(),
help="Directory to store all results",
)
parser.add_argument(
"--vol3path",
type=str,
default=os.path.join(os.getcwd(), "volatility3"),
help="Path ot the volatility 3 directory",
)
parser.add_argument(
"--vol2path",
type=str,
default=os.path.join(os.getcwd(), "volatility"),
help="Path to the volatility 2 directory",
)
parser.add_argument(
"--rekallpath",
type=str,
default=os.path.join(os.getcwd(), "rekall"),
help="Path to the rekall directory",
)
parser.add_argument(
"--frameworks",
nargs="+",
type=str,
choices=[x.short_name.lower() for x in VolatilityTest.__subclasses__()],
default=[x.short_name.lower() for x in VolatilityTest.__subclasses__()],
help="A comma separated list of frameworks to test",
)
parser.add_argument(
"images",
metavar="IMAGE",
type=str,
nargs="+",
help="The list of images to compare",
)
args = parser.parse_args()
vt = VolatilityTester([VolatilityImage(filepath = x) for x in args.images], plugins,
[x.lower() for x in args.frameworks], args.output_dir, args.vol2path, args.vol3path,
args.rekallpath)
vt = VolatilityTester(
[VolatilityImage(filepath=x) for x in args.images],
plugins,
[x.lower() for x in args.frameworks],
args.output_dir,
args.vol2path,
args.vol3path,
args.rekallpath,
)
vt.run_tests()
+27 -24
View File
@@ -7,11 +7,12 @@
# Cleaned up C version (as the basis for my code) here, thanks to Pepijn Bruienne / @bruienne
# https://gist.github.com/bruienne/029494bbcfb358098b41
import os
import struct
import sys
def seekread(f, offset = None, length = 0, relative = True):
def seekread(f, offset=None, length=0, relative=True):
if offset is not None:
# offset provided, let's seek
f.seek(offset, [0, 1, 2][relative])
@@ -22,55 +23,57 @@ def seekread(f, offset = None, length = 0, relative = True):
def parse_pbzx(pbzx_path):
section = 0
xar_out_path = '%s.part%02d.cpio.xz' % (pbzx_path, section)
with open(pbzx_path, 'rb') as f:
xar_out_path = f"{pbzx_path}.part{section:02d}.cpio.xz"
with open(pbzx_path, "rb") as f:
# pbzx = f.read()
# f.close()
magic = seekread(f, length = 4)
if magic != 'pbzx':
magic = seekread(f, length=4)
if magic != "pbzx":
raise RuntimeError("Error: Not a pbzx file")
# Read 8 bytes for initial flags
flags = seekread(f, length = 8)
flags = seekread(f, length=8)
# Interpret the flags as a 64-bit big-endian unsigned int
flags = struct.unpack('>Q', flags)[0]
flags = struct.unpack(">Q", flags)[0]
while flags & (1 << 24):
with open(xar_out_path, 'wb') as xar_f:
with open(xar_out_path, "wb") as xar_f:
xar_f.seek(0, os.SEEK_END)
# Read in more flags
flags = seekread(f, length = 8)
flags = struct.unpack('>Q', flags)[0]
flags = seekread(f, length=8)
flags = struct.unpack(">Q", flags)[0]
# Read in length
f_length = seekread(f, length = 8)
f_length = struct.unpack('>Q', f_length)[0]
xzmagic = seekread(f, length = 6)
if xzmagic != '\xfd7zXZ\x00':
f_length = seekread(f, length=8)
f_length = struct.unpack(">Q", f_length)[0]
xzmagic = seekread(f, length=6)
if xzmagic != "\xfd7zXZ\x00":
# This isn't xz content, this is actually _raw decompressed cpio_ chunk of 16MB in size...
# Let's back up ...
seekread(f, offset = -6, length = 0)
seekread(f, offset=-6, length=0)
# ... and split it out ...
f_content = seekread(f, length = f_length)
f_content = seekread(f, length=f_length)
section += 1
decomp_out = '%s.part%02d.cpio' % (pbzx_path, section)
with open(decomp_out, 'wb') as g:
decomp_out = f"{pbzx_path}.part{section:02d}.cpio"
with open(decomp_out, "wb") as g:
g.write(f_content)
# Now to start the next section, which should hopefully be .xz (we'll just assume it is ...)
section += 1
xar_out_path = '%s.part%02d.cpio.xz' % (pbzx_path, section)
xar_out_path = f"{pbzx_path}.part{section:02d}.cpio.xz"
else:
f_length -= 6
# This part needs buffering
f_content = seekread(f, length = f_length)
tail = seekread(f, offset = -2, length = 2)
f_content = seekread(f, length=f_length)
tail = seekread(f, offset=-2, length=2)
xar_f.write(xzmagic)
xar_f.write(f_content)
if tail != 'YZ':
if tail != "YZ":
raise RuntimeError("Error: Footer is not xar file footer")
def main():
parse_pbzx(sys.argv[1])
print("Now xz decompress the .xz chunks, then 'cat' them all together in order into a single new.cpio file")
print(
"Now xz decompress the .xz chunks, then 'cat' them all together in order into a single new.cpio file"
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+122 -75
View File
@@ -13,10 +13,10 @@ import pdbparse.undecorate
logger = logging.getLogger(__name__)
logger.setLevel(1)
if __name__ == '__main__':
if __name__ == "__main__":
console = logging.StreamHandler()
console.setLevel(1)
formatter = logging.Formatter('%(levelname)-8s %(name)-12s: %(message)s')
formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s")
console.setFormatter(formatter)
logger.addHandler(console)
@@ -25,19 +25,19 @@ class PDBRetreiver:
def retreive_pdb(self, guid: str, file_name: str) -> Optional[str]:
logger.info("Download PDB file...")
file_name = ".".join(file_name.split(".")[:-1] + ['pdb'])
for sym_url in ['http://msdl.microsoft.com/download/symbols']:
file_name = ".".join(file_name.split(".")[:-1] + ["pdb"])
for sym_url in ["http://msdl.microsoft.com/download/symbols"]:
url = sym_url + f"/{file_name}/{guid}/"
result = None
for suffix in [file_name[:-1] + '_', file_name]:
for suffix in [file_name[:-1] + "_", file_name]:
try:
logger.debug(f"Attempting to retrieve {url + suffix}")
logger.debug("Attempting to retrieve %s", url + suffix)
result, _ = request.urlretrieve(url + suffix)
except request.HTTPError as excp:
logger.debug(f"Failed with {excp}")
logger.debug("Failed with %s", excp)
if result:
logger.debug(f"Successfully written to {result}")
logger.debug("Successfully written to %s", result)
break
return result
@@ -69,7 +69,7 @@ class PDBConvertor:
"float": "float",
"double": "float",
"long double": "float",
"void": "void"
"void": "void",
}
base_type_size = {
@@ -122,13 +122,18 @@ class PDBConvertor:
self._seen_ctypes.add(ctype)
return self.ctype[ctype]
def lookup_ctype_pointers(self, ctype_pointer: str) -> Dict[str, Union[str, Dict[str, str]]]:
base_type = ctype_pointer.replace('32P', '').replace('64P', '')
def lookup_ctype_pointers(
self, ctype_pointer: str
) -> Dict[str, Union[str, Dict[str, str]]]:
base_type = ctype_pointer.replace("32P", "").replace("64P", "")
if base_type == ctype_pointer:
# We raise a KeyError, because we've been asked about a type that isn't a pointer
raise KeyError
self._seen_ctypes.add(base_type)
return {"kind": "pointer", "subtype": {"kind": "base", "name": self.ctype[base_type]}}
return {
"kind": "pointer",
"subtype": {"kind": "base", "name": self.ctype[base_type]},
}
def read_pdb(self) -> Dict:
"""Reads in the PDB file and forms essentially a python dictionary of necessary data"""
@@ -137,32 +142,31 @@ class PDBConvertor:
"enums": self.read_enums(),
"metadata": self.generate_metadata(),
"symbols": self.read_symbols(),
"base_types": self.read_basetypes()
"base_types": self.read_basetypes(),
}
return output
def generate_metadata(self) -> Dict[str, Any]:
"""Generates the metadata necessary for this object"""
dbg = self._pdb.STREAM_DBI
last_bytes = str(binascii.hexlify(self._pdb.STREAM_PDB.GUID.Data4), 'ascii')[-16:]
guidstr = u'{:08x}{:04x}{:04x}{}'.format(self._pdb.STREAM_PDB.GUID.Data1, self._pdb.STREAM_PDB.GUID.Data2,
self._pdb.STREAM_PDB.GUID.Data3, last_bytes)
last_bytes = str(binascii.hexlify(self._pdb.STREAM_PDB.GUID.Data4), "ascii")[
-16:
]
guidstr = f"{self._pdb.STREAM_PDB.GUID.Data1:08x}{self._pdb.STREAM_PDB.GUID.Data2:04x}{self._pdb.STREAM_PDB.GUID.Data3:04x}{last_bytes}"
pdb_data = {
"GUID": guidstr.upper(),
"age": self._pdb.STREAM_PDB.Age,
"database": "ntkrnlmp.pdb",
"machine_type": int(dbg.machine)
"machine_type": int(dbg.machine),
}
result = {
"format": "6.0.0",
"producer": {
"datetime": datetime.datetime.now().isoformat(),
"name": "pdbconv",
"version": "0.1.0"
"version": "0.1.0",
},
"windows": {
"pdb": pdb_data
}
"windows": {"pdb": pdb_data},
}
return result
@@ -173,16 +177,21 @@ class PDBConvertor:
stream = self._pdb.STREAM_TPI
for type_index in stream.types:
user_type = stream.types[type_index]
if (user_type.leaf_type == "LF_ENUM" and not user_type.prop.fwdref):
if user_type.leaf_type == "LF_ENUM" and not user_type.prop.fwdref:
output.update(self._format_enum(user_type))
return output
def _format_enum(self, user_enum):
output = {
user_enum.name: {
'base': self.lookup_ctype(user_enum.utype),
'size': self._determine_size(user_enum.utype),
'constants': dict([(enum.name, enum.enum_value) for enum in user_enum.fieldlist.substructs])
"base": self.lookup_ctype(user_enum.utype),
"size": self._determine_size(user_enum.utype),
"constants": dict(
[
(enum.name, enum.enum_value)
for enum in user_enum.fieldlist.substructs
]
),
}
}
return output
@@ -195,14 +204,14 @@ class PDBConvertor:
try:
sects = self._pdb.STREAM_SECT_HDR_ORIG.sections
omap = self._pdb.STREAM_OMAP_FROM_SRC
except AttributeError as e:
except AttributeError:
# In this case there is no OMAP, so we use the given section
# headers and use the identity function for omap.remap
sects = self._pdb.STREAM_SECT_HDR.sections
omap = None
for sym in self._pdb.STREAM_GSYM.globals:
if not hasattr(sym, 'offset'):
if not hasattr(sym, "offset"):
continue
try:
virt_base = sects[sym.segment - 1].VirtualAddress
@@ -223,9 +232,9 @@ class PDBConvertor:
stream = self._pdb.STREAM_TPI
for type_index in stream.types:
user_type = stream.types[type_index]
if (user_type.leaf_type == "LF_STRUCTURE" and not user_type.prop.fwdref):
if user_type.leaf_type == "LF_STRUCTURE" and not user_type.prop.fwdref:
output.update(self._format_usertype(user_type, "struct"))
elif (user_type.leaf_type == "LF_UNION" and not user_type.prop.fwdref):
elif user_type.leaf_type == "LF_UNION" and not user_type.prop.fwdref:
output.update(self._format_usertype(user_type, "union"))
return output
@@ -233,16 +242,22 @@ class PDBConvertor:
"""Produces a single usertype"""
fields: Dict[str, Dict[str, Any]] = {}
[fields.update(self._format_field(s)) for s in usertype.fieldlist.substructs]
return {usertype.name: {'fields': fields, 'kind': kind, 'size': usertype.size}}
return {usertype.name: {"fields": fields, "kind": kind, "size": usertype.size}}
def _format_field(self, field) -> Dict[str, Dict[str, Any]]:
return {field.name: {"offset": field.offset, "type": self._format_kind(field.index)}}
return {
field.name: {"offset": field.offset, "type": self._format_kind(field.index)}
}
def _determine_size(self, field):
output = None
if isinstance(field, str):
output = self.base_type_size[field]
elif (field.leaf_type == "LF_STRUCTURE" or field.leaf_type == "LF_ARRAY" or field.leaf_type == "LF_UNION"):
elif (
field.leaf_type == "LF_STRUCTURE"
or field.leaf_type == "LF_ARRAY"
or field.leaf_type == "LF_UNION"
):
output = field.size
elif field.leaf_type == "LF_POINTER":
output = self.base_type_size[field.ptr_attr.type]
@@ -256,6 +271,7 @@ class PDBConvertor:
output = self._determine_size(field.index)
if output is None:
import pdb
pdb.set_trace()
raise ValueError(f"Unknown size for field: {field.name}")
return output
@@ -267,36 +283,37 @@ class PDBConvertor:
output = self.lookup_ctype_pointers(kind)
except KeyError:
try:
output = {'kind': 'base', 'name': self.lookup_ctype(kind)}
output = {"kind": "base", "name": self.lookup_ctype(kind)}
except KeyError:
output = {'kind': 'base', 'name': kind}
elif kind.leaf_type == 'LF_MODIFIER':
output = {"kind": "base", "name": kind}
elif kind.leaf_type == "LF_MODIFIER":
output = self._format_kind(kind.modified_type)
elif kind.leaf_type == 'LF_STRUCTURE':
output = {'kind': 'struct', 'name': kind.name}
elif kind.leaf_type == 'LF_UNION':
output = {'kind': 'union', 'name': kind.name}
elif kind.leaf_type == 'LF_BITFIELD':
elif kind.leaf_type == "LF_STRUCTURE":
output = {"kind": "struct", "name": kind.name}
elif kind.leaf_type == "LF_UNION":
output = {"kind": "union", "name": kind.name}
elif kind.leaf_type == "LF_BITFIELD":
output = {
'kind': 'bitfield',
'type': self._format_kind(kind.base_type),
'bit_length': kind.length,
'bit_position': kind.position
"kind": "bitfield",
"type": self._format_kind(kind.base_type),
"bit_length": kind.length,
"bit_position": kind.position,
}
elif kind.leaf_type == 'LF_POINTER':
output = {'kind': 'pointer', 'subtype': self._format_kind(kind.utype)}
elif kind.leaf_type == 'LF_ARRAY':
elif kind.leaf_type == "LF_POINTER":
output = {"kind": "pointer", "subtype": self._format_kind(kind.utype)}
elif kind.leaf_type == "LF_ARRAY":
output = {
'kind': 'array',
'count': kind.size // self._determine_size(kind.element_type),
'subtype': self._format_kind(kind.element_type)
"kind": "array",
"count": kind.size // self._determine_size(kind.element_type),
"subtype": self._format_kind(kind.element_type),
}
elif kind.leaf_type == 'LF_ENUM':
output = {'kind': 'enum', 'name': kind.name}
elif kind.leaf_type == 'LF_PROCEDURE':
output = {'kind': "function"}
elif kind.leaf_type == "LF_ENUM":
output = {"kind": "enum", "name": kind.name}
elif kind.leaf_type == "LF_PROCEDURE":
output = {"kind": "function"}
else:
import pdb
pdb.set_trace()
return output
@@ -306,40 +323,70 @@ class PDBConvertor:
if "64" in self._pdb.STREAM_DBI.machine:
ptr_size = 8
output = {"pointer": {"endian": "little", "kind": "int", "signed": False, "size": ptr_size}}
output = {
"pointer": {
"endian": "little",
"kind": "int",
"signed": False,
"size": ptr_size,
}
}
for index in self._seen_ctypes:
output[self.ctype[index]] = {
"endian": "little",
"kind": self.ctype_python_types.get(self.ctype[index], "int"),
"signed": False if "_U" in index else True,
"size": self.base_type_size[index]
"size": self.base_type_size[index],
}
return output
if __name__ == '__main__':
parser = argparse.ArgumentParser(description = "Convertor for PDB files to Volatility 3 Intermediate Symbol Format")
parser.add_argument("-o", "--output", metavar = "OUTPUT", help = "Filename for data output", required = True)
file_group = parser.add_argument_group("file", description = "File-based conversion of PDB to ISF")
file_group.add_argument("-f", "--file", metavar = "FILE", help = "PDB file to translate to ISF")
data_group = parser.add_argument_group("data", description = "Convert based on a GUID and filename pattern")
data_group.add_argument("-p", "--pattern", metavar = "PATTERN", help = "Filename pattern to recover PDB file")
data_group.add_argument("-g",
"--guid",
metavar = "GUID",
help = "GUID + Age string for the required PDB file",
default = None)
data_group.add_argument("-k",
"--keep",
action = "store_true",
default = False,
help = "Keep the downloaded PDB file")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Convertor for PDB files to Volatility 3 Intermediate Symbol Format"
)
parser.add_argument(
"-o",
"--output",
metavar="OUTPUT",
help="Filename for data output",
required=True,
)
file_group = parser.add_argument_group(
"file", description="File-based conversion of PDB to ISF"
)
file_group.add_argument(
"-f", "--file", metavar="FILE", help="PDB file to translate to ISF"
)
data_group = parser.add_argument_group(
"data", description="Convert based on a GUID and filename pattern"
)
data_group.add_argument(
"-p",
"--pattern",
metavar="PATTERN",
help="Filename pattern to recover PDB file",
)
data_group.add_argument(
"-g",
"--guid",
metavar="GUID",
help="GUID + Age string for the required PDB file",
default=None,
)
data_group.add_argument(
"-k",
"--keep",
action="store_true",
default=False,
help="Keep the downloaded PDB file",
)
args = parser.parse_args()
delfile = False
filename = None
if args.guid is not None and args.pattern is not None:
filename = PDBRetreiver().retreive_pdb(guid = args.guid, file_name = args.pattern)
filename = PDBRetreiver().retreive_pdb(guid=args.guid, file_name=args.pattern)
delfile = True
elif args.file:
filename = args.file
@@ -352,7 +399,7 @@ if __name__ == '__main__':
convertor = PDBConvertor(filename)
with open(args.output, "w") as f:
json.dump(convertor.read_pdb(), f, indent = 2, sort_keys = True)
json.dump(convertor.read_pdb(), f, indent=2, sort_keys=True)
if args.keep:
print(f"Temporary PDB file: {filename}")
+8 -9
View File
@@ -1,34 +1,33 @@
import argparse
import json
import logging
import os
import sys
# TODO: Rather nasty hack, when volatility's actually installed this would be unnecessary
sys.path += ".."
import logging
console = logging.StreamHandler()
console.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(levelname)-8s %(name)-12s: %(message)s')
formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s")
console.setFormatter(formatter)
logger = logging.getLogger("")
logger.addHandler(console)
logger.setLevel(logging.DEBUG)
from volatility3 import schemas
from volatility3 import schemas # noqa: E402
if __name__ == '__main__':
if __name__ == "__main__":
parser = argparse.ArgumentParser("Validates ")
parser.add_argument("-s", "--schema", dest = "schema", default = None)
parser.add_argument("filenames", metavar = "FILE", nargs = '+')
parser.add_argument("-s", "--schema", dest="schema", default=None)
parser.add_argument("filenames", metavar="FILE", nargs="+")
args = parser.parse_args()
schema = None
if args.schema:
with open(os.path.abspath(args.schema), 'r') as s:
with open(os.path.abspath(args.schema)) as s:
schema = json.load(s)
failures = []
@@ -36,7 +35,7 @@ if __name__ == '__main__':
try:
if os.path.exists(filename):
print(f"[?] Validating file: {filename}")
with open(filename, 'r') as t:
with open(filename) as t:
test = json.load(t)
if args.schema:
+56 -42
View File
@@ -9,7 +9,7 @@ import requests
import rpmfile
from debian import debfile
DWARF2JSON = './dwarf2json'
DWARF2JSON = "./dwarf2json"
class Downloader:
@@ -17,7 +17,7 @@ class Downloader:
def __init__(self, url_lists: List[List[str]]) -> None:
self.url_lists = url_lists
def download_lists(self, keep = False):
def download_lists(self, keep=False):
for url_list in self.url_lists:
print("Downloading files...")
files_for_processing = self.download_list(url_list)
@@ -35,43 +35,45 @@ class Downloader:
with tempfile.NamedTemporaryFile() as archivedata:
archivedata.write(data.content)
archivedata.seek(0)
if url.endswith('.rpm'):
if url.endswith(".rpm"):
processed_files[url] = self.process_rpm(archivedata)
elif url.endswith('.deb'):
elif url.endswith(".deb"):
processed_files[url] = self.process_deb(archivedata)
return processed_files
def process_rpm(self, archivedata) -> Optional[str]:
rpm = rpmfile.RPMFile(fileobj = archivedata)
rpm = rpmfile.RPMFile(fileobj=archivedata)
member = None
extracted = None
for member in rpm.getmembers():
if 'vmlinux' in member.name or 'System.map' in member.name:
if "vmlinux" in member.name or "System.map" in member.name:
print(f" - Extracting {member.name}")
extracted = rpm.extractfile(member)
break
if not member or not extracted:
return None
with tempfile.NamedTemporaryFile(delete = False,
prefix = 'vmlinux' if 'vmlinux' in member.name else 'System.map') as output:
with tempfile.NamedTemporaryFile(
delete=False, prefix="vmlinux" if "vmlinux" in member.name else "System.map"
) as output:
print(f" - Writing to {output.name}")
output.write(extracted.read())
return output.name
def process_deb(self, archivedata) -> Optional[str]:
deb = debfile.DebFile(fileobj = archivedata)
deb = debfile.DebFile(fileobj=archivedata)
member = None
extracted = None
for member in deb.data.tgz().getmembers():
if member.name.endswith('vmlinux') or 'System.map' in member.name:
if member.name.endswith("vmlinux") or "System.map" in member.name:
print(f" - Extracting {member.name}")
extracted = deb.data.get_file(member.name)
break
if not member or not extracted:
return None
with tempfile.NamedTemporaryFile(delete = False,
prefix = 'vmlinux' if 'vmlinux' in member.name else 'System.map') as output:
with tempfile.NamedTemporaryFile(
delete=False, prefix="vmlinux" if "vmlinux" in member.name else "System.map"
) as output:
print(f" - Writing to {output.name}")
output.write(extracted.read())
return output.name
@@ -83,43 +85,55 @@ class Downloader:
if named_files[i] is None:
print(f"FAILURE: None encountered for {i}")
return
args = [DWARF2JSON, 'linux']
output_filename = 'unknown-kernel.json'
args = [DWARF2JSON, "linux"]
output_filename = "unknown-kernel.json"
for named_file in named_files:
prefix = '--system-map'
if 'System' not in named_files[named_file]:
prefix = '--elf'
output_filename = './' + '-'.join((named_file.split('/')[-1]).split('-')[2:])[:-4] + '.json.xz'
prefix = "--system-map"
if "System" not in named_files[named_file]:
prefix = "--elf"
output_filename = (
"./"
+ "-".join((named_file.split("/")[-1]).split("-")[2:])[:-4]
+ ".json.xz"
)
args += [prefix, named_files[named_file]]
print(f" - Running {args}")
proc = subprocess.run(args, capture_output = True)
proc = subprocess.run(args, capture_output=True)
print(f" - Writing to {output_filename}")
with lzma.open(output_filename, 'w') as f:
with lzma.open(output_filename, "w") as f:
f.write(proc.stdout)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description = "Takes a list of URLs for Centos and downloads them")
parser.add_argument("-f",
"--file",
dest = 'filename',
metavar = "FILENAME",
help = "Filename to be read",
required = True)
parser.add_argument("-d",
"--dwarf2json",
dest = 'dwarfpath',
metavar = "PATH",
default = DWARF2JSON,
help = "Path to the dwarf2json binary",
required = True)
parser.add_argument("-k",
"--keep",
dest = 'keep',
action = 'store_true',
help = 'Keep extracted temporary files after completion',
default = False)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Takes a list of URLs for Centos and downloads them"
)
parser.add_argument(
"-f",
"--file",
dest="filename",
metavar="FILENAME",
help="Filename to be read",
required=True,
)
parser.add_argument(
"-d",
"--dwarf2json",
dest="dwarfpath",
metavar="PATH",
default=DWARF2JSON,
help="Path to the dwarf2json binary",
required=True,
)
parser.add_argument(
"-k",
"--keep",
dest="keep",
action="store_true",
help="Keep extracted temporary files after completion",
default=False,
)
args = parser.parse_args()
DWARF2JSON = args.dwarfpath
@@ -132,4 +146,4 @@ if __name__ == '__main__':
urls += [[lines[2 * i].strip(), lines[(2 * i) + 1].strip()]]
d = Downloader(urls)
d.download_lists(keep = args.keep)
d.download_lists(keep=args.keep)
+5 -7
View File
@@ -19,6 +19,8 @@ import sys
import sphinx.ext.apidoc
from importlib.util import find_spec
def setup(app):
volatility_directory = os.path.abspath(
@@ -124,7 +126,7 @@ def setup(app):
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath("../.."))
from volatility3.framework import constants
from volatility3.framework import constants # noqa: E402
# -- General configuration ------------------------------------------------
@@ -147,13 +149,9 @@ extensions = [
autosectionlabel_prefix_document = True
try:
import sphinx_autodoc_typehints
if find_spec("sphinx_autodoc_typehints") is not None:
extensions.append("sphinx_autodoc_typehints")
except ImportError:
# If the autodoc typehints extension isn't available, carry on regardless
pass
# If the autodoc typehints extension isn't available, carry on regardless
# Add any paths that contain templates here, relative to this directory.
# templates_path = ['tools/templates']
+18 -8
View File
@@ -23,7 +23,7 @@ Alignment
.. _Array:
Array
This represents a list of items, which can be access by an index, which is zero-based (meaning the first
This represents a list of items, which can be accessed by an index, which is zero-based (meaning the first
element has index 0). Items in arrays are almost always the same size (it is not a generic list, as in python)
even if they are :ref:`pointers<pointer>` to different sized objects.
@@ -43,7 +43,14 @@ Dereference
.. _Domain:
Domain
This the grouping for input values for a mapping or mathematical function.
The set of input values for a mapping or mathematical function.
I
-
.. _Intermediate Symbol File (ISF):
Intermediate Symbol File (ISF)
They contain kernel structures and specific offsets formatted as JSON. For macOS and Linux analysis, the kernel needs to be added as an ISF file to the volatility 3 symbols directory. For Windows, the required ISF file can often be generated from PDB files automatically downloaded from Microsoft servers, and therefore does not require manual intervention.
M
-
@@ -54,9 +61,7 @@ Map, mapping
of the :ref:`Range<range>`). Mappings can be seen as a mathematical function, and therefore volatility 3
attempts to use mathematical functional notation where possible. Within volatility a mapping is most often
used to refer to the function for translating addresses from a higher layer (domain) to a lower layer (range).
For further information, please see
`Function (mathematics) in wikipedia https://en.wikipedia.org/wiki/Function_(mathematics)`
For further information, please see `Function (mathematics) in Wikipedia<https://en.wikipedia.org/wiki/Function_(mathematics)>_`.
.. _Member:
@@ -69,7 +74,7 @@ O
.. _Object:
Object
This has a specific meaning within computer programming (as in Object Oriented Programming), but within the world
This has a specific meaning within computer programming (as in object-oriented programming), but within the world
of Volatility it is used to refer to a type that has been associated with a chunk of data, or a specific instance
of a type. See also :ref:`Type<type>`.
@@ -116,6 +121,11 @@ Page Table
possible to use them as a way to map a particular address within a (potentially larger, but sparsely populated)
virtual space to a concrete (and usually contiguous) physical space, through the process of :ref:`mapping<map>`.
.. _Plugin:
Plugin
Plugins are the "functions" of the volatility framework. They carry out algorithms on data stored in layers using objects constructed from symbols. Broadly, plugins take in a number of TranslationLayers (the data, which is a representation of part of an image, in a specified type described by templates) and outputs a TreeGrid.
.. _Pointer:
Pointer
@@ -145,9 +155,9 @@ Struct, Structure
Symbol
This is used in many different contexts, as a short term for many things. Within Volatility, a symbol is a
construct that usually encompasses a specific type :ref:`type<Type>` at a specific :ref:`offset<Offset>`,
construct that usually encompasses a specific :ref:`type<Type>` at a specific :ref:`offset<Offset>`,
representing a particular instance of that type within the memory of a compiled and running program. An example
would be the location in memory of a list of active tcp endpoints maintained by the networking stack
would be the location in memory of a list of active TCP endpoints maintained by the networking stack
within an operating system.
T
+62 -36
View File
@@ -41,24 +41,36 @@ to be able to run properly. Any that are defined as optional need not necessari
@classmethod
def get_requirements(cls):
return [requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel',
architectures = ["Intel32", "Intel64"]),
requirements.ListRequirement(name = 'pid',
element_type = int,
description = "Process IDs to include (all other processes are excluded)",
optional = True),
requirements.PluginRequirement(name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0))]
return [
requirements.ModuleRequirement(
name = 'kernel',
description = 'Windows kernel',
architectures = ["Intel32", "Intel64"]
),
requirements.ListRequirement(
name = 'pid',
element_type = int,
description = "Process IDs to include (all other processes are excluded)",
optional = True
),
requirements.PluginRequirement(
name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0)
),
]
This is a classmethod, because it is called before the specific plugin object has been instantiated (in order to know how
This is a classmethod, so it can be called before the specific plugin object has been instantiated (in order to know how
to instantiate the plugin). At the moment these requirements are fairly straightforward:
::
requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel',
architectures = ["Intel32", "Intel64"]),
requirements.ModuleRequirement(
name = 'kernel',
description = 'Windows kernel',
architectures = ["Intel32", "Intel64"]
),
This requirement specifies the need for a particular submodule. Each module requires a
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>` and a
@@ -85,9 +97,11 @@ not be requested directly from the user.
::
requirements.TranslationLayerRequirement(name = 'primary',
description = 'Memory layer for the kernel',
architectures = ["Intel32", "Intel64"]),
requirements.TranslationLayerRequirement(
name = 'primary',
description = 'Memory layer for the kernel',
architectures = ["Intel32", "Intel64"]
),
This requirement indicates that the plugin will operate on a single
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>`. The name of the
@@ -110,8 +124,10 @@ not be requested directly from the user.
::
requirements.SymbolTableRequirement(name = "nt_symbols",
description = "Windows kernel symbols"),
requirements.SymbolTableRequirement(
name = "nt_symbols",
description = "Windows kernel symbols"
),
This requirement specifies the need for a particular
:py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`
@@ -127,10 +143,12 @@ not be requested directly from the user.
::
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True),
requirements.ListRequirement(
name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True
),
The next requirement is a List Requirement, populated by integers. The description will be presented to the user to
describe what the value represents. The optional flag indicates that the plugin can function without the ``pid`` value
@@ -138,9 +156,11 @@ being defined within the configuration tree at all.
::
requirements.PluginRequirement(name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0))]
requirements.PluginRequirement(
name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0)
)
This requirement indicates that the plugin will make use of another plugin's code, and specifies the version requirements
on that plugin. The version is specified in terms of Semantic Versioning meaning that, to be compatible, the major
@@ -180,16 +200,24 @@ that will be output as part of the :py:class:`~volatility3.framework.interfaces.
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
kernel = self.context.modules[self.config['kernel']]
return renderers.TreeGrid([("PID", int),
("Process", str),
("Base", format_hints.Hex),
("Size", format_hints.Hex),
("Name", str),
("Path", str)],
self._generator(pslist.PsList.list_processes(self.context,
kernel.layer_name,
kernel.symbol_table_name,
filter_func = filter_func)))
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("Base", format_hints.Hex),
("Size", format_hints.Hex),
("Name", str),
("Path", str),
],
self._generator(
pslist.PsList.list_processes(
self.context,
kernel.layer_name,
kernel.symbol_table_name,
filter_func = filter_func
)
)
)
In this instance, the plugin constructs a filter (using the PsList plugin's *classmethod* for creating filters).
It checks the plugin's configuration for the ``pid`` value, and passes it in as a list if it finds it, or None if
@@ -281,5 +309,3 @@ such as ``<table>!_UNICODE``) and the parameters to that type.
Since the cast value must populate a string typed column, it had to be a Python string (such as being cast to the native
type string) and could not have been a special Structure such as ``_UNICODE``. For the format hint columns, the format
hint type must be used to ensure the error checking does not fail.
+8 -8
View File
@@ -3,7 +3,7 @@ Using Volatility 3 as a Library
This portion of the documentation discusses how to access the Volatility 3 framework from an external application.
The general process of using volatility as a library is to as follows:
The general process of using volatility as a library is as follows:
1. :ref:`create_context`
2. (Optional) :ref:`available_plugins`
@@ -21,7 +21,7 @@ Creating a context
First we make sure the volatility framework works the way we expect it (and is the version we expect). The
versioning used is semantic versioning, meaning any version with the same major number and a higher or equal
minor number will satisfy the requirement. An example is below since the CLI doesn't need any of the features
from versions 1.1 or 1.2:
from version 1.1 or later:
::
@@ -86,7 +86,7 @@ List requirements are a list of simple types (integers, booleans, floats and str
options, multiple requirements needs all their subrequirements fulfilled and the other types require the names of
valid translation layers or symbol tables within the context, respectively. Luckily, each of these requirements can
tell you whether they've been fulfilled or not later in the process. For now, they can be used to ask the user to
fill in any parameters they made need to. Some requirements are optional, others are not.
fill in any parameters they may need to. Some requirements are optional, others are not.
The plugin is essentially a multiple requirement. It should also be noted that automagic classes can have requirements
(as can translation layers).
@@ -100,7 +100,7 @@ Once you know what requirements the plugin will need, you can populate them with
The configuration is essentially a hierarchical tree of values, much like the windows registry.
Each plugin is instantiated at a particular branch within the hierarchy and will look for its configuration
options under that hierarchy (if it holds any configurable items, it will likely instantiate those at a point
underneaths its own branch). To set the hierarchy, you'll need to know where the configurables will be constructed.
underneath its own branch). To set the hierarchy, you'll need to know where the configurables will be constructed.
For this example, we'll assume plugins' base_config_path is set as `plugins`, and that automagics are configured under
the `automagic` tree. We'll see later how to ensure this matches up with the plugins and automagic when they're
@@ -139,7 +139,7 @@ A suitable list of automagics for a particular plugin (based on operating system
This will take the plugin module, extract the operating system (first level of the hierarchy) and then return just
the automagics which apply to the operating system. Each automagic can exclude itself from being used for specific
operating systems, so that an automagic designed for linux is not used for windows or mac plugins.
operating systems, such that an automagic designed for linux is not used for windows or mac plugins.
These automagics can then be run by providing the list, the context, the plugin to be run, the hierarchy name that
the plugin will be constructed on ('plugins' by default) and a progress_callback. This is a callable which takes
@@ -157,8 +157,8 @@ Any exceptions that occur during the execution of the automagic will be returned
Run the plugin
--------------
Firstly, we should check whether the plugin will be able to run (ie, whether the configuration options it needs
have been successfully set). We do this as follow (where plugin_config_path is the base_config_path (which defaults
Firstly, we should check whether the plugin will be able to run (i.e., whether the configuration options it needs
have been successfully set). We do this as follows, where plugin_config_path is the base_config_path (which defaults
to `plugins` and then the name of the class itself):
::
@@ -166,7 +166,7 @@ to `plugins` and then the name of the class itself):
unsatisfied = plugin.unsatisfied(context, plugin_config_path)
If unsatisfied is an empty list, then the plugin has been given everything it requires. If not, it will be a
Dictionary of the hierarchy paths and their associated requirements that weren't satisfied.
dict of the hierarchy paths and their associated requirements that weren't satisfied.
The plugin can then be instantiated with the context (containing the plugin's configuration) and the path that the
plugin can find its configuration at. This configuration path only needs to be a unique value to identify where the
+57 -4
View File
@@ -36,7 +36,7 @@ operating system mode for volshell, and the current layer available for use.
(primary) >>>
Volshell itself in essentially a plugin, but an interactive one. As such, most values are accessed through `self`
Volshell itself is essentially a plugin, but an interactive one. As such, most values are accessed through `self`
although there is also a `context` object whenever a context must be provided.
The prompt for the tool will indicate the name of the current layer (which can be accessed as `self.current_layer`
@@ -92,7 +92,7 @@ It can also be provided with an object and will interpret the data for each in t
0x2e8 : UniqueProcessId symbol_table_name1!pointer 4
...
These values can be accessed directory as attributes
These values can be accessed directly as attributes
::
@@ -180,15 +180,68 @@ used:
layer = cc(mynewlayer.MyNewLayer, on_top_of = 'primary', other_parameter = 'important')
with open('output.dmp', 'wb') as fp:
for i in range(0, 1073741824, 0x1000):
for i in range(0, 0x4000000, 0x1000):
data = layer.read(i, 0x1000, pad = True)
fp.write(data)
As this demonstrates, all of the python is accessible, as are the volshell built in functions (such as `cc` which
creates a constructable, like a layer or a symbol table).
User Convenience
----------------
There are functions available that make often-done tasks easier, and generally provide a shell-like experience. These can be listed using `help()` which, as already mentioned, is advertised when volshell starts.
Loading files
-------------
^^^^^^^^^^^^^
Files can be loaded as physical layers using the `load_file` or `lf` command, which takes a filename or a URI. This will be added
to `context.layers` and can be accessed by the name returned by `lf`.
Regex
^^^^^
It is easy to scan for some bytes or a pattern using `regex_scan` or `rx`.
::
(layer_name) >>> rx(rb"(Linux version|Darwin Kernel Version) [0-9]+\.[0-9]+\.[0-9]+")
0x880001400070 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0x880001400080 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0x880001400090 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0x8800014000a0 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0x8800014000b0 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0x8800014000c0 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0x8800014000d0 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0x8800014000e0 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
0x880001769027 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0x880001769037 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0x880001769047 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0x880001769057 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0x880001769067 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0x880001769077 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0x880001769087 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0x880001769097 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
0xffff81400070 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0xffff81400080 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0xffff81400090 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0xffff814000a0 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0xffff814000b0 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0xffff814000c0 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0xffff814000d0 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0xffff814000e0 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
0xffff81769027 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0xffff81769037 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0xffff81769047 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0xffff81769057 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0xffff81769067 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0xffff81769077 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0xffff81769087 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0xffff81769097 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
An optional size can be given for the displayed results as with the other fuctions (db, dw, dd, dq, etc).
You can, of course, specify a different layer name as well.
+22 -2
View File
@@ -20,6 +20,10 @@ full = [
"capstone>=5.0.3,<6",
"pycryptodome>=3.21.0,<4",
"leechcorepyc>=2.19.2,<3; sys_platform != 'darwin'",
# https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
# 10.0.0 dropped support for Python3.7
# 11.0.0 dropped support for Python3.8, which is still supported by Volatility3
"pillow>=10.0.0,<11.0.0",
]
cloud = [
@@ -32,6 +36,7 @@ dev = [
"jsonschema>=4.23.0,<5",
"pyinstaller>=6.11.0,<7",
"pyinstaller-hooks-contrib>=2024.9",
"types-jsonschema>=4.23.0,<5",
]
test = [
@@ -68,8 +73,23 @@ include = ["volatility3*"]
mypy_path = "./stubs"
show_traceback = true
[tool.mypy.overrides]
ignore_missing_imports = true
[tool.ruff]
line-length = 88
target-version = "py38"
[tool.ruff.lint]
select = [
"F", # pyflakes
"E", # pycodestyle errors
"W", # pycodestyle warnings
"G", # flake8-logging-format
"PIE", # flake8-pie
"UP", # pyupgrade
]
ignore = [
"E501", # ignore due to conflict with formatter
]
[build-system]
requires = ["setuptools>=68"]
+3 -2
View File
@@ -2,9 +2,11 @@ import sys
import struct
import traceback
import unittest
sys.path.insert(0, "../../volatility3")
from volatility3.plugins.windows import scheduled_tasks
class TestActionsDecoding(unittest.TestCase):
def test_decode_exe_action(self):
# fmt: off
@@ -84,8 +86,7 @@ class TestActionsDecoding(unittest.TestCase):
self.assertEqual(actions[0].action_type, scheduled_tasks.ActionType.Exe)
except Exception:
self.fail(
"ActionDecoder.decode should not raise exception:\n%s"
% traceback.format_exc()
f"ActionDecoder.decode should not raise exception:\n{traceback.format_exc()}"
)
+231 -50
View File
@@ -39,7 +39,9 @@ def runvol(args, volatility, python):
return p.returncode, stdout, stderr
def runvol_plugin(plugin, img, volatility, python, pluginargs=[], globalargs=[]):
def runvol_plugin(plugin, img, volatility, python, pluginargs=None, globalargs=None):
pluginargs = pluginargs or []
globalargs = globalargs or []
args = (
globalargs
+ [
@@ -54,13 +56,68 @@ def runvol_plugin(plugin, img, volatility, python, pluginargs=[], globalargs=[])
return runvol(args, volatility, python)
def runvolshell(img, volshell, python, volshellargs=None, globalargs=None):
volshellargs = volshellargs or []
globalargs = globalargs or []
args = (
globalargs
+ [
"--single-location",
img,
"-q",
]
+ volshellargs
)
return runvol(args, volshell, python)
#
# TESTS
#
def basic_volshell_test(image, volatility, python, globalargs):
# Basic VolShell test to verify requirements and ensure VolShell runs without crashing
volshell_commands = [
"print(ps())",
"exit()",
]
# FIXME: When the minimum Python version includes 3.12, replace the following with:
# with tempfile.NamedTemporaryFile(delete_on_close=False) as fd: ...
fd, filename = tempfile.mkstemp(suffix=".txt")
try:
volshell_script = "\n".join(volshell_commands)
with os.fdopen(fd, "w") as f:
f.write(volshell_script)
rc, out, _err = runvolshell(
img=image,
volshell=volatility,
python=python,
volshellargs=["--script", filename],
globalargs=globalargs,
)
finally:
with contextlib.suppress(FileNotFoundError):
os.remove(filename)
assert rc == 0
assert out.count(b"\n") >= 4
return out
# WINDOWS
def test_windows_volshell(image, volatility, python):
out = basic_volshell_test(image, volatility, python, globalargs=["-w"])
assert out.count(b"<EPROCESS") > 40
def test_windows_pslist(image, volatility, python):
rc, out, _err = runvol_plugin("windows.pslist.PsList", image, volatility, python)
out = out.lower()
@@ -332,86 +389,91 @@ def test_windows_vadyarascan_yara_string(image, volatility, python):
# LINUX
def test_linux_volshell(image, volatility, python):
out = basic_volshell_test(image, volatility, python, globalargs=["-l"])
assert out.count(b"<task_struct") > 100
def test_linux_pslist(image, volatility, python):
rc, out, _err = runvol_plugin("linux.pslist.PsList", image, volatility, python)
out = out.lower()
assert rc == 0
out = out.lower()
assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
assert out.find(b"watchdog") != -1
assert out.count(b"\n") > 10
assert rc == 0
def test_linux_check_idt(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.check_idt.Check_idt", image, volatility, python
)
out = out.lower()
assert rc == 0
out = out.lower()
assert out.count(b"__kernel__") >= 10
assert out.count(b"\n") > 10
assert rc == 0
def test_linux_check_syscall(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.check_syscall.Check_syscall", image, volatility, python
)
out = out.lower()
assert rc == 0
out = out.lower()
assert out.find(b"sys_close") != -1
assert out.find(b"sys_open") != -1
assert out.count(b"\n") > 100
assert rc == 0
def test_linux_lsmod(image, volatility, python):
rc, out, _err = runvol_plugin("linux.lsmod.Lsmod", image, volatility, python)
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
out = out.lower()
assert out.count(b"\n") > 10
def test_linux_lsof(image, volatility, python):
rc, out, _err = runvol_plugin("linux.lsof.Lsof", image, volatility, python)
out = out.lower()
assert rc == 0
out = out.lower()
assert out.count(b"socket:") >= 10
assert out.count(b"\n") > 35
assert rc == 0
def test_linux_proc_maps(image, volatility, python):
rc, out, _err = runvol_plugin("linux.proc.Maps", image, volatility, python)
out = out.lower()
assert rc == 0
out = out.lower()
assert out.count(b"anonymous mapping") >= 10
assert out.count(b"\n") > 100
assert rc == 0
def test_linux_tty_check(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.tty_check.tty_check", image, volatility, python
)
out = out.lower()
assert rc == 0
out = out.lower()
assert out.find(b"__kernel__") != -1
assert out.count(b"\n") >= 5
assert rc == 0
def test_linux_sockstat(image, volatility, python):
rc, out, _err = runvol_plugin("linux.sockstat.Sockstat", image, volatility, python)
assert rc == 0
assert out.count(b"AF_UNIX") >= 354
assert out.count(b"AF_BLUETOOTH") >= 5
assert out.count(b"AF_INET") >= 32
assert out.count(b"AF_INET6") >= 20
assert out.count(b"AF_PACKET") >= 1
assert out.count(b"AF_NETLINK") >= 43
assert rc == 0
def test_linux_library_list(image, volatility, python):
@@ -423,49 +485,48 @@ def test_linux_library_list(image, volatility, python):
pluginargs=["--pids", "2363"],
)
assert rc == 0
assert re.search(
rb"NetworkManager\s2363\s0x7f52cdda0000\s/lib/x86_64-linux-gnu/libnss_files.so.2",
out,
)
assert out.count(b"\n") > 10
assert rc == 0
def test_linux_pstree(image, volatility, python):
rc, out, _err = runvol_plugin("linux.pstree.PsTree", image, volatility, python)
out = out.lower()
assert rc == 0
out = out.lower()
assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
assert out.count(b"\n") > 10
assert rc == 0
def test_linux_pidhashtable(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.pidhashtable.PIDHashTable", image, volatility, python
)
out = out.lower()
assert rc == 0
out = out.lower()
assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
assert out.count(b"\n") > 10
assert rc == 0
def test_linux_bash(image, volatility, python):
rc, out, _err = runvol_plugin("linux.bash.Bash", image, volatility, python)
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_boottime(image, volatility, python):
rc, out, _err = runvol_plugin("linux.boottime.Boottime", image, volatility, python)
out = out.lower()
assert out.count(b"utc") >= 1
assert rc == 0
out = out.lower()
assert out.count(b"utc") >= 1
def test_linux_capabilities(image, volatility, python):
@@ -482,36 +543,33 @@ def test_linux_capabilities(image, volatility, python):
# However, we can still check that the plugin requirements are met.
return None
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_check_creds(image, volatility, python):
rc, _out, _err = runvol_plugin(
rc, out, _err = runvol_plugin(
"linux.check_creds.Check_creds", image, volatility, python
)
# linux-sample-1.bin has no processes sharing credentials.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_elfs(image, volatility, python):
rc, out, _err = runvol_plugin("linux.elfs.Elfs", image, volatility, python)
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_envars(image, volatility, python):
rc, out, _err = runvol_plugin("linux.envars.Envars", image, volatility, python)
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_kthreads(image, volatility, python):
@@ -528,44 +586,42 @@ def test_linux_kthreads(image, volatility, python):
# However, we can still check that the plugin requirements are met.
return None
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_malfind(image, volatility, python):
rc, _out, _err = runvol_plugin("linux.malfind.Malfind", image, volatility, python)
rc, out, _err = runvol_plugin("linux.malfind.Malfind", image, volatility, python)
# linux-sample-1.bin has no process memory ranges with potential injected code.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_mountinfo(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.mountinfo.MountInfo", image, volatility, python
)
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_psaux(image, volatility, python):
rc, out, _err = runvol_plugin("linux.psaux.PsAux", image, volatility, python)
out = out.lower()
assert out.count(b"\n") > 50
assert rc == 0
assert out.count(b"\n") > 50
def test_linux_ptrace(image, volatility, python):
rc, _out, _err = runvol_plugin("linux.ptrace.Ptrace", image, volatility, python)
rc, out, _err = runvol_plugin("linux.ptrace.Ptrace", image, volatility, python)
# linux-sample-1.bin has no processes being ptreaced.
# linux-sample-1.bin has no processes being ptraced.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_vmaregexscan(image, volatility, python):
@@ -576,10 +632,9 @@ def test_linux_vmaregexscan(image, volatility, python):
python,
pluginargs=["--pid", "1", "--pattern", "\\x7fELF"],
)
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_vmayarascan_yara_rule(image, volatility, python):
@@ -613,9 +668,8 @@ def test_linux_vmayarascan_yara_rule(image, volatility, python):
with contextlib.suppress(FileNotFoundError):
os.remove(filename)
out = out.lower()
assert out.count(b"\n") > 4
assert rc == 0
assert out.count(b"\n") > 4
def test_linux_vmayarascan_yara_string(image, volatility, python):
@@ -626,10 +680,9 @@ def test_linux_vmayarascan_yara_string(image, volatility, python):
python,
pluginargs=["--pid", "1", "--yara-string", "ELF"],
)
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_page_cache_files(image, volatility, python):
@@ -640,8 +693,8 @@ def test_linux_page_cache_files(image, volatility, python):
python,
pluginargs=["--find", "/etc/passwd"],
)
out = out.lower()
assert rc == 0
assert out.count(b"\n") > 4
# inode_num inode_addr ... file_path
@@ -649,12 +702,140 @@ def test_linux_page_cache_files(image, volatility, python):
rb"146829\s0x88001ab5c270.*?/etc/passwd",
out,
)
def test_linux_page_cache_inodepages(image, volatility, python):
inode_address = hex(0x88001AB5C270)
inode_dump_filename = f"inode_{inode_address}.dmp"
try:
rc, out, _err = runvol_plugin(
"linux.pagecache.InodePages",
image,
volatility,
python,
pluginargs=["--inode", inode_address, "--dump"],
)
assert rc == 0
assert out.count(b"\n") > 4
# PageVAddr PagePAddr MappingAddr .. DumpSafe
assert re.search(
rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True",
out,
)
assert os.path.exists(inode_dump_filename)
with open(inode_dump_filename, "rb") as fp:
inode_contents = fp.read()
assert inode_contents.count(b"\n") > 30
assert inode_contents.count(b"root:x:0:0:root:/root:/bin/bash") > 0
finally:
with contextlib.suppress(FileNotFoundError):
os.remove(inode_dump_filename)
def test_linux_check_afinfo(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.check_afinfo.Check_afinfo", image, volatility, python
)
# linux-sample-1.bin has no suspicious results.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_check_modules(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.check_modules.Check_modules", image, volatility, python
)
# linux-sample-1.bin has no suspicious results.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_ebpf_progs(image, volatility, python):
rc, out, err = runvol_plugin(
"linux.ebpf.EBPF",
image,
volatility,
python,
globalargs=["-vvv"],
)
if rc != 0 and err.count(b"Unsupported kernel") > 0:
# The linux-sample-1.bin kernel implementation isn't supported.
# However, we can still check that the plugin requirements are met.
return None
assert rc == 0
assert out.count(b"\n") > 4
def test_linux_iomem(image, volatility, python):
rc, out, _err = runvol_plugin("linux.iomem.IOMem", image, volatility, python)
assert rc == 0
assert out.count(b"\n") > 100
def test_linux_keyboard_notifiers(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.keyboard_notifiers.Keyboard_notifiers", image, volatility, python
)
# linux-sample-1.bin has no suspicious results for this plugin.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_kmesg(image, volatility, python):
rc, out, _err = runvol_plugin("linux.kmsg.Kmsg", image, volatility, python)
assert rc == 0
assert out.count(b"\n") > 100
def test_linux_netfilter(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.netfilter.Netfilter", image, volatility, python
)
# linux-sample-1.bin has no suspicious results for this plugin.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_psscan(image, volatility, python):
rc, out, _err = runvol_plugin("linux.psscan.PsScan", image, volatility, python)
assert rc == 0
assert out.count(b"\n") > 100
def test_linux_hidden_modules(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.hidden_modules.Hidden_modules", image, volatility, python
)
# linux-sample-1.bin has no hidden modules.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
# MAC
def test_mac_volshell(image, volatility, python):
basic_volshell_test(image, volatility, python, globalargs=["-m"])
def test_mac_pslist(image, volatility, python):
rc, out, _err = runvol_plugin("mac.pslist.PsList", image, volatility, python)
out = out.lower()
+11 -19
View File
@@ -19,7 +19,7 @@ import os
import sys
import tempfile
import traceback
from typing import Any, Dict, List, Tuple, Type, Union
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from urllib import parse, request
try:
@@ -57,14 +57,14 @@ formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s")
console.setFormatter(formatter)
class PrintedProgress(object):
class PrintedProgress:
"""A progress handler that prints the progress value and the description
onto the command line."""
def __init__(self):
self._max_message_len = 0
def __call__(self, progress: Union[int, float], description: str = None):
def __call__(self, progress: Union[int, float], description: Optional[str] = None):
"""A simple function for providing text-based feedback.
.. warning:: Only for development use.
@@ -81,7 +81,7 @@ class PrintedProgress(object):
class MuteProgress(PrintedProgress):
"""A dummy progress handler that produces no output when called."""
def __call__(self, progress: Union[int, float], description: str = None):
def __call__(self, progress: Union[int, float], description: Optional[str] = None):
pass
@@ -126,9 +126,7 @@ class CommandLine:
"--help",
action="help",
default=argparse.SUPPRESS,
help="Show this help message and exit, for specific plugin options use '{} <pluginname> --help'".format(
parser.prog
),
help=f"Show this help message and exit, for specific plugin options use '{parser.prog} <pluginname> --help'",
)
parser.add_argument(
"-c",
@@ -360,9 +358,7 @@ class CommandLine:
subparser = parser.add_subparsers(
title="Plugins",
dest="plugin",
description="For plugin specific options, run '{} <plugin> --help'".format(
self.CLI_NAME
),
description=f"For plugin specific options, run '{self.CLI_NAME} <plugin> --help'",
action=volargparse.HelpfulSubparserAction,
metavar="PLUGIN",
)
@@ -416,7 +412,7 @@ class CommandLine:
# UI fills in the config, here we load it from the config file and do it before we process the CL parameters
if args.config:
with open(args.config, "r") as f:
with open(args.config) as f:
json_val = json.load(f)
ctx.config.splice(
plugin_config_path,
@@ -722,9 +718,7 @@ class CommandLine:
if isinstance(requirement, requirements.ListRequirement):
if not isinstance(value, list):
raise TypeError(
"Configuration for ListRequirement was not a list: {}".format(
requirement.name
)
f"Configuration for ListRequirement was not a list: {requirement.name}"
)
value = [requirement.element_type(x) for x in value]
if not inspect.isclass(configurables_list[configurable]):
@@ -797,7 +791,7 @@ class CommandLine:
fd, self._name = tempfile.mkstemp(
suffix=".vol3", prefix="tmp_", dir=output_dir
)
self._file = io.open(fd, mode="w+b")
self._file = open(fd, mode="w+b")
CLIFileHandler.__init__(self, filename)
for item in dir(self._file):
if not item.startswith("_") and item not in (
@@ -870,9 +864,7 @@ class CommandLine:
requirement, interfaces.configuration.RequirementInterface
):
raise TypeError(
"Plugin contains requirements that are not RequirementInterfaces: {}".format(
configurable.__name__
)
f"Plugin contains requirements that are not RequirementInterfaces: {configurable.__name__}"
)
if isinstance(requirement, interfaces.configuration.SimpleTypeRequirement):
additional["type"] = requirement.instance_type
@@ -887,7 +879,7 @@ class CommandLine:
volatility3.framework.configuration.requirements.ListRequirement,
):
# Allow a list of integers, specified with the convenient 0x hexadecimal format
if requirement.element_type == int:
if requirement.element_type is int:
additional["type"] = lambda x: int(x, 0)
else:
additional["type"] = requirement.element_type
+6 -5
View File
@@ -1,7 +1,8 @@
import logging
from typing import Any, List, Optional
from volatility3.framework import constants, interfaces
import re
from typing import Any, List, Optional
from volatility3.framework import constants, interfaces
vollog = logging.getLogger(__name__)
@@ -67,16 +68,16 @@ class ColumnFilter:
) -> None:
self.column_num = column_num
self.pattern = pattern
self.exclude = exclude
self.regex = regex
self.exclude = exclude
def find(self, item) -> bool:
"""Identifies whether an item is found in the appropriate column"""
try:
if self.regex:
return re.search(self.pattern, f"{item}")
return bool(re.search(self.pattern, f"{item}"))
return self.pattern in f"{item}"
except IOError:
except OSError:
return False
def found(self, row: List[Any]) -> bool:
+4 -4
View File
@@ -176,7 +176,7 @@ class QuickTextRenderer(CLIRenderer):
format_hints.HexBytes: optional(hex_bytes_as_text),
format_hints.MultiTypeData: quoted_optional(multitypedata_as_text),
interfaces.renderers.Disassembly: optional(display_disassembly),
bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])),
bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)),
datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")),
"default": optional(lambda x: f"{x}"),
}
@@ -256,7 +256,7 @@ class CSVRenderer(CLIRenderer):
format_hints.HexBytes: optional(hex_bytes_as_text),
format_hints.MultiTypeData: optional(multitypedata_as_text),
interfaces.renderers.Disassembly: optional(display_disassembly),
bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])),
bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)),
datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")),
"default": optional(lambda x: f"{x}"),
}
@@ -450,7 +450,7 @@ class JsonRenderer(CLIRenderer):
format_hints.HexBytes: quoted_optional(hex_bytes_as_text),
interfaces.renderers.Disassembly: quoted_optional(display_disassembly),
format_hints.MultiTypeData: quoted_optional(multitypedata_as_text),
bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])),
bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)),
datetime.datetime: lambda x: (
x.isoformat()
if not isinstance(x, interfaces.renderers.BaseAbsentValue)
@@ -467,7 +467,7 @@ class JsonRenderer(CLIRenderer):
def output_result(self, outfd, result):
"""Outputs the JSON data to a file in a particular format"""
outfd.write("{}\n".format(json.dumps(result, indent=2, sort_keys=True)))
outfd.write(f"{json.dumps(result, indent=2, sort_keys=True)}\n")
def render(self, grid: interfaces.renderers.TreeGrid):
outfd = sys.stdout
+1 -1
View File
@@ -5,7 +5,7 @@
import argparse
import gettext
import re
from typing import List, Optional, Sequence, Any, Union
from typing import Optional, Sequence, Any, Union
# This effectively overrides/monkeypatches the core argparse module to provide more helpful output around choices
+2 -4
View File
@@ -282,9 +282,7 @@ class VolShell(cli.CommandLine):
for plugin in volshell_plugin_list:
subparser = parser.add_argument_group(
title=plugin.capitalize(),
description="Configuration options based on {} options".format(
plugin.capitalize()
),
description=f"Configuration options based on {plugin.capitalize()} options",
)
self.populate_requirements_argparse(subparser, volshell_plugin_list[plugin])
configurables_list[plugin] = volshell_plugin_list[plugin]
@@ -331,7 +329,7 @@ class VolShell(cli.CommandLine):
# UI fills in the config, here we load it from the config file and do it before we process the CL parameters
if args.config:
with open(args.config, "r") as f:
with open(args.config) as f:
json_val = json.load(f)
ctx.config.splice(
plugin_config_path,
+16 -18
View File
@@ -203,7 +203,7 @@ class Volshell(interfaces.plugins.PluginInterface):
connector = " "
if chunk_size < 2:
connector = ""
ascii_data = connector.join([self._ascii_bytes(x) for x in valid_data])
ascii_data = connector.join(self._ascii_bytes(x) for x in valid_data)
print(hex(offset), " ", hex_data, " ", ascii_data)
offset += 16
@@ -240,7 +240,7 @@ class Volshell(interfaces.plugins.PluginInterface):
return None
return self.context.modules[self.current_kernel_name]
def change_layer(self, layer_name: str = None):
def change_layer(self, layer_name: Optional[str] = None):
"""Changes the current default layer"""
if not layer_name:
layer_name = self.current_layer
@@ -250,7 +250,7 @@ class Volshell(interfaces.plugins.PluginInterface):
self.__current_layer = layer_name
sys.ps1 = f"({self.current_layer}) >>> "
def change_symbol_table(self, symbol_table_name: str = None):
def change_symbol_table(self, symbol_table_name: Optional[str] = None):
"""Changes the current_symbol_table"""
if not symbol_table_name:
print("No symbol table provided, not changing current symbol table")
@@ -262,7 +262,7 @@ class Volshell(interfaces.plugins.PluginInterface):
self.__current_symbol_table = symbol_table_name
print(f"Current Symbol Table: {self.current_symbol_table}")
def change_kernel(self, kernel_name: str = None):
def change_kernel(self, kernel_name: Optional[str] = None):
if not kernel_name:
print("No kernel module name provided, not changing current kernel")
if kernel_name not in self.context.modules:
@@ -347,7 +347,7 @@ class Volshell(interfaces.plugins.PluginInterface):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if not isinstance(
@@ -479,7 +479,7 @@ class Volshell(interfaces.plugins.PluginInterface):
if treegrid is not None:
self.render_treegrid(treegrid)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
print("No symbol table provided")
@@ -553,17 +553,16 @@ class Volshell(interfaces.plugins.PluginInterface):
if argname in kwargs:
del kwargs[argname]
for keyword in kwargs:
val = kwargs[keyword]
if not isinstance(
val, interfaces.configuration.BasicTypes
) and not isinstance(val, list):
if not isinstance(val, list) or all(
isinstance(x, interfaces.configuration.BasicTypes) for x in val
):
raise TypeError(
"Configurable values must be simple types (int, bool, str, bytes)"
)
for keyword, val in kwargs.items():
BasicType_or_list_of_BasicType = False # excludes list of lists
if isinstance(val, interfaces.configuration.BasicTypes):
BasicType_or_list_of_BasicType = True
if all(isinstance(x, interfaces.configuration.BasicTypes) for x in val):
BasicType_or_list_of_BasicType = True
if not BasicType_or_list_of_BasicType:
raise TypeError(
"Configurable values must be simple types (int, bool, str, bytes)"
)
self.context.config[config_path + "." + keyword] = val
constructed = clazz(self.context, config_path, **constructor_args)
@@ -585,7 +584,6 @@ class NullFileHandler(io.BytesIO, interfaces.plugins.FileHandlerInterface):
def writelines(self, lines: Iterable[bytes]):
"""Dummy method"""
pass
def write(self, b: bytes):
"""Dummy method"""
+4 -4
View File
@@ -2,7 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import Any, List, Tuple, Union
from typing import Any, List, Optional, Tuple, Union
from volatility3.cli.volshell import generic
from volatility3.framework import constants, interfaces
@@ -20,7 +20,7 @@ class Volshell(generic.Volshell):
name="kernel", description="Linux kernel module"
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.IntRequirement(
name="pid", description="Process ID", optional=True
@@ -61,7 +61,7 @@ class Volshell(generic.Volshell):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
@@ -69,7 +69,7 @@ class Volshell(generic.Volshell):
object = self.current_symbol_table + constants.BANG + object
return super().display_type(object, offset)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
symbol_table = self.current_symbol_table
+3 -3
View File
@@ -2,7 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import Any, List, Tuple, Union
from typing import Any, List, Optional, Tuple, Union
from volatility3.cli.volshell import generic
from volatility3.framework import constants, interfaces
@@ -63,7 +63,7 @@ class Volshell(generic.Volshell):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
@@ -71,7 +71,7 @@ class Volshell(generic.Volshell):
object = self.current_symbol_table + constants.BANG + object
return super().display_type(object, offset)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
symbol_table = self.current_symbol_table
+3 -3
View File
@@ -2,7 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import Any, List, Tuple, Union
from typing import Any, List, Optional, Tuple, Union
from volatility3.cli.volshell import generic
from volatility3.framework import constants, interfaces
@@ -60,7 +60,7 @@ class Volshell(generic.Volshell):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
@@ -68,7 +68,7 @@ class Volshell(generic.Volshell):
object = self.current_symbol_table + constants.BANG + object
return super().display_type(object, offset)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
symbol_table = self.current_symbol_table
+11 -35
View File
@@ -6,28 +6,12 @@
import glob
import sys
import zipfile
required_python_version = (3, 8, 0)
if (
sys.version_info.major != required_python_version[0]
or sys.version_info.minor < required_python_version[1]
or (
sys.version_info.minor == required_python_version[1]
and sys.version_info.micro < required_python_version[2]
)
):
raise RuntimeError(
"Volatility framework requires python version {}.{}.{} or greater".format(
*required_python_version
)
)
import importlib
import inspect
import logging
import os
import traceback
from typing import Any, Dict, Generator, List, Tuple, Type, TypeVar
from typing import Any, Dict, Generator, List, Optional, Tuple, Type, TypeVar
from volatility3.framework import constants, interfaces
@@ -56,27 +40,25 @@ def require_interface_version(*args) -> None:
if len(args):
if args[0] != interface_version()[0]:
raise RuntimeError(
"Framework interface version {} is incompatible with required version {}".format(
interface_version()[0], args[0]
)
f"Framework interface version {interface_version()[0]} is incompatible with required version {args[0]}"
)
if len(args) > 1:
if args[1] > interface_version()[1]:
raise RuntimeError(
"Framework interface version {} is an older revision than the required version {}".format(
".".join([str(x) for x in interface_version()[0:2]]),
".".join([str(x) for x in args[0:2]]),
".".join(str(x) for x in interface_version()[0:2]),
".".join(str(x) for x in args[0:2]),
)
)
class NonInheritable(object):
class NonInheritable:
def __init__(self, value: Any, cls: Type) -> None:
self.default_value = value
self.cls = cls
def __get__(self, obj: Any, get_type: Type = None) -> Any:
if type == self.cls:
def __get__(self, obj: Any, get_type: Optional[Type] = None) -> Any:
if type is self.cls:
if hasattr(self.default_value, "__get__"):
return self.default_value.__get__(obj, get_type)
return self.default_value
@@ -99,8 +81,7 @@ def class_subclasses(cls: Type[T]) -> Generator[Type[T], None, None]:
# The typing system is not clever enough to realize that clazz has a hidden attr after the hasattr check
if not hasattr(clazz, "hidden") or not clazz.hidden: # type: ignore
yield clazz
for return_value in class_subclasses(clazz):
yield return_value
yield from class_subclasses(clazz)
def import_files(base_module, ignore_errors: bool = False) -> List[str]:
@@ -161,9 +142,7 @@ def import_files(base_module, ignore_errors: bool = False) -> List[str]:
def _filter_files(filename: str):
"""Ensures that a filename traversed is an importable python file"""
return (
filename.endswith(".py") or filename.endswith(".pyc")
) and not filename.startswith("__")
return (filename.endswith((".py", ".pyc"))) and not filename.startswith("__")
def import_file(module: str, path: str, ignore_errors: bool = False) -> List[str]:
@@ -187,9 +166,7 @@ def import_file(module: str, path: str, ignore_errors: bool = False) -> List[str
traceback.TracebackException.from_exception(e).format(chain=True)
)
)
vollog.debug(
"Failed to import module {} based on file: {}".format(module, path)
)
vollog.debug(f"Failed to import module {module} based on file: {path}")
failures.append(module)
if not ignore_errors:
raise
@@ -207,8 +184,7 @@ def _zipwalk(path: str):
zip_results[os.path.join(path, os.path.dirname(file.filename))] = (
dirlist
)
for value in zip_results:
yield value, zip_results[value]
yield from zip_results.items()
def list_plugins() -> Dict[str, Type[interfaces.plugins.PluginInterface]]:
+6 -7
View File
@@ -3,12 +3,10 @@
#
import logging
import os
from typing import Optional, Tuple, Type
from typing import Optional, Tuple
from volatility3.framework import constants, interfaces
from volatility3.framework.automagic import symbol_cache, symbol_finder
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import intel, scanners
from volatility3.framework.symbols import linux
@@ -173,9 +171,7 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
if aslr_shift & 0xFFF != 0 or kaslr_shift & 0xFFF != 0:
continue
vollog.debug(
"Linux ASLR shift values determined: physical {:0x} virtual {:0x}".format(
kaslr_shift, aslr_shift
)
f"Linux ASLR shift values determined: physical {kaslr_shift:0x} virtual {aslr_shift:0x}"
)
return kaslr_shift, aslr_shift
@@ -198,5 +194,8 @@ class LinuxSymbolFinder(symbol_finder.SymbolFinder):
banner_config_key = "kernel_banner"
operating_system = "linux"
symbol_class = "volatility3.framework.symbols.linux.LinuxKernelIntermedSymbols"
find_aslr = lambda cls, *args: LinuxIntelStacker.find_aslr(*args)[1]
exclusion_list = ["mac", "windows"]
@classmethod
def find_aslr(cls, *args):
return LinuxIntelStacker.find_aslr(*args)[1]
+1 -3
View File
@@ -3,13 +3,11 @@
#
import logging
import os
import struct
from typing import Optional
from volatility3.framework import constants, exceptions, interfaces, layers
from volatility3.framework.automagic import symbol_cache, symbol_finder
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import intel, scanners
from volatility3.framework.symbols import mac
@@ -184,7 +182,7 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
aslr_shift = 0
for offset, banner in offset_generator:
banner_major, banner_minor = [int(x) for x in banner[22:].split(b".")[0:2]]
banner_major, banner_minor = (int(x) for x in banner[22:].split(b".")[0:2])
tmp_aslr_shift = offset - cls.virtual_to_physical_address(
version_json_address
+1 -3
View File
@@ -215,9 +215,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
return (virtual_layer_name, kvo, kernel)
else:
vollog.debug(
"Potential kernel_virtual_offset did not map to expected location: {}".format(
hex(kvo)
)
f"Potential kernel_virtual_offset did not map to expected location: {hex(kvo)}"
)
except exceptions.InvalidAddressException:
vollog.debug(
+3 -1
View File
@@ -166,7 +166,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
cls,
context: interfaces.context.ContextInterface,
initial_layer: str,
stack_set: List[Type[interfaces.automagic.StackerLayerInterface]] = None,
stack_set: Optional[
List[Type[interfaces.automagic.StackerLayerInterface]]
] = None,
progress_callback: constants.ProgressCallback = None,
):
"""Stacks as many possible layers on top of the initial layer as can be done.
@@ -104,10 +104,11 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
for subclazz in framework.class_subclasses(IdentifierProcessor):
self._classifiers[subclazz.operating_system] = subclazz
@abstractmethod
def add_identifier(self, location: str, operating_system: str, identifier: str):
"""Adds an identifier to the store"""
pass
@abstractmethod
def find_location(
self, identifier: bytes, operating_system: Optional[str]
) -> Optional[str]:
@@ -120,19 +121,19 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
Returns:
The location of the symbols file that matches the identifier
"""
pass
@abstractmethod
def get_local_locations(self) -> Iterable[str]:
"""Returns a list of all the local locations"""
pass
@abstractmethod
def update(self):
"""Locates all files under the symbol directories. Updates the cache with additions, modifications and removals.
This also updates remote locations based on a cache timeout.
"""
pass
@abstractmethod
def get_identifier_dictionary(
self, operating_system: Optional[str] = None, local_only: bool = False
) -> Dict[bytes, str]:
@@ -145,16 +146,16 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
Returns:
A dictionary of identifiers mapped to a location
"""
pass
@abstractmethod
def get_identifier(self, location: str) -> Optional[bytes]:
"""Returns an identifier based on a specific location or None"""
pass
@abstractmethod
def get_identifiers(self, operating_system: Optional[str]) -> List[bytes]:
"""Returns all identifiers for a particular operating system"""
pass
@abstractmethod
def get_location_statistics(
self, location: str
) -> Optional[Tuple[int, int, int, int]]:
@@ -164,6 +165,7 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
A tuple of base_types, types, enums, symbols, or None is location not found
"""
@abstractmethod
def get_hash(self, location: str) -> Optional[str]:
"""Returns the hash of the JSON from within a location ISF"""
@@ -572,6 +574,6 @@ class RemoteIdentifierFormat:
try:
subrbf = RemoteIdentifierFormat(location)
yield from subrbf.process(identifiers, operating_system)
except IOError:
except OSError:
vollog.debug(f"Remote file not found: {location}")
return identifiers
@@ -4,7 +4,7 @@
import logging
import os
from typing import Any, Callable, Iterable, List, Optional, Tuple
from typing import Callable, List, Optional, Tuple
from volatility3.framework import constants, interfaces, layers
from volatility3.framework.automagic import symbol_cache
@@ -0,0 +1,14 @@
import sys
required_python_version = (3, 8, 0)
if (
sys.version_info.major != required_python_version[0]
or sys.version_info.minor < required_python_version[1]
or (
sys.version_info.minor == required_python_version[1]
and sys.version_info.micro < required_python_version[2]
)
):
raise RuntimeError(
f"Volatility framework requires python version {required_python_version[0]}.{required_python_version[1]}.{required_python_version[2]} or greater"
)
@@ -2,4 +2,4 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from volatility3.framework.configuration import requirements
from volatility3.framework.configuration import requirements as requirements
@@ -11,7 +11,7 @@ expect to be in the context (such as particular layers or symboltables).
import abc
import logging
import os
from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type
from urllib import parse, request
from volatility3.framework import constants, interfaces
@@ -111,7 +111,7 @@ class ListRequirement(interfaces.configuration.RequirementInterface):
Args:
element_type: The (requirement) type of each element within the list
max_elements; The maximum number of acceptable elements this list can contain
max_elements: The maximum number of acceptable elements this list can contain
min_elements: The minimum number of acceptable elements this list can contain
"""
super().__init__(*args, **kwargs)
@@ -314,11 +314,11 @@ class TranslationLayerRequirement(
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: interfaces.configuration.ConfigSimpleType = None,
optional: bool = False,
oses: List = None,
architectures: List = None,
oses: Optional[List] = None,
architectures: Optional[List[str]] = None,
) -> None:
"""Constructs a Translation Layer Requirement.
@@ -526,18 +526,18 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
description: Optional[str] = None,
default: bool = False,
optional: bool = False,
component: Type[interfaces.configuration.VersionableInterface] = None,
component: Optional[Type[interfaces.configuration.VersionableInterface]] = None,
version: Optional[Tuple[int, ...]] = None,
) -> None:
if version is None:
raise TypeError("Version cannot be None")
if component is None:
raise TypeError("Component cannot be None")
if description is None:
description = f"Version {'.'.join([str(x) for x in version])} dependency on {component.__module__}.{component.__name__} unmet"
description = f"Version {'.'.join(str(x) for x in version)} dependency on {component.__module__}.{component.__name__} unmet"
super().__init__(
name=name, description=description, default=default, optional=optional
)
if component is None:
raise TypeError("Component cannot be None")
self._component: Type[interfaces.configuration.VersionableInterface] = component
self._version = version
@@ -546,7 +546,7 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
context: interfaces.context.ContextInterface,
config_path: str,
accumulator: Optional[
List[interfaces.configuration.VersionableInterface]
Set[interfaces.configuration.VersionableInterface]
] = None,
) -> Dict[str, interfaces.configuration.RequirementInterface]:
# Mypy doesn't appreciate our classproperty implementation, self._plugin.version has no type
@@ -580,7 +580,7 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
)
if result:
result.update({config_path: self})
result[config_path] = self
return result
context.config[interfaces.configuration.path_join(config_path, self.name)] = (
@@ -604,10 +604,10 @@ class PluginRequirement(VersionRequirement):
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: bool = False,
optional: bool = False,
plugin: Type[interfaces.plugins.PluginInterface] = None,
plugin: Optional[Type[interfaces.plugins.PluginInterface]] = None,
version: Optional[Tuple[int, ...]] = None,
) -> None:
super().__init__(
@@ -627,7 +627,7 @@ class ModuleRequirement(
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: bool = False,
architectures: Optional[List[str]] = None,
optional: bool = False,
@@ -664,9 +664,7 @@ class ModuleRequirement(
if value is not None:
vollog.log(
constants.LOGLEVEL_V,
"TypeError - Module Requirement only accepts string labels: {}".format(
repr(value)
),
f"TypeError - Module Requirement only accepts string labels: {repr(value)}",
)
return {config_path: self}
+7 -7
View File
@@ -13,14 +13,14 @@ import sys
import warnings
from typing import Callable, Optional
import volatility3.framework.constants.linux
import volatility3.framework.constants.windows
from volatility3.framework.constants import linux as linux
from volatility3.framework.constants import windows as windows
from volatility3.framework.constants._version import (
PACKAGE_VERSION,
VERSION_MAJOR,
VERSION_MINOR,
VERSION_PATCH,
VERSION_SUFFIX,
PACKAGE_VERSION as PACKAGE_VERSION,
VERSION_MAJOR as VERSION_MAJOR,
VERSION_MINOR as VERSION_MINOR,
VERSION_PATCH as VERSION_PATCH,
VERSION_SUFFIX as VERSION_SUFFIX,
)
PLUGINS_PATH = [
+2 -2
View File
@@ -1,11 +1,11 @@
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 12 # Number of changes that only add to the interface
VERSION_MINOR = 15 # Number of changes that only add to the interface
VERSION_PATCH = 1 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
PACKAGE_VERSION = (
".".join([str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH]])
".".join(str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH])
+ VERSION_SUFFIX
)
"""The canonical version of the volatility3 package"""
+2 -2
View File
@@ -229,7 +229,7 @@ class Module(interfaces.context.ModuleInterface):
def object(
self,
object_type: str,
offset: int = None,
offset: Optional[int] = None,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs,
@@ -356,7 +356,7 @@ class SizedModule(Module):
return size or 0
@property # type: ignore # FIXME: mypy #5107
@functools.lru_cache()
@functools.lru_cache
def hash(self) -> str:
"""Hashes the module for equality checks.
+8 -8
View File
@@ -13,12 +13,12 @@ components of volatility to write plugins.
# This will also avoid namespace issues, because people can use interfaces.layers to
# avoid clashing with the layers package
from volatility3.framework.interfaces import (
renderers,
configuration,
context,
layers,
objects,
plugins,
symbols,
automagic,
renderers as renderers,
configuration as configuration,
context as context,
layers as layers,
objects as objects,
plugins as plugins,
symbols as symbols,
automagic as automagic,
)
@@ -42,7 +42,7 @@ class AutomagicInterface(
priority = 10
"""An ordering to indicate how soon this automagic should be run"""
exclusion_list = []
exclusion_list: List[str] = []
"""A list of plugin categories (typically operating systems) which the plugin will not operate on"""
def __init__(
@@ -53,7 +53,7 @@ ConfigSimpleType = Optional[Union[SimpleTypes, List[SimpleTypes]]]
def path_join(*args) -> str:
"""Joins configuration paths together."""
# If a path element (particularly the first) is empty, then remove it from the list
args = tuple([arg for arg in args if arg])
args = tuple(arg for arg in args if arg)
return CONFIG_SEPARATOR.join(args)
@@ -82,7 +82,7 @@ class HierarchicalDict(collections.abc.Mapping):
def __init__(
self,
initial_dict: Dict[str, "SimpleTypeRequirement"] = None,
initial_dict: Optional[Dict[str, "SimpleTypeRequirement"]] = None,
separator: str = CONFIG_SEPARATOR,
) -> None:
"""
@@ -94,7 +94,7 @@ class HierarchicalDict(collections.abc.Mapping):
raise TypeError(f"Separator must be a one character string: {separator}")
self._separator = separator
self._data: Dict[str, ConfigSimpleType] = {}
self._subdict: Dict[str, "HierarchicalDict"] = {}
self._subdict: Dict[str, HierarchicalDict] = {}
if isinstance(initial_dict, str):
initial_dict = json.loads(initial_dict)
if isinstance(initial_dict, dict):
@@ -182,9 +182,7 @@ class HierarchicalDict(collections.abc.Mapping):
else:
if not isinstance(value, HierarchicalDict):
raise TypeError(
"HierarchicalDicts can only store HierarchicalDicts within their structure: {}".format(
type(value)
)
f"HierarchicalDicts can only store HierarchicalDicts within their structure: {type(value)}"
)
self._subdict[key] = value
@@ -330,7 +328,7 @@ class RequirementInterface(metaclass=ABCMeta):
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: ConfigSimpleType = None,
optional: bool = False,
) -> None:
@@ -498,9 +496,7 @@ class SimpleTypeRequirement(RequirementInterface):
if not isinstance(value, self.instance_type):
vollog.log(
constants.LOGLEVEL_V,
"TypeError - {} requirements only accept {} type: {}".format(
self.name, self.instance_type.__name__, repr(value)
),
f"TypeError - {self.name} requirements only accept {self.instance_type.__name__} type: {repr(value)}",
)
return {config_path: self}
return {}
@@ -622,7 +618,7 @@ class ConstructableRequirementInterface(RequirementInterface):
self,
context: "interfaces.context.ContextInterface",
config_path: str,
requirement_dict: Dict[str, object] = None,
requirement_dict: Optional[Dict[str, object]] = None,
) -> Optional["interfaces.objects.ObjectInterface"]:
"""Constructs the class, handing args and the subrequirements as
parameters to __init__"""
@@ -656,6 +652,7 @@ class ConstructableRequirementInterface(RequirementInterface):
class ConfigurableRequirementInterface(RequirementInterface):
"""Simple Abstract class to provide build_required_config."""
@abstractmethod
def build_configuration(
self,
context: "interfaces.context.ContextInterface",
@@ -775,17 +772,16 @@ class ConfigurableInterface(metaclass=ABCMeta):
str: The newly generated full configuration path
"""
random_config_dict = "".join(
random.SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
random.SystemRandom().choices(string.ascii_uppercase + string.digits, k=8)
)
new_config_path = path_join(base_config_path, random_config_dict)
# TODO: Check that the new_config_path is empty, although it's not critical if it's not since the values are merged in
# This should check that each k corresponds to a requirement and each v is of the appropriate type
# This would require knowledge of the new configurable itself to verify, and they should do validation in the
# constructor anyway, however, to prevent bad types getting into the config tree we just verify that v is a simple type
# constructor anyway, however, to prevent bad types getting into the config tree we just verify that v is a basic type
for k, v in kwargs.items():
if not isinstance(v, (int, str, bool, float, bytes)):
if not isinstance(v, BasicTypes):
raise TypeError(
"Config values passed to make_subconfig can only be simple types"
)
+12 -2
View File
@@ -85,7 +85,7 @@ class ContextInterface(metaclass=ABCMeta):
object_type: Union[str, "interfaces.objects.Template"],
layer_name: str,
offset: int,
native_layer_name: str = None,
native_layer_name: Optional[str] = None,
**arguments,
) -> "interfaces.objects.ObjectInterface":
"""Object factory, takes a context, symbol, offset and optional
@@ -114,6 +114,7 @@ class ContextInterface(metaclass=ABCMeta):
"""
return copy.deepcopy(self)
@abstractmethod
def module(
self,
module_name: str,
@@ -232,7 +233,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
def object(
self,
object_type: str,
offset: int = None,
offset: Optional[int] = None,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs,
@@ -277,28 +278,36 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
symbol = self.get_symbol(name)
return self.offset + symbol.address
@abstractmethod
def get_type(self, name: str) -> "interfaces.objects.Template":
"""Returns a type from the module's symbol table."""
@abstractmethod
def get_symbol(self, name: str) -> "interfaces.symbols.SymbolInterface":
"""Returns a symbol object from the module's symbol table."""
@abstractmethod
def get_enumeration(self, name: str) -> "interfaces.objects.Template":
"""Returns an enumeration from the module's symbol table."""
@abstractmethod
def has_type(self, name: str) -> bool:
"""Determines whether a type is present in the module's symbol table."""
@abstractmethod
def has_symbol(self, name: str) -> bool:
"""Determines whether a symbol is present in the module's symbol table."""
@abstractmethod
def has_enumeration(self, name: str) -> bool:
"""Determines whether an enumeration is present in the module's symbol table."""
@property
@abstractmethod
def symbols(self) -> List:
"""Lists the symbols contained in the symbol table for this module"""
@abstractmethod
def get_symbols_by_absolute_location(self, offset: int, size: int = 0) -> List[str]:
"""Returns the symbols within table_name (or this module if not specified) that live at the specified
absolute offset provided."""
@@ -344,6 +353,7 @@ class ModuleContainer(collections.abc.Mapping):
def __iter__(self):
return iter(self._modules)
@abstractmethod
def free_module_name(self, prefix: str = "module") -> str:
"""Returns an unused table name to ensure no collision occurs when
inserting a symbol table."""
+3 -6
View File
@@ -188,7 +188,6 @@ class DataLayerInterface(
the object unreadable (exceptions will be thrown using a
DataLayer after destruction)
"""
pass
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -211,7 +210,7 @@ class DataLayerInterface(
context: interfaces.context.ContextInterface,
scanner: ScannerInterface,
progress_callback: constants.ProgressCallback = None,
sections: Iterable[Tuple[int, int]] = None,
sections: Optional[Iterable[Tuple[int, int]]] = None,
) -> Iterable[Any]:
"""Scans a Translation layer by chunk.
@@ -361,9 +360,7 @@ class DataLayerInterface(
data += self.context.layers[layer_name].read(address, chunk_size)
except exceptions.InvalidAddressException:
vollog.debug(
"Invalid address in layer {} found scanning {} at address {:x}".format(
layer_name, self.name, address
)
f"Invalid address in layer {layer_name} found scanning {self.name} at address {address:x}"
)
if len(data) > scanner.chunk_size + scanner.overlap:
@@ -721,7 +718,7 @@ class LayerContainer(collections.abc.Mapping):
raise NotImplementedError("Cycle checking has not yet been implemented")
class DummyProgress(object):
class DummyProgress:
"""A class to emulate Multiprocessing/threading Value objects."""
def __init__(self) -> None:
@@ -374,6 +374,7 @@ class Template:
f"{self.__class__.__name__} object has no attribute {attr}"
)
@abc.abstractmethod
def __call__(
self,
context: "interfaces.context.ContextInterface",
+1 -1
View File
@@ -46,7 +46,7 @@ class FileHandlerInterface(io.RawIOBase):
def preferred_filename(self, filename: str):
"""Sets the preferred filename"""
if self.closed:
raise IOError("FileHandler name cannot be changed once closed")
raise OSError("FileHandler name cannot be changed once closed")
if not isinstance(filename, str):
raise TypeError("FileHandler preferred filenames must be strings")
if os.path.sep in filename:
+10 -6
View File
@@ -26,7 +26,11 @@ from typing import (
Union,
)
Column = NamedTuple("Column", [("name", str), ("type", Any)])
class Column(NamedTuple):
name: str
type: Any
RenderOption = Any
@@ -98,11 +102,11 @@ class TreeNode(abc.Sequence, metaclass=ABCMeta):
"""
class BaseAbsentValue(object):
class BaseAbsentValue:
"""Class that represents values which are not present for some reason."""
class Disassembly(object):
class Disassembly:
"""A class to indicate that the bytes provided should be disassembled
(based on the architecture)"""
@@ -137,7 +141,7 @@ ColumnsType = List[Tuple[str, BaseTypes]]
VisitorSignature = Callable[[TreeNode, _Type], _Type]
class TreeGrid(object, metaclass=ABCMeta):
class TreeGrid(metaclass=ABCMeta):
"""Class providing the interface for a TreeGrid (which contains TreeNodes)
The structure of a TreeGrid is designed to maintain the structure of the tree in a single object.
@@ -179,7 +183,7 @@ class TreeGrid(object, metaclass=ABCMeta):
@abstractmethod
def populate(
self,
function: VisitorSignature = None,
function: Optional[VisitorSignature] = None,
initial_accumulator: Any = None,
fail_on_errors: bool = True,
) -> Optional[Exception]:
@@ -231,7 +235,7 @@ class TreeGrid(object, metaclass=ABCMeta):
node: Optional[TreeNode],
function: VisitorSignature,
initial_accumulator: _Type,
sort_key: ColumnSortKey = None,
sort_key: Optional[ColumnSortKey] = None,
) -> None:
"""Visits all the nodes in a tree, calling function on each one.
+2 -2
View File
@@ -250,13 +250,13 @@ class BaseSymbolTableInterface:
def clear_symbol_cache(self) -> None:
"""Clears the symbol cache of this symbol table."""
pass
class SymbolSpaceInterface(collections.abc.Mapping):
"""An interface for the container that holds all the symbol-containing
tables for use within a context."""
@abstractmethod
def free_table_name(self, prefix: str = "layer") -> str:
"""Returns an unused table name to ensure no collision occurs when
inserting a symbol table."""
@@ -378,7 +378,7 @@ class NativeTableInterface(BaseSymbolTableInterface):
return []
class MetadataInterface(object):
class MetadataInterface:
"""Interface for accessing metadata stored within a symbol table."""
def __init__(self, json_data: Dict) -> None:
+3 -6
View File
@@ -1,7 +1,6 @@
# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import contextlib
import logging
import struct
from typing import Tuple, Optional
@@ -138,7 +137,7 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
ulong_bitmap_array = summary_header.get_buffer_long()
# outer_index points to a 32 bits array inside a list of arrays,
# each bit indicating a page mapping state
for outer_index in range(0, ulong_bitmap_array.vol.count):
for outer_index in range(ulong_bitmap_array.vol.count):
ulong_bitmap = ulong_bitmap_array[outer_index]
# All pages in this 32 bits array are mapped (speedup iteration process)
if ulong_bitmap == 0xFFFFFFFF:
@@ -166,7 +165,7 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
seg_first_bit = None
# Some pages in this 32 bits array are mapped and some aren't
else:
for inner_bit_position in range(0, 32):
for inner_bit_position in range(32):
current_bit = outer_index * 32 + inner_bit_position
page_mapped = ulong_bitmap & (1 << inner_bit_position)
if page_mapped:
@@ -220,9 +219,7 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
for idx, (start_position, mapped_offset, length, _) in enumerate(segments):
vollog.log(
constants.LOGLEVEL_VVVV,
"Segment {}: Position {:#x} Offset {:#x} Length {:#x}".format(
idx, start_position, mapped_offset, length
),
f"Segment {idx}: Position {start_position:#x} Offset {mapped_offset:#x} Length {length:#x}",
)
self._segments = segments
+7 -12
View File
@@ -76,13 +76,13 @@ class Intel(linear.LinearlyMappedLayer):
self._index_shift = math.ceil(math.log2(struct.calcsize(self._entry_format)))
@classproperty
@functools.lru_cache()
@functools.lru_cache
def page_shift(cls) -> int:
"""Page shift for the intel memory layers."""
return cls._page_size_in_bits
@classproperty
@functools.lru_cache()
@functools.lru_cache
def page_size(cls) -> int:
"""Page size for the intel memory layers.
@@ -91,25 +91,25 @@ class Intel(linear.LinearlyMappedLayer):
return 1 << cls._page_size_in_bits
@classproperty
@functools.lru_cache()
@functools.lru_cache
def page_mask(cls) -> int:
"""Page mask for the intel memory layers."""
return ~(cls.page_size - 1)
@classproperty
@functools.lru_cache()
@functools.lru_cache
def bits_per_register(cls) -> int:
"""Returns the bits_per_register to determine the range of an
IntelTranslationLayer."""
return cls._bits_per_register
@classproperty
@functools.lru_cache()
@functools.lru_cache
def minimum_address(cls) -> int:
return 0
@classproperty
@functools.lru_cache()
@functools.lru_cache
def maximum_address(cls) -> int:
return (1 << cls._maxvirtaddr) - 1
@@ -251,12 +251,7 @@ class Intel(linear.LinearlyMappedLayer):
if INTEL_TRANSLATION_DEBUGGING:
vollog.log(
constants.LOGLEVEL_VVVV,
"Entry {} at index {} gives data {} as {}".format(
hex(entry),
hex(index),
hex(struct.unpack(self._entry_format, entry_data)[0]),
name,
),
f"Entry {hex(entry)} at index {hex(index)} gives data {hex(struct.unpack(self._entry_format, entry_data)[0])} as {name}",
)
# Read out the new entry from memory
+1 -1
View File
@@ -48,7 +48,7 @@ if HAS_LEECHCORE:
try:
self._handle = leechcorepyc.LeechCore(self._device)
except TypeError:
raise IOError(f"Unable to open LeechCore device {self._device}")
raise OSError(f"Unable to open LeechCore device {self._device}")
return self._handle
def fileno(self):
+2 -2
View File
@@ -194,7 +194,7 @@ class PdbMSFStream(linear.LinearlyMappedLayer):
) -> None:
super().__init__(context, config_path, name, metadata)
self._base_layer = self.config["base_layer"]
self._pages = self.config.get("pages", None)
self._pages = self.config.get("pages", [])
self._pages_len = len(self._pages)
if not self._pages:
raise PDBFormatException(name, "Invalid/no pages specified")
@@ -225,7 +225,7 @@ class PdbMSFStream(linear.LinearlyMappedLayer):
returned = 0
page_size = self._pdb_layer.page_size
while length > 0:
page = math.floor((offset + returned) / page_size)
page = (offset + returned) // page_size
page_position = (offset + returned) % page_size
chunk_size = min(page_size - page_position, length)
if page >= self._pages_len:
+1 -1
View File
@@ -236,7 +236,7 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
if self._architecture is None:
vollog.log(
constants.LOGLEVEL_VV,
f"QEVM architecture could not be determined",
"QEVM architecture could not be determined",
)
# Once all segments have been read, determine the PCI hole if any
+9 -7
View File
@@ -140,7 +140,13 @@ class RegistryHive(linear.LinearlyMappedLayer):
"""Returns the appropriate Node, interpreted from the Cell based on its
Signature."""
cell = self.get_cell(cell_offset)
signature = cell.cast("string", max_length=2, encoding="latin-1")
try:
signature = cell.cast("string", max_length=2, encoding="latin-1")
except (RegistryInvalidIndex, exceptions.InvalidAddressException):
vollog.debug(
f"Failed to get cell signature for cell (0x{cell.vol.offset:x})"
)
return cell
if signature == "nk":
return cell.u.KeyNode
elif signature == "sk":
@@ -156,9 +162,7 @@ class RegistryHive(linear.LinearlyMappedLayer):
else:
# It doesn't matter that we use KeyNode, we're just after the first two bytes
vollog.debug(
"Unknown Signature {} (0x{:x}) at offset {}".format(
signature, cell.u.KeyNode.Signature, cell_offset
)
f"Unknown Signature {signature} (0x{cell.u.KeyNode.Signature:x}) at offset {cell_offset}"
)
return cell
@@ -178,9 +182,7 @@ class RegistryHive(linear.LinearlyMappedLayer):
if not root_node.vol.type_name.endswith(constants.BANG + "_CM_KEY_NODE"):
raise RegistryFormatException(
self.name,
"Encountered {} instead of _CM_KEY_NODE".format(
root_node.vol.type_name
),
f"Encountered {root_node.vol.type_name} instead of _CM_KEY_NODE",
)
node_key = [root_node]
if key.endswith("\\"):
+2 -2
View File
@@ -29,7 +29,7 @@ except ImportError:
try:
# Import so that the handler is found by the framework.class_subclasses callc
import smb.SMBHandler # lgtm [py/unused-import]
from smb import SMBHandler as SMBHandler # lgtm [py/unused-import]
except ImportError:
# If we fail to import this, it means that SMB handling won't be available
pass
@@ -57,7 +57,7 @@ def cascadeCloseFile(new_fp: IO[bytes], original_fp: IO[bytes]) -> IO[bytes]:
return new_fp
class ResourceAccessor(object):
class ResourceAccessor:
"""Object for opening URLs as files (downloading locally first if
necessary)"""
@@ -5,7 +5,7 @@ import re
from typing import Generator, List, Tuple, Dict, Optional
from volatility3.framework.interfaces import layers
from volatility3.framework.layers.scanners import multiregexp
from volatility3.framework.layers.scanners import multiregexp as multiregexp
class BytesScanner(layers.ScannerInterface):
@@ -72,7 +72,7 @@ class MultiStringScanner(layers.ScannerInterface):
return None
for char in value:
trie[char] = trie.get(char, {})
trie.setdefault(char, {})
trie = trie[char]
# Mark the end of a string
@@ -6,7 +6,7 @@ import re
from typing import Generator, List, Tuple
class MultiRegexp(object):
class MultiRegexp:
"""Algorithm for multi-string matching."""
def __init__(self) -> None:
+4
View File
@@ -57,6 +57,10 @@ class VmwareLayer(segmented.SegmentedLayer):
)
meta_layer = self.context.layers.get(self._meta_layer, None)
if meta_layer is None:
raise exceptions.LayerException(
self._meta_layer, "VMware: Meta layer not found"
)
header_size = struct.calcsize(self.header_structure)
data = meta_layer.read(0, header_size)
magic, unknown, groupCount = struct.unpack(self.header_structure, data)
+1
View File
@@ -54,6 +54,7 @@ class XenCoreDumpLayer(elf.Elf64Layer):
segments = []
self._segment_headers = []
segment_names = None
for sindex in range(ehdr.e_shnum):
shdr = self.context.object(
+9 -9
View File
@@ -35,13 +35,13 @@ def convert_data_to_value(
data_format: DataFormatInfo,
) -> TUnion[int, float, bytes, str, bool]:
"""Converts a series of bytes to a particular type of value."""
if struct_type == int:
if struct_type is int:
return int.from_bytes(
data, byteorder=data_format.byteorder, signed=data_format.signed
)
if struct_type == bool:
if struct_type is bool:
struct_format = "?"
elif struct_type == float:
elif struct_type is float:
float_vals = "zzezfzzzd"
if (
data_format.length > len(float_vals)
@@ -70,7 +70,7 @@ def convert_value_to_data(
f"Written value is not of the correct type for {struct_type.__name__}"
)
if struct_type == int and isinstance(value, int):
if struct_type is int and isinstance(value, int):
# Doubling up on the isinstance is for mypy
return int.to_bytes(
value,
@@ -78,9 +78,9 @@ def convert_value_to_data(
byteorder=data_format.byteorder,
signed=data_format.signed,
)
if struct_type == bool:
if struct_type is bool:
struct_format = "?"
elif struct_type == float:
elif struct_type is float:
float_vals = "zzezfzzzd"
if (
data_format.length > len(float_vals)
@@ -152,7 +152,7 @@ class PrimitiveObject(interfaces.objects.ObjectInterface):
type_name: str,
object_info: interfaces.objects.ObjectInformation,
data_format: DataFormatInfo,
new_value: TUnion[int, float, bool, bytes, str] = None,
new_value: Optional[TUnion[int, float, bool, bytes, str]] = None,
**kwargs,
) -> "PrimitiveObject":
"""Creates the appropriate class and returns it so that the native type
@@ -601,7 +601,7 @@ class Enumeration(interfaces.objects.ObjectInterface, int):
inverse_choices[v] = k
return inverse_choices
def lookup(self, value: int = None) -> str:
def lookup(self, value: Optional[int] = None) -> str:
"""Looks up an individual value and returns the associated name.
If multiple identifiers map to the same value, the first matching identifier will be returned
@@ -690,7 +690,7 @@ class Array(interfaces.objects.ObjectInterface, collections.abc.Sequence):
type_name: str,
object_info: interfaces.objects.ObjectInformation,
count: int = 0,
subtype: templates.ObjectTemplate = None,
subtype: Optional[templates.ObjectTemplate] = None,
) -> None:
super().__init__(context=context, type_name=type_name, object_info=object_info)
self._vol["count"] = count
+2 -2
View File
@@ -22,8 +22,8 @@ def bswap_32(value: int) -> int:
def bswap_64(value: int) -> int:
low = bswap_32((value >> 32))
high = bswap_32((value & 0xFFFFFFFF))
low = bswap_32(value >> 32)
high = bswap_32(value & 0xFFFFFFFF)
return ((high << 32) | low) & 0xFFFFFFFFFFFFFFFF
+4 -6
View File
@@ -7,6 +7,7 @@ import os
import pathlib
import zipfile
from typing import Generator, List
from importlib.util import find_spec
from volatility3 import schemas, symbols
from volatility3.framework import constants, interfaces, renderers
@@ -96,16 +97,12 @@ class IsfInfo(plugins.PluginInterface):
if filter_item in isf_file:
filtered_list.append(isf_file)
try:
import jsonschema
if not self.config["validate"]:
raise ImportError # Act as if we couldn't import if validation is turned off
if find_spec("jsonschema") and self.config["validate"]:
def check_valid(data):
return "True" if schemas.validate(data, True) else "False"
except ImportError:
else:
def check_valid(data):
return "Unknown"
@@ -135,6 +132,7 @@ class IsfInfo(plugins.PluginInterface):
valid = check_valid(data)
except (UnicodeDecodeError, json.decoder.JSONDecodeError):
vollog.warning(f"Invalid ISF: {entry}")
continue
yield (
0,
(
+1 -1
View File
@@ -119,7 +119,7 @@ class LayerWriter(plugins.PluginInterface):
# Update the filename, which may have changed if a file
# with the same name already existed.
output_name = file_handle.preferred_filename
except IOError as excp:
except OSError as excp:
yield 0, (f"Layer cannot be written to {output_name}: {excp}",)
yield 0, (f"Layer has been written to {output_name}",)
+2 -2
View File
@@ -22,7 +22,7 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Recovers bash command history from memory."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_version = (1, 0, 2)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -33,7 +33,7 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.ListRequirement(
name="pid",
@@ -15,7 +15,7 @@ class Boottime(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
"""Shows the time the system was started"""
_required_framework_version = (2, 11, 0)
_version = (1, 0, 1)
_version = (1, 0, 2)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -26,7 +26,7 @@ class Boottime(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
]
@@ -49,8 +49,8 @@ class CapabilitiesData:
class Capabilities(plugins.PluginInterface):
"""Lists process capabilities"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_required_framework_version = (2, 13, 0)
_version = (1, 1, 1)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -61,7 +61,7 @@ class Capabilities(plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.ListRequirement(
name="pids",
@@ -136,7 +136,7 @@ class Capabilities(plugins.PluginInterface):
comm=utility.array_to_string(task.comm),
pid=int(task.pid),
tgid=int(task.tgid),
ppid=int(task.parent.pid),
ppid=int(task.get_parent_pid()),
euid=int(task.cred.euid),
)
@@ -12,7 +12,7 @@ class Check_creds(interfaces.plugins.PluginInterface):
"""Checks if any processes are sharing credential structures"""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 1)
_version = (2, 0, 2)
@classmethod
def get_requirements(cls):
@@ -23,7 +23,7 @@ class Check_creds(interfaces.plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
]
@@ -55,7 +55,7 @@ class Check_creds(interfaces.plugins.PluginInterface):
for cred_addr, pids in creds.items():
if len(pids) > 1:
pid_str = ", ".join([str(pid) for pid in pids])
pid_str = ", ".join(str(pid) for pid in pids)
fields = [
format_hints.Hex(cred_addr),
@@ -53,7 +53,7 @@ class Check_idt(interfaces.plugins.PluginInterface):
address_mask = self.context.layers[vmlinux.layer_name].address_mask
# hw handlers + system call
check_idxs = list(range(0, 20)) + [128]
check_idxs = list(range(20)) + [128]
if is_32bit:
if vmlinux.has_type("gate_struct"):
@@ -103,7 +103,7 @@ class Check_syscall(plugins.PluginInterface):
try:
func_addr = vmlinux.get_symbol(syscall_entry_func).address
except exceptions.SymbolError as e:
except exceptions.SymbolError:
# if we can't find the disassemble function then bail and rely on a different method
return 0
+2 -2
View File
@@ -25,7 +25,7 @@ class Elfs(plugins.PluginInterface):
"""Lists all memory mapped ELF files for all processes."""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 2)
_version = (2, 0, 3)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -36,7 +36,7 @@ class Elfs(plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.ListRequirement(
name="pid",
+88 -73
View File
@@ -3,8 +3,9 @@
#
import logging
from typing import Iterable, Tuple
from volatility3.framework import exceptions, renderers
from volatility3.framework import renderers, interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.framework.objects import utility
@@ -16,8 +17,8 @@ vollog = logging.getLogger(__name__)
class Envars(plugins.PluginInterface):
"""Lists processes with their environment variables"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_required_framework_version = (2, 13, 0)
_version = (2, 0, 0)
@classmethod
def get_requirements(cls):
@@ -29,7 +30,7 @@ class Envars(plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.ListRequirement(
name="pid",
@@ -39,84 +40,98 @@ class Envars(plugins.PluginInterface):
),
]
@staticmethod
def get_task_env_variables(
context: interfaces.context.ContextInterface,
task: interfaces.objects.ObjectInterface,
env_area_max_size: int = 8192,
) -> Iterable[Tuple[str, str]]:
"""Yields environment variables for a given task.
Args:
context: The plugin's operational context.
task: The task object from which to extract environment variables.
env_area_max_size: Maximum allowable size for the environment variables area.
Tasks exceeding this size will be skipped. Default is 8192.
Yields:
Tuples of (key, value) representing each environment variable.
"""
task_name = utility.array_to_string(task.comm)
task_pid = task.pid
env_start = task.mm.env_start
env_end = task.mm.env_end
env_area_size = env_end - env_start
if not (0 < env_area_size <= env_area_max_size):
vollog.debug(
f"Task {task_pid} {task_name} appears to have environment variables of size "
f"{env_area_size} bytes which fails the sanity checking, will not extract "
"any envars."
)
return None
# Get process layer to read envars from
proc_layer_name = task.add_process_layer()
if proc_layer_name is None:
return None
proc_layer = context.layers[proc_layer_name]
# Ensure the entire buffer is readable to prevent relying on exception handling
if not proc_layer.is_valid(env_start, env_area_size):
# Not mapped / swapped out
vollog.debug(
f"Unable to read environment variables for {task_pid} {task_name} starting at "
f" virtual address 0x{env_start:x} for {env_area_size} bytes, will not "
"extract any envars."
)
return None
# Read the full task environment variable buffer.
envar_data = proc_layer.read(env_start, env_area_size)
# Parse envar data, envars are null terminated, keys and values are separated by '='
envar_data = envar_data.rstrip(b"\x00")
for envar_pair in envar_data.split(b"\x00"):
try:
env_key, env_value = envar_pair.decode().split("=", 1)
except ValueError:
# Some legitimate programs, like 'avahi-daemon', avoid reallocating the args
# and instead exploit the fact that the environment variables area is contiguous
# to the args. This allows them to include a longer process name in the listing,
# causing overwrites and incorrect results. In such cases, it's better to abort
# the current task rather than displaying misleading or incorrect output.
break
yield env_key, env_value
def _generator(self, tasks):
"""Generates a listing of processes along with environment variables"""
# walk the process list and return the envars
for task in tasks:
pid = task.pid
# get process name as string
name = utility.array_to_string(task.comm)
# try and get task parent
try:
ppid = task.parent.pid
except exceptions.InvalidAddressException:
vollog.debug(
f"Unable to read parent pid for task {pid} {name}, setting ppid to 0."
)
ppid = 0
# kernel threads never have an mm as they do not have userland mappings
try:
mm = task.mm
except exceptions.InvalidAddressException:
# no mm so cannot get envars
vollog.debug(
f"Unable to access mm for task {pid} {name} it is likely a kernel thread, will not extract any envars."
)
mm = None
if task.is_kernel_thread:
continue
# if mm exists attempt to get envars
if mm:
# get process layer to read envars from
proc_layer_name = task.add_process_layer()
if proc_layer_name is None:
vollog.debug(
f"Unable to construct process layer for task {pid} {name}, will not extract any envars."
)
continue
proc_layer = self.context.layers[proc_layer_name]
task_pid = task.pid
task_name = utility.array_to_string(task.comm)
task_ppid = task.get_parent_pid()
# get the size of the envars with sanity checking
envars_size = task.mm.env_end - task.mm.env_start
if not (0 < envars_size <= 8192):
vollog.debug(
f"Task {pid} {name} appears to have envars of size {envars_size} bytes which fails the sanity checking, will not extract any envars."
)
continue
# attempt to read all envars data
try:
envar_data = proc_layer.read(task.mm.env_start, envars_size)
except exceptions.InvalidAddressException:
vollog.debug(
f"Unable to read full envars for {pid} {name} starting at virtual offset {hex(task.mm.env_start)} for {envars_size} bytes, will not extract any envars."
)
continue
# parse envar data, envars are null terminated, keys and values are separated by '='
envar_data = envar_data.rstrip(b"\x00")
for envar_pair in envar_data.split(b"\x00"):
try:
key, value = envar_pair.decode().split("=", 1)
except ValueError:
vollog.debug(
f"Unable to extract envars for {pid} {name} starting at virtual offset {hex(task.mm.env_start)}, they don't appear to be '=' separated"
)
continue
yield (0, (pid, ppid, name, key, value))
for env_key, env_value in self.get_task_env_variables(self.context, task):
yield (0, (task_pid, task_ppid, task_name, env_key, env_value))
def run(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
return renderers.TreeGrid(
[("PID", int), ("PPID", int), ("COMM", str), ("KEY", str), ("VALUE", str)],
self._generator(
pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=filter_func
)
),
tasks = pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=filter_func
)
headers = [
("PID", int),
("PPID", int),
("COMM", str),
("KEY", str),
("VALUE", str),
]
return renderers.TreeGrid(headers, self._generator(tasks))
@@ -0,0 +1,334 @@
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
import io
from dataclasses import dataclass
from typing import Type, List, Dict, Tuple
from volatility3.framework import constants, exceptions, interfaces
from volatility3.framework.configuration import requirements
from volatility3.framework.renderers import (
format_hints,
TreeGrid,
NotAvailableValue,
UnreadableValue,
)
from volatility3.framework.objects import utility
from volatility3.framework.constants import architectures
from volatility3.framework.symbols import linux
# Image manipulation functions are kept in the plugin,
# to prevent a general exit on missing PIL (pillow) dependency.
try:
from PIL import Image
has_pil = True
except ImportError:
has_pil = False
vollog = logging.getLogger(__name__)
@dataclass
class Framebuffer:
"""Framebuffer object internal representation. This is useful to unify a framebuffer with precalculated
properties and pass it through functions conveniently."""
id: str
xres_virtual: int
yres_virtual: int
line_length: int
bpp: int
"""Bits Per Pixel"""
size: int
color_fields: Dict[str, Tuple[int, int, int]]
fb_info: interfaces.objects.ObjectInterface
class Fbdev(interfaces.plugins.PluginInterface):
"""Extract framebuffers from the fbdev graphics subsystem"""
_version = (1, 0, 0)
_required_framework_version = (2, 11, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=architectures.LINUX_ARCHS,
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 2, 0)
),
requirements.BooleanRequirement(
name="dump",
description="Dump framebuffers",
default=False,
optional=True,
),
]
@classmethod
def parse_fb_pixel_bitfields(
cls, fb_var_screeninfo: interfaces.objects.ObjectInterface
) -> Dict[str, Tuple[int, int, int]]:
"""Organize a framebuffer pixel format into a dictionary.
This is needed to know the position and bitlength of a color inside
a pixel.
Args:
fb_var_screeninfo: a fb_var_screeninfo kernel object instance
Returns:
The color fields mappings
Documentation:
include/uapi/linux/fb.h:
struct fb_bitfield {
__u32 offset; /* beginning of bitfield */
__u32 length; /* length of bitfield */
__u32 msb_right; /* != 0 : Most significant bit is right */
};
"""
# Naturally order by RGBA
color_mappings = [
("R", fb_var_screeninfo.red),
("G", fb_var_screeninfo.green),
("B", fb_var_screeninfo.blue),
("A", fb_var_screeninfo.transp),
]
color_fields = {}
for color_code, fb_bitfield in color_mappings:
color_fields[color_code] = (
int(fb_bitfield.offset),
int(fb_bitfield.length),
int(fb_bitfield.msb_right),
)
return color_fields
@classmethod
def convert_fb_raw_buffer_to_image(
cls,
context: interfaces.context.ContextInterface,
kernel_name: str,
fb: Framebuffer,
):
"""Convert raw framebuffer pixels to an image.
Args:
fb: the relevant Framebuffer object
Returns:
A PIL Image object
Documentation:
include/uapi/linux/fb.h:
/* Interpretation of offset for color fields: All offsets are from the right,
* inside a "pixel" value, which is exactly 'bits_per_pixel' wide (means: you
* can use the offset as right argument to <<). A pixel afterwards is a bit
* stream and is written to video memory as that unmodified.
"""
kernel = context.modules[kernel_name]
kernel_layer = context.layers[kernel.layer_name]
raw_pixels = io.BytesIO(kernel_layer.read(fb.fb_info.screen_base, fb.size))
bytes_per_pixel = fb.bpp // 8
image = Image.new("RGBA", (fb.xres_virtual, fb.yres_virtual))
# This is not designed to be extremely fast (numpy isn't available),
# but convenient and dynamic for any color field layout.
for y in range(fb.yres_virtual):
for x in range(fb.xres_virtual):
raw_pixel = int.from_bytes(raw_pixels.read(bytes_per_pixel), "little")
pixel = [0, 0, 0, 255]
# The framebuffer is expected to have been correctly constructed,
# especially by parse_fb_pixel_bitfields, to get the needed RGBA mappings.
for i, color_code in enumerate(["R", "G", "B", "A"]):
offset, length, msb_right = fb.color_fields[color_code]
if length == 0:
continue
color_value = (raw_pixel >> offset) & (2**length - 1)
if msb_right:
# Reverse bit order
color_value = int(
"{:0{length}b}".format(color_value, length=length)[::-1], 2
)
pixel[i] = color_value
image.putpixel((x, y), tuple(pixel))
return image
@classmethod
def dump_fb(
cls,
context: interfaces.context.ContextInterface,
kernel_name: str,
open_method: Type[interfaces.plugins.FileHandlerInterface],
fb: Framebuffer,
convert_to_png_image: bool,
) -> str:
"""Dump a Framebuffer buffer to disk.
Args:
fb: the relevant Framebuffer object
convert_to_image: a boolean specifying if the buffer should be converted to an image
Returns:
The filename of the dumped buffer.
"""
kernel = context.modules[kernel_name]
kernel_layer = context.layers[kernel.layer_name]
id = "N-A" if isinstance(fb.id, NotAvailableValue) else fb.id
base_filename = f"{id}_{fb.xres_virtual}x{fb.yres_virtual}_{fb.bpp}bpp"
if convert_to_png_image:
image_object = cls.convert_fb_raw_buffer_to_image(context, kernel_name, fb)
raw_io_output = io.BytesIO()
image_object.save(raw_io_output, "PNG")
final_fb_buffer = raw_io_output.getvalue()
filename = f"{base_filename}.png"
else:
final_fb_buffer = kernel_layer.read(fb.fb_info.screen_base, fb.size)
filename = f"{base_filename}.raw"
with open_method(filename) as f:
f.write(final_fb_buffer)
return f.preferred_filename
@classmethod
def parse_fb_info(
cls,
fb_info: interfaces.objects.ObjectInterface,
) -> Framebuffer:
"""Parse an fb_info struct
Args:
fb_info: an fb_info kernel object live instance
Returns:
A Framebuffer object
Documentation:
https://docs.kernel.org/fb/api.html:
- struct fb_fix_screeninfo stores device independent unchangeable information about the frame buffer device and the current format.
Those information can't be directly modified by applications, but can be changed by the driver when an application modifies the format.
- struct fb_var_screeninfo stores device independent changeable information about a frame buffer device, its current format and video mode,
as well as other miscellaneous parameters.
"""
id = utility.array_to_string(fb_info.fix.id) or NotAvailableValue()
color_fields = None
# 0 = color, 1 = grayscale, >1 = FOURCC
if fb_info.var.grayscale in [0, 1]:
color_fields = cls.parse_fb_pixel_bitfields(fb_info.var)
# There a lot of tricky pixel formats used by drivers and vendors in include/uapi/linux/videodev2.h.
# As Volatility3 is not a video format converter, it is best to play it safe and let the user parse
# the raw data manually (with ffmpeg for example).
elif fb_info.var.grayscale > 1:
fourcc = linux.LinuxUtilities.convert_fourcc_code(fb_info.var.grayscale)
warn_msg = f"""Framebuffer "{id}" uses a FOURCC pixel format "{fourcc}" that isn't natively supported.
You can try using ffmpeg to decode the raw buffer. Example usage:
"ffmpeg -pix_fmts" to list supported formats, then
"ffmpeg -f rawvideo -video_size {fb_info.var.xres_virtual}x{fb_info.var.yres_virtual} -i <FILENAME>.raw -pix_fmt <FORMAT> output.png"."""
vollog.warning(warn_msg)
# Prefer using the virtual resolution, instead of the visible one.
# This prevents missing non-visible data stored in the framebuffer.
fb = Framebuffer(
id,
xres_virtual=fb_info.var.xres_virtual,
yres_virtual=fb_info.var.yres_virtual,
line_length=fb_info.fix.line_length,
bpp=fb_info.var.bits_per_pixel,
size=fb_info.var.yres_virtual * fb_info.fix.line_length,
color_fields=color_fields,
fb_info=fb_info,
)
return fb
def _generator(self):
if not has_pil:
vollog.error(
"PIL (pillow) module is required to use this plugin. Please install it manually or through pyproject.toml."
)
return None
kernel_name = self.config["kernel"]
kernel = self.context.modules[kernel_name]
if not kernel.has_symbol("num_registered_fb"):
raise exceptions.SymbolError(
"num_registered_fb",
kernel.symbol_table_name,
"The provided symbol does not exist in the symbol table. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt.",
)
num_registered_fb = kernel.object_from_symbol("num_registered_fb")
if num_registered_fb < 1:
vollog.info("No registered framebuffer in the fbdev API.")
return None
registered_fb = kernel.object_from_symbol("registered_fb")
fb_info_list = utility.array_of_pointers(
registered_fb,
num_registered_fb,
kernel.symbol_table_name + constants.BANG + "fb_info",
self.context,
)
for fb_info in fb_info_list:
fb = self.parse_fb_info(fb_info)
file_output = "Disabled"
if self.config["dump"]:
try:
file_output = self.dump_fb(
self.context, kernel_name, self.open, fb, bool(fb.color_fields)
)
file_output = str(file_output)
except exceptions.InvalidAddressException as excp:
vollog.error(
f'Layer {excp.layer_name} failed to read address {hex(excp.invalid_address)} when dumping framebuffer "{fb.id}".'
)
file_output = UnreadableValue()
try:
fb_device_name = utility.pointer_to_string(
fb.fb_info.dev.kobj.name, 256
)
except exceptions.InvalidAddressException:
fb_device_name = NotAvailableValue()
yield (
0,
(
format_hints.Hex(fb.fb_info.screen_base),
fb_device_name,
fb.id,
fb.size,
f"{fb.xres_virtual}x{fb.yres_virtual}",
fb.bpp,
"RUNNING" if fb.fb_info.state == 0 else "SUSPENDED",
file_output,
),
)
def run(self):
columns = [
("Address", format_hints.Hex),
("Device", str),
("ID", str),
("Size", int),
("Virtual resolution", str),
("BPP", int),
("State", str),
("Filename", str),
]
return TreeGrid(
columns,
self._generator(),
)
+2 -2
View File
@@ -149,7 +149,7 @@ class ABCKmsg(ABC):
# This might seem insignificant but it could cause some issues
# when compared with userland tool results or when used in
# timelines.
return "%lu.%06lu" % (nsec / 1000000000, (nsec % 1000000000) / 1000)
return f"{nsec / 1000000000:lu}.{(nsec % 1000000000) / 1000:06lu}"
def get_timestamp_in_sec_str(self, obj) -> str:
# obj could be log, printk_log or printk_info
@@ -166,7 +166,7 @@ class ABCKmsg(ABC):
def get_caller_text(self, caller_id):
caller_name = "CPU" if caller_id & 0x80000000 else "Task"
caller = "%s(%u)" % (caller_name, caller_id & ~0x80000000)
caller = f"{caller_name}({caller_id & ~0x80000000:u})"
return caller
def get_prefix(self, obj) -> Tuple[int, int, str, str]:
@@ -20,7 +20,7 @@ class Kthreads(plugins.PluginInterface):
"""Enumerates kthread functions"""
_required_framework_version = (2, 11, 0)
_version = (1, 0, 1)
_version = (1, 0, 2)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -34,7 +34,7 @@ class Kthreads(plugins.PluginInterface):
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
@@ -21,7 +21,7 @@ class LibraryList(interfaces.plugins.PluginInterface):
"""Enumerate libraries loaded into processes"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_version = (1, 0, 2)
@classmethod
def get_requirements(cls):
@@ -32,7 +32,7 @@ class LibraryList(interfaces.plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.ListRequirement(
name="pids",
+1 -2
View File
@@ -54,8 +54,7 @@ class Lsmod(plugins.PluginInterface):
table_name = modules.vol.type_name.split(constants.BANG)[0]
for module in modules.to_list(table_name + constants.BANG + "module", "list"):
yield module
yield from modules.to_list(table_name + constants.BANG + "module", "list")
def _generator(self):
try:
+2 -2
View File
@@ -110,7 +110,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists open files for each processes."""
_required_framework_version = (2, 0, 0)
_version = (2, 0, 1)
_version = (2, 0, 2)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -121,7 +121,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
@@ -18,7 +18,7 @@ class Malfind(interfaces.plugins.PluginInterface):
"""Lists process memory ranges that potentially contain injected code."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_version = (1, 0, 2)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -29,7 +29,7 @@ class Malfind(interfaces.plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.ListRequirement(
name="pid",
@@ -36,7 +36,7 @@ class MountInfo(plugins.PluginInterface):
"""Lists mount points on processes mount namespaces"""
_required_framework_version = (2, 2, 0)
_version = (1, 2, 2)
_version = (1, 2, 3)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -47,7 +47,7 @@ class MountInfo(plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
@@ -462,7 +462,7 @@ class InodePages(plugins.PluginInterface):
f.seek(current_fp)
f.write(page_bytes)
except IOError as e:
except OSError as e:
vollog.error("Unable to write to file (%s): %s", filename, e)
def _generator(self):
@@ -483,6 +483,9 @@ class InodePages(plugins.PluginInterface):
if inode_in.path == self.config["find"]:
inode = inode_in.inode
break # Only the first match
else:
vollog.error("Unable to find inode with path %s", self.config["find"])
return None
elif self.config["inode"]:
inode = vmlinux.object("inode", self.config["inode"], absolute=True)
@@ -3,7 +3,7 @@
#
import logging
from typing import List
from typing import List, Iterable
from volatility3.framework import renderers, interfaces, constants
from volatility3.framework.symbols import linux
@@ -19,7 +19,7 @@ class PIDHashTable(plugins.PluginInterface):
"""Enumerates processes through the PID hash table"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 2)
_version = (1, 0, 3)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -30,7 +30,7 @@ class PIDHashTable(plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
@@ -218,7 +218,7 @@ class PIDHashTable(plugins.PluginInterface):
return None
def get_tasks(self) -> interfaces.objects.ObjectInterface:
def get_tasks(self) -> Iterable[interfaces.objects.ObjectInterface]:
"""Enumerates processes through the PID hash table
Yields:
@@ -231,14 +231,16 @@ class PIDHashTable(plugins.PluginInterface):
yield from sorted(pid_func(), key=lambda t: (t.tgid, t.pid))
def _generator(
self, decorate_comm: bool = False
) -> interfaces.objects.ObjectInterface:
def _generator(self, decorate_comm: bool = False):
for task in self.get_tasks():
offset, pid, tid, ppid, name, _creation_time = (
pslist.PsList.get_task_fields(task, decorate_comm)
task_fields = pslist.PsList.get_task_fields(task, decorate_comm)
fields = (
format_hints.Hex(task_fields.offset),
task_fields.user_pid,
task_fields.user_tid,
task_fields.user_ppid,
task_fields.name,
)
fields = format_hints.Hex(offset), pid, tid, ppid, name
yield 0, fields
def run(self):
+6 -6
View File
@@ -21,7 +21,7 @@ class Maps(plugins.PluginInterface):
"""Lists all memory maps for all processes."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_version = (1, 0, 2)
MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb
@@ -35,7 +35,7 @@ class Maps(plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.ListRequirement(
name="pid",
@@ -125,9 +125,7 @@ class Maps(plugins.PluginInterface):
proc_layer_name = task.add_process_layer()
except exceptions.InvalidAddressException as excp:
vollog.debug(
"Process {}: invalid address {} in layer {}".format(
pid, excp.invalid_address, excp.layer_name
)
f"Process {pid}: invalid address {excp.invalid_address} in layer {excp.layer_name}"
)
return None
vm_size = vm_end - vm_start
@@ -165,7 +163,9 @@ class Maps(plugins.PluginInterface):
address_list = self.config.get("address", None)
if not address_list:
# do not filter as no address_list was supplied
vma_filter_func = lambda _: True
def vma_filter_func(_):
return True
else:
# filter for any vm_start that matches the supplied address config
def vma_filter_function(x: interfaces.objects.ObjectInterface) -> bool:
+4 -10
View File
@@ -14,8 +14,8 @@ from volatility3.plugins.linux import pslist
class PsAux(plugins.PluginInterface):
"""Lists processes with their command line arguments"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_required_framework_version = (2, 13, 0)
_version = (1, 1, 1)
@classmethod
def get_requirements(cls):
@@ -27,7 +27,7 @@ class PsAux(plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.ListRequirement(
name="pid",
@@ -98,14 +98,8 @@ class PsAux(plugins.PluginInterface):
# walk the process list and report the arguments
for task in tasks:
pid = task.pid
try:
ppid = task.parent.pid
except exceptions.InvalidAddressException:
ppid = 0
ppid = task.get_parent_pid()
name = utility.array_to_string(task.comm)
args = self._get_command_line_args(task, name)
yield (0, (pid, ppid, name, args))
+65 -27
View File
@@ -2,7 +2,9 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import datetime
from typing import Any, Callable, Iterable, List, Tuple
import dataclasses
import contextlib
from typing import Any, Callable, Iterable, List, Optional
from volatility3.framework import interfaces, renderers
from volatility3.framework.configuration import requirements
@@ -14,11 +16,25 @@ from volatility3.plugins import timeliner
from volatility3.plugins.linux import elfs
@dataclasses.dataclass
class TaskFields:
offset: int
user_pid: int
user_tid: int
user_ppid: int
name: str
uid: Optional[int]
gid: Optional[int]
euid: Optional[int]
egid: Optional[int]
creation_time: Optional[datetime.datetime]
class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists the processes present in a particular linux memory image."""
_required_framework_version = (2, 0, 0)
_version = (3, 0, 0)
_required_framework_version = (2, 13, 0)
_version = (4, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -58,7 +74,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
]
@classmethod
def create_pid_filter(cls, pid_list: List[int] = None) -> Callable[[Any], bool]:
def create_pid_filter(
cls, pid_list: Optional[List[int]] = None
) -> Callable[[Any], bool]:
"""Constructs a filter function for process IDs.
Args:
@@ -82,7 +100,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
@classmethod
def get_task_fields(
cls, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False
) -> Tuple[int, int, int, int, str, datetime.datetime]:
) -> TaskFields:
"""Extract the fields needed for the final output
Args:
@@ -91,21 +109,34 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
and of Kernel threads in square brackets.
Defaults to False.
Returns:
A tuple with the fields to show in the plugin output.
A TaskFields object with the fields to show in the plugin output.
"""
pid = task.tgid
tid = task.pid
ppid = task.parent.tgid if task.parent else 0
name = utility.array_to_string(task.comm)
start_time = task.get_create_time()
if decorate_comm:
if task.is_kernel_thread:
name = f"[{name}]"
elif task.is_user_thread:
name = f"{{{name}}}"
task_fields = (task.vol.offset, pid, tid, ppid, name, start_time)
return task_fields
# This function may be called with a partially initialized/uninitialized task.
# Ensure it always returns a valid TaskFields object, ready for use in a plugin.
valid_cred = task.cred and task.cred.is_readable()
creation_time = None
with contextlib.suppress(Exception):
creation_time = task.get_create_time()
return TaskFields(
offset=task.vol.offset,
user_pid=task.tgid,
user_tid=task.pid,
user_ppid=task.get_parent_pid(),
name=name,
uid=task.cred.uid if valid_cred else None,
gid=task.cred.gid if valid_cred else None,
euid=task.cred.euid if valid_cred else None,
egid=task.cred.egid if valid_cred else None,
creation_time=creation_time,
)
def _get_file_output(self, task: interfaces.objects.ObjectInterface) -> str:
"""Extract the elf for the process if requested
@@ -179,17 +210,19 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
else:
file_output = "Disabled"
offset, pid, tid, ppid, name, creation_time = self.get_task_fields(
task, decorate_comm
)
task_fields = self.get_task_fields(task, decorate_comm)
yield 0, (
format_hints.Hex(offset),
pid,
tid,
ppid,
name,
creation_time or renderers.NotAvailableValue(),
format_hints.Hex(task_fields.offset),
task_fields.user_pid,
task_fields.user_tid,
task_fields.user_ppid,
task_fields.name,
task_fields.uid or renderers.NotAvailableValue(),
task_fields.gid or renderers.NotAvailableValue(),
task_fields.euid or renderers.NotAvailableValue(),
task_fields.egid or renderers.NotAvailableValue(),
task_fields.creation_time or renderers.NotAvailableValue(),
file_output,
)
@@ -238,6 +271,10 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
("TID", int),
("PPID", int),
("COMM", str),
("UID", int),
("GID", int),
("EUID", int),
("EGID", int),
("CREATION TIME", datetime.datetime),
("File output", str),
]
@@ -251,10 +288,11 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
for task in self.list_tasks(
self.context, self.config["kernel"], filter_func, include_threads=True
):
offset, user_pid, user_tid, _user_ppid, name, creation_time = (
self.get_task_fields(task)
task_fields = self.get_task_fields(task)
description = f"Process {task_fields.user_pid}/{task_fields.user_tid} {task_fields.name} ({task_fields.offset})"
yield (
description,
timeliner.TimeLinerType.CREATED,
task_fields.creation_time,
)
description = f"Process {user_pid}/{user_tid} {name} ({offset})"
yield (description, timeliner.TimeLinerType.CREATED, creation_time)
+20 -36
View File
@@ -2,15 +2,15 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import logging
from typing import Iterable, List, Tuple
from typing import Iterable, List
import struct
from enum import Enum
from volatility3.framework import renderers, interfaces, symbols, constants, exceptions
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.framework.layers import scanners
from volatility3.framework.renderers import format_hints
from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
@@ -27,8 +27,8 @@ class DescExitStateEnum(Enum):
class PsScan(interfaces.plugins.PluginInterface):
"""Scans for processes present in a particular linux image."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_required_framework_version = (2, 13, 0)
_version = (2, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -38,37 +38,11 @@ class PsScan(interfaces.plugins.PluginInterface):
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
]
def _get_task_fields(
self, task: interfaces.objects.ObjectInterface
) -> Tuple[int, int, int, str, str]:
"""Extract the fields needed for the final output
Args:
task: A task object from where to get the fields.
Returns:
A tuple with the fields to show in the plugin output.
"""
pid = task.tgid
tid = task.pid
ppid = 0
if task.parent.is_readable():
ppid = task.parent.tgid
name = utility.array_to_string(task.comm)
exit_state = DescExitStateEnum(task.exit_state).name
task_fields = (
format_hints.Hex(task.vol.offset),
pid,
tid,
ppid,
name,
exit_state,
)
return task_fields
def _generator(self):
"""Generates the tasks found from scanning."""
@@ -78,8 +52,18 @@ class PsScan(interfaces.plugins.PluginInterface):
for task in self.scan_tasks(
self.context, vmlinux_module_name, vmlinux.layer_name
):
row = self._get_task_fields(task)
yield (0, row)
task_fields = pslist.PsList.get_task_fields(task)
exit_state = DescExitStateEnum(task.exit_state).name
fields = (
format_hints.Hex(task_fields.offset),
task_fields.user_pid,
task_fields.user_tid,
task_fields.user_ppid,
task_fields.name,
exit_state,
)
yield (0, fields)
@classmethod
def scan_tasks(
@@ -133,7 +117,7 @@ class PsScan(interfaces.plugins.PluginInterface):
)
elif len(kernel_layer.dependencies) == 0:
vollog.error(
f"Kernel layer has no dependencies, meaning there is no memory layer for this plugin to scan."
"Kernel layer has no dependencies, meaning there is no memory layer for this plugin to scan."
)
raise exceptions.LayerException(
kernel_layer_name, f"Layer {kernel_layer_name} has no dependencies"
+14 -10
View File
@@ -12,8 +12,8 @@ class PsTree(interfaces.plugins.PluginInterface):
"""Plugin for listing processes in a tree based on their parent process
ID."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_required_framework_version = (2, 13, 0)
_version = (1, 1, 1)
@classmethod
def get_requirements(cls):
@@ -25,7 +25,7 @@ class PsTree(interfaces.plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.ListRequirement(
name="pid",
@@ -56,9 +56,9 @@ class PsTree(interfaces.plugins.PluginInterface):
seen = set([pid])
level = 0
proc = self._tasks.get(pid)
while proc and proc.parent and proc.parent.pid not in seen:
while proc and proc.get_parent_pid() not in seen:
if proc.is_thread_group_leader:
parent_pid = proc.parent.pid
parent_pid = proc.get_parent_pid()
else:
parent_pid = proc.tgid
@@ -101,13 +101,17 @@ class PsTree(interfaces.plugins.PluginInterface):
def yield_processes(pid):
task = self._tasks[pid]
offset, pid, tid, ppid, name, _creation_time = (
pslist.PsList.get_task_fields(task, decorate_comm)
task_fields = pslist.PsList.get_task_fields(task, decorate_comm)
fields = (
format_hints.Hex(task_fields.offset),
task_fields.user_pid,
task_fields.user_tid,
task_fields.user_ppid,
task_fields.name,
)
fields = format_hints.Hex(offset), pid, tid, ppid, name
yield (self._levels[tid] - 1, fields)
yield (self._levels[task_fields.user_tid] - 1, fields)
for child_pid in sorted(self._children.get(tid, [])):
for child_pid in sorted(self._children.get(task_fields.user_tid, [])):
yield from yield_processes(child_pid)
for pid, level in self._levels.items():
@@ -19,7 +19,7 @@ class Ptrace(plugins.PluginInterface):
"""Enumerates ptrace's tracer and tracee tasks"""
_required_framework_version = (2, 10, 0)
_version = (1, 0, 1)
_version = (1, 0, 2)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -30,7 +30,7 @@ class Ptrace(plugins.PluginInterface):
architectures=architectures.LINUX_ARCHS,
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
]
@@ -372,7 +372,7 @@ class SockHandlers(interfaces.configuration.VersionableInterface):
bt_sock = sock.cast("bt_sock")
def bt_addr(addr):
return ":".join(reversed(["%02x" % x for x in addr.b]))
return ":".join(reversed([f"{x:02x}" for x in addr.b]))
src_addr = src_port = dst_addr = dst_port = None
bt_protocol = bt_sock.get_protocol()
@@ -438,7 +438,7 @@ class Sockstat(plugins.PluginInterface):
"""Lists all network connections for all processes."""
_required_framework_version = (2, 0, 0)
_version = (3, 0, 1)
_version = (3, 0, 2)
@classmethod
def get_requirements(cls):
@@ -455,7 +455,7 @@ class Sockstat(plugins.PluginInterface):
name="lsof", plugin=lsof.Lsof, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
@@ -21,7 +21,7 @@ class VmaRegExScan(plugins.PluginInterface):
"""Scans all virtual memory areas for tasks using RegEx."""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 1)
_version = (1, 0, 2)
MAXSIZE_DEFAULT = 128
@@ -35,7 +35,7 @@ class VmaRegExScan(plugins.PluginInterface):
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.ListRequirement(
name="pid",
@@ -18,7 +18,7 @@ class VmaYaraScan(interfaces.plugins.PluginInterface):
"""Scans all virtual memory areas for tasks using yara."""
_required_framework_version = (2, 4, 0)
_version = (1, 0, 1)
_version = (1, 0, 2)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -31,7 +31,7 @@ class VmaYaraScan(interfaces.plugins.PluginInterface):
optional=True,
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.PluginRequirement(
name="yarascan", plugin=yarascan.YaraScan, version=(2, 0, 0)
@@ -60,7 +60,7 @@ class Check_sysctl(plugins.PluginInterface):
return var_str
def _process_sysctl_list(self, kernel, sysctl_list, recursive=0):
if type(sysctl_list) == volatility3.framework.objects.Pointer:
if type(sysctl_list) is volatility3.framework.objects.Pointer:
sysctl_list = sysctl_list.dereference().cast("sysctl_oid_list")
sysctl = sysctl_list.slh_first
@@ -93,10 +93,9 @@ class Check_sysctl(plugins.PluginInterface):
val = self._parse_global_variable_sysctls(kernel, name)
elif ctltype == "CTLTYPE_NODE":
if sysctl.oid_handler == 0:
for info in self._process_sysctl_list(
yield from self._process_sysctl_list(
kernel, sysctl.oid_arg1, recursive=1
):
yield info
)
val = "Node"
@@ -80,7 +80,7 @@ class Kauth_scopes(interfaces.plugins.PluginInterface):
(
identifier,
format_hints.Hex(scope.ks_idata),
len([l for l in scope.get_listeners()]),
len([listener for listener in scope.get_listeners()]),
format_hints.Hex(callback),
module_name,
symbol_name,
+1 -2
View File
@@ -119,8 +119,7 @@ class Kevents(interfaces.plugins.PluginInterface):
return None
for klist in klist_array:
for kn in mac.MacUtilities.walk_slist(klist, "kn_link"):
yield kn
yield from mac.MacUtilities.walk_slist(klist, "kn_link")
@classmethod
def _get_task_kevents(cls, kernel, task):
+1 -2
View File
@@ -49,8 +49,7 @@ class Mount(plugins.PluginInterface):
list_head = kernel.object_from_symbol(symbol_name="mountlist")
for mount in mac.MacUtilities.walk_tailq(list_head, "mnt_list"):
yield mount
yield from mac.MacUtilities.walk_tailq(list_head, "mnt_list")
def _generator(self):
for mount in self.list_mounts(self.context, self.config["kernel"]):
@@ -115,9 +115,7 @@ class Maps(interfaces.plugins.PluginInterface):
proc_layer_name = task.add_process_layer()
except exceptions.InvalidAddressException as excp:
vollog.debug(
"Process {}: invalid address {} in layer {}".format(
pid, excp.invalid_address, excp.layer_name
)
f"Process {pid}: invalid address {excp.invalid_address} in layer {excp.layer_name}"
)
return None
vm_size = vm_end - vm_start
@@ -154,7 +152,9 @@ class Maps(interfaces.plugins.PluginInterface):
address_list = self.config.get("address", None)
if not address_list:
# do not filter as no address_list was supplied
vma_filter_func = lambda _: True
def vma_filter_func(_):
return True
else:
# filter for any vm_start that matches the supplied address config
def vma_filter_function(task: interfaces.objects.ObjectInterface) -> bool:
+7 -3
View File
@@ -4,7 +4,7 @@
import datetime
import logging
from typing import Callable, Dict, Iterable, List
from typing import Callable, Dict, Iterable, List, Optional
from volatility3.framework import exceptions, interfaces, renderers
from volatility3.framework.configuration import requirements
@@ -82,8 +82,12 @@ class PsList(interfaces.plugins.PluginInterface):
return list_tasks
@classmethod
def create_pid_filter(cls, pid_list: List[int] = None) -> Callable[[int], bool]:
filter_func = lambda _: False
def create_pid_filter(
cls, pid_list: Optional[List[int]] = None
) -> Callable[[int], bool]:
def filter_func(_):
return False
# FIXME: mypy #4973 or #2608
pid_list = pid_list or []
filter_list = [x for x in pid_list if x is not None]
+5 -6
View File
@@ -54,7 +54,9 @@ class Timeliner(interfaces.plugins.PluginInterface):
self.automagics: Optional[List[interfaces.automagic.AutomagicInterface]] = None
@classmethod
def get_usable_plugins(cls, selected_list: List[str] = None) -> List[Type]:
def get_usable_plugins(
cls, selected_list: Optional[List[str]] = None
) -> List[Type]:
# Initialize for the run
plugin_list = list(framework.class_subclasses(TimeLinerInterface))
@@ -143,9 +145,7 @@ class Timeliner(interfaces.plugins.PluginInterface):
times = self.timeline.get((plugin_name, item), {})
if times.get(timestamp_type, None) is not None:
vollog.debug(
"Multiple timestamps for the same plugin/file combination found: {} {}".format(
plugin_name, item
)
f"Multiple timestamps for the same plugin/file combination found: {plugin_name} {item}"
)
times[timestamp_type] = timestamp
self.timeline[(plugin_name, item)] = times
@@ -206,8 +206,7 @@ class Timeliner(interfaces.plugins.PluginInterface):
)
vollog.log(logging.DEBUG, traceback.format_exc())
for data_item in sorted(data, key=self._sort_function):
yield data_item
yield from sorted(data, key=self._sort_function)
# Write out a body file if necessary
if self.config.get("create-bodyfile", True):
@@ -84,9 +84,7 @@ class CmdLine(interfaces.plugins.PluginInterface):
result_text = f"Required memory at {exp.invalid_address:#x} is not valid (process exited?)"
except exceptions.InvalidAddressException as exp:
result_text = "Process {}: Required memory at {:#x} is not valid (incomplete layer {}?)".format(
proc_id, exp.invalid_address, exp.layer_name
)
result_text = f"Process {proc_id}: Required memory at {exp.invalid_address:#x} is not valid (incomplete layer {exp.layer_name}?)"
yield (0, (proc.UniqueProcessId, process_name, result_text))
@@ -95,9 +95,7 @@ class Consoles(interfaces.plugins.PluginInterface):
except exceptions.InvalidAddressException as excp:
vollog.debug(
"Process {}: invalid address {} in layer {}".format(
proc_id, excp.invalid_address, excp.layer_name
)
f"Process {proc_id}: invalid address {excp.invalid_address} in layer {excp.layer_name}"
)
@classmethod
@@ -176,12 +174,7 @@ class Consoles(interfaces.plugins.PluginInterface):
)
vollog.debug(
"Determined OS Version: {}.{} {}.{}".format(
kuser.NtMajorVersion,
kuser.NtMinorVersion,
vers.MajorVersion,
vers.MinorVersion,
)
f"Determined OS Version: {kuser.NtMajorVersion}.{kuser.NtMinorVersion} {vers.MajorVersion}.{vers.MinorVersion}"
)
if nt_major_version == 10 and arch == "x64":
@@ -260,9 +253,7 @@ class Consoles(interfaces.plugins.PluginInterface):
if ver:
conhost_mod_version = ver[3]
vollog.debug(
"Determined conhost.exe's FileVersion: {}".format(
conhost_mod_version
)
f"Determined conhost.exe's FileVersion: {conhost_mod_version}"
)
else:
vollog.debug("Could not determine conhost.exe's FileVersion.")
@@ -311,12 +302,7 @@ class Consoles(interfaces.plugins.PluginInterface):
else:
raise NotImplementedError(
"This version of Windows is not supported: {}.{} {}.{}!".format(
nt_major_version,
nt_minor_version,
vers.MajorVersion,
vers_minor_version,
)
f"This version of Windows is not supported: {nt_major_version}.{nt_minor_version} {vers.MajorVersion}.{vers_minor_version}!"
)
vollog.debug(f"Determined symbol filename: {filename}")
@@ -433,6 +433,8 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface):
proc_layer = self.context.layers[proc_layer_name]
vads = self.get_vad_maps(proc)
if not vads:
continue
# for each valid process, look for malicious syscall invocations
for address, vad_path in self._get_rule_hits(

Some files were not shown because too many files have changed in this diff Show More