Skip to content

Commit 683448a

Browse files
committed
runtime, syscall: only search for Windows DLLs in the System32 directory
Make sure that for any DLL that Go uses itself, we only look for the DLL in the Windows System32 directory, guarding against DLL preloading attacks. (Unless the Windows version is ancient and LoadLibraryEx is unavailable, in which case the user probably has bigger security problems anyway.) This does not change the behavior of syscall.LoadLibrary or NewLazyDLL if the DLL name is something unused by Go itself. This change also intentionally does not add any new API surface. Instead, x/sys is updated with a LoadLibraryEx function and LazyDLL.Flags in: https://golang.org/cl/21388 Updates #14959 Change-Id: I8d29200559cc19edf8dcf41dbdd39a389cd6aeb9 Reviewed-on: https://go-review.googlesource.com/21140 Reviewed-by: Russ Cox <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 59fc42b commit 683448a

15 files changed

+299
-59
lines changed

src/cmd/dist/deps.go

Lines changed: 29 additions & 28 deletions
Large diffs are not rendered by default.

src/go/build/deps_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,10 @@ var pkgDeps = map[string][]string{
132132
// End of linear dependency definitions.
133133

134134
// Operating system access.
135-
"syscall": {"L0", "internal/race", "unicode/utf16"},
135+
"syscall": {"L0", "internal/race", "internal/syscall/windows/sysdll", "unicode/utf16"},
136136
"internal/syscall/unix": {"L0", "syscall"},
137-
"internal/syscall/windows": {"L0", "syscall"},
138-
"internal/syscall/windows/registry": {"L0", "syscall", "unicode/utf16"},
137+
"internal/syscall/windows": {"L0", "syscall", "internal/syscall/windows/sysdll"},
138+
"internal/syscall/windows/registry": {"L0", "syscall", "internal/syscall/windows/sysdll", "unicode/utf16"},
139139
"time": {"L0", "syscall", "internal/syscall/windows/registry"},
140140
"os": {"L1", "os", "syscall", "time", "internal/syscall/windows"},
141141
"path/filepath": {"L2", "os", "syscall"},

src/internal/syscall/windows/registry/syscall.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ package registry
88

99
import "syscall"
1010

11-
//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall_windows.go syscall.go
11+
//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall_windows.go -systemdll syscall.go
1212

1313
const (
1414
_REG_OPTION_NON_VOLATILE = 0

src/internal/syscall/windows/registry/zsyscall_windows.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ package registry
44

55
import "unsafe"
66
import "syscall"
7+
import "internal/syscall/windows/sysdll"
78

89
var _ unsafe.Pointer
910

1011
var (
11-
modadvapi32 = syscall.NewLazyDLL("advapi32.dll")
12-
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
12+
modadvapi32 = syscall.NewLazyDLL(sysdll.Add("advapi32.dll"))
13+
modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll"))
1314

1415
procRegCreateKeyExW = modadvapi32.NewProc("RegCreateKeyExW")
1516
procRegDeleteKeyW = modadvapi32.NewProc("RegDeleteKeyW")

src/internal/syscall/windows/syscall_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package windows
66

77
import "syscall"
88

9-
//go:generate go run ../../../syscall/mksyscall_windows.go -output zsyscall_windows.go syscall_windows.go
9+
//go:generate go run ../../../syscall/mksyscall_windows.go -output zsyscall_windows.go -systemdll syscall_windows.go
1010

1111
const GAA_FLAG_INCLUDE_PREFIX = 0x00000010
1212

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2016 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package sysdll is an internal leaf package that records and reports
6+
// which Windows DLL names are used by Go itself. These DLLs are then
7+
// only loaded from the System32 directory. See Issue 14959.
8+
package sysdll
9+
10+
// IsSystemDLL reports whether the named dll key (a base name, like
11+
// "foo.dll") is a system DLL which should only be loaded from the
12+
// Windows SYSTEM32 directory.
13+
//
14+
// Filenames are case sensitive, but that doesn't matter because
15+
// the case registered with Add is also the same case used with
16+
// LoadDLL later.
17+
//
18+
// It has no associated mutex and should only be mutated serially
19+
// (currently: during init), and not concurrent with DLL loading.
20+
var IsSystemDLL = map[string]bool{}
21+
22+
// Add notes that dll is a system32 DLL which should only be loaded
23+
// from the Windows SYSTEM32 directory. It returns its argument back,
24+
// for ease of use in generated code.
25+
func Add(dll string) string {
26+
IsSystemDLL[dll] = true
27+
return dll
28+
}

src/internal/syscall/windows/zsyscall_windows.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ package windows
44

55
import "unsafe"
66
import "syscall"
7+
import "internal/syscall/windows/sysdll"
78

89
var _ unsafe.Pointer
910

1011
var (
11-
modiphlpapi = syscall.NewLazyDLL("iphlpapi.dll")
12-
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
12+
modiphlpapi = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll"))
13+
modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll"))
1314

1415
procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses")
1516
procGetComputerNameExW = modkernel32.NewProc("GetComputerNameExW")

src/runtime/export_windows_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ func NumberOfProcessors() int32 {
1515
stdcall1(_GetSystemInfo, uintptr(unsafe.Pointer(&info)))
1616
return int32(info.dwnumberofprocessors)
1717
}
18+
19+
func LoadLibraryExStatus() (useEx, haveEx, haveFlags bool) {
20+
return useLoadLibraryEx, _LoadLibraryExW != nil, _AddDllDirectory != nil
21+
}

src/runtime/os1_windows.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,11 @@ var (
9393

9494
// Following syscalls are only available on some Windows PCs.
9595
// We will load syscalls, if available, before using them.
96+
_AddDllDirectory,
9697
_AddVectoredContinueHandler,
97-
_GetQueuedCompletionStatusEx stdFunction
98+
_GetQueuedCompletionStatusEx,
99+
_LoadLibraryExW,
100+
_ stdFunction
98101
)
99102

100103
type sigset struct{}
@@ -117,8 +120,10 @@ func loadOptionalSyscalls() {
117120
return stdFunction(unsafe.Pointer(f))
118121
}
119122
if l != 0 {
123+
_AddDllDirectory = findfunc("AddDllDirectory")
120124
_AddVectoredContinueHandler = findfunc("AddVectoredContinueHandler")
121125
_GetQueuedCompletionStatusEx = findfunc("GetQueuedCompletionStatusEx")
126+
_LoadLibraryExW = findfunc("LoadLibraryExW")
122127
}
123128
}
124129

@@ -127,6 +132,11 @@ func getLoadLibrary() uintptr {
127132
return uintptr(unsafe.Pointer(_LoadLibraryW))
128133
}
129134

135+
//go:nosplit
136+
func getLoadLibraryEx() uintptr {
137+
return uintptr(unsafe.Pointer(_LoadLibraryExW))
138+
}
139+
130140
//go:nosplit
131141
func getGetProcAddress() uintptr {
132142
return uintptr(unsafe.Pointer(_GetProcAddress))
@@ -161,13 +171,31 @@ const (
161171
// in sys_windows_386.s and sys_windows_amd64.s
162172
func externalthreadhandler()
163173

174+
// When loading DLLs, we prefer to use LoadLibraryEx with
175+
// LOAD_LIBRARY_SEARCH_* flags, if available. LoadLibraryEx is not
176+
// available on old Windows, though, and the LOAD_LIBRARY_SEARCH_*
177+
// flags are not available on some versions of Windows without a
178+
// security patch.
179+
//
180+
// https://msdn.microsoft.com/en-us/library/ms684179(v=vs.85).aspx says:
181+
// "Windows 7, Windows Server 2008 R2, Windows Vista, and Windows
182+
// Server 2008: The LOAD_LIBRARY_SEARCH_* flags are available on
183+
// systems that have KB2533623 installed. To determine whether the
184+
// flags are available, use GetProcAddress to get the address of the
185+
// AddDllDirectory, RemoveDllDirectory, or SetDefaultDllDirectories
186+
// function. If GetProcAddress succeeds, the LOAD_LIBRARY_SEARCH_*
187+
// flags can be used with LoadLibraryEx."
188+
var useLoadLibraryEx bool
189+
164190
func osinit() {
165191
asmstdcallAddr = unsafe.Pointer(funcPC(asmstdcall))
166192

167193
setBadSignalMsg()
168194

169195
loadOptionalSyscalls()
170196

197+
useLoadLibraryEx = (_LoadLibraryExW != nil && _AddDllDirectory != nil)
198+
171199
disableWER()
172200

173201
externalthreadhandlerp = funcPC(externalthreadhandler)

src/runtime/syscall_windows.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,41 @@ func compileCallback(fn eface, cleanstack bool) (code uintptr) {
8888
return callbackasmAddr(n)
8989
}
9090

91+
const _LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800
92+
93+
//go:linkname syscall_loadsystemlibrary syscall.loadsystemlibrary
94+
//go:nosplit
95+
func syscall_loadsystemlibrary(filename *uint16) (handle, err uintptr) {
96+
c := &getg().m.syscall
97+
98+
if useLoadLibraryEx {
99+
c.fn = getLoadLibraryEx()
100+
c.n = 3
101+
args := struct {
102+
lpFileName *uint16
103+
hFile uintptr // always 0
104+
flags uint32
105+
}{filename, 0, _LOAD_LIBRARY_SEARCH_SYSTEM32}
106+
c.args = uintptr(noescape(unsafe.Pointer(&args)))
107+
} else {
108+
// User is on Windows XP or something ancient.
109+
// The caller wanted to only load the filename DLL
110+
// from the System32 directory but that facility
111+
// doesn't exist, so just load it the normal way. This
112+
// is a potential security risk, but so is Windows XP.
113+
c.fn = getLoadLibrary()
114+
c.n = 1
115+
c.args = uintptr(noescape(unsafe.Pointer(&filename)))
116+
}
117+
118+
cgocall(asmstdcallAddr, unsafe.Pointer(c))
119+
handle = c.r1
120+
if handle == 0 {
121+
err = c.err
122+
}
123+
return
124+
}
125+
91126
//go:linkname syscall_loadlibrary syscall.loadlibrary
92127
//go:nosplit
93128
func syscall_loadlibrary(filename *uint16) (handle, err uintptr) {

src/runtime/syscall_windows_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ package runtime_test
77
import (
88
"bytes"
99
"fmt"
10+
"internal/syscall/windows/sysdll"
11+
"internal/testenv"
1012
"io/ioutil"
1113
"os"
1214
"os/exec"
@@ -771,3 +773,94 @@ func TestNumCPU(t *testing.T) {
771773
t.Fatalf("SetProcessAffinityMask didn't set newmask of 0x%x. Current mask is 0x%x.", newmask, mask)
772774
}
773775
}
776+
777+
// See Issue 14959
778+
func TestDLLPreloadMitigation(t *testing.T) {
779+
if _, err := exec.LookPath("gcc"); err != nil {
780+
t.Skip("skipping test: gcc is missing")
781+
}
782+
783+
dir0, err := os.Getwd()
784+
if err != nil {
785+
t.Fatal(err)
786+
}
787+
defer os.Chdir(dir0)
788+
789+
const src = `
790+
#include <stdint.h>
791+
#include <windows.h>
792+
793+
uintptr_t cfunc() {
794+
SetLastError(123);
795+
}
796+
`
797+
tmpdir, err := ioutil.TempDir("", "TestDLLPreloadMitigation")
798+
if err != nil {
799+
t.Fatal("TempDir failed: ", err)
800+
}
801+
defer os.RemoveAll(tmpdir)
802+
803+
srcname := "nojack.c"
804+
err = ioutil.WriteFile(filepath.Join(tmpdir, srcname), []byte(src), 0)
805+
if err != nil {
806+
t.Fatal(err)
807+
}
808+
name := "nojack.dll"
809+
cmd := exec.Command("gcc", "-shared", "-s", "-Werror", "-o", name, srcname)
810+
cmd.Dir = tmpdir
811+
out, err := cmd.CombinedOutput()
812+
if err != nil {
813+
t.Fatalf("failed to build dll: %v - %v", err, string(out))
814+
}
815+
dllpath := filepath.Join(tmpdir, name)
816+
817+
dll := syscall.MustLoadDLL(dllpath)
818+
dll.MustFindProc("cfunc")
819+
dll.Release()
820+
821+
// Get into the directory with the DLL we'll load by base name
822+
// ("nojack.dll") Think of this as the user double-clicking an
823+
// installer from their Downloads directory where a browser
824+
// silently downloaded some malicious DLLs.
825+
os.Chdir(tmpdir)
826+
827+
// First before we can load a DLL from the current directory,
828+
// loading it only as "nojack.dll", without an absolute path.
829+
delete(sysdll.IsSystemDLL, name) // in case test was run repeatedly
830+
dll, err = syscall.LoadDLL(name)
831+
if err != nil {
832+
t.Fatalf("failed to load %s by base name before sysdll registration: %v", name, err)
833+
}
834+
dll.Release()
835+
836+
// And now verify that if we register it as a system32-only
837+
// DLL, the implicit loading from the current directory no
838+
// longer works.
839+
sysdll.IsSystemDLL[name] = true
840+
dll, err = syscall.LoadDLL(name)
841+
if err == nil {
842+
dll.Release()
843+
if wantLoadLibraryEx() {
844+
t.Fatalf("Bad: insecure load of DLL by base name %q before sysdll registration: %v", name, err)
845+
}
846+
t.Skip("insecure load of DLL, but expected")
847+
}
848+
}
849+
850+
// wantLoadLibraryEx reports whether we expect LoadLibraryEx to work for tests.
851+
func wantLoadLibraryEx() bool {
852+
return testenv.Builder() == "windows-amd64-gce" || testenv.Builder() == "windows-386-gce"
853+
}
854+
855+
func TestLoadLibraryEx(t *testing.T) {
856+
use, have, flags := runtime.LoadLibraryExStatus()
857+
if use {
858+
return // success.
859+
}
860+
if wantLoadLibraryEx() {
861+
t.Fatalf("Expected LoadLibraryEx+flags to be available. (LoadLibraryEx=%v; flags=%v)",
862+
have, flags)
863+
}
864+
t.Skipf("LoadLibraryEx not usable, but not expected. (LoadLibraryEx=%v; flags=%v)",
865+
have, flags)
866+
}

src/syscall/dll_windows.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package syscall
66

77
import (
8+
"internal/syscall/windows/sysdll"
89
"sync"
910
"sync/atomic"
1011
"unsafe"
@@ -26,6 +27,7 @@ func Syscall9(trap, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 u
2627
func Syscall12(trap, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 uintptr) (r1, r2 uintptr, err Errno)
2728
func Syscall15(trap, nargs, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2 uintptr, err Errno)
2829
func loadlibrary(filename *uint16) (handle uintptr, err Errno)
30+
func loadsystemlibrary(filename *uint16) (handle uintptr, err Errno)
2931
func getprocaddress(handle uintptr, procname *uint8) (proc uintptr, err Errno)
3032

3133
// A DLL implements access to a single DLL.
@@ -34,13 +36,19 @@ type DLL struct {
3436
Handle Handle
3537
}
3638

37-
// LoadDLL loads DLL file into memory.
38-
func LoadDLL(name string) (dll *DLL, err error) {
39+
// LoadDLL loads the named DLL file into memory.
40+
func LoadDLL(name string) (*DLL, error) {
3941
namep, err := UTF16PtrFromString(name)
4042
if err != nil {
4143
return nil, err
4244
}
43-
h, e := loadlibrary(namep)
45+
var h uintptr
46+
var e Errno
47+
if sysdll.IsSystemDLL[name] {
48+
h, e = loadsystemlibrary(namep)
49+
} else {
50+
h, e = loadlibrary(namep)
51+
}
4452
if e != 0 {
4553
return nil, &DLLError{
4654
Err: e,

0 commit comments

Comments
 (0)