Skip to content

process_spoofing plugin #1826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from

Conversation

SolitudePy
Copy link
Contributor

Hello, just playing with memory & OS internals.
apparently some legitimate processes do these techniques to have enriched information in their cmdline or so. here are some such processes:

PID     PPID    Exe_Basename    Cmdline_Basename        Comm    Notes

966     1       platform-python3.6      platform-python firewalld       ['Potential cmdline spoofing: exe_file=platform-python3.6;cmdline=platform-python', 'Potential comm spoofing: exe_file=platform-python3.6;comm=firewalld']
991     1       platform-python3.6      platform-python tuned   ['Potential cmdline spoofing: exe_file=platform-python3.6;cmdline=platform-python', 'Potential comm spoofing: exe_file=platform-python3.6;comm=tuned']
1257    1       login   login -- root   login   ['Potential cmdline spoofing: exe_file=login;cmdline=login -- root']
1923    1903    bash    bash    entrypoint.sh   ['Potential comm spoofing: exe_file=bash;comm=entrypoint.sh']
3475    3472    systemd (sd-pam)        (sd-pam)        ['Potential cmdline spoofing: exe_file=systemd;cmdline=(sd-pam)', 'Potential comm spoofing: exe_file=syst
(venv) ubuntu@ubuntuPC:~/Dev/volatility3$ vol -f ~/dumps/procspoof_dump_lin.raw -r json linux.process_spoofing | jq 'map(select(.Notes != "OK"))'
Volatility 3 Framework 2.26.2
Progress:  100.00               Stacking attempts finished           
[
  {
    "Cmdline_Basename": "[malwareX]",
    "Comm": "change_argv",
    "Exe_Basename": "change_argv",
    "Notes": "['Potential cmdline spoofing: exe_file=change_argv;cmdline=[malwareX]']",
    "PID": 6717,
    "PPID": 3482,
    "__children": []
  },
  {
    "Cmdline_Basename": "change_comm",
    "Comm": "malwareX",
    "Exe_Basename": "change_comm",
    "Notes": "['Potential comm spoofing: exe_file=change_comm;comm=malwareX']",
    "PID": 6727,
    "PPID": 3482,
    "__children": []
  }
]

@SolitudePy
Copy link
Contributor Author

(venv) ubuntu@ubuntuPC:~/Dev/volatility3$ vol -f ~/dumps/deleted_proc_dump.raw -r json linux.process_spoofing --pid 4868
Volatility 3 Framework 2.26.2
/home/ubuntu/Dev/volatility3/volatility3/framework/deprecation.py:105: FutureWarning: This plugin (PluginRequirement) has been renamed and will be removed in the first release after 2026-06-01. PluginRequirement is to be deprecated. Use VersionRequirement instead.
  warnings.warn(
Progress:  100.00               Stacking attempts finished           
[
  {
    "Cmdline_Basename": "copied_bash",
    "Comm": "copied_bash",
    "Exe_Basename": "copied_bash (deleted)",
    "Notes": "['Potential Process image deletion: exe_file=copied_bash (deleted)']",
    "PID": 4868,
    "PPID": 4633,
    "__children": []
  }
]

Copy link
Member

@ikelos ikelos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your submission and sorry it took so long to find time to review, this plugin looks fun! It's mostly fine, but it smushes the results together into a string, which is longwinded for humans to read and difficult for programs/plugins to parse. Hopefully this should be pretty straight forward to refactor into returning boolean values. It also feels like as number of the methods here may be useful for other plugins, so first check that other plugins don't already implement them and otherwise consider converting them into classmethods so that they can be called externally...

)

except Exception as e:
vollog.debug(f"Error processing task at {task.vol.offset:#x}: {e}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to make this a vollog.warning, since otherwise this won't show up for most users? It's also not all that useful to user? It's generally better to catch specific exceptions you're expecting rather than just a catchall "something went wrong" because it makes it very difficult for anyone to diagnose what went wrong (and it prevents volatility's general exception handlers from being able to step in and give the user a nice message, or a developer a full traceback of the exception).

),
]

def _get_executable_path(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these functions would be useful externally by other plugins, consider making them public classmethods (using @classmethod and not starting the method name with an _)? It looks like they only really need self to get a context, so if you add the context as the first method parameter (in keeping with all our other plugins/classmethods) then it would make them usable elsewhere.

if not exe_file or not exe_file.is_readable():
return None

exe_inode = exe_file.dereference().f_path.dentry.d_inode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't require explicit dereferencing. Requesting a specific member (like f_path) from a pointer will automatically dereference it and return the value from the dereferenced structure. Including it explicitly perpetuates the idea that it's necessary and could lead to misunderstanding of how pointers work in volatility.

try:
mm = task.mm
if not mm or not mm.is_readable():
return renderers.NotAvailableValue()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The renderer values are part of the UI, and generally not all that useful to plugins wanting to process the data. You return None almost everywhere else apart from here, it would be better to be consistent in your return values unless you're explicitly trying to differentiate something being NotAvailable over some other AbsentValue option. Typically, we'd pass None through, and then just as it comes out of the generator, convert Nones to the responses that makes the most sense for a None for that field.

return None

try:
argv = proc_layer.read(start, size_to_read)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd check to see if existing argument handling isn't done somewhere else (either in another plugin or a task structure). It seems like it might be more useful exposed that way, just to avoid people rewriting it themselves each time and possibly making slight parsing differences each time.

else:
return None

except (exceptions.InvalidAddressException, AttributeError):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of generic error catching without any notice to a developer that some possible output got swallowed or why.

1 for name in [exe_basename, cmdline_basename, comm] if name
)

is_deleted = exe_basename.endswith(self.deleted)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a good way of identifying this, it should be a separate boolean flag handed around. Stuffing it on the end of a string and then parsing it back out again feels very brittle. I'd make each of your three methods return a tuple (basename, deleted) and then process those, the other two methods can return None but you really should be loading up the basename, and certainly not at the end, because then you can't tell the difference between random file that's been deleted and random file (deleted), within your code the two are indistinguishable.

notes.append(f"'Potential Process image deletion: exe_file={exe_basename}'")
exe_basename = exe_basename[: len(self.deleted) * -1]

if available_sources < 2:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely you'd want to highlight that the file was only present in one source? Seems like a sure fire sign that something unusual happened?

return None

if exe_basename != cmdline_basename:
notes.append(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This combines data fields, so creating a list of strings is not going to be easy for a volatility plugin to process (and, gets collapsed into a single string, which would need parsing again to be used by another program/a different tool). This should probably be returning the three compared values, and then a boolean to say which tests succeeded and which failed. Humans could read that fine (it would be less text to read) and other programs/plugins could parse the data easily.


exe_basename, cmdline_basename, comm = self._extract_process_names(task)

notes = self._detect_spoofing(exe_basename, cmdline_basename, comm)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, this is a bad design pattern. Keep the tests separate and display them each in their own column as a true/false value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants