思路

作为windows服务,要想在用户桌面端启动一个程序,而且是以管理员身份运行的,这个思路其实跟其他语言一样的.

步骤

winlogon.exeWinSta0\DefaultCreateProcessAsUserW

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天时间.
最后各种方式,总算实验出来了上面的代码可行性.

注意

如果你使用上面的代码写出程序,使用以管理员身份运行,但是,依旧不会启动指定的程序.这是因为启动的权限还是较低.
所以你需要创建成服务,然后启动它.

预感

这段代码调试成功时的场景,隐约以前经历过.
嗯,海马效应....
有种不好的感觉浮现.