off by one初探
Table of Contents
Description
off-by-one漏洞是一种特殊的缓冲区溢出漏洞,其特殊之处在于off-by-one漏洞仅允许溢出一个字节,且该溢出字节未必是可控的。off-by-one漏洞常见于以下两种情况:
- 错误地设置了循环的边界(如将"<“误写为”<=");
- 错误地使用了字符串处理函数字符串处理函数&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