这篇文章是csapp第三章第10节的阅读笔记。
0x00 指针
- 每一个指针都有对应的类型。不过指针类型不包含在汇编代码中,而是C对程序员提供的一种抽象;
- 每一个指针都有值。这个值就是某个对象的地址,特殊的值
NULL
(0)表明这个指针不指向任何地址; - 通过&操作符创建指针。这会使用
leaq
指令来完成; - 指针通过*操作符进行解引用。就是读取内存中的内容;
- 数组和指针很相似。数组名字就像指针,但是不能更新值;
- 指针类型可以转换;
- 指针还可以指向函数。
0x01 超出范围的内存引用和缓冲区溢出
C语言不对数组引用的边界进行检查,同样函数的本地变量和栈上的一些信息存放在一起,比如保存的寄存器的值和函数返回地址等。
这样就可能发生超出范围的内存引用,导致一些信息被覆盖。
比如下面的例子:
/* Implementation of library function gets() */
char* gets(char* s) {
int c;
char* dest = s;
while ((c = getchar()) != '\n' && c != EOF)
*dest++ = c;
if (c == EOF && dest == s)
return NULL;
*dest++ = '\0';
return s;
}
/* Read input line and write it back */
void echo() {
char buf[8];
gets(buf);
puts(buf);
}
编译后的代码:
echo:
subq %24, %rsp ; Allocate 24 bytes on stack
movq %rsp, %rdi ; Compute buf as %rsp
call gets ; Call gets
movq %rsp, %rdi ; Compute buf as %rsp
call puts ; Call puts
addq $24, %rsp ; Deallocate stack space
ret ; Return
运行时栈的布局如下:
当写入的字符超过24个时,就会覆盖返回地址。
Characters typed | Additional corrupted state |
---|---|
0-7 | None |
9-23 | Unused stack space |
24-31 | Return address |
32+ | Saved state in caller |
这就需要有些方式防止这种问题与攻击。
0x02 防止缓冲区溢出攻击
现在的gcc可以通过如下几种方式来防止缓冲区溢出攻击。
2.1 Stack Randomization
为了达到缓冲区溢出攻击的目的,攻击者需要能够计算出函数返回地址在栈中的偏移量,然后准确计算出需要覆盖的值。
所以可以在申请栈空间的时候引入一个随机量,这样不同的机器函数返回地址在栈中的偏移量是不同的,就增加了攻击的难度。
这就是stack randomization,可以通过alloca
函数完成。
通过下面的代码可以查看当前栈的地址:
int main() {
long local;
printf("local at %p\n", &local);
return 0;
}
不过一个攻击者可以通过蛮力来搜索,并且添加一系列的nop
指令来跳过无用的地址。
比如如果上面的代码执行10000次,发现大致的范围是0xffffb754
到0xffffd754
,那么随机的空间就是2^13,如果攻击者使用了128个字节的nop
指令的话,那么需要尝试64次就可以了。
2.2 Stack Corruption Detection
第二种方法可以检测是否发生了对超出范围的栈空间进行了写入操作。
这个方法是在申请的空间后面添加一个标志,函数返回的时候检查这个标志,如果有变动,说明发生了错误。
这个特殊的值叫做canary,可以从内存中随机选取:
现在的编译器默认就是启用了这个特征,如果不启用的话,可以使用-fno-stack-protector
选项。
下面的代码是启用了这个特征的代码:
echo:
subq $24, %rsp ; Allocate 24 bytes on stack
movq %fs:40, %rax ; Retrieve canary
movq %rax, 8(%rsp) ; Store on stack
xorl %eax, %eax ; Zero out register
movq %rsp, %rdi ; Compute buf as %rsp
call gets ; Call gets
movq %rsp, %rdi ; Compute buf as %rsp
call puts ; Call puts
movq 8(%rsp), %rax ; Retrieve canary
xorq %fs:40, %rax ; Compare to stored value
je .L9 ; If =, goto ok
call __stack_chk_fail ; Stack corrupted!
.L9: ; ok:
addq $24, %rsp ; Deallocate stack space
ret
上面的代码可以清楚地看出这个策略。
2.3 Limiting Executable Code Regions
第三种方法就是限制可执行的代码区域,这样即使插入代码也不能执行,防止了缓冲区溢出攻击。
0x03 可变大小的栈Frame
前面的一些例子中,编译期间就可以知道所需要的栈空间大小。
还有一种情况,需要使用可变大小的栈空间,这里看看是如何实现的。
比如下面的函数:
long vframe(long n, long idx, long *p) {
long i;
long *p[n];
p[0] = &i;
for (i = 1; i < n; i++)
p[i] = q;
return *p[idx];
}
这里就是根据参数n
在栈上创建一个数组,这个大小在编译期间是不确定的,需要在执行的时候才能知道大小。
编译后的代码:
vframe:
pushq %rbp ; Save old %rbp
movq %rsp, %rbp ; Set frame pointer
subq $16, %rsp ; Allocate space for i (%rsp = s1)
leaq 22(,%rdi,8), %rax ; Compute 22 + n*8 as x
andq $-16, %rax ; Make x = 16 + n*8(n is odd) or 8 + n*8(n is even)
subq %rax, %rsp ; Allocate space for array p (%rsp = s2)
leaq 7(%rsp), %rax ; Add bias to round up
shrq $3, %rax ; divided by 8
leaq 0(,%rax,8), %r8 ; Set %r8 to &p[0]
movq %r8, %rcx ; Set %rcx to &p[0] (%rcx = p)
.L3: ; loop:
movq %rdx, (%rcx,%rax,8) ; Set p[i] to q
addq $1, %rax ; Increment i
movq %rax, -8(%rbp) ; Store on stack
.L2:
movq -8(%rbp), %rax ; Retrieve i from stack
cmpq %rdi, %rax ; Compare i:n
jl .L3 ; If <, goto loop
leave ; Restore %rbp and %rsp
ret ; Return
栈的布局如下:
上面的代码使用了一系列的技巧,来在栈中申请一块满足对齐的空间。
首先在第2行将%rbp保存在栈中,这个%rbp作为base pointer(使用%rsp作为初始值);
第3行中在栈中申请16字节的空间,用来保存函数的局部变量i;
参数n存在%rdi中,第四行计算22+n*8,结果保存在%rax中,开始计算所需空间了;
第6行的
andq
指令使用了一个小技巧,-16的编码是0xFFFFFFF0
,将%rax中低4位的值置零,是为了得到一个刚好是16的倍数。这样%rax中的值就是16+n*8
(n为奇数)或8+n*8
(n为偶数);第7行就直接在栈上申请空间了;
接下来需要挪动一下p的值,来进行对齐;
8-10行就是对p进行对齐的,其中的7就是bias,向上取整,然后p的值就存在%rcx中了;
接下来就是循环操作了;
最后的
leave
指令恢复%rbp和%rsp的值,效果同:movq %rbp, %rsp popq %rbp
下面的两个例子展示了不同情况下图中的一些值:
n | s1 | s2 | p | e1 | e2 |
---|---|---|---|---|---|
5 | 2065 | 2017 | 2024 | 1 | 7 |
6 | 2064 | 2000 | 2000 | 16 | 0 |