csapp ch03 (part 5): Combining Control and Data

这篇文章是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次,发现大致的范围是0xffffb7540xffffd754,那么随机的空间就是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

 Previous
new & make in Golang new & make in Golang
new和make都可以用来分配空间和初始化类型,但是它们又有一些不同。 1. new(T)返回的是T的指针new(T)为一个T类型新值分配空间并将此空间初始化为T的零值,返回的是新值的地址,也就是T类型的指针*T,该指针指向T的新分配的
2020-07-03
Next 
csapp ch03 (part 4): Data Structures csapp ch03 (part 4): Data Structures
这篇文章是csapp第三章第8和第9节的阅读笔记。 接下来,从汇编代码的角度来看看C语言中数据结构的实现。 0x00 Array先看看数组。 1.1 基本规则对于具有N个类型T的元素的数组来说,C语言中是这么声明的: T A[N]; 令
2020-06-15
  You Will See...