为什么在 Go 程序中,personality(2) 的返回值有时会不正确?

huangapple go评论76阅读模式
英文:

Why is the return value of personality(2) in a Go program sometimes incorrect?

问题

我已经写了一个Go程序,通过将ADDR_NO_RANDOMIZE作为persona参数传递给personality(2)系统调用来禁用ASLR。它使用golang.org/x/sys/unix包中的unix.Syscall函数来实现。

在禁用ASLR后,程序运行一个无限循环,在循环中通过调用带有persona参数为0xffffffffpersonality(2)来获取当前的persona,而不改变它,并报告ADDR_NO_RANDOMIZE位是否被设置。

当运行程序时,persona似乎在0x00040000(具有设置ADDR_NO_RANDOMIZE位的预期值)和0x00000000(没有设置任何位,这是意外的)之间来回切换。

程序的示例输出:

good - ASLR is disabled (0x00040000)
good - ASLR is disabled (0x00040000)
good - ASLR is disabled (0x00040000)
... [重复多次]
bad - ASLR is not disabled (0x00000000)
bad - ASLR is not disabled (0x00000000)
bad - ASLR is not disabled (0x00000000)
... [重复多次]
good - ASLR is disabled (0x00040000)
good - ASLR is disabled (0x00040000)
good - ASLR is disabled (0x00040000)
... [重复多次]
... [以此类推]

以下是程序的源代码:

package main

import (
	"fmt"
	"syscall"

	"golang.org/x/sys/unix"
)

// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/personality.h
const addrNoRandomize = 0x0040000

func main() {
	disableAslr()

	for {
		curPersona := getCurrentPersona()
		if curPersona&addrNoRandomize != 0 {
			fmt.Printf("good - ASLR is disabled (0x%08x)\n", curPersona)
		} else {
			fmt.Printf("bad - ASLR is not disabled (0x%08x)\n", curPersona)
		}
	}
}

func disableAslr() {
	persona := getCurrentPersona()

	if persona&addrNoRandomize == 0 {
		ret, _, errno := unix.Syscall(syscall.SYS_PERSONALITY, uintptr(persona|addrNoRandomize), 0, 0)
		if int(ret) == -1 {
			panic(fmt.Errorf("disable aslr: %v", errno))
		}
	}
}

func getCurrentPersona() int {
	persona, _, errno := unix.RawSyscall(syscall.SYS_PERSONALITY, uintptr(0xffffffff), 0, 0)
	if int(persona) == -1 {
		panic(fmt.Errorf("get current persona: %v", errno))
	}
	return int(persona)
}

这种行为是意外的,因为:

  1. 手册中没有任何提示表明persona应该像这样来回切换
  2. 等效的C程序始终显示persona被设置为0x00040000,它从不恢复为0
  3. 监视Go程序的PID的/proc/<PID>/personality始终显示00040000。它从不恢复为0。这使我认为ADDR_NO_RANDOMIZE确实被正确设置,但程序错误地报告了它
  4. 使用strace跟踪Go程序显示,personality(2)只被调用了一次,参数为0x00040000(禁用ASLR)或0xffffffff(获取当前的persona而不改变它)。没有其他调用将其改回0。
英文:

I've written a Go program that is supposed to disable ASLR by passing ADDR_NO_RANDOMIZE as the persona argument to the personality(2) system call. It uses the unix.Syscall function from the golang.org/x/sys/unix package to do so.

After disabling ASLR, the program runs an infinite loop, in which it gets the current persona without changing it by calling personality(2) with a persona argument of 0xffffffff (as described in the man page) and reports whether the ADDR_NO_RANDOMIZE bit is set.

When the program is run, the persona seems to go back and forth between 0x00040000 (the expected value with the ADDR_NO_RANDOMIZE bit set) and 0x00000000 (no bits set, which is unexpected).

Sample output of the program:

good - ASLR is disabled (0x00040000)
good - ASLR is disabled (0x00040000)
good - ASLR is disabled (0x00040000)
... [Repeats many times]
bad - ASLR is not disabled (0x00000000)
bad - ASLR is not disabled (0x00000000)
bad - ASLR is not disabled (0x00000000)
... [Repeats many times]
good - ASLR is disabled (0x00040000)
good - ASLR is disabled (0x00040000)
good - ASLR is disabled (0x00040000)
... [Repeats many times]
... [And so on]

