前言
参考:
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/off-by-one
程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节
原理
单字节缓冲区溢出,往往与边界验证不严和字符串操作有关:
- 使用循环语句向堆块中写入数据时,循环的次数设置错误导致多写入了一个字节。
- 字符串操作不合适
void receive(int socket){
char buf[MAX];
int nbytes = rev(socket, buf, sizeof(buf), 0); //接收字节数为sizeof(buf)
buf[nbytes] = '\0'; //间接引用将会在分配内存边界+1的地方写入空字节
...
}
利用思路
- 溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据
- 溢出字节为 NULL 字节:在 size 为 0x100 的时候,溢出 NULL 字节可以使得
prev_in_use
位被清,这样前块会被认为是 free 块- 这时可以选择使用 unlink 方法进行处理
- 另外,这时
prev_size
域就会启用,就可以伪造prev_size
,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照prev_size
找到的块的大小与prev_size
是否一致(libc-2.29之后被check)
示例
边界验证不严
int my_gets(char *ptr,int size)
{
int i;
for(i=0;i<=size;i++)
{
ptr[i]=getchar();
}
return i;
}
int main()
{
void *chunk1,*chunk2;
chunk1=malloc(16);
chunk2=malloc(16);
puts("Get Input:");
my_gets(chunk1,16);
return 0;
}
我们自己编写的 my_gets 函数导致了一个 off-by-one 漏洞,原因是 for 循环的边界没有控制好(i=0;i<=size;i++
)导致写入多执行了一次,这也被称为栅栏错误
栅栏错误:”建造一条直栅栏(即不围圈),长 30 米、每条栅栏柱间相隔 3 米,需要多少条栅栏柱?”
最容易想到的答案 10 是错的。这个栅栏有 10 个间隔,11 条栅栏柱
编译并用gdb调试一下程序(添加 -g
选项以保留调试符号)
gcc -g -o off_by_one off_by_one.c
下断点走到输入之前
gdb ./off_by_one
pwndbg> b main
pwndbg> r
# 在第一次 malloc 处暂停
pwndbg> break malloc
pwndbg> continue
# 退出第一次 malloc
pwndbg> finish
# 在第二次 malloc 处暂停
pwndbg> continue
pwndbg> finish
# 查看堆
pwndbg> heap
此时堆状态:
Allocated chunk | PREV_INUSE
Addr: 0x555555559000 ← 堆起始地址(heap base)
Size: 0x291 ← 初始 top chunk 大小(0x290 字节)
Allocated chunk | PREV_INUSE
Addr: 0x555555559290 ← chunk1 的起始地址
Size: 0x21 ← chunk1 的大小(0x20 字节,包含元数据)
Allocated chunk | PREV_INUSE
Addr: 0x5555555592b0 ← chunk2 的起始地址
Size: 0x21 ← chunk2 的大小(0x20 字节,包含元数据)
Top chunk | PREV_INUSE
Addr: 0x5555555592d0 ← 当前 top chunk 起始地址
Size: 0x20d31 ← 剩余可用堆空间
chunk1
和 chunk2
的地址:
chunk1
的地址是0x555555559290
(由malloc
返回的指针)。chunk2
的地址是0x5555555592b0
。
用户数据区地址:
chunk1
的数据区从0x5555555592a0
开始(chunk1
地址 + 0x10)。chunk2
的数据区从0x5555555592c0
开始(chunk2
地址 + 0x10)。
看一下 chunk1 和 chunk2 的内容
pwndbg> telescope 0x555555559290 10
00:0000│ 0x555555559290 ◂— 0x0
01:0008│ 0x555555559298 ◂— 0x21 /* '!' */ ← chunk1 的元数据(size=0x20)
02:0010│ 0x5555555592a0 ◂— 0x0
... ↓ 2 skipped
05:0028│ 0x5555555592b8 ◂— 0x21 /* '!' */ ← chunk2 的元数据(size=0x20)
06:0030│ rax r9 0x5555555592c0 ◂— 0x0
... ↓ 2 skipped
09:0048│ 0x5555555592d8 ◂— 0x20d31
然后下断点到 my_gets 和 return 准备触发堆溢出
b my_gets
b 17
c
continue 直到停下等待输入,此时输入 17 个 A
pwndbg> c
Continuing.
AAAAAAAAAAAAAAAAA
Breakpoint 5, main () at off_by_one.c:17
17 return 0;
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────
*RAX 0x11
*RBX 0x0
*RCX 0x10
*RDX 0x41
*RDI 0x0
*RSI 0x5555555596f0 ◂— 'AAAAAAAAAAAAAAAAA\n'
R8 0x0
*R9 0x5555555596f0 ◂— 'AAAAAAAAAAAAAAAAA\n'
*R10 0x77
R11 0x246
R12 0x7fffffffdcf8 —▸ 0x7fffffffdf6b ◂— 0xe62f642f746e6d2f
R13 0x5555555551d1 (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 0x7fffffffdbe0 ◂— 0x1
*RSP 0x7fffffffdbd0 —▸ 0x5555555592a0 ◂— 'AAAAAAAAAAAAAAAAA'
*RIP 0x555555555219 (main+72) ◂— mov eax, 0
───────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────
► 0x555555555219 <main+72> mov eax, 0
0x55555555521e <main+77> leave
0x55555555521f <main+78> ret
↓
0x7ffff7daed90 <__libc_start_call_main+128> mov edi, eax
0x7ffff7daed92 <__libc_start_call_main+130> call exit <exit>
0x7ffff7daed97 <__libc_start_call_main+135> call __nptl_deallocate_tsd <__nptl_deallocate_tsd>
0x7ffff7daed9c <__libc_start_call_main+140> lock dec dword ptr [rip + 0x1ef505] <__nptl_nthreads>
0x7ffff7daeda3 <__libc_start_call_main+147> sete al
0x7ffff7daeda6 <__libc_start_call_main+150> test al, al
0x7ffff7daeda8 <__libc_start_call_main+152> jne __libc_start_call_main+168 <__libc_start_call_main+168>
0x7ffff7daedaa <__libc_start_call_main+154> mov edx, 0x3c
─────────────────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────────────────
In file: /mnt/d/pwn_learning/heap/off_by_one.c
12 void *chunk1,*chunk2;
13 chunk1=malloc(16);
14 chunk2=malloc(16);
15 puts("Get Input:");
16 my_gets(chunk1,16);
► 17 return 0;
18 }
─────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdbd0 —▸ 0x5555555592a0 ◂— 'AAAAAAAAAAAAAAAAA'
01:0008│ 0x7fffffffdbd8 —▸ 0x5555555592c0 ◂— 0x0
02:0010│ rbp 0x7fffffffdbe0 ◂— 0x1
03:0018│ 0x7fffffffdbe8 —▸ 0x7ffff7daed90 (__libc_start_call_main+128) ◂— mov edi, eax
04:0020│ 0x7fffffffdbf0 ◂— 0x0
05:0028│ 0x7fffffffdbf8 —▸ 0x5555555551d1 (main) ◂— endbr64
06:0030│ 0x7fffffffdc00 ◂— 0x1ffffdce0
07:0038│ 0x7fffffffdc08 —▸ 0x7fffffffdcf8 —▸ 0x7fffffffdf6b ◂— 0xe62f642f746e6d2f
───────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────
► 0 0x555555555219 main+72
1 0x7ffff7daed90 __libc_start_call_main+128
2 0x7ffff7daee40 __libc_start_main+128
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg>
此时观察堆内容:
pwndbg> x/gx 0x5555555592b0-0x10
0x5555555592a0: 0x4141414141414141
pwndbg> telescope 0x555555559290 10
00:0000│ 0x555555559290 ◂— 0x0
01:0008│ 0x555555559298 ◂— 0x21 /* '!' */
02:0010│ 0x5555555592a0 ◂— 'AAAAAAAAAAAAAAAAA'
03:0018│ 0x5555555592a8 ◂— 'AAAAAAAAA'
04:0020│ 0x5555555592b0 ◂— 0x41 /* 'A' */
05:0028│ 0x5555555592b8 ◂— 0x21 /* '!' */
06:0030│ 0x5555555592c0 ◂— 0x0
... ↓ 2 skipped
09:0048│ 0x5555555592d8 ◂— 0x411
可以看到原本是 chunk2 元数据区的 0x5555555592b0 被溢出覆盖了 0x41
字符串操作
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;
}
看着没啥问题(不考虑栈溢出),但是 strlen
和 strcpy
的行为不一致却导致了 off-by-one 的发生
strlen 在计算字符串长度时是不把结束符 '\x00'
计算在内的,但是 strcpy 在复制字符串时会拷贝结束符 '\x00'
这就导致了我们实际向 chunk1 中写入了 25 个字节