diff options
-rw-r--r-- | TODO.md | 4 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | pkg/goexec/io.go | 115 | ||||
-rw-r--r-- | pkg/goexec/scmr/change.go | 43 | ||||
-rw-r--r-- | pkg/goexec/scmr/scmr.go | 108 |
5 files changed, 151 insertions, 121 deletions
@@ -47,8 +47,8 @@ We wanted to make development of this project as transparent as possible, so we' - [X] (Fixed) Proxy - EPM doesn't use the proxy dialer - [X] (Fixed) Kerberos requests don't dial through proxy - [X] (Fixed) Panic when closing nil log file -- [ ] `scmr change` doesn't revert service cmdline -- [ ] Fix SCMR `change` method so that dependencies field isn't permanently overwritten +- [X] `scmr change` doesn't revert service cmdline +- [X] Fix SCMR `change` method so that dependencies field isn't permanently overwritten ## Lower Priority @@ -14,6 +14,7 @@ require ( github.com/spf13/pflag v1.0.6 golang.org/x/net v0.39.0 golang.org/x/term v0.31.0 + golang.org/x/text v0.24.0 ) require ( @@ -32,6 +33,5 @@ require ( github.com/oiweiwei/gokrb5.fork/v9 v9.0.2 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect software.sslmate.com/src/go-pkcs12 v0.5.0 // indirect ) diff --git a/pkg/goexec/io.go b/pkg/goexec/io.go index 1d4358f..c8ad417 100644 --- a/pkg/goexec/io.go +++ b/pkg/goexec/io.go @@ -1,98 +1,99 @@ package goexec import ( - "context" - "fmt" - "io" - "strings" + "context" + "fmt" + "io" + "strings" ) type OutputProvider interface { - GetOutput(ctx context.Context, writer io.Writer) (err error) - Clean(ctx context.Context) (err error) + GetOutput(ctx context.Context, writer io.Writer) (err error) + Clean(ctx context.Context) (err error) } type ExecutionIO struct { - Cleaner + Cleaner - Input *ExecutionInput - Output *ExecutionOutput + Input *ExecutionInput + Output *ExecutionOutput } type ExecutionOutput struct { - NoDelete bool - RemotePath string - Provider OutputProvider - Writer io.WriteCloser + NoDelete bool + RemotePath string + Provider OutputProvider + Writer io.WriteCloser } type ExecutionInput struct { - StageFile io.ReadCloser - Executable string - ExecutablePath string - Arguments string - Command string + StageFile io.ReadCloser + Executable string + ExecutablePath string + Arguments string + Command string } func (execIO *ExecutionIO) GetOutput(ctx context.Context) (err error) { - if execIO.Output.Provider != nil { - return execIO.Output.Provider.GetOutput(ctx, execIO.Output.Writer) - } - return nil + if execIO.Output.Provider != nil { + return execIO.Output.Provider.GetOutput(ctx, execIO.Output.Writer) + } + return nil } func (execIO *ExecutionIO) Clean(ctx context.Context) (err error) { - if execIO.Output.Provider != nil { - return execIO.Output.Provider.Clean(ctx) - } - return nil + if execIO.Output.Provider != nil { + return execIO.Output.Provider.Clean(ctx) + } + return nil } func (execIO *ExecutionIO) CommandLine() (cmd []string) { - if execIO.Output.Provider != nil && execIO.Output.RemotePath != "" { - return []string{ - `C:\Windows\System32\cmd.exe`, - fmt.Sprintf(`/C %s > %s 2>&1`, execIO.Input.String(), execIO.Output.RemotePath), - } - } - return execIO.Input.CommandLine() + if execIO.Output.Provider != nil && execIO.Output.RemotePath != "" { + return []string{ + `C:\Windows\System32\cmd.exe`, + fmt.Sprintf(`/C %s > %s 2>&1`, execIO.Input.String(), execIO.Output.RemotePath), + } + } + return execIO.Input.CommandLine() } -func (execIO *ExecutionIO) String() string { - cmd := execIO.CommandLine() - - // Ensure that executable paths are quoted - if strings.Contains(cmd[0], " ") { - return fmt.Sprintf(`%q %s`, cmd[0], strings.Join(cmd[1:], " ")) - } - return strings.Join(cmd, " ") +func (execIO *ExecutionIO) String() (str string) { + cmd := execIO.CommandLine() + // Ensure that executable paths are quoted + if strings.Contains(cmd[0], " ") { + str = fmt.Sprintf(`%q %s`, cmd[0], strings.Join(cmd[1:], " ")) + } else { + str = strings.Join(cmd, " ") + } + return strings.Trim(str, " \t\n\r") // trim whitespace } func (i *ExecutionInput) CommandLine() (cmd []string) { - cmd = make([]string, 2) - cmd[1] = i.Arguments + cmd = make([]string, 2) + cmd[1] = i.Arguments - switch { - case i.Command != "": - return strings.SplitN(i.Command, " ", 2) + switch { + case i.Command != "": + return strings.SplitN(i.Command, " ", 2) - case i.ExecutablePath != "": - cmd[0] = i.ExecutablePath + case i.ExecutablePath != "": + cmd[0] = i.ExecutablePath - case i.Executable != "": - cmd[0] = i.Executable - } + case i.Executable != "": + cmd[0] = i.Executable + } - return cmd + return cmd } func (i *ExecutionInput) String() string { - return strings.Join(i.CommandLine(), " ") + return strings.Join(i.CommandLine(), " ") } func (i *ExecutionInput) Reader() (reader io.Reader) { - if i.StageFile != nil { - return i.StageFile - } - return strings.NewReader(i.String()) + if i.StageFile != nil { + return i.StageFile + } + return strings.NewReader(i.String()) } diff --git a/pkg/goexec/scmr/change.go b/pkg/goexec/scmr/change.go index 3dd506e..eaf4c25 100644 --- a/pkg/goexec/scmr/change.go +++ b/pkg/goexec/scmr/change.go @@ -2,7 +2,6 @@ package scmrexec import ( "context" - "errors" "fmt" "github.com/FalconOpsLLC/goexec/pkg/goexec" "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" @@ -24,6 +23,7 @@ type ScmrChange struct { IO goexec.ExecutionIO NoStart bool + NoRevert bool ServiceName string } @@ -107,9 +107,11 @@ func (m *ScmrChange) Execute(ctx context.Context, in *goexec.ExecutionIO) (err e LoadOrderGroup: svc.originalConfig.LoadOrderGroup, ServiceStartName: svc.originalConfig.ServiceStartName, TagID: svc.originalConfig.TagID, - //Dependencies: []byte(svc.originalConfig.Dependencies), // TODO + Dependencies: parseDependencies(svc.originalConfig.Dependencies), } + bpn := svc.originalConfig.BinaryPathName + _, err = m.ctl.ChangeServiceConfigW(ctx, req) if err != nil { @@ -117,26 +119,35 @@ func (m *ScmrChange) Execute(ctx context.Context, in *goexec.ExecutionIO) (err e return fmt.Errorf("change service config request: %w", err) } - m.AddCleaners(func(ctxInner context.Context) error { - req.BinaryPathName = svc.originalConfig.BinaryPathName - - if ctxInner.Err() != nil && errors.Is(ctxInner.Err(), context.DeadlineExceeded) { - ctxInner = context.WithoutCancel(context.Background()) - } - _, err := m.ctl.ChangeServiceConfigW(ctxInner, req) - + if !m.NoStart { + err = m.startService(ctx, svc) if err != nil { - return fmt.Errorf("restore service config: %w", err) + log.Error().Err(err).Msg("Failed to start service") } - return nil - }) + } - if !m.NoStart { + if !m.NoRevert { + if svc.handle == nil { + + if err = m.Reconnect(ctx); err != nil { + return err + } + svc, err = m.openService(ctx, svc.name) + + if err != nil { + log.Error().Err(err).Msg("Failed to reopen service handle") + return fmt.Errorf("reopen service: %w", err) + } + } + req.BinaryPathName = bpn + req.Service = svc.handle + _, err := m.ctl.ChangeServiceConfigW(ctx, req) - err = m.startService(ctx, svc) if err != nil { - log.Error().Err(err).Msg("Failed to start service") + log.Error().Err(err).Msg("Failed to restore original service configuration") + return fmt.Errorf("restore service config: %w", err) } + log.Info().Msg("Restored original service configuration") } return diff --git a/pkg/goexec/scmr/scmr.go b/pkg/goexec/scmr/scmr.go index 696415b..3bf416c 100644 --- a/pkg/goexec/scmr/scmr.go +++ b/pkg/goexec/scmr/scmr.go @@ -1,55 +1,73 @@ package scmrexec import ( - "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" + "github.com/oiweiwei/go-msrpc/msrpc/scmr/svcctl/v2" + "golang.org/x/text/encoding/unicode" + "strings" ) const ( - ErrorServiceRequestTimeout uint32 = 0x0000041d - ErrorServiceNotActive uint32 = 0x00000426 - - ServiceDemandStart uint32 = 0x00000003 - ServiceWin32OwnProcess uint32 = 0x00000010 - - // https://learn.microsoft.com/en-us/windows/win32/services/service-security-and-access-rights - - ServiceQueryConfig uint32 = 0x00000001 - ServiceChangeConfig uint32 = 0x00000002 - ServiceStart uint32 = 0x00000010 - ServiceStop uint32 = 0x00000020 - ServiceDelete uint32 = 0x00010000 // special permission - ServiceControlStop uint32 = 0x00000001 - ScManagerCreateService uint32 = 0x00000002 - - /* - // Windows error codes - ERROR_FILE_NOT_FOUND uint32 = 0x00000002 - ERROR_SERVICE_DOES_NOT_EXIST uint32 = 0x00000424 - - // Windows service/scm constants - SERVICE_BOOT_START uint32 = 0x00000000 - SERVICE_SYSTEM_START uint32 = 0x00000001 - SERVICE_AUTO_START uint32 = 0x00000002 - SERVICE_DISABLED uint32 = 0x00000004 - - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-scmr/4e91ff36-ab5f-49ed-a43d-a308e72b0b3c - SERVICE_CONTINUE_PENDING uint32 = 0x00000005 - SERVICE_PAUSE_PENDING uint32 = 0x00000006 - SERVICE_PAUSED uint32 = 0x00000007 - SERVICE_RUNNING uint32 = 0x00000004 - SERVICE_START_PENDING uint32 = 0x00000002 - SERVICE_STOP_PENDING uint32 = 0x00000003 - SERVICE_STOPPED uint32 = 0x00000001 - */ - - ServiceDeleteAccess = ServiceDelete - ServiceModifyAccess = ServiceQueryConfig | ServiceChangeConfig | ServiceStop | ServiceStart | ServiceDelete - ServiceCreateAccess = ScManagerCreateService | ServiceStart | ServiceStop | ServiceDelete - ServiceAllAccess = ServiceCreateAccess | ServiceModifyAccess + ErrorServiceRequestTimeout uint32 = 0x0000041d + ErrorServiceNotActive uint32 = 0x00000426 + + ServiceDemandStart uint32 = 0x00000003 + ServiceWin32OwnProcess uint32 = 0x00000010 + + // https://learn.microsoft.com/en-us/windows/win32/services/service-security-and-access-rights + + ServiceQueryConfig uint32 = 0x00000001 + ServiceChangeConfig uint32 = 0x00000002 + ServiceStart uint32 = 0x00000010 + ServiceStop uint32 = 0x00000020 + ServiceDelete uint32 = 0x00010000 // special permission + ServiceControlStop uint32 = 0x00000001 + ScManagerCreateService uint32 = 0x00000002 + + /* + // Windows error codes + ERROR_FILE_NOT_FOUND uint32 = 0x00000002 + ERROR_SERVICE_DOES_NOT_EXIST uint32 = 0x00000424 + + // Windows service/scm constants + SERVICE_BOOT_START uint32 = 0x00000000 + SERVICE_SYSTEM_START uint32 = 0x00000001 + SERVICE_AUTO_START uint32 = 0x00000002 + SERVICE_DISABLED uint32 = 0x00000004 + + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-scmr/4e91ff36-ab5f-49ed-a43d-a308e72b0b3c + SERVICE_CONTINUE_PENDING uint32 = 0x00000005 + SERVICE_PAUSE_PENDING uint32 = 0x00000006 + SERVICE_PAUSED uint32 = 0x00000007 + SERVICE_RUNNING uint32 = 0x00000004 + SERVICE_START_PENDING uint32 = 0x00000002 + SERVICE_STOP_PENDING uint32 = 0x00000003 + SERVICE_STOPPED uint32 = 0x00000001 + */ + + ServiceDeleteAccess = ServiceDelete + ServiceModifyAccess = ServiceQueryConfig | ServiceChangeConfig | ServiceStop | ServiceStart | ServiceDelete + ServiceCreateAccess = ScManagerCreateService | ServiceStart | ServiceStop | ServiceDelete + ServiceAllAccess = ServiceCreateAccess | ServiceModifyAccess ) type service struct { - name string - handle *svcctl.Handle - originalConfig *svcctl.QueryServiceConfigW + name string + handle *svcctl.Handle + originalConfig *svcctl.QueryServiceConfigW +} + +// parseDependencies will parse the dependencies returned from a RQueryServiceConfigW +// response (svcctl.QueryServiceConfigWResponse) into a raw byte array compatible with +// the lpDependencies field as defined in the microsoft docs. +// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-scmr/3ab258d6-87b0-459e-8d83-a2cdd8038b78 +func parseDependencies(deps string) (out []byte) { + if deps != "" && deps != "/" { + + if out, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes( + []byte(strings.ReplaceAll(deps, "/", "\x00") + "\x00"), + ); err == nil { + return out + } + } + return nil } |