思路
作为windows服务,要想在用户桌面端启动一个程序,而且是以管理员身份运行的,这个思路其实跟其他语言一样的.
步骤
- 自身权限要高(这个应该没问题,服务的运行身份都是System)
- 找
winlogon.exe
(根据快照遍历所有进程,匹配名称,得到这个进程) - 获取它的访问令牌
- 复制访问令牌或者直接用
- 构建运行环境
WinSta0\Default
- 用Win32 API:
CreateProcessAsUserW
启动
golang 实现
import (
"strconv"
"strings"
"github.com/axgle/mahonia"
)
const (
CREATE_UNICODE_ENVIRONMENT = 0x00000400
CREATE_NO_WINDOW = 0x08000000
NORMAL_PRIORITY_CLASS = 0x20
INVALID_SESSION_ID = 0xFFFFFFFF
WTS_CURRENT_SERVER_HANDLE = 0
TOKEN_DUPLICATE = 0x0002
MAXIMUM_ALLOWED = 0x2000000
CREATE_NEW_CONSOLE = 0x00000010
IDLE_PRIORITY_CLASS = 0x40
HIGH_PRIORITY_CLASS = 0x80
REALTIME_PRIORITY_CLASS = 0x100
GENERIC_ALL_ACCESS = 0x10000000
)
// 先来两个API,这个貌似使用syscall也可以.
// 刚刚开始写,不知道syscall已经实现了一部分API,就自己动手写了
// Win32进程结构体
type PROCESSENTRY32 struct {
dwSize uint32 // 结构大小
cntUsage uint32 // 此进程的引用计数
th32ProcessID uint32 // 进程id
th32DefaultHeapID uintptr // 进程默认堆id
th32ModuleID uint32 // 进程模块id
cntThreads uint32 // 进程的线程数
th32ParentProcessID uint32 // 父进程id
pcPriClassBase uint32 // 线程优先权
dwFlags uint32 // 保留
szExeFile [260]byte // 进程全名
}
type SW struct {
SW_HIDE uint16 // 0,
SW_SHOWNORMAL uint16 // 1,
SW_NORMAL uint16 // 1,
SW_SHOWMINIMIZED uint16 // 2,
SW_SHOWMAXIMIZED uint16 // 3,
SW_MAXIMIZE uint16 // 3,
SW_SHOWNOACTIVATE uint16 // 4,
SW_SHOW uint16 // 5,
SW_MINIMIZE uint16 // 6,
SW_SHOWMINNOACTIVE uint16 // 7,
SW_SHOWNA uint16 // 8,
SW_RESTORE uint16 // 9,
SW_SHOWDEFAULT uint16 // 10,
SW_MAX uint16 // 10
}
var ISW = SW{
SW_HIDE: 0,
SW_SHOWNORMAL: 1,
SW_NORMAL: 1,
SW_SHOWMINIMIZED: 2,
SW_SHOWMAXIMIZED: 3,
SW_MAXIMIZE: 3,
SW_SHOWNOACTIVATE: 4,
SW_SHOW: 5,
SW_MINIMIZE: 6,
SW_SHOWMINNOACTIVE: 7,
SW_SHOWNA: 8,
SW_RESTORE: 9,
SW_SHOWDEFAULT: 10,
SW_MAX: 10,
}
func (p *PROCESSENTRY32) Name() string {
// string(process.szExeFile[0:]
decoder := mahonia.NewDecoder("gbk") //"github.com/axgle/mahonia" 为了中文名称
name := decoder.ConvertString(string(p.szExeFile[0:])) //string(p.szExeFile[0:])
name = name[:strings.LastIndex(name, ".exe")+4]
return name
}
func (p *PROCESSENTRY32) ModuleID() string {
return strconv.Itoa(int(p.th32ModuleID))
}
func (p *PROCESSENTRY32) PID() uint32 {
return p.th32ProcessID
}
// 这个Error请不要作为判断return依据.因为你有可能是获取到了,但是会给你来个提示
func OpenProcess(dwDesiredAccess uint, bInheritHandle bool, dwProcesssId uint32) (uintptr, error) {
if runtime.GOOS != "windows" {
return 0, fmt.Errorf("Only Support Windows")
}
kernel32 := syscall.NewLazyDLL("kernel32.dll") // 加载dll
openProcess := kernel32.NewProc("OpenProcess") // 获得接口函数
pHandle, _, err := openProcess.Call(uintptr(dwDesiredAccess), uintptr(unsafe.Pointer(&bInheritHandle)), uintptr(dwProcesssId))
return pHandle, err
}
// 关闭句柄 服务开发必须在使用完令牌句柄之后关闭它们。
func CloseHandle(handle uintptr) {
if runtime.GOOS != "windows" {
return
}
kernel32 := syscall.NewLazyDLL("kernel32.dll") // 加载dll
closeHandle := kernel32.NewProc("CloseHandle") // 获得接口函数
closeHandle.Call(handle)
}
//
// GetProcessByName 根据pid获取windows系统的某一个进程
// 参数:
// name string 进程名称, 建议加上.exe结尾
// return Process
func GetProcessByName(name string) (PROCESSENTRY32, error) {
var targetProcess PROCESSENTRY32
targetProcess = PROCESSENTRY32{
dwSize: 0,
}
if runtime.GOOS != "windows" {
return targetProcess, fmt.Errorf("Not On Windows OS")
}
kernel32 := syscall.NewLazyDLL("kernel32.dll")
CreateToolhelp32Snapshot := kernel32.NewProc("CreateToolhelp32Snapshot")
pHandle, _, _ := CreateToolhelp32Snapshot.Call(uintptr(0x2), uintptr(0x0))
if int(pHandle) == -1 {
return targetProcess, fmt.Errorf("error:Can not find any proess.")
}
defer CloseHandle(pHandle)
Process32Next := kernel32.NewProc("Process32Next")
for {
var proc PROCESSENTRY32
proc.dwSize = uint32(unsafe.Sizeof(proc))
if rt, _, _ := Process32Next.Call(uintptr(pHandle), uintptr(unsafe.Pointer(&proc))); int(rt) == 1 {
pname := proc.Name()
xpoint := strings.LastIndex(pname, ".exe")
if pname == name || (xpoint > 0 && pname[:xpoint] == name) {
return proc, nil
}
} else {
break
}
}
return targetProcess, fmt.Errorf("error:Can not find any proess.")
}
func StartProcessByPassUAC(applicationCmd string) error {
winlogonEntry, err := GetProcessByName("winlogon.exe")
if err != nil {
return err
}
// 获取winlogon 进程的句柄
winlogonProcess, err := OpenProcess(MAXIMUM_ALLOWED, false, winlogonEntry.PID())
// 此处可能会返回异常,但是不用担心,只要成功获取到进程就可以
// if err != nil { // The operation completed successfully
// Ilog.Debug("OpenProcess:", err)
// return err
// }
defer CloseHandle(winlogonProcess)
// flags that specify the priority and creation method of the process
dwCreationFlags := CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT
// func() uint32 {
// if visible {
// return CREATE_NEW_CONSOLE
// } else {
// return CREATE_NO_WINDOW
// }
// }() | CREATE_UNICODE_ENVIRONMENT
var syshUserTokenDup syscall.Token
syscall.OpenProcessToken(syscall.Handle(winlogonProcess), MAXIMUM_ALLOWED, &syshUserTokenDup)
defer syshUserTokenDup.Close()
var syslpProcessInformation syscall.ProcessInformation //= &syscall.ProcessInformation{}
var syslpStartipInfo syscall.StartupInfo = syscall.StartupInfo{
Desktop: windows.StringToUTF16Ptr(`WinSta0\Default`),
ShowWindow: ISW.SW_SHOW, // func() uint16 {
// if visible {
// return ISW.SW_SHOW
// } else {
// return ISW.SW_HIDE
// }
// }()
}
syslpStartipInfo.Cb = uint32(unsafe.Sizeof(syslpStartipInfo))
var syslpProcessAttributes *syscall.SecurityAttributes
starterr := syscall.CreateProcessAsUser(
syshUserTokenDup,
nil,
windows.StringToUTF16Ptr(applicationCmd),
syslpProcessAttributes,
syslpProcessAttributes,
false,
uint32(dwCreationFlags),
nil,
nil,
&syslpStartipInfo,
&syslpProcessInformation)
return starterr
}
调用
func StartProcess(bySilence bool) error {
application := `C:\xtcp.exe` // 程序路径
cmdLine := `--port 808` // 参数
cmdLineAll := "\"" + application + "\"" + " " + cmdLine // 程序路径.exe args...
// 这里其实没有使用API的路径,使用了命令行的方式.所以在上面的API函数里面使用apppath使用nil,路径是也nil
return StartProcessByPassUAC(cmdLineAll)
}
吐槽
其实开始实现时时不知道syscall这个模块实现了不少API的调用,也不知道windows.call.毕竟才刚刚使用go不久
所以自己动手实现了不少Win32API的接口,StartProcessByPassUAC是自己手写了API的,
在调试的时候,无论怎么传参数,API总是返回错误:
The system cannot find the path specified.
The directory name is invalid.
The filename, directory name, or volume label syntax is incorrect.
The filename or extension is too long.
上面这四个错误,我换了很多参数,换了短地址,换了windows.call,换了其他的API,换了各种数据类型,代码写出来一天,调试错误倒是用了4天时间.
最后各种方式,总算实验出来了上面的代码可行性.
注意
如果你使用上面的代码写出程序,使用以管理员身份运行,但是,依旧不会启动指定的程序.这是因为启动的权限还是较低.
所以你需要创建成服务,然后启动它.
预感
这段代码调试成功时的场景,隐约以前经历过.
嗯,海马效应....
有种不好的感觉浮现.