4 min to read
Why do stack variables have only the low 4 bits fixed
rather than 12 bits

这周打比赛遇到了一个栈题,需要爆破栈地址的第二个 hex number,也就是 4 ~ 7 bits,这就引出了本人一直有点疑惑的问题:为什么栈变量的低 4 bits 是固定的,而不是 12 bits?因为我们知道的 aslr 是以页力度对齐的,所以想知道我们 OS 在它的基础上,对栈地址又做了什么样的随机化
在 google 上搜了一波,发现大部分说法都是只提到两点:1. aslr 2. 栈地址必须 16 字节对齐,否则会出锅,而对上述问题没有很好的回答,所以就决定自己动手试一试了
嗯 首先提一句,为什么栈随机化是低4 bit 固定的,因为它要保证 16 byte 的 alignment,不然的话,像 movaps
这种要求栈 16 字节对齐的指令会挂掉
determining the part of stack which is randomized
拿随便一个程序简单测了一下,在输出栈上内容的时候,发现打印了较多环境变量的字符串
而多年的 pwn 经验告诉我们, environ pointer 到某个函数栈帧的偏移是固定的,而 environ pointer 存的是环境变量字符串的初始地址
嗯 其中的 environ pointer 就是指你挂 gdb 上去, p (long long*)environ
所得到的输出,也是一般用来 leak 栈地址的值
所以我们进行一个简单的二分,看如下的哪个部分会被随机化: 1. 环境变量字符串到栈 segment 的上界的距离 2. environ pointer 到环境变量字符串的距离
多次测试发现:environ pointer 到环境变量字符串的距离会被随机化!环境变量字符串到栈 segment 的上界的距离在固定机子上多次测量结果不变!
就像是下面一次调试的截图,那两个红色箭头的距离是会变的hhh
并且输出environ pointer 到环境变量字符串的内存,发现有大段的空白区域,说明有大概率是刻意而为的随机化
kernel implementation - stack randomization with page granularity
一开始拷打了一波 claude,让它帮我找是哪个内核函数对栈地址进行了随机化,找到的是 randomize_stack_top 函数,它是在 load_elf_binary 函数中调用的,该函数的目的是拿到初始的栈地址(由宏定义),然后加/减一个随机的页对齐的值,来实现栈地址的随机化
#ifndef STACK_RND_MASK
#define STACK_RND_MASK (0x7ff >> (PAGE_SHIFT - 12)) /* 8MB of VA */
#endif
unsigned long randomize_stack_top(unsigned long stack_top)
{
unsigned long random_variable = 0;
if (current->flags & PF_RANDOMIZE) {
random_variable = get_random_long();
random_variable &= STACK_RND_MASK;
random_variable <<= PAGE_SHIFT;
}
#ifdef CONFIG_STACK_GROWSUP
return PAGE_ALIGN(stack_top) + random_variable;
#else
return PAGE_ALIGN(stack_top) - random_variable;
#endif
}
该操作完,栈地址还是一个页对齐的值
kernel implementation - stack randomization with 4 bits fixed
那我们去找 kernel 是在哪里把栈上 environ 指针初始化的,经过一番 prompt 和搜索,找到了 create_elf_tables 函数,该函数开头就调用了 arch_align_stack 函数,而看到该函数做了我们一直在找的低 4 bits 对齐的随机化操作,代码如下:
// https://elixir.bootlin.com/linux/v6.6.92/source/arch/x86/kernel/process.c#L1029
unsigned long arch_align_stack(unsigned long sp)
{
if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
sp -= get_random_u32_below(8192);
return sp & ~0xf;
}
在读代码的时候,发现该函数在随机化 sp 之后,确实有把栈上 environ 和 argv 的地址依次设置为栈上环境变量字符串的地址,如下段代码所示
// https://elixir.bootlin.com/linux/v6.6.92/source/fs/binfmt_elf.c#L293
#ifdef CONFIG_STACK_GROWSUP
sp = (elf_addr_t __user *)bprm->p - items - ei_index;
bprm->exec = (unsigned long)sp; /* XXX: PARISC HACK */
#else
sp = (elf_addr_t __user *)bprm->p;
#endif
/* Now, let's put argc (and argv, envp if appropriate) on the stack */
if (put_user(argc, sp++))
return -EFAULT;
/* Populate list of argv pointers back to argv strings. */
p = mm->arg_end = mm->arg_start;
while (argc-- > 0) {
size_t len;
if (put_user((elf_addr_t)p, sp++))
return -EFAULT;
len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
if (!len || len > MAX_ARG_STRLEN)
return -EINVAL;
p += len;
}
if (put_user(0, sp++))
return -EFAULT;
mm->arg_end = p;
/* Populate list of envp pointers back to envp strings. */
mm->env_end = mm->env_start = p;
while (envc-- > 0) {
size_t len;
if (put_user((elf_addr_t)p, sp++))
return -EFAULT;
len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
if (!len || len > MAX_ARG_STRLEN)
return -EINVAL;
p += len;
}
所以就可以知道,栈上 environ pointer 的地址是通过 arch_align_stack
函数来随机化的
然后为什么做这步,其实初始的想法是进一步增强栈的安全性,因为栈溢出漏洞通常来说,利用难度还是比堆漏洞低一些的
但是看 注释 里出现了这个,应该是为了防止并行等情况下,过于频繁访问同一个 L1 cache 的 cache line,导致该 cache line 被频繁换入换出造成的开销
In some cases (e.g. Hyper-Threading), we want to avoid L1 evictions by the processes running on the same package. One thing we can do is to shuffle the initial stack for them.
顺手看了一下,把环境变量字符串放到栈上的函数在比较前的地方,是被 kernel_execve
函数中的 copy_strings_kernel(bprm->envc, envp, bprm)
做的,在比较前面的地方,所以必然在咱们 create_elf_tables 函数前
结论
嗯 所以就是这样
Comments