从汇编层看64位程序运行——栈保护

《从汇编层看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获得的地址是一样的。

在这里插入图片描述

参考资料

  • 26
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值