diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index d20dde011..9179143e2 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -29,8 +29,10 @@ import ( "github.com/rabbitstack/fibratus/pkg/pe" "github.com/rabbitstack/fibratus/pkg/ps" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/util/signature" "github.com/rabbitstack/fibratus/pkg/util/va" + log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/windows" @@ -39,6 +41,7 @@ import ( "path/filepath" "testing" "time" + "unsafe" ) var cfg = &config.Config{ @@ -351,7 +354,7 @@ func TestThreadFilter(t *testing.T) { {`thread.callstack.modules in ('C:\\WINDOWS\\System32\\KERNELBASE.dll', 'C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll')`, true}, {`thread.callstack.symbols imatches ('KERNELBASE.dll!CreateProcess*', 'Java_java_lang_ProcessImpl_create')`, true}, {`thread.callstack.protections in ('RWX')`, true}, - {`thread.callstack.allocation_sizes > 500`, true}, + {`thread.callstack.allocation_sizes > 0`, false}, {`length(thread.callstack.callsite_leading_assembly) > 0`, true}, {`thread.callstack.callsite_trailing_assembly matches ('*mov r10, rcx mov eax, 0x* syscall*')`, true}, {`thread.callstack.is_unbacked`, true}, @@ -366,7 +369,7 @@ func TestThreadFilter(t *testing.T) { {`thread.callstack[0].is_unbacked = true`, true}, {`thread.callstack[2].is_unbacked = false`, true}, {`thread.callstack[kernelbase.dll].symbol = 'CreateProcessW'`, true}, - {`thread.callstack[1].allocation_size >= 400`, true}, + {`thread.callstack[1].allocation_size = 0`, true}, {`thread.callstack[1].protection = 'RWX'`, true}, {`thread.callstack[1].callsite_trailing_assembly matches ('*mov r10, rcx mov eax, 0x* syscall*')`, true}, } @@ -382,6 +385,77 @@ func TestThreadFilter(t *testing.T) { t.Errorf("%d. %q thread filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) } } + + // spawn a new process + var si windows.StartupInfo + si.Flags = windows.STARTF_USESHOWWINDOW + var pi windows.ProcessInformation + + argv := windows.StringToUTF16Ptr(filepath.Join(os.Getenv("windir"), "regedit.exe")) + + err = windows.CreateProcess( + nil, + argv, + nil, + nil, + true, + 0, + nil, + nil, + &si, + &pi) + require.NoError(t, err) + + for { + if sys.IsProcessRunning(pi.Process) { + break + } + time.Sleep(time.Millisecond * 100) + log.Infof("%d pid not yet ready", pi.ProcessId) + } + defer windows.TerminateProcess(pi.Process, 0) + + kevt.PID = pi.ProcessId + + // try until a valid address is returned + // or fail if max attempts are exhausted + j := 50 + ntdll := getNtdllAddress(pi.ProcessId) + for ntdll == 0 && j > 0 { + ntdll = getNtdllAddress(pi.ProcessId) + time.Sleep(time.Millisecond * 250) + j-- + } + + // overwrite ntdll address with dummy bytes + // to reproduce module stomping technique + var protect uint32 + require.NoError(t, windows.VirtualProtectEx(pi.Process, ntdll, uintptr(len(insns)), windows.PAGE_EXECUTE_READWRITE, &protect)) + + var n uintptr + require.NoError(t, windows.WriteProcessMemory(pi.Process, ntdll, &insns[0], uintptr(len(insns)), &n)) + + kevt.Callstack.PushFrame(kevent.Frame{Addr: va.Address(ntdll), Offset: 0, Symbol: "?", Module: "C:\\Windows\\System32\\ntdll.dll"}) + + var tests1 = []struct { + filter string + matches bool + }{ + + {`thread.callstack.allocation_sizes > 0`, true}, + } + + for i, tt := range tests1 { + f := New(tt.filter, cfg) + err := f.Compile() + if err != nil { + t.Fatal(err) + } + matches := f.Run(kevt) + if matches != tt.matches { + t.Errorf("%d. %q thread filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) + } + } } func TestFileFilter(t *testing.T) { @@ -1208,3 +1282,21 @@ func BenchmarkFilterRun(b *testing.B) { f.Run(kevt) } } + +func getNtdllAddress(pid uint32) uintptr { + var moduleHandles [1024]windows.Handle + var cbNeeded uint32 + proc, err := windows.OpenProcess(windows.PROCESS_ALL_ACCESS, false, pid) + if err != nil { + return 0 + } + if err := windows.EnumProcessModules(proc, &moduleHandles[0], 1024, &cbNeeded); err != nil { + return 0 + } + moduleHandle := moduleHandles[1] + var moduleInfo windows.ModuleInfo + if err := windows.GetModuleInformation(proc, moduleHandle, &moduleInfo, uint32(unsafe.Sizeof(moduleInfo))); err != nil { + return 0 + } + return moduleInfo.BaseOfDll +} diff --git a/pkg/kevent/callstack.go b/pkg/kevent/callstack.go index a66218294..026f43ad5 100644 --- a/pkg/kevent/callstack.go +++ b/pkg/kevent/callstack.go @@ -27,6 +27,7 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/arch/x86/x86asm" "golang.org/x/sys/windows" + "os" "path/filepath" "strconv" "strings" @@ -45,6 +46,11 @@ var callstackFlushes = expvar.NewInt("callstack.flushes") // unbacked represents the identifier for unbacked regions in stack frames const unbacked = "unbacked" +var pageSize = uint64(os.Getpagesize()) + +// buildNumber stores the Windows OS build number +var _, _, buildNumber = windows.RtlGetNtVersionNumbers() + // Frame describes a single stack frame. type Frame struct { Addr va.Address // return address @@ -57,18 +63,43 @@ type Frame struct { // from unbacked memory section func (f Frame) IsUnbacked() bool { return f.Module == unbacked } -// AllocationSize calculates the region size -// to which the frame return address pertains if -// the memory pages within the region are private. +// AllocationSize calculates the private region size +// to which the frame return address pertains if the +// memory pages within the region are private and +// non-shareable pages. func (f *Frame) AllocationSize(proc windows.Handle) uint64 { if f.Addr.InSystemRange() { return 0 } + r := va.VirtualQuery(proc, f.Addr.Uint64()) - if r == nil || r.Type != va.MemPrivate { + if r == nil || (r.State != windows.MEM_COMMIT || r.Protect == windows.PAGE_NOACCESS || r.Type != va.MemImage) { return 0 } - return r.Size + + var size uint64 + + // traverse all pages in the region + for n := uint64(0); n < r.Size; n += pageSize { + addr := f.Addr.Inc(n) + ws := va.QueryWorkingSet(proc, addr.Uint64()) + if ws == nil || !ws.Valid() { + continue + } + + // use SharedOriginal after RS3/1709 + if buildNumber >= 16299 { + if !ws.SharedOriginal() { + size += pageSize + } + } else { + if !ws.Shared() { + size += pageSize + } + } + } + + return size } // Protection resolves the memory protection @@ -93,6 +124,7 @@ func (f *Frame) CallsiteAssembly(proc windows.Handle, pre bool) string { if f.Addr.InSystemRange() { return "" } + size := uint(512) base := f.Addr.Uintptr() if pre { @@ -102,6 +134,7 @@ func (f *Frame) CallsiteAssembly(proc windows.Handle, pre bool) string { if len(b) == 0 || va.Zeroed(b) { return "" } + var asm strings.Builder for i := 0; i < len(b); { ins, err := x86asm.Decode(b[i:], 64) @@ -112,6 +145,7 @@ func (f *Frame) CallsiteAssembly(proc windows.Handle, pre bool) string { asm.WriteRune(' ') i += ins.Len } + return asm.String() } diff --git a/pkg/sys/mem.go b/pkg/sys/mem.go new file mode 100644 index 000000000..bb6598310 --- /dev/null +++ b/pkg/sys/mem.go @@ -0,0 +1,72 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sys + +// MemoryWorkingSetExInformation describes the attributes of the memory region. +type MemoryWorkingSetExInformation struct { + VirtualAddress uintptr + VirtualAttributes MemoryWorkingSetExBlock +} + +type MemoryWorkingSetExBlock uintptr + +// Valid if this bit is 1, the subsequent members are valid. Otherwise, they should be ignored. +func (b MemoryWorkingSetExBlock) Valid() bool { + return b&1 != 0 +} + +// ShareCount specifies the number of processes that share this page. The maximum value of this member is 7. +func (b MemoryWorkingSetExBlock) ShareCount() uintptr { + return (uintptr(b) >> 1) & ((1 << 3) - 1) +} + +// Win32Protection specifies the memory protection attributes of the page. +func (b MemoryWorkingSetExBlock) Win32Protection() uintptr { + return (uintptr(b) >> 4) & ((1 << 11) - 1) +} + +// Shared evaluates to true if the page can be shared or false otherwise. +func (b MemoryWorkingSetExBlock) Shared() bool { + return b&(1<<15) != 0 +} + +// Node represents the NUMA node. The maximum value of this member is 63. +func (b MemoryWorkingSetExBlock) Node() uintptr { + return (uintptr(b) >> 16) & ((1 << 6) - 1) +} + +// Locked returns true if the virtual page is locked in physical memory. +func (b MemoryWorkingSetExBlock) Locked() bool { + return b&(1<<15) != 0 +} + +// LargePage returns true if the page is a large page. +func (b MemoryWorkingSetExBlock) LargePage() bool { + return b&(1<<16) != 0 +} + +// SharedOriginal evaluates to true if the page can be shared or false otherwise. +func (b MemoryWorkingSetExBlock) SharedOriginal() bool { + return b&(1<<30) != 0 +} + +// Bad indicates the page has been reported as bad. +func (b MemoryWorkingSetExBlock) Bad() bool { + return b&(1<<31) != 0 +} diff --git a/pkg/sys/syscall.go b/pkg/sys/syscall.go index 9e185b935..aeac71b66 100644 --- a/pkg/sys/syscall.go +++ b/pkg/sys/syscall.go @@ -82,3 +82,6 @@ package sys //sys ShellNotifyIcon(msg NotifyIconMessage, data *NotifyIconData) (err error) [failretval==0] = shell32.Shell_NotifyIconW //sys SHGetStockIconInfo(id int32, flags uint32, icon *ShStockIcon) (err error) [failretval!=0] = shell32.SHGetStockIconInfo //sys FreeConsole() = kernel32.FreeConsole + +// Memory functions +//sys QueryWorkingSet(handle windows.Handle, ws *MemoryWorkingSetExInformation, size uint32) (err error) = psapi.QueryWorkingSetEx diff --git a/pkg/sys/zsyscall_windows.go b/pkg/sys/zsyscall_windows.go index c22f380c4..ca8c39bd3 100644 --- a/pkg/sys/zsyscall_windows.go +++ b/pkg/sys/zsyscall_windows.go @@ -73,6 +73,7 @@ var ( procEnumDeviceDrivers = modpsapi.NewProc("EnumDeviceDrivers") procGetDeviceDriverFileNameW = modpsapi.NewProc("GetDeviceDriverFileNameW") procGetMappedFileNameW = modpsapi.NewProc("GetMappedFileNameW") + procQueryWorkingSetEx = modpsapi.NewProc("QueryWorkingSetEx") procSHGetStockIconInfo = modshell32.NewProc("SHGetStockIconInfo") procShell_NotifyIconW = modshell32.NewProc("Shell_NotifyIconW") procPathIsDirectoryW = modshlwapi.NewProc("PathIsDirectoryW") @@ -265,6 +266,14 @@ func GetMappedFileName(handle windows.Handle, addr uintptr, filename *uint16, si return } +func QueryWorkingSet(handle windows.Handle, ws *MemoryWorkingSetExInformation, size uint32) (err error) { + r1, _, e1 := syscall.Syscall(procQueryWorkingSetEx.Addr(), 3, uintptr(handle), uintptr(unsafe.Pointer(ws)), uintptr(size)) + if r1 == 0 { + err = errnoErr(e1) + } + return +} + func SHGetStockIconInfo(id int32, flags uint32, icon *ShStockIcon) (err error) { r1, _, e1 := syscall.Syscall(procSHGetStockIconInfo.Addr(), 3, uintptr(id), uintptr(flags), uintptr(unsafe.Pointer(icon))) if r1 != 0 { diff --git a/pkg/util/va/region.go b/pkg/util/va/region.go index 594f55304..e6616aa01 100644 --- a/pkg/util/va/region.go +++ b/pkg/util/va/region.go @@ -60,6 +60,7 @@ type RegionInfo struct { BaseAddr uint64 Size uint64 proc windows.Handle + State uint32 } // IsMapped determines if the region is backed by the section object. @@ -178,9 +179,23 @@ func VirtualQuery(process windows.Handle, addr uint64) *RegionInfo { BaseAddr: addr, Size: uint64(mem.RegionSize), proc: process, + State: mem.State, } } +// QueryWorkingSet retrieves extended information about +// the pages at specific virtual addresses in the address +// space of the specified process. +func QueryWorkingSet(process windows.Handle, addr uint64) *sys.MemoryWorkingSetExBlock { + var ws sys.MemoryWorkingSetExInformation + ws.VirtualAddress = uintptr(addr) + err := sys.QueryWorkingSet(process, &ws, uint32(unsafe.Sizeof(sys.MemoryWorkingSetExInformation{}))) + if err != nil { + return nil + } + return &ws.VirtualAttributes +} + // Remove removes the process handle from cache and closes it. // It returns true if the handle was closed successfully. func (p *RegionProber) Remove(pid uint32) bool { diff --git a/pkg/util/va/region_test.go b/pkg/util/va/region_test.go index c6d4e3b65..74be473b2 100644 --- a/pkg/util/va/region_test.go +++ b/pkg/util/va/region_test.go @@ -125,6 +125,19 @@ func TestReadArea(t *testing.T) { require.True(t, Zeroed(zeroArea)) } +func TestQueryWorkingSet(t *testing.T) { + addr, err := getModuleBaseAddress(uint32(os.Getpid())) + require.NoError(t, err) + + b := QueryWorkingSet(windows.CurrentProcess(), uint64(addr)) + require.NotNil(t, b) + + require.True(t, b.Valid()) + require.False(t, b.Bad()) + require.True(t, b.SharedOriginal()) + require.True(t, (b.Win32Protection()&windows.PAGE_READONLY) != 0) +} + func getModuleBaseAddress(pid uint32) (uintptr, error) { var moduleHandles [1024]windows.Handle var cbNeeded uint32