off by one初探

Table of Contents

Description

off-by-one漏洞是一种特殊的缓冲区溢出漏洞,其特殊之处在于off-by-one漏洞仅允许溢出一个字节,且该溢出字节未必是可控的。off-by-one漏洞常见于以下两种情况:

  1. 错误地设置了循环的边界(如将"<“误写为”<=");
  2. 错误地使用了字符串处理函数字符串处理函数&zhida_source=entity)(不同的字符串处理函数对字符串末尾的"\0“的处理方式不同,如果将二者混用便可能导致末尾的”\0“发生溢出)。

循环边界设置不当

char a[16];
for(int i = 0;i<=16;i++)
{
    read(0,a,1);
}

在上述代码片段中看出其实这个循环进行了17次,多向a中读入了一个字节,造成了溢出,攻击者可以通过这个漏洞达成许多攻击效果。

这一错误也被称为栅栏错误 wikipedia: 栅栏错误(有时也称为电线杆错误或者灯柱错误)是差一错误的一种。如以下问题:

建造一条直栅栏(即不围圈),长 30 米、每条栅栏柱间相隔 3 米,需要多少条栅栏柱?

最容易想到的答案 10 是错的。这个栅栏有 10 个间隔,11 条栅栏柱。

我在 puts 处(my_gets前)和"19”(return 0)处下断点;

pwndbg> b puts
Breakpoint 1 at 0x1070
pwndbg> b 19
Breakpoint 2 at 0x1214: file example.c, line 19.
pwndbg> r
_____________________________________________________________________
pwndbg> r
Starting program: /home/zer0ptr/Pwn-Research/Heap-overflow-basic/off_by_one_example/example/offbyone_1 

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, __GI__IO_puts (str=0x555555556004 "Get Input:") at ./libio/ioputs.c:33
33      ./libio/ioputs.c: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
───────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────────────────────────────────────────────────────────────────────
 RAX  0x555555556004 ◂— 'Get Input:'
 RBX  0
 RCX  0x21
 RDX  0
 RDI  0x555555556004 ◂— 'Get Input:'
 RSI  0x5555555592d0 ◂— 0
 R8   0x21001
 R9   0x5555555592c0 ◂— 0
 R10  0xfffffffffffff000
 R11  0x7ffff7e1ace0 (main_arena+96) —▸ 0x5555555592d0 ◂— 0
 R12  0x7fffffffd718 —▸ 0x7fffffffda52 ◂— '/home/zer0ptr/Pwn-Research/Heap-overflow-basic/off_by_one_example/example/offbyone_1'
 R13  0x5555555551cc (main) ◂— endbr64 
 R14  0x555555557db0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555140 (__do_global_dtors_aux) ◂— endbr64 
 R15  0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
 RBP  0x7fffffffd600 ◂— 1
 RSP  0x7fffffffd5e8 —▸ 0x555555555203 (main+55) ◂— mov rax, qword ptr [rbp - 0x10]
 RIP  0x7ffff7c80e50 (puts) ◂— endbr64 
────────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────────────────────────────────────────────────────────────────────
 ► 0x7ffff7c80e50 <puts>       endbr64 
   0x7ffff7c80e54 <puts+4>     push   r14
   0x7ffff7c80e56 <puts+6>     push   r13
   0x7ffff7c80e58 <puts+8>     push   r12
   0x7ffff7c80e5a <puts+10>    mov    r12, rdi      R12 => 0x555555556004 ◂— 'Get Input:'
   0x7ffff7c80e5d <puts+13>    push   rbp
   0x7ffff7c80e5e <puts+14>    push   rbx
   0x7ffff7c80e5f <puts+15>    sub    rsp, 0x10     RSP => 0x7fffffffd5b0 (0x7fffffffd5c0 - 0x10)
   0x7ffff7c80e63 <puts+19>    call   *ABS*+0xa86a0@plt           <*ABS*+0xa86a0@plt>
 
   0x7ffff7c80e68 <puts+24>    mov    r13, qword ptr [rip + 0x198fc9]     R13, [0x7ffff7e19e38] => 0x7ffff7e1b868 (stdout)
   0x7ffff7c80e6f <puts+31>    mov    rbx, rax
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffd5e8 —▸ 0x555555555203 (main+55) ◂— mov rax, qword ptr [rbp - 0x10]
01:0008│-010 0x7fffffffd5f0 —▸ 0x5555555592a0 ◂— 0
02:0010│-008 0x7fffffffd5f8 —▸ 0x5555555592c0 ◂— 0
03:0018│ rbp 0x7fffffffd600 ◂— 1
04:0020│+008 0x7fffffffd608 —▸ 0x7ffff7c29d90 (__libc_start_call_main+128) ◂— mov edi, eax
05:0028│+010 0x7fffffffd610 ◂— 0
06:0030│+018 0x7fffffffd618 —▸ 0x5555555551cc (main) ◂— endbr64 
07:0038│+020 0x7fffffffd620 ◂— 0x1ffffd700
────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0   0x7ffff7c80e50 puts
   1   0x555555555203 main+55
   2   0x7ffff7c29d90 __libc_start_call_main+128
   3   0x7ffff7c29e40 __libc_start_main+128
   4   0x5555555550c5 _start+37

从上面这一大坨拉出来:

pwndbg> x/10gx 0x5555555592a0
0x5555555592a0: 0x0000000000000000      0x0000000000000000
0x5555555592b0: 0x0000000000000000      0x0000000000000021
0x5555555592c0: 0x0000000000000000      0x0000000000000000
0x5555555592d0: 0x0000000000000000      0x0000000000020d31
0x5555555592e0: 0x0000000000000000      0x0000000000000000
pwndbg> x/10gx 0x5555555592c0
0x5555555592c0: 0x0000000000000000      0x0000000000000000
0x5555555592d0: 0x0000000000000000      0x0000000000020d31
0x5555555592e0: 0x0000000000000000      0x0000000000000000
0x5555555592f0: 0x0000000000000000      0x0000000000000000
0x555555559300: 0x0000000000000000      0x0000000000000000

这里已经分配了两个用户区域为16的堆块 当我们执行 my_gets 进行输入之后,可以看到数据发生了溢出:第25个字节0x61覆盖了下一个堆块的size字段的低字节

pwndbg> x/10gx 0x5555555592a0
0x5555555592a0: 0x6161616161616161      0x6161616161616161
0x5555555592b0: 0x0000000000000061      0x0000000000000021
0x5555555592c0: 0x0000000000000000      0x0000000000000000
0x5555555592d0: 0x0000000000000000      0x0000000000000411
0x5555555592e0: 0x75706e4920746547      0x00000000000a3a74

字符串结束符

第二种常见的导致 off-by-one 的场景就是字符串操作了,常见的原因是字符串的结束符计算有误:

#include <stdio.h>
#include <stdlib.h>  
#include <string.h>  

int main(void)
{
    char buffer[40]="";
    void *chunk1;
    chunk1=malloc(24);
    puts("Get Input");
    gets(buffer);
    if(strlen(buffer)==24)
    {
        strcpy(chunk1,buffer);
    }
    return 0;
}

// gcc -g -o offbyone_2 offbyone_2.c -no-pie -fno-stack-protector -z execstack

程序乍看上去没有任何问题(不考虑栈溢出),可能很多人在实际的代码中也是这样写的。 但是 strlen 和 strcpy 的行为不一致却导致了 off-by-one 的发生。 strlen 是我们很熟悉的计算 ascii 字符串长度的函数,这个函数在计算字符串长度时是不把结束符 ‘\x00’ 计算在内的,但是 strcpy 在复制字符串时会拷贝结束符 ‘\x00’ 。这就导致了我们向 chunk1 中写入了 25 个字节,我们使用 gdb 进行调试可以看到这一点。
我在*main+62处(malloc调用后的返回地址)和 *main+141处(程序执行完毕返回地址处) strcpy下断点:

pwndbg> disassemble main
Dump of assembler code for function main:
   0x00000000004011b6 <+0>:     endbr64 
   0x00000000004011ba <+4>:     push   rbp
   0x00000000004011bb <+5>:     mov    rbp,rsp
   0x00000000004011be <+8>:     sub    rsp,0x30
   0x00000000004011c2 <+12>:    mov    QWORD PTR [rbp-0x30],0x0
   0x00000000004011ca <+20>:    mov    QWORD PTR [rbp-0x28],0x0
   0x00000000004011d2 <+28>:    mov    QWORD PTR [rbp-0x20],0x0
   0x00000000004011da <+36>:    mov    QWORD PTR [rbp-0x18],0x0
   0x00000000004011e2 <+44>:    mov    QWORD PTR [rbp-0x10],0x0
   0x00000000004011ea <+52>:    mov    edi,0x18
   0x00000000004011ef <+57>:    call   0x4010c0 <malloc@plt>
   0x00000000004011f4 <+62>:    mov    QWORD PTR [rbp-0x8],rax
   0x00000000004011f8 <+66>:    lea    rax,[rip+0xe05]        # 0x402004
   0x00000000004011ff <+73>:    mov    rdi,rax
   0x0000000000401202 <+76>:    call   0x401090 <puts@plt>
   0x0000000000401207 <+81>:    lea    rax,[rbp-0x30]
   0x000000000040120b <+85>:    mov    rdi,rax
   0x000000000040120e <+88>:    mov    eax,0x0
   0x0000000000401213 <+93>:    call   0x4010b0 <gets@plt>
   0x0000000000401218 <+98>:    lea    rax,[rbp-0x30]
   0x000000000040121c <+102>:   mov    rdi,rax
   0x000000000040121f <+105>:   call   0x4010a0 <strlen@plt>
   0x0000000000401224 <+110>:   cmp    rax,0x18
   0x0000000000401228 <+114>:   jne    0x40123d <main+135>
   0x000000000040122a <+116>:   lea    rdx,[rbp-0x30]
   0x000000000040122e <+120>:   mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000401232 <+124>:   mov    rsi,rdx
   0x0000000000401235 <+127>:   mov    rdi,rax
   0x0000000000401238 <+130>:   call   0x401080 <strcpy@plt>
   0x000000000040123d <+135>:   mov    eax,0x0
   0x0000000000401242 <+140>:   leave  
   0x0000000000401243 <+141>:   ret    
End of assembler dump.
pwndbg> b *main+62
Breakpoint 2 at 0x4011f4: file offbyone_2.c, line 9.
pwndbg> b strcpy
Breakpoint 3 at 0x401243: file offbyone_2.c, line 14.
pwndbg> 

malloc调用后chunk1如下:

pwndbg> x/10gx 0x4052a0
0x4052a0:       0x0000000000000000      0x0000000000000000
0x4052b0:       0x0000000000000000      0x0000000000020d51
0x4052c0:       0x0000000000000000      0x0000000000000000
0x4052d0:       0x0000000000000000      0x0000000000000000
0x4052e0:       0x0000000000000000      0x0000000000000000

然后c输入b’a’ * 24后观察:

pwndbg> x/10gx 0x4052a0
0x4052a0:       0x6161616161616161      0x6161616161616161
0x4052b0:       0x6161616161616161      0x0000000000000400
0x4052c0:       0x75706e4920746547      0x0000000000000a74
0x4052d0:       0x0000000000000000      0x0000000000000000
0x4052e0:       0x0000000000000000      0x0000000000000000
pwndbg> 

可以看到 next chunk 的 size 域低字节被结束符 '\x00' 覆盖,这种又属于 off-by-one 的一个分支称为 NULL byte off-by-one,我们在后面会看到 off-by-one 与 NULL byte off-by-one 在利用上的区别。 还是有一点就是为什么是低字节被覆盖呢,因为我们通常使用的 CPU 的字节序都是小端法的,比如一个 DWORD 值在使用小端法的内存中是这样储存的:

DWORD 0x41424344
内存  0x44,0x43,0x42,0x41

References