【问题标题】:Debugging ERROR_ACCESS_DENIED on Windows API (CreateProcessAsUser)在 Windows API (CreateProcessAsUser) 上调试 ERROR_ACCESS_DENIED
【发布时间】:2021-07-11 04:05:48
【问题描述】:

我正在使用 JNA 从 Windows API 调用 CreateProcessAsUserW。我正在向CreateProcessAsUserW 传递一个简单的命令:

C:\Windows\System32\cmd.exe /c C:\Windows\SysWOW64\whoami.exe > whoami.txt

基本上,它应该将当前用户(通过whoami.exe 获得)打印到一个名为whoami.txt 的文件中。 成功了。 执行我的 Java 程序后,我看到一个新文件 whoami.txt 包含我的用户名。

但是,我打印了GetLastError 的返回值,并看到错误代码在调用CreateProcessAsUserW 后立即从0 变为5System Error Codes 表示 5ERROR_ACCESS_DENIED 错误。

我从以管理员身份运行的命令提示符运行 Java 进程。值得注意的是,我调用CreateProcessAsUserW时并没有切换用户。

我的预感是这个函数的许多参数之一导致了一个表面上的权限错误,这个错误得到了优雅的处理。但是,也可能是我缺少一些用户权限。

我是 Windows 新手,不熟悉调试此类错误的更好方法。你们都推荐哪些工具来获取有关拒绝访问错误的更多信息?在 Linux 上,journalctl 在这里会很有帮助。

这是我的代码:

// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfow
final STARTUPINFOW.ByReference startupInfoW =
    new STARTUPINFOW.ByReference();
startupInfoW.cb = startupInfoW.size();
startupInfoW.lpReserved = Pointer.NULL;
startupInfoW.lpDesktop = Pointer.NULL;
startupInfoW.lpTitle = Pointer.NULL;
startupInfoW.dwFlags
    = startupInfoW.dwX = startupInfoW.dwY
    = startupInfoW.dwXSize = startupInfoW.dwYSize
    = startupInfoW.dwXCountChars = startupInfoW.dwYCountChars
    = startupInfoW.dwFillAttribute
    = startupInfoW.wShowWindow
    = 0;
startupInfoW.cbReserved2  = 0;
startupInfoW.lpReserved2 = Pointer.NULL;
startupInfoW.hStdInput = startupInfoW.hStdOutput
    = startupInfoW.hStdError
    = Pointer.NULL;
// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-process_information
final PROCESS_INFORMATION.ByReference processInformation =
    new PROCESS_INFORMATION.ByReference();
processInformation.hProcess = processInformation.hThread
    = Pointer.NULL;
processInformation.dwProcessId = processInformation.dwThreadId
    = 0;
// Converts string to char array with 0 as last element.
final char[] whoamiCmd = toCString(
    "C:\\Windows\\System32\\cmd.exe /c C:\\Windows\\SysWOW64\\whoami.exe > whoami.txt"
);
System.out.printf(
    "last err code = %d\n",
    ErrHandlingApi.INSTANCE.GetLastError()
);
final boolean createProcessOk = MyProcessThreadsApi.INSTANCE
    .CreateProcessAsUserW(
        userPrimaryToken.getValue(),
        Pointer.NULL,
        whoamiCmd,
        Pointer.NULL,
        Pointer.NULL,
        false,
        WinBase.CREATE_UNICODE_ENVIRONMENT,
        new PointerByReference(),
        Pointer.NULL,
        startupInfoW,
        processInformation
    );
System.out.printf(
    "last err code = %d\n",
    ErrHandlingApi.INSTANCE.GetLastError()
);
System.out.printf("ok = %b\n", createProcessOk);
System.out.printf(
    "dwProcessId = %d\n", processInformation.dwProcessId
);
public static char[] toCString(final String str) {
    final char[] cString = new char[str.length() + 1];
    for (int i = 0; i < str.length(); i++) {
        cString[i] = str.charAt(i);
    }
    // c-strings end in 0 for lack of bounds checking
    cString[cString.length-1] = 0;
    return cString;
}
C:\Users\zjoseal\Desktop>whoami
ant\zjoseal

C:\Users\zjoseal\Desktop>java -cp windows-credentials-poc-1.0-SNAPSHOT.jar Main
...
last err code = 0
last err code = 5
ok = true
dwProcessId = 6628

