2024-02-20 13:25:31 +00:00
|
|
|
package artifact
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
2024-02-21 13:49:38 +00:00
|
|
|
"path/filepath"
|
2024-02-20 13:25:31 +00:00
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/naoina/toml"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
|
|
|
|
"code.dumpstack.io/tools/out-of-tree/config/dotfiles"
|
|
|
|
"code.dumpstack.io/tools/out-of-tree/distro"
|
|
|
|
"code.dumpstack.io/tools/out-of-tree/qemu"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Kernel struct {
|
|
|
|
// TODO
|
|
|
|
// Version string
|
|
|
|
// From string
|
|
|
|
// To string
|
|
|
|
|
|
|
|
// prev. ReleaseMask
|
|
|
|
Regex string
|
|
|
|
ExcludeRegex string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Target defines the kernel
|
|
|
|
type Target struct {
|
|
|
|
Distro distro.Distro
|
|
|
|
|
|
|
|
Kernel Kernel
|
|
|
|
}
|
|
|
|
|
|
|
|
// DockerName is returns stable name for docker container
|
|
|
|
func (km Target) DockerName() string {
|
|
|
|
distro := strings.ToLower(km.Distro.ID.String())
|
|
|
|
release := strings.Replace(km.Distro.Release, ".", "__", -1)
|
|
|
|
return fmt.Sprintf("out_of_tree_%s_%s", distro, release)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ArtifactType is the kernel module or exploit
|
|
|
|
type ArtifactType int
|
|
|
|
|
|
|
|
const (
|
|
|
|
// KernelModule is any kind of kernel module
|
|
|
|
KernelModule ArtifactType = iota
|
|
|
|
// KernelExploit is the privilege escalation exploit
|
|
|
|
KernelExploit
|
|
|
|
// Script for information gathering or automation
|
|
|
|
Script
|
|
|
|
)
|
|
|
|
|
|
|
|
func (at ArtifactType) String() string {
|
|
|
|
return [...]string{"module", "exploit", "script"}[at]
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalTOML is for support github.com/naoina/toml
|
|
|
|
func (at *ArtifactType) UnmarshalTOML(data []byte) (err error) {
|
|
|
|
stype := strings.Trim(string(data), `"`)
|
|
|
|
stypelower := strings.ToLower(stype)
|
|
|
|
if strings.Contains(stypelower, "module") {
|
|
|
|
*at = KernelModule
|
|
|
|
} else if strings.Contains(stypelower, "exploit") {
|
|
|
|
*at = KernelExploit
|
|
|
|
} else if strings.Contains(stypelower, "script") {
|
|
|
|
*at = Script
|
|
|
|
} else {
|
|
|
|
err = fmt.Errorf("type %s is unsupported", stype)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarshalTOML is for support github.com/naoina/toml
|
|
|
|
func (at ArtifactType) MarshalTOML() (data []byte, err error) {
|
|
|
|
s := ""
|
|
|
|
switch at {
|
|
|
|
case KernelModule:
|
|
|
|
s = "module"
|
|
|
|
case KernelExploit:
|
|
|
|
s = "exploit"
|
|
|
|
case Script:
|
|
|
|
s = "script"
|
|
|
|
default:
|
|
|
|
err = fmt.Errorf("cannot marshal %d", at)
|
|
|
|
}
|
|
|
|
data = []byte(`"` + s + `"`)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Duration type with toml unmarshalling support
|
|
|
|
type Duration struct {
|
|
|
|
time.Duration
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalTOML for Duration
|
|
|
|
func (d *Duration) UnmarshalTOML(data []byte) (err error) {
|
|
|
|
duration := strings.Replace(string(data), "\"", "", -1)
|
|
|
|
d.Duration, err = time.ParseDuration(duration)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarshalTOML for Duration
|
|
|
|
func (d Duration) MarshalTOML() (data []byte, err error) {
|
|
|
|
data = []byte(`"` + d.Duration.String() + `"`)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
type PreloadModule struct {
|
|
|
|
Repo string
|
|
|
|
Path string
|
|
|
|
TimeoutAfterLoad Duration
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extra test files to copy over
|
|
|
|
type FileTransfer struct {
|
|
|
|
User string
|
|
|
|
Local string
|
|
|
|
Remote string
|
|
|
|
}
|
|
|
|
|
|
|
|
type Patch struct {
|
|
|
|
Path string
|
|
|
|
Source string
|
|
|
|
Script string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Artifact is for .out-of-tree.toml
|
|
|
|
type Artifact struct {
|
2024-04-22 15:58:35 +00:00
|
|
|
Name string
|
|
|
|
Type ArtifactType
|
|
|
|
|
|
|
|
SourcePath string
|
|
|
|
SourceFiles []string
|
|
|
|
|
|
|
|
TestFiles []FileTransfer
|
|
|
|
|
|
|
|
Targets []Target
|
2024-02-20 13:25:31 +00:00
|
|
|
|
|
|
|
Script string
|
|
|
|
|
|
|
|
Qemu struct {
|
|
|
|
Cpus int
|
|
|
|
Memory int
|
|
|
|
Timeout Duration
|
|
|
|
AfterStartTimeout Duration
|
|
|
|
}
|
|
|
|
|
|
|
|
Docker struct {
|
|
|
|
Timeout Duration
|
|
|
|
}
|
|
|
|
|
|
|
|
Mitigations struct {
|
|
|
|
DisableSmep bool
|
|
|
|
DisableSmap bool
|
|
|
|
DisableKaslr bool
|
|
|
|
DisableKpti bool
|
|
|
|
}
|
|
|
|
|
|
|
|
Patches []Patch
|
|
|
|
|
|
|
|
Make struct {
|
|
|
|
Target string
|
|
|
|
}
|
|
|
|
|
|
|
|
StandardModules bool
|
|
|
|
|
|
|
|
Preload []PreloadModule
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read is for read .out-of-tree.toml
|
|
|
|
func (Artifact) Read(path string) (ka Artifact, err error) {
|
|
|
|
f, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
buf, err := io.ReadAll(f)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = toml.Unmarshal(buf, &ka)
|
|
|
|
|
|
|
|
if len(strings.Fields(ka.Name)) != 1 {
|
|
|
|
err = errors.New("artifact name should not contain spaces")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ka Artifact) checkSupport(ki distro.KernelInfo, target Target) (
|
|
|
|
supported bool, err error) {
|
|
|
|
|
|
|
|
if target.Distro.Release == "" {
|
|
|
|
if ki.Distro.ID != target.Distro.ID {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if !ki.Distro.Equal(target.Distro) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
r, err := regexp.Compile(target.Kernel.Regex)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
exr, err := regexp.Compile(target.Kernel.ExcludeRegex)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !r.MatchString(ki.KernelRelease) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if target.Kernel.ExcludeRegex != "" && exr.MatchString(ki.KernelRelease) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
supported = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Supported returns true if given kernel is supported by artifact
|
|
|
|
func (ka Artifact) Supported(ki distro.KernelInfo) (supported bool, err error) {
|
|
|
|
for _, km := range ka.Targets {
|
|
|
|
supported, err = ka.checkSupport(ki, km)
|
|
|
|
if supported {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-10-07 22:06:04 +00:00
|
|
|
// TODO too many parameters
|
2024-02-20 13:25:31 +00:00
|
|
|
func (ka Artifact) Process(slog zerolog.Logger, ki distro.KernelInfo,
|
2024-10-07 22:06:04 +00:00
|
|
|
outputOnSuccess, realtimeOutput, endless bool, cBinary,
|
2024-02-20 13:25:31 +00:00
|
|
|
cEndlessStress string, cEndlessTimeout time.Duration,
|
|
|
|
dump func(q *qemu.System, ka Artifact, ki distro.KernelInfo,
|
|
|
|
result *Result)) {
|
|
|
|
|
|
|
|
slog.Info().Msg("start")
|
|
|
|
testStart := time.Now()
|
|
|
|
defer func() {
|
|
|
|
slog.Debug().Str("test_duration",
|
|
|
|
time.Since(testStart).String()).
|
|
|
|
Msg("")
|
|
|
|
}()
|
|
|
|
|
|
|
|
kernel := qemu.Kernel{KernelPath: ki.KernelPath, InitrdPath: ki.InitrdPath}
|
|
|
|
q, err := qemu.NewSystem(qemu.X86x64, kernel, ki.RootFS)
|
|
|
|
if err != nil {
|
|
|
|
slog.Error().Err(err).Msg("qemu init")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
q.Log = slog
|
|
|
|
|
|
|
|
if ka.Qemu.Timeout.Duration != 0 {
|
|
|
|
q.Timeout = ka.Qemu.Timeout.Duration
|
|
|
|
}
|
|
|
|
if ka.Qemu.Cpus != 0 {
|
|
|
|
q.Cpus = ka.Qemu.Cpus
|
|
|
|
}
|
|
|
|
if ka.Qemu.Memory != 0 {
|
|
|
|
q.Memory = ka.Qemu.Memory
|
|
|
|
}
|
|
|
|
|
|
|
|
q.SetKASLR(!ka.Mitigations.DisableKaslr)
|
|
|
|
q.SetSMEP(!ka.Mitigations.DisableSmep)
|
|
|
|
q.SetSMAP(!ka.Mitigations.DisableSmap)
|
|
|
|
q.SetKPTI(!ka.Mitigations.DisableKpti)
|
|
|
|
|
|
|
|
if ki.CPU.Model != "" {
|
|
|
|
q.CPU.Model = ki.CPU.Model
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(ki.CPU.Flags) != 0 {
|
|
|
|
q.CPU.Flags = ki.CPU.Flags
|
|
|
|
}
|
|
|
|
|
|
|
|
if endless {
|
|
|
|
q.Timeout = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
qemuStart := time.Now()
|
|
|
|
|
|
|
|
slog.Debug().Msgf("qemu start %v", qemuStart)
|
|
|
|
err = q.Start()
|
|
|
|
if err != nil {
|
|
|
|
slog.Error().Err(err).Msg("qemu start")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer q.Stop()
|
|
|
|
|
|
|
|
slog.Debug().Msgf("wait %v", ka.Qemu.AfterStartTimeout)
|
|
|
|
time.Sleep(ka.Qemu.AfterStartTimeout.Duration)
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
time.Sleep(time.Minute)
|
|
|
|
for !q.Died {
|
|
|
|
slog.Debug().Msg("still alive")
|
|
|
|
time.Sleep(time.Minute)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
tmp, err := os.MkdirTemp(dotfiles.Dir("tmp"), "")
|
|
|
|
if err != nil {
|
|
|
|
slog.Error().Err(err).Msg("making tmp directory")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer os.RemoveAll(tmp)
|
|
|
|
|
|
|
|
result := Result{}
|
|
|
|
if !endless {
|
|
|
|
defer dump(q, ka, ki, &result)
|
|
|
|
}
|
|
|
|
|
|
|
|
var cTest string
|
|
|
|
|
|
|
|
if ka.Type == Script {
|
2024-02-21 13:42:08 +00:00
|
|
|
result.BuildDir = ka.SourcePath
|
2024-02-20 13:25:31 +00:00
|
|
|
result.Build.Ok = true
|
2024-02-21 13:49:38 +00:00
|
|
|
ka.Script = filepath.Join(ka.SourcePath, ka.Script)
|
2024-02-20 13:25:31 +00:00
|
|
|
cTest = ka.Script
|
|
|
|
} else if cBinary == "" {
|
|
|
|
// TODO: build should return structure
|
|
|
|
start := time.Now()
|
|
|
|
result.BuildDir, result.BuildArtifact, result.Build.Output, err =
|
|
|
|
Build(slog, tmp, ka, ki, ka.Docker.Timeout.Duration)
|
|
|
|
slog.Debug().Str("duration", time.Since(start).String()).
|
|
|
|
Msg("build done")
|
|
|
|
if err != nil {
|
2024-10-07 20:40:58 +00:00
|
|
|
slog.Error().Err(err).Msgf("build failure\n%v\n", result.Build.Output)
|
2024-02-20 13:25:31 +00:00
|
|
|
return
|
2024-10-07 16:47:39 +00:00
|
|
|
} else {
|
2024-10-07 22:06:04 +00:00
|
|
|
if outputOnSuccess && !realtimeOutput {
|
2024-10-07 20:59:16 +00:00
|
|
|
slog.Info().Msgf("build success\n%v\n", result.Build.Output)
|
|
|
|
} else {
|
|
|
|
slog.Info().Msg("build success")
|
|
|
|
}
|
2024-02-20 13:25:31 +00:00
|
|
|
}
|
|
|
|
result.Build.Ok = true
|
|
|
|
} else {
|
|
|
|
result.BuildArtifact = cBinary
|
|
|
|
result.Build.Ok = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if cTest == "" {
|
|
|
|
cTest = result.BuildArtifact + "_test"
|
|
|
|
if _, err := os.Stat(cTest); err != nil {
|
|
|
|
slog.Debug().Msgf("%s does not exist", cTest)
|
|
|
|
cTest = tmp + "/source/" + "test.sh"
|
|
|
|
} else {
|
|
|
|
slog.Debug().Msgf("%s exist", cTest)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ka.Qemu.Timeout.Duration == 0 {
|
|
|
|
ka.Qemu.Timeout.Duration = time.Minute
|
|
|
|
}
|
|
|
|
|
|
|
|
err = q.WaitForSSH(ka.Qemu.Timeout.Duration)
|
|
|
|
if err != nil {
|
|
|
|
result.InternalError = err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
slog.Debug().Str("qemu_startup_duration",
|
|
|
|
time.Since(qemuStart).String()).
|
|
|
|
Msg("ssh is available")
|
|
|
|
|
|
|
|
remoteTest, err := copyTest(q, cTest, ka)
|
|
|
|
if err != nil {
|
|
|
|
result.InternalError = err
|
|
|
|
slog.Error().Err(err).Msg("copy test script")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if ka.StandardModules {
|
|
|
|
// Module depends on one of the standard modules
|
|
|
|
start := time.Now()
|
|
|
|
err = CopyStandardModules(q, ki)
|
|
|
|
if err != nil {
|
|
|
|
result.InternalError = err
|
|
|
|
slog.Error().Err(err).Msg("copy standard modules")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
slog.Debug().Str("duration", time.Since(start).String()).
|
|
|
|
Msg("copy standard modules")
|
|
|
|
}
|
|
|
|
|
|
|
|
err = PreloadModules(q, ka, ki, ka.Docker.Timeout.Duration)
|
|
|
|
if err != nil {
|
|
|
|
result.InternalError = err
|
|
|
|
slog.Error().Err(err).Msg("preload modules")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-10-07 20:40:58 +00:00
|
|
|
var qemuTestOutput string
|
2024-10-07 22:06:04 +00:00
|
|
|
q.SetQemuOutputHandler(func(s string) {
|
|
|
|
if realtimeOutput {
|
|
|
|
fmt.Printf("kmsg: %s\n", s)
|
|
|
|
} else {
|
|
|
|
qemuTestOutput += s + "\n"
|
|
|
|
}
|
2024-10-07 20:40:58 +00:00
|
|
|
})
|
|
|
|
|
2024-10-07 22:06:04 +00:00
|
|
|
if realtimeOutput {
|
|
|
|
q.SetCommandsOutputHandler(func(s string) {
|
|
|
|
fmt.Printf("test: %s\n", s)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-02-20 13:25:31 +00:00
|
|
|
start := time.Now()
|
2024-10-07 22:06:04 +00:00
|
|
|
copyArtifactAndTest(slog, q, ka, &result, remoteTest, outputOnSuccess, realtimeOutput)
|
2024-02-20 13:25:31 +00:00
|
|
|
slog.Debug().Str("duration", time.Since(start).String()).
|
|
|
|
Msgf("test completed (success: %v)", result.Test.Ok)
|
|
|
|
|
2024-10-07 20:40:58 +00:00
|
|
|
if result.Build.Ok {
|
|
|
|
if !result.Run.Ok || !result.Test.Ok {
|
|
|
|
slog.Error().Msgf("qemu output\n%v\n", qemuTestOutput)
|
2024-10-07 22:06:04 +00:00
|
|
|
} else if outputOnSuccess && !realtimeOutput {
|
2024-10-07 20:40:58 +00:00
|
|
|
slog.Info().Msgf("qemu output\n%v\n", qemuTestOutput)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-07 22:06:04 +00:00
|
|
|
if realtimeOutput {
|
|
|
|
q.CloseCommandsOutputHandler()
|
|
|
|
}
|
|
|
|
q.CloseQemuOutputHandler()
|
2024-10-07 20:40:58 +00:00
|
|
|
|
2024-02-20 13:25:31 +00:00
|
|
|
if !endless {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
dump(q, ka, ki, &result)
|
|
|
|
|
|
|
|
if !result.Build.Ok || !result.Run.Ok || !result.Test.Ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
slog.Info().Msg("start endless tests")
|
|
|
|
|
|
|
|
if cEndlessStress != "" {
|
|
|
|
slog.Debug().Msg("copy and run endless stress script")
|
|
|
|
err = q.CopyAndRunAsync("root", cEndlessStress)
|
|
|
|
if err != nil {
|
|
|
|
q.Stop()
|
|
|
|
//f.Sync()
|
|
|
|
slog.Fatal().Err(err).Msg("cannot copy/run stress")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
output, err := q.Command("root", remoteTest)
|
|
|
|
if err != nil {
|
|
|
|
q.Stop()
|
|
|
|
//f.Sync()
|
|
|
|
slog.Fatal().Err(err).Msg(output)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
slog.Debug().Msg(output)
|
|
|
|
|
|
|
|
slog.Info().Msg("test success")
|
|
|
|
|
|
|
|
slog.Debug().Msgf("wait %v", cEndlessTimeout)
|
|
|
|
time.Sleep(cEndlessTimeout)
|
|
|
|
}
|
|
|
|
}
|