Here is the source code for the program:

package main

import (
	&quot;fmt&quot;
	&quot;syscall&quot;

	&quot;golang.org/x/sys/unix&quot;
)

// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/personality.h
const addrNoRandomize = 0x0040000

func main() {
	disableAslr()

	for {
		curPersona := getCurrentPersona()
		if curPersona&amp;addrNoRandomize != 0 {
			fmt.Printf(&quot;good - ASLR is disabled (0x%08x)\n&quot;, curPersona)
		} else {
			fmt.Printf(&quot;bad - ASLR is not disabled (0x%08x)\n&quot;, curPersona)
		}
	}
}

func disableAslr() {
	persona := getCurrentPersona()

	if persona&amp;addrNoRandomize == 0 {
		ret, _, errno := unix.Syscall(syscall.SYS_PERSONALITY, uintptr(persona|addrNoRandomize), 0, 0)
		if int(ret) == -1 {
			panic(fmt.Errorf(&quot;disable aslr: %v&quot;, errno))
		}
	}
}

func getCurrentPersona() int {
	persona, _, errno := unix.RawSyscall(syscall.SYS_PERSONALITY, uintptr(0xffffffff), 0, 0)
	if int(persona) == -1 {
		panic(fmt.Errorf(&quot;get current persona: %v&quot;, errno))
	}
	return int(persona)
}

This behavior is unexpected because:

  1. The man page says nothing to suggest that the personality should go back and forth like this
  2. An equivalent C program consistently shows the personality being set to 0x00040000 - it never reverts back to 0
  3. Watching /proc/&lt;PID&gt;/personality for the PID of the Go program consistently shows 00040000. It never goes back to 0. This suggests to me that ADDR_NO_RANDOMIZE is indeed set properly, but the program is reporting it incorrectly
  4. Using strace on the Go program shows that personality(2) is only called with an argument of 0x00040000 (to disable ASLR) or 0xffffffff (to get the current persona without changing it). There are no other calls changing it back to 0

答案1

得分: 3

我在personality(2)的手册页中没有看到关于这个问题的详细信息,但这似乎与为什么在很长一段时间内,Go在Linux下不支持setuid()和相关函数的问题相同。

Go对线程的理解并不容易理解。你可以使用runtime.LockOSThread()强制一个单一的操作系统线程遵循一段代码的执行,但是让Go程序中的所有操作系统线程共享许多内核维护的属性有时候是困难和不明显的。

有一个包kernel.org/pub/linux/libs/security/libcap/psx,它可以在所有操作系统线程上镜像系统调用。在这种情况下,它解决了你所看到的问题。

将你的应用程序(prog.go)重写为使用psx包,并通过命令行标志进行控制,可以使其按照你的预期工作:

package main

import (
	"flag"
	"fmt"
	"os"
	"runtime"
	"syscall"

	"kernel.org/pub/linux/libs/security/libcap/psx"
)

var wPSX = flag.Bool("psx", false, "disable ASLR with PSX thread mirroring")

// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/personality.h
const addrNoRandomize = 0x0040000

func main() {
	flag.Parse()

	disableAslr()

	for i := 0; i < 100; i++ {
		runtime.LockOSThread()
		curPersona := getCurrentPersona()
		tid := syscall.Gettid()
		runtime.UnlockOSThread()
		if curPersona&addrNoRandomize != 0 {
			fmt.Printf("%d: good - ASLR is disabled (0x%08x)\n", tid, curPersona)
		} else {
			fmt.Printf("%d: bad - ASLR is not disabled (0x%08x)\n", tid, curPersona)
			os.Exit(1)
		}
		go func() {
			// Cause one OS thread to exit.
			runtime.LockOSThread()
		}()
		// Allow this code to run on a different thread.
		runtime.Gosched()
	}
}