C:\Users\zjoseal\Desktop>type whoami.txt
ant\zjoseal

修改代码 1

你们中的一些人问 toCString 方法是否可能导致错误代码更改。它只是将字符串的内容复制到 char[] 并附加 0 以使其成为以空值结尾的 c 字符串。我将它移到第一个打印语句之前,以保证它不对错误代码负责。

有人还指出 print 语句可能会弄乱错误代码,所以我确保 GetLastError 调用在 CreateProcessAsUserW 调用之后立即发生。

【问题讨论】:

  • CreateProcessAsUserW 的第三个参数(命令行参数)需要一个可修改的字符缓冲区。您现在拥有的 Java 代码是否提供此功能? Read the documentation here。请注意,LPCWSTRLPWSTR 之间存在巨大差异——前者可以是字符串文字,后者必须是可修改的字符数组。
  • 另外,您应该在调用 API 函数后立即调用GetLastError。在得到最后一个错误之前发出 System.out 调用会使错误报告变得无用和误导。
  • @PaulMcKenzie 我认为 Java 端的方法调用不会在这里产生任何影响?似乎只有在同一个线程上有另一个 WinAPI 调用才会覆盖最后一个错误?
  • @DanielWiddis Java 代码可能会调用底层操作系统函数,该函数可能会重置最后一个错误标志。我从编写 C++ 代码中了解到这一事实,该代码“延迟”了对最后一个错误的查询,并调用了几个看似不相关的非 Windows API 函数。我错误地认为可以重置 Windows 上一个错误标志。
  • 应该说:“我错误地认为 Windows 上一个错误标志不能被重置。”

标签: java c++ windows winapi jna


【解决方案1】:

在您的输出中,您指出变量createProcessOkCreateProcessAsUserW() 的返回值,是true(非零)。该函数的 WinAPI documentation 声明:

如果函数成功,则返回值非零。

如果函数失败,返回值为零。要获取扩展的错误信息,请调用 GetLastError。

既然函数成功了,这就是你需要知道的一切。除非函数失败,否则您不应该检查 GetLastError

GetLastError 在每个线程的基础上工作。结果is documented

大多数设置线程最后错误代码的函数在失败时都会设置它。但是,某些函数在成功时也会设置最后一个错误代码。 如果该函数没有记录设置最后一个错误代码,则该函数返回的值只是最近设置的最后一个错误代码;一些函数在成功时将最后一个错误代码设置为 0,而其他函数则没有。

由于函数成功,“没有记录设置最后一个错误代码”。您看到的“5”结果是“只是最近设置的最后一个错误代码”。正如您在问题中指出的那样,可能有一些内部 WinAPI 调用作为内部实现的一部分导致错误,但有一个优雅的回退。

正如@PaulMcKenzie 在 cmets 中指出的那样,API 需要一个可修改的 Unicode(宽)字符串 (LPWSTR) 作为第三个参数。你的代码有一个神秘的toCString() 调用,你还没有发布它的源代码。如果该方法无法生成可修改的 UTF-16 字符数组,则可能是错误的根源,Windows 无论如何都会帮助处理结果。

【讨论】:

  • 我进行了编辑以澄清 c 字符串问题。 toCString 返回一个以 0 结尾的 char[],它满足以空字符结尾的 16 位可修改字符数组要求。好的,我同意你的分析。但是,假设我需要深入研究导致该错误的原因(想象函数返回 false)。我能做些什么来调查原因? Windows 上有什么东西可以追踪正在发生的事情吗?
  • @JoséAlvaradoTorre 作为普通用户,您不能。我查看了 Windows 源代码,其中公共 API 调用了一个内部进程创建函数 (CreateProcessInternalW),该函数执行一大堆其他 API 调用。仔细观察,并非所有这些失败都会导致创建过程失败(例如,对 UpdateProcThreadAttribute 的调用涉及分离标志)。此外,您正在调用 CMD.EXE 应用程序及其所有内部 API 调用,以及 WHOAMI.EXE 及其所有内部 API 调用。其中任何一个都可以优雅地处理预期的 WinAPI 故障。
  • 如果函数返回 false 并显示该错误,您可以确定您无权访问相关的可执行路径之一,因为在这种情况下它会立即返回该错误。跨度>
  • 好的,这很公平。除非函数返回 false,否则我不会担心错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-05-25
  • 1970-01-01
  • 2010-10-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多