1.问题
我有一个复杂的批处理文件,其中某些部分需要以提升的/管理权限运行(例如与Windows服务交互),我找到了一种Powershell方法来做到这一点:
powershell.exe -command "try {$proc = start-process -wait -Verb runas -filepath '%~nx0' -ArgumentList '<arguments>'; exit $proc.ExitCode} catch {write-host $Error; exit -10}"
但是有一个巨大的警告!我的脚本的提升实例 (%~nx0
) 以环境变量的全新副本开始,并且我之前设置“var=content”
的所有内容均不可用。
2.到目前为止我所尝试的
这个Powershell脚本也没有帮助,因为Verb =“RunAs”
需要UseShellExecute = $true
,而这又与/互斥使用 StartInfo.EnvironmentVariables.Add()
$p = New-Object System.Diagnostics.Process
$p.StartInfo.FileName = "cmd.exe";
$p.StartInfo.Arguments = '/k set blasfg'
$p.StartInfo.UseShellExecute = $true;
$p.StartInfo.Verb = "RunAs";
$p.StartInfo.EnvironmentVariables.Add("blasfg", "C:\\Temp")
$p.Start() | Out-Null
$p.WaitForExit()
exit $p.ExitCode
即使这可行,我仍然需要传输数十个变量...
3. 没有吸引力的半解决方案
因为规避问题并不是正确的解决方案。
- 像 hstart 这样的辅助工具 - 因为我无法依赖外部工具。只有 CMD、Powershell,也许还有 VBscript(但看起来
runas
加上 wait
和 errorlevel/ExitCode
处理在 vbs 中是不可能的) 。
- 传递(仅必需的)变量作为参数 - 因为我需要数十个变量并且转义它们是一件丑陋的苦差事(无论是结果还是执行过程)。
- 重新启动整个脚本 - 因为所有解析、检查处理和其他任务再次发生(一次又一次......)的效率很低。我希望将提升的部分保持在最低限度,并且稍后可以以普通用户身份运行某些操作(例如服务启动/停止)。
- 将环境写入文件并在提升的实例中重新读取它 - 因为这是一个丑陋的黑客,我希望有一个更干净的选项。将可能敏感的信息写入文件甚至比将其临时存储在环境变量中更糟糕。
1. Problem
I have a complicated batch file where some parts need to run with elevated/admin rights (e.g. interacting with Windows services) and I found a Powershell way to do that:
powershell.exe -command "try {$proc = start-process -wait -Verb runas -filepath '%~nx0' -ArgumentList '<arguments>'; exit $proc.ExitCode} catch {write-host $Error; exit -10}"
But there's a huge caveat! The elevated instance of my script (%~nx0
) starts with a fresh copy of environment variables and everything I set "var=content"
before is unavailable.
2. What I've tried so far
This Powershell script doesn't help either because Verb = "RunAs"
requires UseShellExecute = $true
which in turn is mutually exclusive to/with StartInfo.EnvironmentVariables.Add()
$p = New-Object System.Diagnostics.Process
$p.StartInfo.FileName = "cmd.exe";
$p.StartInfo.Arguments = '/k set blasfg'
$p.StartInfo.UseShellExecute = $true;
$p.StartInfo.Verb = "RunAs";
$p.StartInfo.EnvironmentVariables.Add("blasfg", "C:\\Temp")
$p.Start() | Out-Null
$p.WaitForExit()
exit $p.ExitCode
And even if that would work I'd still need to transfer dozens of variables...
3. unappealing semi-solutions
because circumventing the problem is no proper solution.
- helper tools like hstart - because I can't relay on external tools. Only CMD, Powershell and maybe VBscript (but it looks like
runas
plus wait
and errorlevel/ExitCode
processing isn't possible with/in vbs).
- passing (only required) variables as arguments - because I need dozens and escaping them is an ugly chore (both the result and doing it).
- restarting the whole script - because it's inefficient with all the parsing, checking processing and other tasks happening again (and again and ...). I'd like to keep the elevated parts to a minimum and some actions can later be run as a normal user (e.g service start/stop).
- Writing the environment to a file and rereading it in the elevated instance - because it's an ugly hack and I'd hope there's a cleaner option out there. And writing possibly sensitive information to a file is even worse than storing it temporarily in an environment variable.
发布评论
评论(3)
以下是使用以下方法的概念证明:
使
powershell
调用调用另一个,aux。powershell
实例作为提升的目标进程。这允许外部
powershell
实例“烘焙”Set-Item
语句,这些语句重新创建调用者的环境变量(外部实例继承了这些变量,因此可以使用Get-ChilItem Env:
枚举)到传递给 aux 的-command
字符串中。实例,然后重新调用原始批处理文件。警告:此解决方案盲目地重新创建提升进程中调用者进程中定义的所有环境变量 - 考虑进行预过滤,可能通过名称模式,例如通过共享前缀;例如,要将变量重新创建限制为名称以
foo
开头的变量,请将Get-ChildItem Env:
替换为Get-ChildItem Env:foo*
在下面的命令中。注意:
trap { [Console]::Error.WriteLine($_); exit -667 }
处理用户拒绝提升提示的情况,这会导致trap
语句捕获(使用尝试
/围绕
语句也是一种选择,通常是更好的选择,但在这种情况下,Start-Process
调用的 catchtrap
在语法上更容易) .指定传递参数(在
cmd /c '\`\"%thisBatchFilePath:'=''%\`\ 之后直接传递到(提升的)批处理文件重新调用的参数“
上面部分):'
,则必须将它们加倍 (''
)'\`\"...\`\"
(sic) 中,如\`\" 带引号的参数所示上面的\`\"
。cmd /c '<批处理文件>; &不幸的是,需要重新调用技术来确保稳健退出代码报告 - 请参阅此答案了解详细信息。
重新调用批处理文件后需要显式
exit $LASTEXITCODE
语句,以使 PowerShell CLI 报告批处理文件报告的特定退出代码 - 没有该语句,任何非零值退出代码将映射到1
。有关 PowerShell 中退出代码的全面讨论,请参阅此答案。Here's a proof of concept that uses the following approach:
Make the
powershell
call invoke another, aux.powershell
instance as the elevated target process.This allows the outer
powershell
instance to "bake"Set-Item
statements that re-create the caller's environment variables (which the outer instance inherited, and which can therefore be enumerated withGet-ChilItem Env:
) into the-command
string passed to the aux. instance, followed by a re-invocation of the original batch file.Caveat: This solution blindly recreates all environment variables defined in the caller's process in the elevated process - consider pre-filtering, possibly by name patterns, such as by a shared prefix; e.g., to limit variable re-creation to those whose names start with
foo
, replaceGet-ChildItem Env:
withGet-ChildItem Env:foo*
in the command below.Note:
trap { [Console]::Error.WriteLine($_); exit -667 }
handles the case where the user declines the elevation prompt, which causes a statement-terminating error that thetrap
statement catches (using atry
/catch
statement around theStart-Process
call is also an option, and usually the better choice, but in this casetrap
is syntactically easier).Specifying pass-through arguments (arguments to pass directly to the re-invocation of the (elevated) batch file, after the
cmd /c '\`\"%thisBatchFilePath:'=''%\`\"
part above):'
, you must double them (''
)'\`\"...\`\"
(sic), as shown with\`\"quoted argument\`\"
above.The
cmd /c '<batch-file> & exit'
re-invocation technique is required to ensure robust exit-code reporting, unfortunately - see this answer for details.The explicit
exit $LASTEXITCODE
statement after the batch-file re-invocation is required to make the PowerShell CLI report the specific exit code reported by the batch file - without that, any nonzero exit code would be mapped to1
. See this answer for a comprehensive discussion of exit codes in PowerShell.滚烫的解决方案
源自 mklement0 的工作概念证明和 Jeroen Mostert 的 Base64 建议,我用这个构建了一个解决方案方法:
它更加灵活,因为您不受环境变量的限制(您基本上可以传递任何内容(基于文本)),并且不需要编辑 Powershell 命令来选择通过管道传输的内容。但它有一些限制 mklement0 的实现不会受到影响:
barz< /代码>)。
fooDoubleQouting
是一个反例)。示例/测试批次:
注释:
exit /b X
,则不需要“exit”重新调用技术。然后用&\`\"%_selfBat%\`\"
而不是CMD /C ... & exit
就足够了(使用单独分隔的参数:'arg1','arg2'
)。'/D','/T:4F',
- 忽略 CMD 的注册表自动运行命令并将前景色/背景色设置为深红底白字。echo[
而不是echo
更安全、更快(cmd 不需要搜索名为echo.*
的实际可执行文件)。pause
或set /P ...
),在提升的实例中需要nul
(?) 传递给任何要求的内容它(我的假设)。我确信有一种方法可以从管道中拯救 stdin 并将其重新连接到con
(也许是某种break
通过 从这里)。barl
的反引号被破坏了。逃离地狱
这里是动态的中间和内部命令行来显示正在发生的事情并消除一些逃避的魔法:
即使在我的默认环境中,命令行也大约有 5kB 大(!),这要归功于
<>
。A piping hot solution
Derived from mklement0's working proof of concept and Jeroen Mostert's Base64 suggestion I've built a solution with this approach:
It is more flexible because you're not limited to environment variables (you can essentially pass on anything (text based)) and the Powershell command doesn't need to be edited to choose what gets piped through. But it has a few limitations mklement0's implementation doesn't suffer from:
barz
).fooDoubleQouting
is a negative example).Example / test batch:
Notes:
CMD /C '<batch-file_withEscaped'> & exit'
re-invocation technique isn't required if you consistentlyexit /b X
in your batch file. Then&\`\"%_selfBat%\`\"
instead ofCMD /C ... & exit
is enough (with separately separated arguments:'arg1','arg2'
).'/D','/T:4F',
- Ignore CMD's registry AutoRun commands and set fore-/background colors to white on dark red.echo[
instead ofecho
is safer and quicker (cmd doesn't need to search for actual executables namedecho.*
).<con
is required in the elevated instance for anything needing user interaction (eg.pause
orset /P ...
). Without<con
the now empty(?) piped in standard input (pipe#0) still deliversnul
(?) to anything asking for it (my assumption). I'm sure there is a way too rescue stdin from the pipe and reattach it tocon
(maybe some kinde ofbreak
throu from in here).barl
's backticks get mangled.Escaping hell
Here are the dynamic middle and inner command lines to show whats going on and shave away some escaping magic:
Even with just my default environment that command line is somewhere around 5kB big(!), thanks to the
<<BASE64_BLOB>>
.所以,是的,就像已经说过的那样,由于安全隐患,环境并不是按照设计从一个用户传递到另一个用户的。这并不意味着它不能完成,即使这不是你“应该”做的事情。虽然我确实认为你应该研究一下你真正想要实现的目标,但我绝对讨厌人们告诉你你“通常”应该做什么而不回答实际问题的类型。所以我在这里给你两种选择。
“传递”环境
这里有几个选项
从提升的子进程中,使用 NtQueryInformationProcess 和 ReadProcessMemory API。
然后使用 WriteProcessMemory 或者像平常一样设置它们。您只需使用 Powershell 即可实现此目的,但需要添加一些 C# 代码来调用所需的 API 函数。
在我的 Enable-Privilege.ps1 示例中,您可以看到如何实现 NtQueryInformationProcess PowerShell并操作父进程(我编写它的目的是为了修改内部的特权令牌自身/父进程/任何进程)。您实际上想要使用它的一部分,因为您需要启用SeDebugPrivilege才能操作其他进程的内存。
这个选项可能是最“干净”和最强大的解决方案,没有任何立即明显的警告。有关详细信息,请参阅此 codeproject 文章:读取以下环境字符串远程进程
在未提升的父进程内,迭代所有环境变量并将它们作为字符串写入单个环境变量。然后在生成提升的子进程时将该单个环境值作为参数传递,您可以在其中解析该字符串并将这些环境值写回。不过,您可能会遇到与此处选项 3 相同的警告。
将变量从父级传递到子级,就像这里已经提出的那样。这里的问题是批处理器非常挑剔,并且解析和转义的规则非常混乱,因此使用此选项很可能会遇到特殊字符和其他类似警告的问题。
使用内核模式驱动程序,用提升的进程覆盖未提升进程的安全令牌,完成后写回原始令牌。从表面上看,这似乎是完美的解决方案,因为您实际上可以留在以前未提升的进程中并保留其环境而不更改上下文,只有安全上下文会被替换。与在内核模式中一样,您可以修改所有内容,安全令牌是内核内存中的简单内存结构,您可以更改它们。这种方法的问题在于它完全绕过了 Windows 安全模型,因为它应该不可能更改现有进程的安全令牌。因为它应该是“不可能的”,所以它深入到未记录的领域,并且在内核内部,如果你不知道自己在做什么,你可以很容易地破坏东西,所以这绝对是“高级”选项(即使这个特定的选项)事情并不太复杂,基本上只是写一些内存)。由于这是您不应该做的事情,因此有可能会破坏某些内容,因为 Windows 不希望进程突然具有不同的安全上下文。话虽这么说,我过去使用这种方法没有任何问题。尽管安全设计发生任何变化,但未来它可能会被破坏。您还需要启用 testsigning (又名禁用驱动程序签名强制),对您的驱动程序进行数字签名或使用其他方法来绕过此要求(例如通过管理程序或漏洞利用),但这超出了本讨论的范围回答。
achsually版本
“因为规避问题不是正确的解决方案。”
在这种情况下,我会这样做。由于您的问题性质如此,因此不存在简单的解决方案,因为它不受设计支持。很难提出具体的解决方案,因为缺乏您实际上想要实现的目标的信息。
我将尝试从一般意义上涵盖这一点。首先要考虑一下您实际上想要实现的目标是什么部分。您需要执行哪些需要抬高的操作?有多种方法可以以受支持的方式实现任何目标。
示例:
对于您需要读取/写入/修改的任何内容,您可以更改目标(而不是源)的安全设置。这意味着,假设您需要访问特定的注册表项、服务、文件、文件夹等,您可以简单地修改目标的ACL以允许源(即用户)执行您需要的任何操作。例如,如果您需要修改单个服务,您可以仅为该单个进程添加启动/停止/修改权限。
如果您需要的是特定于操作类型而不是特定目标,您可以添加所需的 “用户”组的特权。或者创建一个具有所需权限的新组,然后将用户添加到该组。
如果您希望更精细地控制可以/不可以执行的操作和/或特定的操作,您可以编写一个简单的程序并将其作为提升的服务运行。然后,您可以告诉该服务从未提升的批处理脚本中执行所需的操作,因此不需要请求提升和生成新进程。您可以简单地从批处理中执行
my-service.exe do-the-thing
,然后 my-service 将执行您需要的操作。您也可以始终在脚本开头请求提升权限,但很明显您不想使用完全管理员权限执行此操作,因此您可以为此目的创建一个新用户,并将其添加到新用户中具有您所需权限的组。请注意,如果不诉诸上述内核模式“黑客”,您无法为用户即时添加新权限,只能启用/禁用/删除现有的。您可以做的是根据您的需要预先添加它们,但这需要在流程开始之前进行。
So yeah, like has been said, the environment is not meant to be passed from a user to another, by design, because of the security implications. It doesn't mean that it can't be done, even if it's not something you're "supposed" to do. While I do think you should look into what do you actually want to achieve, I absolutely hate the type of answers where people tell you what you achsually "should" do and not answer the actual question at all. So I'm giving you both options here.
"Passing" the environment
You have several options here
From the elevated child process, read the environment variables from the unelevated caller parent process' memory using the NtQueryInformationProcess and ReadProcessMemory APIs.
Then either overwrite the variables on the target process (in your case, the current process) with WriteProcessMemory or just set them as you normally would. You can achieve this with only Powershell, albeit you need to add some C# code to call the required API functions.
Here in my Enable-Privilege.ps1 example you can see how to implement NtQueryInformationProcess in PowerShell and manipulate a parent process (I wrote it for the purpose of modifying privilege tokens inside self/parent/any process). You would actually want to use a part of it because you need to enable SeDebugPrivilege to be able to manipulate memory of other processes.
This option would probably be most "clean" and robust solution without any instantly obvious caveats. See this codeproject article for more information: Read Environment Strings of Remote Process
Inside the unelevated parent process, iterate through all the environment variables and write them as a string to a single environment variable. Then pass that single environment value as an argument when spawning the elevated child process, where you can parse that string and write those environment values back. You would likely run to the same caveats as option 3 here though.
Pipe the variables from the parent to the child, like has been proposed here already. The problem here is that the batch processor is really finicky and the rules of parsing and escaping are super janky, so it's very likely you would run to issues with special characters and other similar caveats with this option.
Using a kernel-mode driver, overwrite the security token of the unelevated process with a elevated one, writing back the original token after you are done. On the surface this would seem like the perfect solution, since you could actually stay inside the previously-unelevated process and retain it's environment without changing context, only the security context would be replaced. As in kernel-mode you can modify everything, the security tokens are simple memory structs inside kernel memory which you can change. The problem with this approach is that it completely bypasses the windows security model, as it's supposed to be impossible to change the security token of an existing process. Because it's supposed to be "impossible", it goes deep into undocumented territory and inside the kernel you can easily break stuff if you don't know what you're doing, so this is definitely the "advanced" option (even though this particular thing is not too complicated, it's basically just writing some memory). As it's something you're not supposed to be doing, there is a possibility it breaks something since Windows does not expect a process to suddenly have a different security context. That being said, I've used this approach with no problems in the past. It could be broken in the future though by any change in the security design. You would also need to either enable testsigning (aka Disable Driver Signature Enforcement), digitally sign your driver or use some other method to bypass this requirement (f.ex. through a hypervisor or an exploit), but that is out of the scope of this answer.
The achsually version
"because circumventing the problem is no proper solution."
In this case, I would do exactly that. Since your problem is of such nature that a easy solution for it doesn't exist, because it's not supported by design. It's hard to propose a specific solution since the lack of information of what it is you're actually trying to achieve here.
I'm gonna try to cover this in a general sense. First is to think about the what it is you're actually trying to achieve here part. What are the operations you need to do which require elevation? There would be multiple ways to achieve whatever it is in a supported fashion.
Examples:
For whatever you need to read/write/modify, you could change the security security settings of the target (instead of the source). Meaning that let's say you need to access a specific registry key, service, file, folder, whatever, you could simply modify the ACL of the target to allow the source (i.e. the user) to do whatever operation you need. If you need to modify a single service for example, you could add the start/stop/modify right for only that single process.
If the thing you need is specific to the types of operations rather than specific targets, you could add the required privileges to the "Users" group. Or make a new group with the required privileges, and then add the user to that group.
If you want more granular control on what can/can't be done and/or the operations are specific, you could write a simple program and run it as a elevated service. Then you could just tell that service to do the required operations from the unelevated batch script, so no requesting elevation and spawning new process would be needed. You could simply do
my-service.exe do-the-thing
from batch, and that my-service would do the operation you need.You could also always ask for the elevation in beginning of the script, but as it's clear you don't want to do this with full administrator rights, you could create a new user for just this purpose which you add to a new group for it which has the required privileges you need. Note that without resorting to the aforementioned kernel-mode ""hacks"", you cannot add new privileges for a user on-the-fly, only enable/disable/remove existing ones. What you can do though is add them beforehand based on what you need, but that will need to happen before the process is started.