func disableAslr() {
	persona := getCurrentPersona()
	if persona&addrNoRandomize != 0 {
		return
	}
	var ret uintptr
	var errno syscall.Errno
	if *wPSX {
		ret, _, errno = psx.Syscall3(syscall.SYS_PERSONALITY, uintptr(persona|addrNoRandomize), 0, 0)
	} else {
		ret, _, errno = syscall.Syscall(syscall.SYS_PERSONALITY, uintptr(persona|addrNoRandomize), 0, 0)
	}
	if int(ret) == -1 {
		panic(fmt.Errorf("disable aslr: %v", errno))
	}
}

func getCurrentPersona() int {
	persona, _, errno := syscall.RawSyscall(syscall.SYS_PERSONALITY, uintptr(0xffffffff), 0, 0)
	if int(persona) == -1 {
		panic(fmt.Errorf("get current persona: %v", errno))
	}
	return int(persona)
}

我删除了unix包,因为在这种情况下它对任何事情都没有帮助,而且你已经包含了syscall包。

$ go mod init prog
$ go mod tidy
$ go build
$ ./prog --psx=false
24381: good - ASLR is disabled (0x00040000)
24381: good - ASLR is disabled (0x00040000)
24381: good - ASLR is disabled (0x00040000)
24387: bad - ASLR is not disabled (0x00000000)
$ ./prog --psx=true
24368: good - ASLR is disabled (0x00040000)
24369: good - ASLR is disabled (0x00040000)
24370: good - ASLR is disabled (0x00040000)
24371: good - ASLR is disabled (0x00040000)
24372: good - ASLR is disabled (0x00040000)
24374: good - ASLR is disabled (0x00040000)
24373: good - ASLR is disabled (0x00040000)
...
$
英文:

I don't see any detail about this in the man page for personality(2), but this seems to
be the same issue as why setuid() and friends were not supported by Go, under Linux, for a very long time.

Go's idea of threading is not easy to reason about. You can force a single OS thread to follow the execution of a single sequence of code with runtime.LockOSThread(), but getting all OS threads in a Go program to share many kernel maintained attributes is sometimes difficult and non-obvious.

There is a package, kernel.org/pub/linux/libs/security/libcap/psx, that mirrors system calls over all OS threads. In cases like this it addresses the problem you are seeing.

Rewriting your application (prog.go) to use the psx package, controlled with a command line flag, makes it work as you expect:

package main
import (
&quot;flag&quot;
&quot;fmt&quot;
&quot;os&quot;
&quot;runtime&quot;
&quot;syscall&quot;
&quot;kernel.org/pub/linux/libs/security/libcap/psx&quot;
)
var wPSX = flag.Bool(&quot;psx&quot;, false, &quot;disable ASLR with PSX thread mirroring&quot;)
// https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/personality.h
const addrNoRandomize = 0x0040000
func main() {
flag.Parse()
disableAslr()
for i := 0; i &lt; 100; i++ {
runtime.LockOSThread()
curPersona := getCurrentPersona()
tid := syscall.Gettid()
runtime.UnlockOSThread()
if curPersona&amp;addrNoRandomize != 0 {
fmt.Printf(&quot;%d: good - ASLR is disabled (0x%08x)\n&quot;, tid, curPersona)
} else {
fmt.Printf(&quot;%d: bad - ASLR is not disabled (0x%08x)\n&quot;, tid, curPersona)
os.Exit(1)
}
go func() {
// Cause one OS thread to exit.
runtime.LockOSThread()
}()
// Allow this code to run on a different thread.
runtime.Gosched()
}
}
func disableAslr() {
persona := getCurrentPersona()
if persona&amp;addrNoRandomize != 0 {
return
}
var ret uintptr
var errno syscall.Errno
if *wPSX {
ret, _, errno = psx.Syscall3(syscall.SYS_PERSONALITY, uintptr(persona|addrNoRandomize), 0, 0)
} else {
ret, _, errno = syscall.Syscall(syscall.SYS_PERSONALITY, uintptr(persona|addrNoRandomize), 0, 0)
}
if int(ret) == -1 {
panic(fmt.Errorf(&quot;disable aslr: %v&quot;, errno))
}
}
func getCurrentPersona() int {
persona, _, errno := syscall.RawSyscall(syscall.SYS_PERSONALITY, uintptr(0xffffffff), 0, 0)
if int(persona) == -1 {
panic(fmt.Errorf(&quot;get current persona: %v&quot;, errno))
}
return int(persona)
}

