Skip to content

Commit 7db5adb

Browse files
committed
fix(callstack): Treat surrogate process callstack
Process creation can happen through brokered processes (Secondary Logon, App Info, etc). In this case the callstack is not reported by the parent process. So, we obtain the callstack from the surrogate process when the initial thread is created inside the remote process.
1 parent 7a6d0c1 commit 7db5adb

File tree

5 files changed

+191
-15
lines changed

5 files changed

+191
-15
lines changed

pkg/event/event_windows.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ package event
2121
import (
2222
"encoding/binary"
2323
"fmt"
24+
"os"
25+
"strings"
26+
"sync"
27+
"unsafe"
28+
2429
"github.com/rabbitstack/fibratus/pkg/event/params"
2530
"github.com/rabbitstack/fibratus/pkg/sys"
2631
"github.com/rabbitstack/fibratus/pkg/sys/etw"
@@ -29,10 +34,6 @@ import (
2934
"github.com/rabbitstack/fibratus/pkg/util/hostname"
3035
"github.com/rabbitstack/fibratus/pkg/util/ntstatus"
3136
"golang.org/x/sys/windows"
32-
"os"
33-
"strings"
34-
"sync"
35-
"unsafe"
3637
)
3738

3839
var (
@@ -266,6 +267,17 @@ func (e *Event) IsOpenDisposition() bool {
266267
return e.IsCreateFile() && e.Params.MustGetUint32(params.FileOperation) == windows.FILE_OPEN
267268
}
268269

270+
// IsCreateRemoteThread indicates if the remote thread creation occurred.
271+
func (e *Event) IsCreateRemoteThread() bool {
272+
return e.Type == CreateThread && e.PID != e.Params.MustGetPid()
273+
}
274+
275+
// IsSurrogateProcess indicates if the process creation event parent id
276+
// differs from the real process parent identifier.
277+
func (e *Event) IsSurrogateProcess() bool {
278+
return e.IsCreateProcess() && e.Params.MustGetUint32(params.ProcessParentID) != e.Params.MustGetUint32(params.ProcessRealParentID)
279+
}
280+
269281
// StackID returns the integer that is used to identify the callstack present in the StackWalk event.
270282
func (e *Event) StackID() uint64 { return uint64(e.PID + e.Tid) }
271283

pkg/event/stackwalk.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ package event
2020

2121
import (
2222
"expvar"
23+
"sync"
24+
"time"
25+
2326
"github.com/rabbitstack/fibratus/pkg/event/params"
2427
"github.com/rabbitstack/fibratus/pkg/util/multierror"
2528
log "github.com/sirupsen/logrus"
26-
"sync"
27-
"time"
2829
)
2930

3031
// maxQueueTTLPeriod specifies the maximum period
@@ -61,6 +62,8 @@ type StackwalkDecorator struct {
6162

6263
flusher *time.Ticker
6364
quit chan struct{}
65+
66+
procs map[uint32]*Event // stores CreateProcess events with surrogate parent
6467
}
6568

6669
// NewStackwalkDecorator creates a new callstack return
@@ -70,6 +73,7 @@ func NewStackwalkDecorator(q *Queue) *StackwalkDecorator {
7073
s := &StackwalkDecorator{
7174
q: q,
7275
buckets: make(map[uint64][]*Event),
76+
procs: make(map[uint32]*Event),
7377
flusher: time.NewTicker(flusherInterval),
7478
quit: make(chan struct{}, 1),
7579
}
@@ -84,6 +88,13 @@ func (s *StackwalkDecorator) Push(e *Event) {
8488
s.mux.Lock()
8589
defer s.mux.Unlock()
8690

91+
// the process is created on behalf of brokered
92+
// process and the callstack return addresses
93+
// need to be obtained from the surrogate process
94+
if e.IsSurrogateProcess() {
95+
s.procs[e.Params.MustGetPid()] = e
96+
}
97+
8798
// append the event to the bucket indexed by stack id
8899
id := e.StackID()
89100
q, ok := s.buckets[id]
@@ -121,9 +132,38 @@ func (s *StackwalkDecorator) Pop(e *Event) *Event {
121132
return e
122133
}
123134

135+
if evt.IsSurrogateProcess() && s.procs[evt.Params.MustGetPid()] != nil {
136+
delete(s.procs, evt.Params.MustGetPid())
137+
}
138+
124139
callstack := e.Params.MustGetSlice(params.Callstack)
125140
evt.AppendParam(params.Callstack, params.Slice, callstack)
126141

142+
// obtain the callstack from the CreateThread event
143+
// generated by the surrogate process, such as Seclogon.
144+
// If the remote process id is present in the procs map
145+
// the stack is attached to the cached event and then
146+
// pushed to the queue immediately
147+
if (evt.IsCreateRemoteThread() && evt.PS != nil) &&
148+
(evt.PS.IsSeclogonSvc() || evt.PS.IsAppinfoSvc()) {
149+
pid := evt.Params.MustGetPid()
150+
ev, ok := s.procs[pid]
151+
if ok {
152+
ev.AppendParam(params.Callstack, params.Slice, callstack)
153+
s.q.push(ev)
154+
delete(s.procs, pid)
155+
// find the most recent CreateProcess event and
156+
// remove it from buckets as we have the callstack
157+
qu := s.buckets[ev.StackID()]
158+
for i := len(qu) - 1; i >= 0; i-- {
159+
if !(qu[i].IsCreateProcess() && qu[i].Params.MustGetPid() == pid) {
160+
continue
161+
}
162+
s.buckets[ev.StackID()] = append(qu[:i], qu[i+1:]...)
163+
}
164+
}
165+
}
166+
127167
return evt
128168
}
129169

pkg/event/stackwalk_test.go

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
package event
2020

2121
import (
22+
"testing"
23+
"time"
24+
2225
"github.com/rabbitstack/fibratus/pkg/event/params"
2326
"github.com/rabbitstack/fibratus/pkg/fs"
27+
pstypes "github.com/rabbitstack/fibratus/pkg/ps/types"
2428
"github.com/rabbitstack/fibratus/pkg/util/va"
2529
"github.com/stretchr/testify/assert"
26-
"testing"
27-
"time"
2830
)
2931

3032
func TestStackwalkDecorator(t *testing.T) {
@@ -90,6 +92,77 @@ func TestStackwalkDecorator(t *testing.T) {
9092
assert.Equal(t, "C:\\Windows\\system32\\user32.dll", evt.GetParamAsString(params.FilePath))
9193
}
9294

95+
func TestStackwalkDecoratorSurrogateProcess(t *testing.T) {
96+
q := NewQueue(50, false, true)
97+
cd := NewStackwalkDecorator(q)
98+
99+
e := &Event{
100+
Type: CreateProcess,
101+
Tid: 2484,
102+
PID: 859,
103+
CPU: 1,
104+
Seq: 2,
105+
Name: "CreateProcess",
106+
Timestamp: time.Now(),
107+
Category: Process,
108+
Params: Params{
109+
params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(859)},
110+
params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(4523)},
111+
params.ProcessRealParentID: {Name: params.ProcessRealParentID, Type: params.PID, Value: uint32(8846)},
112+
},
113+
}
114+
115+
e1 := &Event{
116+
Type: CreateThread,
117+
Tid: 2484,
118+
PID: 1411,
119+
CPU: 1,
120+
Seq: 3,
121+
Name: "CreateThread",
122+
Timestamp: time.Now(),
123+
Category: Thread,
124+
PS: &pstypes.PS{
125+
Name: "svchost.exe",
126+
Exe: `C:\WINDOWS\system32\svchost.exe`,
127+
Cmdline: `C:\WINDOWS\system32\svchost.exe -k netsvcs -p -s seclogon`,
128+
},
129+
Params: Params{
130+
params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(859)},
131+
},
132+
}
133+
134+
cd.Push(e)
135+
136+
assert.Len(t, cd.buckets[e.StackID()], 1)
137+
assert.Len(t, cd.buckets[e1.StackID()], 0)
138+
assert.Len(t, cd.procs, 1)
139+
140+
cd.Push(e1)
141+
assert.Len(t, cd.buckets[e1.StackID()], 1)
142+
143+
sw := &Event{
144+
Type: StackWalk,
145+
Tid: 2484,
146+
PID: 1411,
147+
CPU: 1,
148+
Seq: 4,
149+
Name: "StackWalk",
150+
Timestamp: time.Now(),
151+
Params: Params{
152+
params.Callstack: {Name: params.Callstack, Type: params.Slice, Value: []va.Address{0x7ffb5eb70dc4, 0x7ffb5c191deb, 0x7ffb3138592e}},
153+
},
154+
}
155+
156+
thread := cd.Pop(sw)
157+
proc := <-q.Events()
158+
assert.Equal(t, CreateProcess, proc.Type)
159+
assert.Equal(t, CreateThread, thread.Type)
160+
assert.Len(t, cd.buckets[e.StackID()], 0)
161+
assert.Len(t, cd.buckets[e1.StackID()], 0)
162+
assert.True(t, proc.Params.Contains(params.Callstack))
163+
assert.True(t, thread.Params.Contains(params.Callstack))
164+
}
165+
93166
func init() {
94167
maxQueueTTLPeriod = time.Second * 2
95168
flusherInterval = time.Second

pkg/ps/types/types_windows.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,23 @@ package types
2121
import (
2222
"encoding/binary"
2323
"fmt"
24+
"path/filepath"
25+
"strings"
26+
"sync"
27+
2428
"github.com/rabbitstack/fibratus/pkg/sys"
2529
"github.com/rabbitstack/fibratus/pkg/util/cmdline"
2630
"github.com/rabbitstack/fibratus/pkg/util/va"
31+
"github.com/rabbitstack/fibratus/pkg/util/wildcard"
2732
"golang.org/x/sys/windows"
28-
"path/filepath"
29-
"strings"
30-
"sync"
3133

3234
"github.com/rabbitstack/fibratus/pkg/cap/section"
3335
htypes "github.com/rabbitstack/fibratus/pkg/handle/types"
3436
"github.com/rabbitstack/fibratus/pkg/pe"
3537

36-
"github.com/rabbitstack/fibratus/pkg/util/bootid"
3738
"time"
39+
40+
"github.com/rabbitstack/fibratus/pkg/util/bootid"
3841
)
3942

4043
// PS encapsulates process' state such as allocated resources and other metadata.
@@ -291,6 +294,21 @@ func (ps *PS) Ancestors() []string {
291294
return ancestors
292295
}
293296

297+
// IsSeclogonSvc returns true if this is the Secondary Logon Service process.
298+
func (ps *PS) IsSeclogonSvc() bool {
299+
return ps.IsSvchost() && strings.HasSuffix(ps.Cmdline, "-s seclogon")
300+
}
301+
302+
// IsAppinfoSvc returns true if this is the AppInfo Service process.
303+
func (ps *PS) IsAppinfoSvc() bool {
304+
return ps.IsSvchost() && strings.HasSuffix(ps.Cmdline, "-s Appinfo")
305+
}
306+
307+
// IsSvchost returns true if this is the Service Host process.
308+
func (ps *PS) IsSvchost() bool {
309+
return wildcard.Match(`?:\windows\system32\svchost.exe`, strings.ToLower(ps.Exe))
310+
}
311+
294312
// Thread stores metadata about a thread that's executing in process's address space.
295313
type Thread struct {
296314
// Tid is the unique identifier of thread inside the process.

pkg/ps/types/types_windows_test.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919
package types
2020

2121
import (
22+
"os"
23+
"testing"
24+
"time"
25+
2226
"github.com/rabbitstack/fibratus/pkg/util/bootid"
2327
"github.com/stretchr/testify/assert"
2428
"github.com/stretchr/testify/require"
2529
"golang.org/x/sys/windows"
26-
"os"
27-
"testing"
28-
"time"
2930
)
3031

3132
func TestVisit(t *testing.T) {
@@ -97,3 +98,35 @@ func TestUUID(t *testing.T) {
9798
tsUUID := (bootid.Read() << 30) + uint64(os.Getpid()) | uint64(now.UnixNano())
9899
assert.True(t, ps2.UUID() > 0 && ps2.UUID() != tsUUID)
99100
}
101+
102+
func TestIsSeclogonSvc(t *testing.T) {
103+
var tests = []struct {
104+
ps *PS
105+
ok bool
106+
}{
107+
{&PS{Name: "svchost.exe", Exe: `C:\WINDOWS\system32\svchost.exe`, Cmdline: `C:\WINDOWS\system32\svchost.exe -k netsvcs -p -s Appinfo`}, false},
108+
{&PS{Name: "svchost.exe", Exe: `C:\WINDOWS\system32\svchost.exe`, Cmdline: `C:\WINDOWS\system32\svchost.exe -k netsvcs -p -s seclogon`}, true},
109+
}
110+
111+
for _, tt := range tests {
112+
t.Run(tt.ps.Cmdline, func(t *testing.T) {
113+
assert.Equal(t, tt.ok, tt.ps.IsSeclogonSvc())
114+
})
115+
}
116+
}
117+
118+
func TestIsAppinfoSvc(t *testing.T) {
119+
var tests = []struct {
120+
ps *PS
121+
ok bool
122+
}{
123+
{&PS{Name: "svchost.exe", Exe: `C:\WINDOWS\system32\svchost.exe`, Cmdline: `C:\WINDOWS\system32\svchost.exe -k netsvcs -p -s Appinfo`}, true},
124+
{&PS{Name: "svchost.exe", Exe: `C:\WINDOWS\system32\svchost.exe`, Cmdline: `C:\WINDOWS\System32\svchost.exe -k LocalServiceNoNetwork -p -s DPS`}, false},
125+
}
126+
127+
for _, tt := range tests {
128+
t.Run(tt.ps.Cmdline, func(t *testing.T) {
129+
assert.Equal(t, tt.ok, tt.ps.IsAppinfoSvc())
130+
})
131+
}
132+
}

0 commit comments

Comments
 (0)