简介:RunAsSystem是一款面向64位Windows平台的开源权限提升工具,旨在帮助用户在无需频繁切换管理员账户的情况下,以系统账户上下文运行可执行文件与批处理脚本。7z压缩包内共52个文件,整体仅49KB,包含8个exe可执行程序、VB.NET项目源码(vb、resx、settings等)、解决方案与项目配置文件(sln、vbproj),以及pdb调试符号和xml文档,既可直接运行,也便于研究其调用CreateProcessAsUser等API实现权限提升的原理。已有155人学习下载。对于希望了解Windows权限模型、需要临时以System身份执行工具的技术爱好者或开发者而言,这份开源代码提供了透明可查的实现范例,也可基于源码自行定制或改进功能,是兼具实用性与学习价值的轻量资源。
1. RunAsSystem 是什么:解决“管理员还不够用”的开源系统权限执行器
RunAsSystem 这个开源小工具,解决的是 Windows 系统维护里最憋屈的一件事:右键“以管理员身份运行”点了,改注册表照样报“拒绝访问”,清系统目录照样提示没权限。管理员令牌只是比标准用户高一级的拆分令牌,很多维护操作真正需要的是 SYSTEM 账户。RunAsSystem 一条命令就能把指定程序提到系统账户上下文执行,单文件、命令行驱动、无交互界面,适合软件部署、驱动调试、系统目录清理。对写运维脚本和打包工具链的人来说,是在不引入一整套商业提权方案的前提下,性价比很高的选择。
2. 提权原理与工具选型:RunAsSystem 的底层机制,为什么比直接注册服务省心
2.1 临时服务法:让系统自己把程序拉起来
RunAsSystem 这类开源工具,最常见的实现核心并不是什么高深的动态注入,而是“临时服务法”。它依赖 Windows 自带的服务控制管理器(SCM):服务控制管理器能以 LocalSystem 身份启动一个服务进程,而这些工具只是把“你要执行的那条命令”临时伪装成一个服务,跑完就删。整个过程大致是:
- 工具启动后先检查当前进程是否已经拿到管理员令牌;
- 如果没有,在带 UAC 的环境里会触发提权确认,或者直接退出并提示;
- 通过 SCM 创建一个临时服务,服务名一般带随机后缀,避免和已有服务撞名;
- 服务类型设为“独立进程”,二进制路径是你传入的那条命令,通常会用 cmd /c 包一层;
- SCM 把服务拉起来,由于服务配置的账户是 LocalSystem,命令就在 SYSTEM 上下文执行;
- 命令退出后,工具删除这个临时服务,完成自我清理。
理解了这套流程,你就明白为什么这类工具对权限等级的要求是“硬性”的:创建服务这个动作本身需要管理员权限,所以别指望在普通用户窗口里直接调用就能起飞。
这里还要澄清一个常见误会:SYSTEM 账户并不是“加强版管理员”。管理员的权限来自 Administrators 组,而 SYSTEM 是内核和服务宿主默认的账户,它拥有对本机几乎所有对象(包括文件、注册表键、内核对象)的访问权,且不受 UAC 拆分令牌的影响。你要部署的驱动安装、系统目录清理、受保护注册表项修改,本质都是“服务宿主经常做的事”,所以用 SYSTEM 身份去执行才是语义上最合适的。
那为什么命令要包一层 cmd /c 而不是直接传路径?因为 SCM 的服务配置里 ImagePath 会按“可执行文件 + 参数”来解析,如果直接传 RunAsSystem.exe "D:\my tool\x.exe --flag",Windows 会从第一个空格处截断路径,服务直接报“没有可执行文件”。包上 cmd /c 之后,整段字符串原样交给 cmd 解释,空格和参数都归 cmd 托管,这也是后面避坑章节里大量引号问题的根源。
顺带说一句,这套机制不是某个作者的独创思路,Windows 上很多“以系统身份运行”的工具都是同一套路。它不涉及内核级东西,也不是漏洞利用——系统本来就会这样做,工具只是把这个能力封装成了一条可脚本化的命令。
2.2 四类“以 SYSTEM 身份运行”的方式对比
先摆一张对比表,方便你判断 RunAsSystem 这类工具到底赢在哪、输在哪:
| 方式 | 交互复杂度 | 输出可见性 | 生命周期 | 适用场景 |
|---|---|---|---|---|
| RunAsSystem 类开源工具 | 单条命令行 | 直接继承控制台 | 随命令结束即清理 | 脚本、部署、临时调试 |
| sc create + net start | 需要先写服务配置 | 基本拿不到输出,要靠日志文件 | 常驻,需手动删除 | 正规做服务的场景 |
| 计划任务以 SYSTEM 身份运行 | 界面或 XML 配置 | 事件日志或重定向文件 | 定时触发 | 定时型维护任务 |
| 远程运维类工具的提权参数 | 需部署工具本体 | 可控但依赖通道 | 随会话结束 | 多机远程管理 |
选型我一般就按三条来判断:第一,是不是单次、非交互的执行;第二,退出码和输出能不能快速带回脚本;第三,有没有引入额外的 Agent 或常驻组件。RunAsSystem 恰好同时满足前两条、规避第三条。反过来,如果你需要的是一个长期存活的后台逻辑,那就老老实实做成真正的 Windows 服务,让服务管理器接管生命周期——拿临时提权工具顶班做常驻任务,是最常见的误用,后面避坑章节里会提到为什么你会因此翻车。
说白了,这个工具的定位是“开发者和运维者在当前机器上临时借用 SYSTEM 身份”,不是“系统权限管理的完整方案”。认清边界,才能真正把它的价值吃干榨净。
2.3 一个容易被绕晕的点:LocalSystem 不等于所有高权限场景都适用
很多刚接触的人以为只要拿到 SYSTEM,什么都能干。实际上 LocalSystem 账户的网络凭证是“机器账户”,访问网络共享时会以计算机名去认证,跟当前登录用户的网络凭据不是一回事。如果你的命令要写共享目录、调用需要用户网络身份的 API,SYSTEM 身份反而可能失败——这是账户语义问题,不是工具的 bug。所以遇到“明明是 SYSTEM 却连共享文件夹都写不了”的怪现象,先别怀疑提权没生效,先确认你需要的到底是本机最高权限还是网络身份。这个判断做对了,能省掉大量无效排查时间。
3. 从源码编译到最小启动:RunAsSystem 的构建与第一条验证命令
3.1 环境准备与源码编译
RunAsSystem 的常见实现是 C# 写的,目标框架因版本而异,当前主线的构建一般落在 .NET 6/7/8 这个区间。先确认本机装了对应的 SDK,然后按下面这套命令走:
git clone https://example.local/RunAsSystem.git cd RunAsSystem dotnet restore dotnet build -c Release -r win-x64 --self-contained false find . -name "*.exe" -type f逻辑说明:dotnet restore 只做依赖还原,网络源不稳定时,这一步能提前暴露 NuGet 源的问题,而不是等到编译中途才报错;dotnet build 加 -r win-x64 是明确目标运行时,避免产出物被当成 AnyCPU 在和 32 位工具宿主交互时出怪问题;--self-contained false 表示不携带运行时,产物更小,但如果目标机器没装对应的 .NET Runtime,建议改成 --self-contained true,体积换稳定。
参数说明:
- -c Release:必须指定。Debug 版在提权链路上的行为差异偶尔会出现,别在这种地方给自己埋雷;
- -r win-x64:如果是 32 位系统或 ARM 设备,要换成对应的 RID(比如 win-x86、win-arm64);
- 产物名取决于项目输出的 AssemblyName,通常是 RunAsSystem.exe;找不到时用 find 或 dir 翻一下 bin 下对应的框架目录。
如果你不想折腾编译,直接拿 release 区的二进制也能用。但我个人始终建议至少亲手编译一次,原因有两个:提权类工具被杀软隔离的概率本身就偏高,自己编译能少一点对“别人的二进制”的焦虑;另外,后面你需要改超时、改服务名前缀、加日志时,早晚要回到构建这一步,趁早把环境趟熟不亏。
3.2 最小启动命令:用 whoami 验证整条链路
打开一个“以管理员身份运行”的命令提示符,执行:
RunAsSystem.exe "cmd /c whoami"一切正常时,输出是:
nt authority\system这行输出是整条链路畅通的最直白信号:命令已经被放到 LocalSystem 账户上下文里执行。如果回显的还是你自己的用户名(比如 desktop-xxx\admin),说明提权动作没生效,多半是当前进程没拿到管理员令牌,或者实现版本在提权失败时选择了静默回退。下面这张表覆盖最常用的几种调用形态:
| 用法 | 说明 | 示例 |
|---|---|---|
| "cmd /c xxx" | 整条命令包成字符串,推荐先用这个 | RunAsSystem.exe "cmd /c ipconfig /all" |
| -p 程序路径 | 指定要启动的 exe,不经过 cmd | RunAsSystem.exe -p C:\tools\x.exe |
| -t 秒数 | 超时时间,命令超时后强制结束并返回非零 | RunAsSystem.exe "cmd /c ping -n 5 127.0.0.1 >nul" -t 30 |
| -d 毫秒数 | 服务启动的延迟缓冲,个别机器上抗慢服务响应 | RunAsSystem.exe -d 1000 "cmd /c whoami" |
参数命名在不同分支实现里可能略有出入,拿到新版本先跑一遍 RunAsSystem.exe /? 确认当前分支支持哪些开关。我的习惯是:目标是 exe 就用 -p,目标是批处理、带管道重定向或复杂参数的命令,一律用 cmd /c 包成字符串整传。
3.3 三招验证权限是“真提权”而不是“看起来像”
whoami 显示的只是账户名,不等于权限项都齐了。我验证时一般做三层:
第一层,看完整性级别。运行 whoami /groups,在输出里找 Mandatory Label 那一行:SYSTEM 进程的完整性级别通常对应 System(S-1-16-16384)或 High(S-1-16-12288),如果看到的是 Low,说明令牌被限制过,权限不完整。
第二层,尝试写一个只有 SYSTEM 能写的位置,比如 C:\Windows\System32\config 目录下一个临时文件,或者某个受保护的注册表键。能写进去,才是“权限够用”的直接证据。
第三层,用 PowerShell 从当前身份拉一次 SID:
(New-Object System.Security.Principal.NTAccount("SYSTEM")).Translate([System.Security.Principal.SecurityIdentifier]).Value这段代码会输出 S-1-5-18,也就是 LocalSystem 账户的固定 SID。如果你的进程真的在 SYSTEM 上下文,[System.Security.Principal.WindowsIdentity]::GetCurrent().Name 返回的也会是 NT AUTHORITY\SYSTEM。两个值对得上,这个黑匣子才算验证完毕。
4. 把 RunAsSystem 接进真实工程:部署脚本、计划任务与输出回收
4.1 在软件部署脚本里内联特权命令
最典型的落地场景是软件部署阶段:安装驱动前要停旧服务、清理注册表、操作系统目录。这些动作要么需要 SYSTEM 身份,要么需要在受保护路径下执行。部署脚本里我会把关键两步直接交给 RunAsSystem:
RunAsSystem.exe "cmd /c sc stop SomeDriver & sc delete SomeDriver" RunAsSystem.exe "cmd /c reg delete HKLM\SYSTEM\CurrentControlSet\Services\SomeDriver /f"第一句停服务并删除,第二句清掉对应的注册表项。需要注意 & 的语义是顺序执行,前一条命令失败后后一条照跑;如果你想“停止失败就不删”,把 & 换成 &&。外层用一个双引号把整条命令包住,内部语法完全交给 cmd,不要自己去拼凑引号层级。
部署脚本最容易翻车的地方不是逻辑错,而是“命令看起来执行了,但没达到权限要求”。所以脚本开头我会先做一个前置检查:
whoami /groups | findstr /c:"S-1-16-12288" >nul if errorlevel 1 ( echo 当前窗口的完整性级别不是 High,请右键“以管理员身份运行”。 exit /b 1 )这里没有按习惯去检查 Administrators 组 SID(S-1-5-32-544),是因为 UAC 拆分令牌下,非提升进程的令牌里也带着管理员组 SID,那个判断会骗人。改成检查完整性级别 S-1-16-12288,只有提升后的主令牌才满足。很多“提权没生效”的血泪经验,根子都是启动 RunAsSystem 的父进程连这一步都没过。
4.2 配合计划任务做定时特权维护
另一类常见需求是定时维护:每天凌晨清临时目录、压缩事件日志、清理缓存。我的做法不是让 RunAsSystem 常驻,而是由计划任务用 SYSTEM 身份拉起一个批次脚本,批次脚本内部再按需调用 RunAsSystem。有人会问:计划任务本身就能配最高权限运行,为什么还要绕一层?
因为“身份够了”和“上下文对了”是两码事。计划任务在后台会话里启动的进程,和当前交互控制台没有直接关联,日志要靠自己重定向才能拿到;而 RunAsSystem 的优势恰好是把 SYSTEM 身份的进程挂到当前控制台,输出直接可见,调试成本低得多。所以维护脚本的拆分思路是:计划任务只负责定时触发,RunAsSystem 负责把具体动作在当前会话里执行。
配置时有两点容易被忽略:第一,触发器里要勾选“使用最高权限运行”,否则计划任务仍以标准用户令牌启动;第二,操作里的“程序”填 RunAsSystem.exe 的完整路径,“参数”填带引号的整条命令,任务的工作目录最好也固定到工具所在目录,避免相对路径解析出错。
4.3 从外部程序接管输出与退出码
如果你不是写批处理,而是从 Python 这类语言里发起调用,就要注意 RunAsSystem 是控制台程序而不是 API,跨进程调用必须用子进程方式。先看一段最小封装:
import subprocess cmd = "RunAsSystem.exe \"cmd /c whoami\"" proc = subprocess.run( cmd, shell=True, capture_output=True, text=True, timeout=30, ) print("stdout:", proc.stdout.strip()) print("stderr:", proc.stderr.strip()) print("exit:", proc.returncode)这段代码把整条命令交给系统命令解释器去跑,转义规则和你手动敲 cmd 时完全一致。不过我更推荐 shell=False 的写法,把命令拆成列表传进去,少一层引号解析,少一类脏问题:
import subprocess proc = subprocess.run( [ r"C:\tools\RunAsSystem.exe", "cmd /c whoami", ], capture_output=True, text=True, timeout=30, ) print("exit:", proc.returncode)参数说明:
- timeout 必须有:RunAsSystem 内部没有默认超时时,如果你传的命令是交互式的(比如打开了编辑器),子进程会无限挂起,CI 任务直接卡死;
- capture_output:不要省。SYSTEM 身份下命令的控制台输出会穿透到父进程终端,不带捕获的话日志会变得很难归一处理;
- returncode 不为 0 时一定要单独处理,服务创建失败、超时被杀都会返回非零,只判断有没有抛异常是远远不够的。
注意:这里展示的 Python 封装只负责“发起并回收结果”,它本身并不具备管理员权限,所以外部程序也得在提升后的父进程里启动 RunAsSystem,否则 4.1 的完整性级别问题会原样复现。
5. 避坑指南:RunAsSystem 最常见的 5 个翻车现场与排查方法
5.1 现象:进程起来了,whoami 却显示当前用户
原因:这是最隐蔽的翻车方式。启动 RunAsSystem 的父进程不是管理员,创建临时服务失败,部分实现不会报错,而是静默回退到当前令牌继续执行命令,看起来像“跑通了”,实际上根本没提权。
解决:写脚本时把 4.1 的完整性级别检查放在调用之前。遇到已经跑完但身份不对的情况,用进程查看工具看 RunAsSystem 父进程的 Integrity Level 是不是 High,如果不是,先解决父进程的提权方式,别在 RunAsSystem 参数上瞎试。
5.2 现象:工具一闪而过,什么输出都没留下
原因:分两种。一种是你执行的是 GUI 程序,控制台窗口不属于它,运行完就关;另一种是输出没能传导回当前控制台,尤其当外部调用方是 32 位进程、工具本体是 64 位时,stdout 的接管链路会断。
解决:把输出重定向到文件,强制留下痕迹:
RunAsSystem.exe "cmd /c whoami > C:\Windows\Temp\who.out 2>&1" type C:\Windows\Temp\who.out这段命令让 cmd 自己负责重定向,谁接管输出谁就有最终解释权。注意退出码不会自动写进文件,想看命令有没有正常结束,重定向后再用 echo %errorlevel% 单独捕获,或者让外部封装判断 returncode。
5.3 现象:执行完之后系统里残留了一个服务
原因:命令一直不退(比如 ping -t、等待键输入、或者脚本里起了守护进程),RunAsSystem 主进程会一直等它结束;此时如果直接关掉窗口或被任务管理器强杀,清理临时服务的那一步就被跳过了,注册表里留下随机命名的服务项。下次再跑同类命令,还有可能因为服务名冲突再次失败。
解决:调用时带超时参数,部署脚本的收尾段固定做一次清理:
RunAsSystem.exe "cmd /c ping -n 5 127.0.0.1 >nul" -t 30 sc query type= service state= all | findstr /i runsas sc delete "残留服务名"我的习惯是把“残留服务清理”直接写进部署脚本最后一段,无论上一步成功失败都执行一次查询和清理,成本极低,但能避免后续执行被同名服务挡住。另一个更省心的做法:自己编译时把临时服务名加上固定前缀(比如 cus_ 开头),这样清理时 findstr 只匹配你的前缀,不会误删系统服务。
5.4 现象:命令里带引号、带 % 变量,怎么传都是错
原因:这是 Windows 命令行传参的经典双重解析问题。外层是你调用 RunAsSystem 时 cmd 做的引号解析,内层是 cmd /c 再解析一次。命令里出现 %PATH% 这类变量时,外层会先做变量展开,把当前用户环境下的值写死进去,SYSTEM 会话里再展开时,拿到的已经不是 SYSTEM 的变量了。
解决:凡命令里出现 %、!、嵌套引号的,优先写成临时 .bat 文件再交给 RunAsSystem:
echo echo %USERPROFILE% > C:\Windows\Temp\task.bat RunAsSystem.exe "cmd /c C:\Windows\Temp\task.bat" type C:\Windows\Temp\task.bat这样变量展开被推迟到 SYSTEM 上下文里,避免外层解释器抢先处理。记住这个原则,可以省下大量抠转义的时间:遇到特殊符号,先落成脚本文件,别在命令行里硬拼引号。
5.5 现象:杀毒软件把 RunAsSystem 当风险程序隔离
原因:提权工具的静态特征(服务落地、启动 SYSTEM 进程)和不少恶意程序的后渗透手法非常接近,误报率高是这个品类的通病,不是某个版本特有的问题。
解决:从轻到重有三层手段。第一,优先从源码自己编译,改掉默认文件名和产物元数据,能明显降低静态误报概率;第二,在内部测试环境用目录白名单放行,但不要全局放行所有提权工具;第三,如果团队有代码签名证书,给编译产物签上名。此外还有一个容易忽略的点:杀软对“非标准位置的可执行文件创建服务”拦截特别严,尽量把工具放在固定目录调用,别临时解压到 Downloads 里现场跑。
6. 进阶验证:如何确认 RunAsSystem 执行结果的真实性
6.1 一套能直接复用的完整校验脚本
把身份、SID、完整性级别和写入测试合并成一个脚本,部署时跑一次就能拿到结论:
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $sid = (New-Object System.Security.Principal.NTAccount("SYSTEM")).Translate([System.Security.Principal.SecurityIdentifier]).Value Write-Host "当前身份: $($id.Name)" Write-Host "当前SID: $($id.User.Value)" Write-Host "SYSTEM SID: $sid" if ($id.User.Value -eq $sid) { Write-Host "身份校验: PASS" } else { Write-Host "身份校验: FAIL" } whoami /groups | Select-String "S-1-16-16384|S-1-16-12288"这段脚本把 3.3 的分散验证收拢成一个输出块。值得指出的是,它只能证明“当前进程的令牌”,要证明那条命令跑在 SYSTEM 下,最稳妥的办法是在 RunAsSystem 启动后立即执行这个脚本,让脚本进程继承该令牌。顺序反了,验证的就是你自己父进程的身份,意义不大。
6.2 从系统事件日志反向锁定执行记录
部署完以后,打开事件查看器 → Windows 日志 → 系统,按时间点找 Service Control Manager 来源的事件,常规选项卡里能看到服务名和映像路径。如果映像路径就是你传给 RunAsSystem 的那条命令,服务账户又是 LocalSystem,就相当于系统侧替你留了一份执行凭证。这个习惯对审计场景很有用,事后追查时不用争论“当时到底跑没跑”。
6.3 收尾习惯
说实话,我最早拿这类工具时也翻过车:一条清理命令交出去,回来发现系统里躺着一个随机命名的残留服务,窘到不敢跟同事提。后来养成的固定习惯是:任何一条交给 RunAsSystem 的命令,先写重定向文件、设超时、部署尾部三件套清理,再进生产。就是这个三板斧,帮我省下了后面好几轮排查时间。临时提权这件事,说穿了就是个链路管理问题:父进程令牌对了,命令包好了,输出回收了,残留清掉了,它就不再是玄学。希望帮到你。
本文还有配套的精品资源,点击获取