Skip to content

Commit d4f1a56

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 0aada01 commit d4f1a56

File tree

5 files changed

+181
-7
lines changed

5 files changed

+181
-7
lines changed

pkg/event/event_windows.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,17 @@ func (e *Event) StackPID() uint32 {
284284
return e.PID
285285
}
286286

287+
// IsCreateRemoteThread indicates if the remote thread creation occurred.
288+
func (e *Event) IsCreateRemoteThread() bool {
289+
return e.Type == CreateThread && e.PID != e.Params.MustGetPid()
290+
}
291+
292+
// IsSurrogateProcess indicates if the process creation event parent id
293+
// differs from the real process parent identifier.
294+
func (e *Event) IsSurrogateProcess() bool {
295+
return e.IsCreateProcess() && e.Params.MustGetUint32(params.ProcessParentID) != e.Params.MustGetUint32(params.ProcessRealParentID)
296+
}
297+
287298
// RundownKey calculates the rundown event hash. The hash is
288299
// used to determine if the rundown event was already processed.
289300
func (e *Event) RundownKey() uint64 {

pkg/event/stackwalk.go

Lines changed: 43 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,39 @@ 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+
proc := qu[i]
160+
if !proc.IsCreateProcess() && proc.Params.MustGetPid() != pid {
161+
continue
162+
}
163+
s.buckets[ev.StackID()] = append(qu[:i], qu[i+1:]...)
164+
}
165+
}
166+
}
167+
127168
return evt
128169
}
129170

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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/rabbitstack/fibratus/pkg/sys"
2929
"github.com/rabbitstack/fibratus/pkg/util/cmdline"
3030
"github.com/rabbitstack/fibratus/pkg/util/va"
31+
"github.com/rabbitstack/fibratus/pkg/util/wildcard"
3132
"golang.org/x/sys/windows"
3233

3334
"github.com/rabbitstack/fibratus/pkg/cap/section"
@@ -304,6 +305,21 @@ func (ps *PS) Ancestors() []string {
304305
return ancestors
305306
}
306307

308+
// IsSeclogonSvc returns true if this is the Secondary Logon Service process.
309+
func (ps *PS) IsSeclogonSvc() bool {
310+
return ps.IsSvchost() && strings.HasSuffix(ps.Cmdline, "-s seclogon")
311+
}
312+
313+
// IsAppinfoSvc returns true if this is the AppInfo Service process.
314+
func (ps *PS) IsAppinfoSvc() bool {
315+
return ps.IsSvchost() && strings.HasSuffix(ps.Cmdline, "-s Appinfo")
316+
}
317+
318+
// IsSvchost returns true if this is the Service Host process.
319+
func (ps *PS) IsSvchost() bool {
320+
return wildcard.Match(`?:\windows\system32\svchost.exe`, strings.ToLower(ps.Exe))
321+
}
322+
307323
// Thread stores metadata about a thread that's executing in process's address space.
308324
type Thread struct {
309325
// 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)