I've dropped the unix package as it doesn't help with anything in this case, and you are already including the syscall package.

$ go mod init prog
$ go mod tidy
$ go build
$ ./prog --psx=false
24381: good - ASLR is disabled (0x00040000)
24381: good - ASLR is disabled (0x00040000)
24381: good - ASLR is disabled (0x00040000)
24387: bad - ASLR is not disabled (0x00000000)
$ ./prog --psx=true
24368: good - ASLR is disabled (0x00040000)
24369: good - ASLR is disabled (0x00040000)
24370: good - ASLR is disabled (0x00040000)
24371: good - ASLR is disabled (0x00040000)
24372: good - ASLR is disabled (0x00040000)
24374: good - ASLR is disabled (0x00040000)
24373: good - ASLR is disabled (0x00040000)
...
$

答案2

得分: 1

我不喜欢泼冷水,但是我突然想到...

  1. 如果启用了 ASLR(地址空间布局随机化),禁用当前程序的 ASLR 将几乎没有效果,因为它发生在加载过程的太晚阶段。内核(以及可能的 ELF 解释器)已经应用了 ASLR。
  2. 禁用 ASLR 只对我们使用 execvp 的程序产生影响。
  3. 要禁用当前程序的 ASLR,需要先禁用它,然后执行当前程序及其参数的 exec

请注意,C 程序不需要为线程执行此操作,因为线程尚未创建。

对于 go 程序,我们可能需要像 Tinkerer 在他的回答中所做的那样(例如使用 psx 等来捕获所有线程)。

由于我对 go 不太熟悉,这里是一个 C 程序(我们需要在 go 中找到相应的等价物):

package main

import (
	"fmt"
	"os"
	"os/exec"
	"syscall"
)

func disableASLR() bool {
	// 获取当前状态
	pers, _ := syscall.Personality()

	// 判断是否需要禁用(并执行)
	doExec := (pers & syscall.ADDR_NO_RANDOMIZE) == 0

	// 禁用 ASLR(对 execvp 的子进程)
	if doExec {
		syscall.Personality(pers | syscall.ADDR_NO_RANDOMIZE)
	}

	return doExec
}

func main() {
	// 禁用 ASLR 并自行执行(如果需要)
	if disableASLR() {
		cmd := exec.Command(os.Args[0], os.Args[1:]...)
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		cmd.Run()
		return
	}

	// 真正的操作...

	fmt.Println("Hello, World!")
}

希望这对你有帮助!

英文:

I hate to rain on the parade, but, it just occurred to me ...

  1. Disabling ASLR [if enabled] for the current program will have little effect, because it happens too late in the loading process. The kernel [and, possibly, the ELF interpreter] has already applied ASLR.
  2. Disabling ASLR will only have an effect on a program that we do execvp on.
  3. To disable ASLR for the current program requires that we disable it, and, then, do exec of the current program and its args.

Note that a C program doesn't have to do this for threads because they haven't been created yet.

For a go program, we may have to do what Tinkerer did in his answer (e.g. using psx, etc., to catch all threads).

Since I'm not too familiar with go, here is a C program (and we need the equivalent in go):

#include &lt;unistd.h&gt;
#include &lt;sys/personality.h&gt;
int
disableAslr(void)
{
// get current state
unsigned int pers = personality(0xFFFFFFFF);
// decide if we need to disable (and exec)
int doexec = ((pers &amp; ADDR_NO_RANDOMIZE) == 0);
// disable ASLR (for child of execvp)
if (doexec)
personality(pers | ADDR_NO_RANDOMIZE);
return doexec;
}
int
main(int argc,char **argv)
{
// disable ASLR and self exec [if necessary]
if (disableAslr())
execvp(argv[0],argv);
// real stuff ...
return 0;
}

huangapple
  • 本文由 发表于 2023年3月12日 03:36:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/75709043.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定