大纲
在《从汇编层看64位程序运行——ROP攻击以控制程序执行流程》中,我们看到可以通过“微操”栈空间控制程序执行流程。现实中,黑客一般会利用栈溢出改写Next RIP地址,这就会修改连续的栈空间。而编译器针对这种场景,设计了“栈保护”机制。
栈保护
比如下面的代码,buffer[8] = 'b’这句会导致栈溢出。
int main() {
char buffer[8] = {0};
buffer[0] = 'a';
buffer[1] = buffer[0] + 1;
buffer[8] = 'b';
return 0;
}
如果我们采用“栈保护”机制编译,即增加-fstack-protector-strong(或者-fstack-protector、-fstack-protector-all等)编译选项。
# makefile
# 定义编译器
CC := gcc
# 定义编译选项
CFLAGS := -Wall -Werror -O0 -fstack-protector-strong
# 定义目标目录
OBJDIR := build
BINDIR := bin
# 定义源文件和目标文件
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c,$(OBJDIR)/%.o,$(SRCS))
# 获取当前工作目录名,即工程目录名
PROJECT_DIR_NAME := $(notdir $(shell pwd))
# 最终目标:编译所有文件并生成可执行文件
$(BINDIR)/$(PROJECT_DIR_NAME): $(BINDIR) $(OBJS)
$(CC) $(CFLAGS) $(OBJS) -o $@
# 通用规则:如何从每个.c文件生成.o文件
$(OBJDIR)/%.o: src/%.c $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
# 规则:创建build目录
$(OBJDIR):
mkdir -p $(OBJDIR)
# 规则:创建bin目录
$(BINDIR):
mkdir -p $(BINDIR)
# 伪目标:清理项目
.PHONY: clean
clean:
rm -rf $(OBJDIR) $(BINDIR)
# 伪目标:打印变量(用于调试)
.PHONY: print
print:
@echo "SRCS = $(SRCS)"
@echo "OBJS = $(OBJS)"
@echo "PROJECT_DIR_NAME = $(PROJECT_DIR_NAME)"
则程序在运行时,会报出栈移除提示。
那编译器如何做到“栈保护”的呢?
也许听着挺高大上,但是代码面前了无秘密,其原理也非常的简单。
我们看下这段代码的汇编
+12和+21这两行,汇编将段寄存器fs偏移+0x28的值保存到main函数栈帧的第一个局部变量位置(-0x8(%rbp))。这个局部变量是一个隐藏变量,后续没有代码对其进行改动。
+58和+62这两行,将检测第一个局部变量的值和寄存器fs偏移+0x28的值是否一致。如果一致就说明没问题,如果不一致就说明栈被污染了。
延伸阅读
那么段寄存器fs偏移+0x28是什么值?它为什么会在函数调用期间值不变?
这是因为fs断寄存器保存的是当前线程的TLS(Thread Local Storage),这样这个函数在线程上调度时,并不会被其他线程影响这段空间值。
但是GDB却无法打印出FS段地址,这个需要我们对代码进行改造,引入自定义的fs_base方法
#include <asm/prctl.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdint.h>
uint64_t fs_base() {
uint64_t fs_base;
long result = syscall(SYS_arch_prctl,ARCH_GET_FS,&fs_base);
if (result == -1) {
return 0;
}
return fs_base;
}
uint64_t gs_base() {
uint64_t gs_base;
long result = syscall(SYS_arch_prctl,ARCH_GET_GS,&gs_base);
if (result == -1) {
return 0;
}
return gs_base;
}
int main() {
char buffer[8] = {0};
buffer[0] = 'a';
buffer[1] = buffer[0] + 1;
buffer[8] = 'b';
return 0;
}
然后我们调试这段代码,可以看到被赋值到rax寄存器中的,%fs:0x28指向的值是0x6ac935d29472ff00。
而fs段地址通过gdb看不到
我们在gdb中使用下面指令来查看fs段地址
p/x (uint64_t) fs_base()
然后查看偏移0x28的空间中的值,可以发现这个值和上面被赋值到rax寄存器中的值一致。
那我们如何确定它是TLS中的地址呢?
我们可以通过下面指令查看TLS的起始地址
p (void*) pthread_self()
我们会发现这个地址和我们通过fs_base获得的地址是一样的。