目录

  1. 1. 前言
  2. 2. 原理
  3. 3. 利用思路
  4. 4. 示例
    1. 4.1. 边界验证不严
    2. 4.2. 字符串操作

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Off-By-One

2025/3/18 Pwn Heap
  |     |   总文章阅读量:

前言

参考:

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的地方写入空字节
        ...
}

利用思路

  1. 溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据
  2. 溢出字节为 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          ← 剩余可用堆空间

chunk1chunk2 的地址

  • 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;
}

看着没啥问题(不考虑栈溢出),但是 strlenstrcpy 的行为不一致却导致了 off-by-one 的发生

strlen 在计算字符串长度时是不把结束符 '\x00' 计算在内的,但是 strcpy 在复制字符串时会拷贝结束符 '\x00'

这就导致了我们实际向 chunk1 中写入了 25 个字节