栈溢出入门01 ret2text

先简单介绍一下使用的平台,在VM虚拟机里使用的kali-2024.02的一个版本,gdb是自己编译的一个14.02版本,并且安装了pwntools和peda两个插件(如果你安装了pwndbg就不用再安装peda了,反之亦然)。

pwndbg和peda基本上能互换,比如为了测试缓冲区溢出长度而生成特定字符串:peda用的是pattern create 200和pattern_offset;pwndbg用的是cyclic 200和cyclic -l 。

本博客适合什么程度的人?适合已经学完c语言并且c语言二级考试到优秀这一档的(鄙人C二级92分还是94分来着),会Linux基本操作,学过操作系统原理,略微懂汇编语言,会基本的ida操作。

一、程序堆栈结构

我知道这玩意很枯燥,但是这个比较重要,搞不懂这个,以后的东西就都学不明白。

众所周知,程序的栈是由高地址向低地址增长的,普通C语言程序的参数是从右向左压栈的。

调用test32的程序是从0x08049179开始到0x08049186结束依次压栈,压栈顺序正好是从由向左依次压栈。程序是我多年前从网上看来的来源忘了,但是从Dump of assember code for function main:开始是我从本机用GDB调试出来的,由此可知在32位程序传参是依靠堆栈进行的,且参数从右向左依次压栈。注意:这里说的是32位的程序,64位的程序以及内核里的程序和我们说的传参方式不一样!

所以我们由如下的一个程序调用栈典型分布图

 

                                          图1.函数调用栈典型分布

 

好了上例题:

文件名stackov1.c

 1 #include <stdio.h>
 2 //目标是调用这个success函数,相当于一个后门函数
 3 void success()
 4 {
 5     puts("you success get the flag!");//system("/bin/sh");即可以获得shell权限
 6 }
 7 //存在漏洞的函数
 8 void vul()
 9 {
10     char s[14];
11     gets(s);//漏洞出现的原因gets函数不安全,可以读入任意长度字符
12     puts(s);
13 }
14 int main()
15 {
16     vul();//在此处调用漏洞函数
17     return 0;
18 }

使用gcc编译:gcc -m32 -z execstack -z norelro -no-pie -fno-stack-protector stackov1.c -o stackov1.o

更新:gcc -m32 -z execstack -z norelro -fno-stack-protector -no-pie -fno-pie -o stackov1.o stackov1.c

应该使用后边的这一条编译命令,否则在main函数里溢出函数的时候会有canary,main函数的汇编里边也会多一条__x86.get_pc_thunk.ax指令;并且溢出位置也会和ida计算出来的后移4字节。
该命令的意思是编译32为程序关闭堆栈不可执行(NX),PIE(),Canary,并且指定输出文件名为stackov1.o

接下来进入gdb(安装了peda插件),使用checksec命令查看stackov1.o启用了哪些保护

 如上图所示,四行红色的都是disabled表示所有保护均未启用。

用ida看看vul函数中局部变量的地址:

就是用ida打开stackov1.o文件然后从最左边函数名称那里找到vul函数双击进入该函数,能看到是vul函数的反汇编代码,之后按F5反编译就有下图了。

可以看到局部变量s有18字节长,但是gets函数可以读取任意长度字符,如果超过18字节就有可能栈溢出。

 注意我们源代码中明明s长度写的是14但是编译出来变成18了,可能是编译器字节对齐的原因。

并且我们观察到ida反编译后边有个注释[ebp-16h],这说明s距离栈底指针ebp有0x16个字节,参考图1 函数调用栈的典型分布可以得到vul函数的栈分布

 显而易见,如果我们想将return address覆盖成我们自己的地址还需要向上溢出:0x16+0x4(old ebp的位置)+我们的后门地址。

我们用pattern create 30和pattern_offset就可以测量出s与return address之间的偏移量(应该为0x1A)

 上图所示,我们使用gdb stackov1.o调试,进入gdb后用b vul在vul函数处下了断点,然后用pattern create 30创建了 一个30个字符的测试字符。

之后使用r命令运行stackov1.o

 再然后使用c命令继续运行程序到我们可以输入的时候,这是输入测试字符,之后可以发现gdb报告了Segmentation fault即段错误。

 继续可以看出0x41414441是栈溢出时eip的内容,所以使用pattern_offsett 0x41414441计算出s到return address的偏移量为26,即我们先前计算的0x1A(我们刚才手工计算出来了偏移地址!)

为了保险起见我们再实验一下26个字符+return address的结果对不对,为此我们构造如下字符01234567890123456789012345abcd,可以数一下前边的数字是26个,所以返回地址被我们覆盖成abcd了。我们按r重新运行

然后按c到我们输入的地方并且输入我们刚才构造的字符

 报了段错误继续看最底下的报错

 报错是0x64636261是从ascii码翻译一下是dcba?为什么哪,是因为我们的计算机使用的是小段序存储的数据,与之对应的还有一个大端序这里不展开讲了,非常简单的知识点,提一下自己查。

最后我们想让函数执行success函数,那么success函数的地址在哪里找?因为没有启动PIE,就是程序每一次加载都是从同样的地方,程序基址不变,所以可以直接从ida查。

 从上图可以看出success地址是0x08049176

翻译成ascii码会有不可见字符,咱们直接明文输入是不行了,所以需要使用pwntools发送payload进行攻击。

编写exp.py文件

1 from pwn import *
2 sh = process('./stackov1.o')#运行二进制文件
3 buf2_addr = 0x08049176#success的地址
4 sh.sendline(b'A'*26 + p32(buf2_addr))#输入26个A字母和转换为小端序的32为地址
5 sh.interactive()#交互命令

编写完后运行exp.py文件成功显示you success get the flag! 证明我们成功调用了success,并且右下角$号变成红色(sh.interactive()的原因,实际上没获得shell)

 结束!

 

posted @ 2024-07-04 23:06  24K砖家  阅读(47)  评论(0编辑  收藏  举报