Skip to content

Commit

Permalink
Server artifact runner now respects timeout. (Velocidex#1020)
Browse files Browse the repository at this point in the history
  • Loading branch information
scudette authored Apr 16, 2021
1 parent ce78aec commit 49fd0b7
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 63 deletions.
126 changes: 64 additions & 62 deletions artifacts/definitions/Windows/Detection/AMCache.yaml
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
name: Windows.Detection.Amcache
author: Matt Green - @mgreen27
description: |
This artifact collects AMCache entries with a SHA1 hash to enable threat
detection.
AmCache is an artifact which stores metadata related to PE execution and
program installation on Windows 7 and Server 2008 R2 and above. This artifact
includes EntryName, EntryPath and SHA1 as great data points for IOC collection.
Secondary datapoints include publisher/company, BinaryType and OriginalFileName.
Available filters include:
- SHA1regex - regex entries to filter by SHA1.
- PathRegex - filter on path if available.
This artifact collects AMCache entries with a SHA1 hash to enable threat
detection.
AmCache is an artifact which stores metadata related to PE execution and
program installation on Windows 7 and Server 2008 R2 and above. This artifact
includes EntryName, EntryPath and SHA1 as great data points for IOC collection.
Secondary datapoints include publisher/company, BinaryType and OriginalFileName.
Available filters include:
- SHA1regex - regex entries to filter by SHA1.
- PathRegex - filter on path if available.
- NameRegex - filter on EntryName / binary.
NOTE:
- Secondary fields are not consistent across AMCache types and some legacy
versions do not return these fields.
- Some enrichment has occured but any secondary fields should be treated as
guidance only.
- This artifact collects only entries with a SHA1, for complete AMCache
NOTE:
- Secondary fields are not consistent across AMCache types and some legacy
versions do not return these fields.
- Some enrichment has occured but any secondary fields should be treated as
guidance only.
- This artifact collects only entries with a SHA1, for complete AMCache
analysis please download raw artifact sets.
reference:
- https://www.ssi.gouv.fr/uploads/2019/01/anssi-coriin_2019-analysis_amcache.pdf

parameters:
- name: AMCacheGlob
default: "%SYSTEMROOT%/appcompat/Programs/Amcache.hve"
Expand All @@ -42,57 +42,63 @@ parameters:
description: Regex of recorded path.
- name: NameRegex
description: Regex of entry / binary name


sources:
- queries:
- |
LET files <= SELECT FullPath FROM glob(globs=expand(path=AMCacheGlob))
SELECT * FROM foreach(row=files,
SELECT * FROM foreach(row=files,
query={
SELECT
url(parse=Key.FullPath).Path As HivePath,
url(parse=Key.FullPath).Fragment as EntryKey,
Key.ModTime as KeyMTime,
parse_string_with_regex(string=Key.FullPath,
regex='%5CRoot%5C(\\w+)%5C').g1 as EntryType,
if(condition= FileId,
then = strip(string=FileId,prefix='0000'),
else= if(condition= `101`,
then= strip(string=`101`,prefix='0000'),
else= if(condition= DriverId,
then= strip(string=DriverId,prefix='0000')))) as SHA1,
if(condition=Name,
then=Name,
else= if(condition=FriendlyName,
then=FriendlyName,
else=if(condition=`15`,
then=split(string=str(str=`15`), sep='\\\\')[-1],
else= if(condition=DriverName,
then=DriverName)))) as EntryName,
if(condition=LowerCaseLongPath,
if(condition=get(member="FileId"),
then=strip(string=FileId, prefix='0000'),
else=if(condition=get(member="101"),
then=strip(string=`101`, prefix='0000'),
else=if(condition=get(member="DriverId"),
then=strip(string=DriverId, prefix='0000')))) as SHA1,
if(condition=get(member="Name"),
then=Name,
else=if(condition=get(member="FriendlyName"),
then=FriendlyName,
else=if(condition=get(member="15"),
then=split(string=str(str=`15`), sep='\\\\')[-1],
else=if(condition=get(member="DriverName"),
then=DriverName)))) as EntryName,
if(condition=get(member="LowerCaseLongPath"),
then=LowerCaseLongPath,
else= if(condition=`15`,
then=`15`,
else=if(condition=AddinCLSID,
then=AddinCLSID))) as EntryPath,
if(condition=Publisher,
else=if(condition=get(member="15"),
then=`15`,
else=if(condition=get(member="AddinCLSID"),
then=AddinCLSID))) as EntryPath,
if(condition=get(member="Publisher"),
then=Publisher,
else=if(condition=Provider,
then=Provider,
else= if(condition=DriverCompany,
then=DriverCompany))) as Publisher,
OriginalFileName,
if(condition =BinaryType,
then= BinaryType,
else=if(condition=AddInType,
then=AddinType + ' ' + OfficeArchitecture,
else= if(condition=Key.FullPath=~ 'InventoryDevicePnp',
then='DevicePnp',
else=if(condition=Key.FullPath=~'InventoryDriverBinary',
then='DriverBinary')))) as BinaryType
else=if(condition=get(member="Provider"),
then=Provider,
else=if(condition=get(member="DriverCompany"),
then=DriverCompany))) as Publisher,
get(member="OriginalFileName") AS OriginalFileName,
if(condition=get(member="BinaryType"),
then=BinaryType,
else=if(condition=get(member="AddInType"),
then=AddinType + ' ' + OfficeArchitecture,
else=if(condition=Key.FullPath =~ 'InventoryDevicePnp',
then='DevicePnp',
else=if(condition=Key.FullPath =~ 'InventoryDriverBinary',
then='DriverBinary')))) as BinaryType
FROM read_reg_key(globs=url(scheme='file', path=FullPath,
fragment=KeyPathGlob).String,accessor='raw_reg')
WHERE SHA1
Expand All @@ -104,7 +110,3 @@ sources:
then= EntryPath =~ PathRegex,
else= True)
})




7 changes: 6 additions & 1 deletion services/server_artifacts/server_artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,13 @@ func (self *ServerArtifactsRunner) runQuery(
return errors.New("Query should be specified")
}

timeout := time.Duration(arg.Timeout) * time.Second
if timeout == 0 {
timeout = self.timeout
}

// Cancel the query after this deadline
deadline := time.After(self.timeout)
deadline := time.After(timeout)
collection_context.Modify(
func(context *flows_proto.ArtifactCollectorContext) {
context.StartTime = uint64(time.Now().UnixNano() / 1000)
Expand Down
202 changes: 202 additions & 0 deletions services/server_artifacts/server_artifacts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package server_artifacts

import (
"context"
"testing"
"time"

"github.com/alecthomas/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"www.velocidex.com/golang/velociraptor/acls"
api_proto "www.velocidex.com/golang/velociraptor/api/proto"
"www.velocidex.com/golang/velociraptor/config"
config_proto "www.velocidex.com/golang/velociraptor/config/proto"
"www.velocidex.com/golang/velociraptor/file_store/test_utils"
"www.velocidex.com/golang/velociraptor/flows"
flows_proto "www.velocidex.com/golang/velociraptor/flows/proto"
"www.velocidex.com/golang/velociraptor/paths"
"www.velocidex.com/golang/velociraptor/services"
"www.velocidex.com/golang/velociraptor/services/journal"
"www.velocidex.com/golang/velociraptor/services/launcher"
"www.velocidex.com/golang/velociraptor/services/notifications"
"www.velocidex.com/golang/velociraptor/services/repository"
vql_subsystem "www.velocidex.com/golang/velociraptor/vql"
"www.velocidex.com/golang/velociraptor/vtesting"

_ "www.velocidex.com/golang/velociraptor/vql/functions"
)

type ServerArtifactsTestSuite struct {
suite.Suite
config_obj *config_proto.Config
sm *services.Service
ctx context.Context
}

func (self *ServerArtifactsTestSuite) SetupTest() {
var err error
self.config_obj, err = new(config.Loader).WithFileLoader(
"../../http_comms/test_data/server.config.yaml").
WithRequiredFrontend().
WithWriteback().
WithVerbose(true).
LoadAndValidate()
require.NoError(self.T(), err)

// Start essential services.
self.ctx, _ = context.WithTimeout(context.Background(), time.Second*60)
self.sm = services.NewServiceManager(self.ctx, self.config_obj)

t := self.T()
assert.NoError(t, self.sm.Start(journal.StartJournalService))
assert.NoError(t, self.sm.Start(notifications.StartNotificationService))
assert.NoError(t, self.sm.Start(launcher.StartLauncherService))
assert.NoError(t, self.sm.Start(repository.StartRepositoryManager))
assert.NoError(t, self.sm.Start(StartServerArtifactService))

// Create an administrator user
err = acls.GrantRoles(self.config_obj, "admin",
[]string{"administrator"})
assert.NoError(self.T(), err)
}

func (self *ServerArtifactsTestSuite) TearDownTest() {
self.sm.Close()
test_utils.GetMemoryFileStore(self.T(), self.config_obj).Clear()
test_utils.GetMemoryDataStore(self.T(), self.config_obj).Clear()
}

func (self *ServerArtifactsTestSuite) LoadArtifacts(definition string) services.Repository {
manager, _ := services.GetRepositoryManager()
repository, _ := manager.GetGlobalRepository(self.config_obj)

_, err := repository.LoadYaml(definition, false)
assert.NoError(self.T(), err)

return repository
}

func (self *ServerArtifactsTestSuite) ScheduleAndWait(
name, user string) *api_proto.FlowDetails {

manager, _ := services.GetRepositoryManager()
repository, _ := manager.GetGlobalRepository(self.config_obj)

launcher, err := services.GetLauncher()
assert.NoError(self.T(), err)

acl_manager := vql_subsystem.NewServerACLManager(self.config_obj, user)

// Schedule a job for the server runner.
flow_id, err := launcher.ScheduleArtifactCollection(
self.ctx, self.config_obj, acl_manager,
repository, &flows_proto.ArtifactCollectorArgs{
Creator: user,
ClientId: "server",
Artifacts: []string{name},
})
assert.NoError(self.T(), err)

// Notify it about the new job
notifier := services.GetNotifier()
err = notifier.NotifyListener(self.config_obj, "server")
assert.NoError(self.T(), err)

// Wait for the collection to complete
var details *api_proto.FlowDetails
vtesting.WaitUntil(time.Second*5, self.T(), func() bool {
details, err = flows.GetFlowDetails(self.config_obj,
"server", flow_id)
assert.NoError(self.T(), err)

return details.Context.State == flows_proto.ArtifactCollectorContext_FINISHED
})

return details
}

func (self *ServerArtifactsTestSuite) TestServerArtifacts() {
self.LoadArtifacts(`
name: Test1
type: SERVER
sources:
- query: SELECT "Foo" FROM scope()
`)
details := self.ScheduleAndWait("Test1", "admin")

// One row is collected
assert.Equal(self.T(), uint64(1), details.Context.TotalCollectedRows)

// How long we took to run - should be immediate
run_time := (details.Context.ActiveTime - details.Context.StartTime) / 1000000
assert.True(self.T(), run_time < 1)
}

// Collect a long lived artifact with specified timeout.
func (self *ServerArtifactsTestSuite) TestServerArtifactsTimeout() {
self.LoadArtifacts(`
name: Test2
type: SERVER
resources:
timeout: 1
sources:
- query: SELECT sleep(time=200) FROM scope()
`)

details := self.ScheduleAndWait("Test2", "admin")

// No rows are collected because the query timed out.
assert.Equal(self.T(), uint64(0), details.Context.TotalCollectedRows)

// How long we took to run - should be around 1 second
run_time := (details.Context.ActiveTime - details.Context.StartTime) / 1000000
assert.True(self.T(), run_time < 3)
assert.True(self.T(), run_time >= 1)

flow_path_manager := paths.NewFlowPathManager(
"server", details.Context.SessionId)
log_data := test_utils.FileReadAll(self.T(), self.config_obj,
flow_path_manager.Log().Path())
assert.Contains(self.T(), log_data, "Query timed out after 1 seconds")
}

// The server artifact runner impersonates the flow creator for ACL
// checks - this makes it safe for low privilege users to run some
// server artifacts that accommodate their access levels, but stops
// them from escalating to higher permissions.
func (self *ServerArtifactsTestSuite) TestServerArtifactsACLs() {

// The info plugin requires MACHINE_STATE permission
self.LoadArtifacts(`
name: Test
type: SERVER
sources:
- query: SELECT * FROM info()
`)

details := self.ScheduleAndWait("Test", "admin")

// Admin user should be able to collect since it has EXECVE
assert.Equal(self.T(), uint64(1), details.Context.TotalCollectedRows)

// Create a reader user called gumby - reader role lacks the
// MACHINE_STATE permission.
err := acls.GrantRoles(self.config_obj, "gumby", []string{"reader"})
assert.NoError(self.T(), err)

details = self.ScheduleAndWait("Test", "gumby")

// Gumby user has no permissions to run the artifact.
assert.Equal(self.T(), uint64(0), details.Context.TotalCollectedRows)

flow_path_manager := paths.NewFlowPathManager(
"server", details.Context.SessionId)
log_data := test_utils.FileReadAll(self.T(), self.config_obj,
flow_path_manager.Log().Path())
assert.Contains(self.T(), log_data, "Permission denied: [MACHINE_STATE]")
}

func TestServerArtifacts(t *testing.T) {
suite.Run(t, &ServerArtifactsTestSuite{})
}
Loading

0 comments on commit 49fd0b7

Please sign in to comment.