rmmagent/agent/system/system_windows.go
redanthrax 91c9de6e34 tests
2022-06-21 09:58:08 -07:00

486 lines
11 KiB
Go

package system
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/amidaware/rmmagent/agent/utils"
ps "github.com/elastic/go-sysinfo"
"github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
wapf "github.com/wh1te909/go-win64api"
trmm "github.com/wh1te909/trmm-shared"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
const (
ProgFilesName = "TacticalAgent"
winExeName = "tacticalrmm.exe"
)
func RunScript(code string, shell string, args []string, timeout int) (stdout, stderr string, exitcode int, e error) {
content := []byte(code)
dir := filepath.Join(os.TempDir(), "trmm")
if !trmm.FileExists(dir) {
utils.CreateTRMMTempDir()
}
const defaultExitCode = 1
var (
outb bytes.Buffer
errb bytes.Buffer
exe string
ext string
cmdArgs []string
)
switch shell {
case "powershell":
ext = "*.ps1"
case "python":
ext = "*.py"
case "cmd":
ext = "*.bat"
}
tmpfn, err := ioutil.TempFile(dir, ext)
if err != nil {
return "", err.Error(), 85, err
}
defer os.Remove(tmpfn.Name())
if _, err := tmpfn.Write(content); err != nil {
return "", err.Error(), 85, err
}
if err := tmpfn.Close(); err != nil {
return "", err.Error(), 85, err
}
switch shell {
case "powershell":
exe = "Powershell"
cmdArgs = []string{"-NonInteractive", "-NoProfile", "-ExecutionPolicy", "Bypass", tmpfn.Name()}
case "python":
exe = GetPythonBin()
cmdArgs = []string{tmpfn.Name()}
case "cmd":
exe = tmpfn.Name()
}
if len(args) > 0 {
cmdArgs = append(cmdArgs, args...)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
var timedOut bool = false
cmd := exec.Command(exe, cmdArgs...)
cmd.Stdout = &outb
cmd.Stderr = &errb
if cmdErr := cmd.Start(); cmdErr != nil {
return "", cmdErr.Error(), 65, cmdErr
}
pid := int32(cmd.Process.Pid)
// custom context handling, we need to kill child procs if this is a batch script,
// otherwise it will hang forever
// the normal exec.CommandContext() doesn't work since it only kills the parent process
go func(p int32) {
<-ctx.Done()
_ = utils.KillProc(p)
timedOut = true
}(pid)
cmdErr := cmd.Wait()
if timedOut {
stdout = utils.CleanString(outb.String())
stderr = fmt.Sprintf("%s\nScript timed out after %d seconds", utils.CleanString(errb.String()), timeout)
exitcode = 98
//a.Logger.Debugln("Script check timeout:", ctx.Err())
} else {
stdout = utils.CleanString(outb.String())
stderr = utils.CleanString(errb.String())
// get the exit code
if cmdErr != nil {
if exitError, ok := cmdErr.(*exec.ExitError); ok {
if ws, ok := exitError.Sys().(syscall.WaitStatus); ok {
exitcode = ws.ExitStatus()
} else {
exitcode = defaultExitCode
}
} else {
exitcode = defaultExitCode
}
} else {
if ws, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok {
exitcode = ws.ExitStatus()
} else {
exitcode = 0
}
}
}
return stdout, stderr, exitcode, nil
}
func CMD(exe string, args []string, timeout int, detached bool) (output [2]string, e error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
var outb, errb bytes.Buffer
cmd := exec.CommandContext(ctx, exe, args...)
if detached {
cmd.SysProcAttr = &windows.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP,
}
}
cmd.Stdout = &outb
cmd.Stderr = &errb
err := cmd.Run()
if err != nil {
return [2]string{"", ""},
fmt.Errorf("%s: %s", err, utils.CleanString(errb.String()))
}
if ctx.Err() == context.DeadlineExceeded {
return [2]string{"", ""}, ctx.Err()
}
return [2]string{
utils.CleanString(outb.String()),
utils.CleanString(errb.String()),
}, nil
}
func SetDetached() *syscall.SysProcAttr {
return &syscall.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP,
}
}
func CMDShell(shell string, cmdArgs []string, command string, timeout int, detached bool) (output [2]string, e error) {
var (
outb bytes.Buffer
errb bytes.Buffer
cmd *exec.Cmd
timedOut = false
)
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
if len(cmdArgs) > 0 && command == "" {
switch shell {
case "cmd":
cmdArgs = append([]string{"/C"}, cmdArgs...)
cmd = exec.Command("cmd.exe", cmdArgs...)
case "powershell":
cmdArgs = append([]string{"-NonInteractive", "-NoProfile"}, cmdArgs...)
cmd = exec.Command("powershell.exe", cmdArgs...)
}
} else {
switch shell {
case "cmd":
cmd = exec.Command("cmd.exe")
cmd.SysProcAttr = &windows.SysProcAttr{
CmdLine: fmt.Sprintf("cmd.exe /C %s", command),
}
case "powershell":
cmd = exec.Command("Powershell", "-NonInteractive", "-NoProfile", command)
}
}
// https://docs.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
if detached {
cmd.SysProcAttr = &windows.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP,
}
}
cmd.Stdout = &outb
cmd.Stderr = &errb
cmd.Start()
pid := int32(cmd.Process.Pid)
go func(p int32) {
<-ctx.Done()
_ = utils.KillProc(p)
timedOut = true
}(pid)
err := cmd.Wait()
if timedOut {
return [2]string{
utils.CleanString(outb.String()),
utils.CleanString(errb.String())},
ctx.Err()
}
if err != nil {
return [2]string{
utils.CleanString(outb.String()),
utils.CleanString(errb.String())},
err
}
return [2]string{
utils.CleanString(outb.String()),
utils.CleanString(errb.String())},
nil
}
func GetProgramDirectory() string {
pd := filepath.Join(os.Getenv("ProgramFiles"), ProgFilesName)
return pd
}
func GetProgramEXE() string {
exe := filepath.Join(GetProgramDirectory(), winExeName)
return exe
}
func GetPythonBin() string {
var pybin string
switch runtime.GOARCH {
case "amd64":
pybin = filepath.Join(GetProgramDirectory(), "py38-x64", "python.exe")
case "386":
pybin = filepath.Join(GetProgramDirectory(), "py38-x32", "python.exe")
}
return pybin
}
// LoggedOnUser returns the first logged on user it finds
func LoggedOnUser() string {
pyCode := `
import psutil
try:
u = psutil.users()[0].name
if u.isascii():
print(u, end='')
else:
print('notascii', end='')
except Exception as e:
print("None", end='')
`
// try with psutil first, if fails, fallback to golang
user, err := RunPythonCode(pyCode, 5, []string{})
if err == nil && user != "notascii" {
return user
}
users, err := wapf.ListLoggedInUsers()
if err != nil {
//a.Logger.Debugln("LoggedOnUser error", err)
return "None"
}
if len(users) == 0 {
return "None"
}
for _, u := range users {
// remove the computername or domain
return strings.Split(u.FullUser(), `\`)[1]
}
return "None"
}
func PlatVer() (string, error) {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.ALL_ACCESS)
if err != nil {
return "n/a", err
}
defer k.Close()
dv, _, err := k.GetStringValue("DisplayVersion")
if err == nil {
return dv, nil
}
relid, _, err := k.GetStringValue("ReleaseId")
if err != nil {
return "n/a", err
}
return relid, nil
}
// EnablePing enables ping
func EnablePing() {
args := make([]string, 0)
cmd := `netsh advfirewall firewall add rule name="ICMP Allow incoming V4 echo request" protocol=icmpv4:8,any dir=in action=allow`
_, err := CMDShell("cmd", args, cmd, 10, false)
if err != nil {
fmt.Println(err)
}
}
// EnableRDP enables Remote Desktop
func EnableRDP() {
k, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Terminal Server`, registry.ALL_ACCESS)
if err != nil {
fmt.Println(err)
}
defer k.Close()
err = k.SetDWordValue("fDenyTSConnections", 0)
if err != nil {
fmt.Println(err)
}
args := make([]string, 0)
cmd := `netsh advfirewall firewall set rule group="remote desktop" new enable=Yes`
_, cerr := CMDShell("cmd", args, cmd, 10, false)
if cerr != nil {
fmt.Println(cerr)
}
}
// DisableSleepHibernate disables sleep and hibernate
func DisableSleepHibernate() {
k, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Power`, registry.ALL_ACCESS)
if err != nil {
fmt.Println(err)
}
defer k.Close()
err = k.SetDWordValue("HiberbootEnabled", 0)
if err != nil {
fmt.Println(err)
}
args := make([]string, 0)
var wg sync.WaitGroup
currents := []string{"ac", "dc"}
for _, i := range currents {
wg.Add(1)
go func(c string) {
defer wg.Done()
_, _ = CMDShell("cmd", args, fmt.Sprintf("powercfg /set%svalueindex scheme_current sub_buttons lidaction 0", c), 5, false)
_, _ = CMDShell("cmd", args, fmt.Sprintf("powercfg /x -standby-timeout-%s 0", c), 5, false)
_, _ = CMDShell("cmd", args, fmt.Sprintf("powercfg /x -hibernate-timeout-%s 0", c), 5, false)
_, _ = CMDShell("cmd", args, fmt.Sprintf("powercfg /x -disk-timeout-%s 0", c), 5, false)
_, _ = CMDShell("cmd", args, fmt.Sprintf("powercfg /x -monitor-timeout-%s 0", c), 5, false)
}(i)
}
wg.Wait()
_, _ = CMDShell("cmd", args, "powercfg -S SCHEME_CURRENT", 5, false)
}
// NewCOMObject creates a new COM object for the specifed ProgramID.
func NewCOMObject(id string) (*ole.IDispatch, error) {
unknown, err := oleutil.CreateObject(id)
if err != nil {
return nil, fmt.Errorf("unable to create initial unknown object: %v", err)
}
defer unknown.Release()
obj, err := unknown.QueryInterface(ole.IID_IDispatch)
if err != nil {
return nil, fmt.Errorf("unable to create query interface: %v", err)
}
return obj, nil
}
// SystemRebootRequired checks whether a system reboot is required.
func SystemRebootRequired() (bool, error) {
regKeys := []string{
`SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired`,
}
for _, key := range regKeys {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, key, registry.QUERY_VALUE)
if err == nil {
k.Close()
return true, nil
} else if err != registry.ErrNotExist {
return false, err
}
}
return false, nil
}
func KillHungUpdates() {
procs, err := ps.Processes()
if err != nil {
return
}
for _, process := range procs {
p, err := process.Info()
if err != nil {
continue
}
if strings.Contains(p.Exe, "winagent-v") {
//a.Logger.Debugln("killing process", p.Exe)
utils.KillProc(int32(p.PID))
}
}
}
func OsString() string {
host, _ := ps.Host()
info := host.Info()
osInf := info.OS
var arch string
switch info.Architecture {
case "x86_64":
arch = "64 bit"
case "x86":
arch = "32 bit"
}
var osFullName string
platver, err := PlatVer()
if err != nil {
osFullName = fmt.Sprintf("%s, %s (build %s)", osInf.Name, arch, osInf.Build)
} else {
osFullName = fmt.Sprintf("%s, %s v%s (build %s)", osInf.Name, arch, platver, osInf.Build)
}
return osFullName
}
func AddDefenderExlusions() error {
code := `
Add-MpPreference -ExclusionPath 'C:\Program Files\TacticalAgent\*'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\winagent-v*.exe'
Add-MpPreference -ExclusionPath 'C:\Windows\Temp\trmm\*'
Add-MpPreference -ExclusionPath 'C:\Program Files\Mesh Agent\*'
`
_, _, _, err := RunScript(code, "powershell", []string{}, 20)
if err != nil {
return err
}
return nil
}