Skip to content
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

Starting from windows 8.1, get commandline content using NtQueryInformationProcess (see #1384) #1398

Merged
merged 14 commits into from
Feb 3, 2019

Conversation

EccoTheFlintstone
Copy link
Contributor

@EccoTheFlintstone EccoTheFlintstone commented Jan 26, 2019

ecco added 2 commits January 26, 2019 14:24
@giampaolo
Copy link
Owner

Beautiful! I will try take a look at this soon.

ecco added 3 commits January 26, 2019 15:04
…InformationProcess (this way, we get the real params used even if they've been patched in the PEB)
PyErr_Clear(); // reset that we had an error, and retry with NtQueryInformationProcess
// if it fails, fallback to NtQueryInformationProcess (for protected processes)
NtQueryInformationProcess = get_NtQueryInformationProcess();
if (NtQueryInformationProcess != NULL) {
Copy link
Owner

Choose a reason for hiding this comment

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

In order to stay consistent with the rest of the code I suggest to check for failure instead of success. This way we will avoid long if blocks and indentation. Also avoid else at all (you save one indented block). In practice:

if (NtQueryInformationProcess == NULL) {
    PyErr_SetFromWindowsErr(0);
    goto out;
}
...

}

hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION);
if (hProcess != NULL) {
Copy link
Owner

Choose a reason for hiding this comment

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

Same here (check for failure, not success)

&ret_length
);
CloseHandle(hProcess);
if (NT_SUCCESS(status)) {
Copy link
Owner

Choose a reason for hiding this comment

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

and here

@@ -160,19 +159,17 @@ psutil_set_privilege(HANDLE hToken, LPCTSTR Privilege, BOOL bEnablePrivilege) {
int
psutil_set_se_debug() {
HANDLE hToken;
if (! OpenThreadToken(GetCurrentThread(),
if (!OpenProcessToken(GetCurrentProcess(),
Copy link
Owner

Choose a reason for hiding this comment

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

looks like this was accidental/unnecessary

&hToken)
) {
if (GetLastError() == ERROR_NO_TOKEN) {
if (!ImpersonateSelf(SecurityImpersonation)) {
CloseHandle(hToken);
return 0;
}
if (!OpenThreadToken(GetCurrentThread(),
if (!OpenProcessToken(GetCurrentProcess(),
Copy link
Owner

Choose a reason for hiding this comment

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

looks like this was accidental/unnecessary

@@ -198,17 +195,15 @@ psutil_set_se_debug() {
int
psutil_unset_se_debug() {
HANDLE hToken;
if (! OpenThreadToken(GetCurrentThread(),
if (!OpenProcessToken(GetCurrentProcess(),
Copy link
Owner

Choose a reason for hiding this comment

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

looks like this was accidental/unnecessary

&hToken)
) {
if (GetLastError() == ERROR_NO_TOKEN) {
if (! ImpersonateSelf(SecurityImpersonation))
return 0;
if (!OpenThreadToken(GetCurrentThread(),
if (!OpenProcessToken(GetCurrentProcess(),
Copy link
Owner

Choose a reason for hiding this comment

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

looks like this was accidental/unnecessary

DWORD dwBuildNumber;
static int windows_version = WINDOWS_UNITIALIZED;
// didn't get version yet
if (windows_version == WINDOWS_UNITIALIZED) {
Copy link
Owner

Choose a reason for hiding this comment

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

It looks like this is unnecessary as windows_version is already set as WINDOWS_UNITIALIZED in the previous line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

windows_version is a static var, this check is taken only the first time the function is called

Copy link
Owner

Choose a reason for hiding this comment

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

Gotcha. I'd add a comment then ('cause I will forget for sure =)).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done :)


hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION);
if (hProcess == NULL) {
PyErr_SetFromWindowsErr(0);
Copy link
Owner

Choose a reason for hiding this comment

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

psutil_handle_from_pid already sets the Python errorcode/exception so this is unnecessary

psutil/arch/windows/process_info.c Outdated Show resolved Hide resolved
// Windows 10, Windows Server 2016
else if (dwMajorVersion == 10) {
windows_version = WINDOWS_10;
}
Copy link
Owner

Choose a reason for hiding this comment

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

Nitpick: how about using a switch/case/default block instead? Not terribly important... up to you.

TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
FALSE,
Copy link
Owner

Choose a reason for hiding this comment

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

mmm this looks wrong; aren't you now passing 3 args instead of 4?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm using GetProcessToken instead of GetThreadToken and it needs 1 less argument
In the case where the actual privileged call is performed from another thread than the one getting th SE_DEBUG privilege, it might be best to have the whole process enable the privilege

Copy link
Owner

Choose a reason for hiding this comment

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

I see (didn't notice you changed syscall). I checked ProcessHacker source code and it also uses OpenProcessToken so I guess it makes sense to do the same. Good one.

windows_version = get_windows_version();
if (windows_version >= WINDOWS_81) {
// by defaut, still use PEB (if command line params have been patched in PEB, we will get the actual ones)
if (psutil_get_process_data(pid, KIND_CMDLINE, &data, &size) != 0) {
Copy link
Owner

Choose a reason for hiding this comment

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

Mmmm... I'm a bit confused about this. It appears regardless of the Windows version you first attempt to use the PEB method first, and fallback on using NtQueryInformationProcess in case of failure on Win >= 8.1. Wouldn't it make sense to always use NtQueryInformationProcess on Win >= 8.1? I a bit against trying 2 different methods and suppressing error in case the first one fails. Unless there's a good reason I think it's better to always use 1 method only.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reading the PEB to get the command line parameters still seem to be the best method if somebody has tampered with the parameters after creating the process.
For instance, create a process as suspended, patch the command line in its PEB and unfreeze it.
The process will use the "new" parameters whereas the system (with NtQueryInformationProcess) will give you the "old" ones (see here : https://blog.xpnsec.com/how-to-argue-like-cobalt-strike/)
By default, reading from PEB is still allowed if one has sufficient privileges except for protected processes where one must use NtQueryInformationProcess.
So the PEB method will work for almost all processes on windows 8.1+ except the few (4-5) protected processes lying around (where we can fallback on NtQIP), and it will accurately report the actual command line parameters used

Copy link
Owner

Choose a reason for hiding this comment

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

OK, I see. In this case I would recommend the following:

  • use psutil_handle_from_pid(pid, PROCESS_QUERY_INFORMATION | PROCESS_VM_READ)
  • if it succeeds use the PEB method (and only that)
  • if it doesn't (for any reason), use NtQueryInformationProcess if Win >= 8.1, else fail
    In both cases you should simply close the returned handle: the only reason to get the handle is to see if you have permissions to do so.
    Makes sense?

Also, please add a full explanation of why one method is preferred over the other one (you can include the information you wrote in the comment above).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, I think using psutil_handle_from_pid(pid, PROCESS_QUERY_INFORMATION | PROCESS_VM_READ) is redundant as reading from PEB will also open process with those rights, so for every process, we would open/close it twice

Copy link
Owner

@giampaolo giampaolo Feb 1, 2019

Choose a reason for hiding this comment

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

Mmm.. true. Another possibility is to use the PEB method and check for GetLastError() == ERROR_ACCESS_DENIED in case it fails, and only in that case use NtQueryInformationProcess. Also, for PID 0 I suspect you will always get ERROR_ACCESS_DENIED.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As it is, the code works. What I could do is add an explanation as why the PEB method is used first for windows >= 8.1 as you propose, but go with this code anyway, no?

Copy link
Owner

Choose a reason for hiding this comment

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

Yes but as-is it tries the other method for any error. I would like to limit the check to ERROR_ACCESS_DENIED. If it fails for any other reason I think it's saner to just raise exception.

Copy link
Owner

@giampaolo giampaolo Feb 1, 2019

Choose a reason for hiding this comment

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

So something like this (not tested):

    int ret;

    ret = psutil_get_process_data(pid, KIND_CMDLINE, &data, &size);
    if ((ret != 0) && 
            (GetLastError() == ERROR_ACCESS_DENIED) && 
            (windows_version >= WINDOWS_81)) 
    {
        // try NtQueryInformationProcess()
        PyErr_Clear();
        ...
    }
    else {
        return NULL;
    }

Copy link
Owner

@giampaolo giampaolo Feb 1, 2019

Choose a reason for hiding this comment

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

...also, I suggest to put the whole NtQueryInformationProcess() block in a separate function, similarly to what it has been done for psutil_get_process_data

Copy link
Owner

Choose a reason for hiding this comment

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

hint: function name could be psutil_get_cmdline_data

GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");
}
return NtQueryInformationProcess;
}
Copy link
Owner

Choose a reason for hiding this comment

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

I like putting this in a utility function because it can be reused. Would you mind using this utility function also in other parts of the code? Also, for consistency I think it's better to rename this function to psutil_NtQueryInformationProcess.

@@ -162,6 +162,72 @@ const int STATUS_INFO_LENGTH_MISMATCH = 0xC0000004;
const int STATUS_BUFFER_TOO_SMALL = 0xC0000023L;



#define WINDOWS_UNITIALIZED 0
Copy link
Owner

Choose a reason for hiding this comment

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

Typo? WINDOWS_UNITIALIZED -> WINDOWS_UNINITIALIZED

TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
FALSE,
Copy link
Owner

Choose a reason for hiding this comment

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

I see (didn't notice you changed syscall). I checked ProcessHacker source code and it also uses OpenProcessToken so I guess it makes sense to do the same. Good one.

windows_version = get_windows_version();
if (windows_version >= WINDOWS_81) {
// by defaut, still use PEB (if command line params have been patched in PEB, we will get the actual ones)
if (psutil_get_process_data(pid, KIND_CMDLINE, &data, &size) != 0) {
Copy link
Owner

Choose a reason for hiding this comment

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

OK, I see. In this case I would recommend the following:

  • use psutil_handle_from_pid(pid, PROCESS_QUERY_INFORMATION | PROCESS_VM_READ)
  • if it succeeds use the PEB method (and only that)
  • if it doesn't (for any reason), use NtQueryInformationProcess if Win >= 8.1, else fail
    In both cases you should simply close the returned handle: the only reason to get the handle is to see if you have permissions to do so.
    Makes sense?

Also, please add a full explanation of why one method is preferred over the other one (you can include the information you wrote in the comment above).

Copy link
Owner

@giampaolo giampaolo left a comment

Choose a reason for hiding this comment

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

hopefully the last change


hProcess = psutil_handle_from_pid(pid, PROCESS_QUERY_LIMITED_INFORMATION);
if (hProcess == NULL) {
// psutil_handle_from_pid sets errorcode/exception, don't need to do it it
Copy link
Owner

Choose a reason for hiding this comment

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

I think you need to free(cmdline_buffer); here - you can use a goto error and put all the cleanup logic in there

// set error before closing handle to keep original error
// CloseHandle might fail and set a new errno/GetLastError
PyErr_SetFromWindowsErr(0);
CloseHandle(hProcess);
Copy link
Owner

Choose a reason for hiding this comment

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

Same here cmdline_buffer has to be freed (as goto error)

cmdline_buffer_wchar = (WCHAR *)calloc(string_size, sizeof(WCHAR));

if (cmdline_buffer_wchar == NULL) {
free(cmdline_buffer);
Copy link
Owner

Choose a reason for hiding this comment

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

...and here (use goto instead)

@giampaolo giampaolo merged commit a453659 into giampaolo:master Feb 3, 2019
@giampaolo
Copy link
Owner

Merged. Thanks for the hard work. ;)

@EccoTheFlintstone
Copy link
Contributor Author

EccoTheFlintstone commented Feb 3, 2019 via email

giampaolo added a commit that referenced this pull request Feb 15, 2019
…plementations can be called separately (and tested separately)
giampaolo added a commit that referenced this pull request Feb 28, 2019
…t time to get the right buf size (ProcessHacker does this)
nlevitt added a commit to nlevitt/psutil that referenced this pull request Apr 9, 2019
* origin/master: (150 commits)
  Linux / CPU freq, fixes giampaolo#1481
  improve pmap.py script
  reuse ps.py script in psutil.test()
  move get_terminal_size() in _compat.py
  improve ps.py script
  improve ps.py script
  move bytes2human() into psutil._common and reused it from scripts dir
  fix windows failure re. py 2 vs. 3
  fix linux tests
  fix giampaolo#1474: fix formatting of psutil.tests() which mimicks 'ps aux' output
  give CREDITS for giampaolo#1480
  remove outdated tests
  Fix read access violation in psutil.cpu_count(logical=False) (giampaolo#1480)
  update doc
  update doc
  [Win] Process IO priority constants + high priority (giampaolo#1479 / giampaolo#1476)
  don't fail if there are not prev failed tests
  fix giampaolo#1478: add make command to re-run tests failed on last run
  [Win] return value is not properly handled for undocumented NT* Windows APIs. (giampaolo#1477)
  fix error on py 2.7 where OSError doesn't always have winerror attribute
  update HISTORY for giampaolo#1475
  properly check OSError.winerror
  refactor ionice() on Linux
  refactor ionice() on Linux
  ionice test refactoring
  giampaolo#1404: fix regression not returning CPUs > 9
  give CREDITS to Daniel Beer for giampaolo#1471
  Fix spurious exception when iterating processes on Solaris (giampaolo#1471)
  give CREDITS for giampaolo#1470
  Fix corner case when /etc/mtab doesn't exist and procfs=/proc (giampaolo#1470)
  update DEVNOTES
  Typo fixed (giampaolo#1469)
  giampaolo#1458: implement colors on Windows
  update HISTORY / CREDITS, fix some C warnings
  Make uptime type consitent to fix boot time error. (giampaolo#1225)
  fix giampaolo#1463: cpu_distribution.py script is broken
  update issue template
  issue giampaolo#1404 / linux / phys CPUs count
  Big docfix (giampaolo#1464)
  Big docfix (giampaolo#1464)
  Make tests invariant to LANG setting (giampaolo#1462)
  test runner: show errors on KeyboardInterrupt
  test runner refactoring (avoid code duplication)
  Coloured tests (giampaolo#1459)
  pre-release
  [Windows] calculate USS memory by using NtQueryVirtualMemory (giampaolo#1453)
  fix version highlighting in docs/index (giampaolo#1455)
  fix version highlighting in README (giampaolo#1454)
  run win specific tests twice as fast
  test refactoring
  test: avoid failing at import time
  mention how to run tests in INSTALL guide
  giampaolo#1448: fix Wine support due to missing rtlIpv6AddressToStringA
  update HISTORY
  Fix giampaolo#1329: [AIX] disable some functions based on availability in libperfstat (giampaolo#1349)
  bump up version, fix some doc issues
  fix ResourceWarning
  pre-release
  fix giampaolo#1447: we weren't use @wrap_exceptions around oneshot() (doh\!)
  update doc + change git hook location
  update doc
  make pre-release checks/install src dist in a venv
  add new make command to check tar.gz sanity
  move doc; rephrase it a bit
  add issue templates for 'bug' and 'enhancement' types
  remove issue template commited by accident
  Update issue templates
  giampaolo#1291: (BACKWARD-INCOMPATIBLE) remove memory_maps() on OSX
  Restore Win-7 support on GIT master (5.5.1 was OK) (giampaolo#1446)
  try to fix ntext.h
  restore previous def
  fix compiler warning
  fix compiler warning
  fix compiler warning
  fix compiler warning
  take defs from PH
  set proper  SYSTEM_PROCESS_INFORMATION struct from PH
  fix compilation warnings
  giampaolo#1398 / win / cmdline: call NtQueryInformationProcess twice, the first time to get the right buf size (ProcessHacker does this)
  update doc
  better print formatting for print scripts
  fix giampaolo#1442: use python3 as Makefile default
  appveyor: run print scripts after tests
  highlight top 6 slowest calls
  add printerr() and exit() to shared utils module
  add arg parser for ad script
  introduce a new scriptsutils.py private module shared by all internal utils + refactor print_access_speed.py script
  giampaolo#1291 / OSX: mark memory_maps() as deprecated and make it alwats raise AccessDenied
  OSX memory_maps() - add error handling
  add script for to benchmark API calls
  move access_denied script
  _assert_alive() refactor (linux)
  refactor
  fix NetBSD: Process.connections() may return incomplete results if using oneshot() giampaolo#1439
  add win tests related to send_signal(CTRL_C_EVENT) giampaolo#1227
  fix win test
  fix win tests
  fix backslash warnings
  refactor README a bit
  #fix 1438: do not return any parent() for PID 0 + update doc
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants