抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

前置知识:汇编语言

系统篇

栈溢出

缓冲区是一片有限的、连续的内存区域。在C语言中最常见的缓冲区是数组。

因为在C和C++语言中没有考虑检查缓冲区的内在边界,所以使栈溢出成为可能。当输入的数据足够大时,将会溢出缓冲区的范围,从而改写从而改写其他缓存区域。

函数与栈

调用函数的整个过程如下

    1. 把函数的参数压入栈
    1. 把函数的返回地址压入栈
      • 主程序调用子程序,子程序结束后继续上次的位置执行主程序
    1. 调用函数

为了使函数可以引用栈上的数据,必须改变栈底指针EBP的值,把EBP的当前值压入栈,把当前的栈顶ESP复制到EBP,函数接受后再恢复。这样我们就可以方便地引用栈地址了。

接着编译器计算函数的局部变量所需的地址空间和栈上的保留空间,然后从ESP减去变量的大小,为程序保留必要的空间,最后把函数的局部变量压入栈(这我们举例:数组)。结构如下:

低内存地址,栈顶
数组
EBP
RET
参数
高内存地址,栈底

栈上的缓冲区溢出

由上述结构可见,如果数组很大,将会溢出,然后改写其他缓存区域。

  • 控制EIP
    • CPU执行什么指令由CS:IP的指向决定
    • 我们只要精心设计溢出的数据,这些地址将写入缓冲器并改写保留在缓冲区的EBP和RET。当系统从栈中取出RET的值并放入EIP时,这个地址指向的指令将被执行。

利用漏洞获得root权限

我们可以攻击以root权限运行的进程,通过溢出强制它执行shell,这个shell将继承root权限。然而缓冲区只认得机器指令(opcode)。为了把opcode插入缓存区,必须吧派生的shell的C代码编译成汇编指令,然后从可读的汇编指令中提取opcode。这些被称为shellcode或opcode的代码可以注入缓冲区,并可执行。

地址问题

当试图执行用户提交的shellcode时,所面临的问题是找出shellcode的起始地址。(想办法使EIP指向这个地址)

先介绍一种使用最广的方法:猜。每个程序的栈都以同样的地址开始。(现在大多数操作系统故意变化栈地址,从而使这类的攻击变得困难)知道这个地址就可以猜测shellcode的起始地址和RET的地址。

  • 首先要知道ESP的地址,那么根据这个地址来猜测当前地址和shellcode之间的偏移距离。从而的到shellcode的起始地址
  • 通过尝试输入过长数据造成溢出,从造成故障的长度来猜测RET地址
  • 最后把RET地址改为shellcode的起始地址以达到执行shellcode的目的

例1 :简单试炼,破解以下程序,假设我们不知道被攻击程序的内部结构

1
2
3
4
5
6
7
8
9
// victim.c

int main(int argc, char *argv){
char array[512];

if(argc>1){
strcpy(array, argv[1]);
}
}
  • 如果每个程序的栈都是以同样的地址开始,则固定的程序RET地址不会改变,我们可以通过溢出找出RET地址
    • $ ./victim.c $(printf "%0524" 0)利用bash快速尝试输入多个0(长数据)
  • 假设我们的shellcode有40B,上一步在524个数据时溢出,那么我们可以将上一步的长数据改成shellcode+(524-len(shellcode))个0+shellcode起始地址
    • 这里还没获得shellcode起始地址,我们先用别的地址测试填充0的个数是否正确

building…

NOP法

一个个猜太过麻烦,可以选用NOP法来增加潜在的偏移量的数量。思路就是创建一大段不运行的指令区,放在shellcode前面,当执行完NOP之后,就会执行shellcode。这样就不用精确地猜到偏移量了。

战胜不可执行栈

前面所讲的漏洞利用程序能工作,是因为可以在栈上执行指令。许多操作系统不允许在栈上执行代码。当遇到不可执行栈的时候,可以用”返回libc”方法。

栈溢出原理上其实是利用了EIP指针,那么如果我们可以完全EIP指针,那么就可以把任意想执行的代码放入EIP。返回libc是把控制权交给特定的动态库函数。动态库函数不在栈上,所以我们就可以绕开不可执行栈的限制。

为了攻击成功,需要仔细挑选动态库函数。理论上,它必须符合以下两个条件:

  • 它必须的常见的动态库函数,在绝大多数程序中出现,才便于利用
  • 函数库里的函数应该给予我们很大的灵活性,以便我们能派生shell或做其他事

libc就是满足条件的一个库函数。我们只要把执行流程指向想用的库函数的地址,它将被执行。

以下以派生shell讨论。最好用的libc函数是system()。system()接受一个参数,然后用/bin/sh/执行这个参数。根据经验主程序执行一个函数(设为func)时,参数入栈的顺序和它在代码里的顺序相反,根据这点,我们需要进行以下工作:

  • 确定system()地址
    • 主程序执行一个函数func时,call func,会把返回地址RET压入栈
  • 确定/bin/sh地址(参数地址)
    • 第一个参数位于RET之后
  • 找出exit()地址,以便干净地退出被攻击的程序

用memfetch工具可以找到/bin/sh的地址,memfetch的功能是把指定进程的内存数据全部转存到一个二进制文件中,我们可以在这个文件里找/bin/sh的地址

破解过程如下:

    1. 用垃圾数据填满缓冲区和返回地址之间的空间
    1. 用system()的地址改写返回地址
    1. 在system()后加上exit()的地址
    1. 再加上/bin/sh的地址

shellcode

shellcode是一组可注入(机械码)的程序,可以在被攻击的程序里运行。因为shellcode要直接操作寄存器和程序的函数,所以通常用汇编语言编写并翻译为十六进制操作码。之所以叫shellcode是因为通常用这种操作来派生root权限的shell。

理解系统调用

我们想让目标程序不同于设计者预期的方式运行,而操纵程序的方法之一是强制它产生系统调用(中断)。可以通过系统调用访问特定的操作系统的函数,如接受输入、处理输出、退出进程、执行二进制文件等。

通过系统调用可以直接访问系统内核,即可以访问读写文件之类的低级函数。系统调用也是受保护的内核模式与用户模式之间的接口。受保护的内核模式会阻止用户的应用程序干涉或危及操作系统。当用户模式下的程序企图访问内核的内存空间时,系统将产生异常。但是,某些程序在正常运行时,需要请求一些系统级的服务,这时系统调用就作为正常用户模式和内核模式之间的接口,在保证安全的情况下尽量相应这些请求。

在Linux里有两种方法来执行系统调用:

  • 间接方法:C库函数(libc)
  • 直接方法:汇编指令(把适当的参数加载到寄存器,然后调用中断)执行系统调用

在Linux里,程序通过int 0x80软中断来执行系统调用。当程序在用户模式下执行int 0x80时,CPU切换到内核模式并执行相应的系统调用。系统调用的过程如下:

    1. 把系统调用编号载入EAX,通过载入编号来调用对应系统函数
    1. 把系统调用的参数压入其它寄存器
    1. 执行int 0x80指令
    1. CPU切换到内核模式
    1. 执行系统函数

思考下列程序:

1
2
3
4
main(){
exit(0);
}
// gcc -static -o exit exit.c

反汇编生成的二进制文件:

1
2
3
4
5
6
7
8
9
$ gdb exit
(gdb) disas _exit
address <line>: mov 0x4(%esp,1),%ebx
address <line>: mov $Oxfc,%eax ;对应的系统调用编号被放入%EAX中
address <line>: int $0x80 ;通过int 0x80指令得知发生了系统调用
address <line>: mov $0x1,%eax ;对应的系统调用编号被放入%EAX中
address <line>: int $0x80
address <line>: hlt
address <line>: nop

exit()对应的系统调用编号是1,exit_group()对应的系统调用编号是252。在反汇编生成的代码里还有一条指令,它把系统调用的参数加载到EBX。这个参数是0,是在系统调用之前入栈的mov 0x4(%esp,1),%ebx

int 0x80指令把CPU切换到内核模式,并且执行系统调用

为exit()系统调用写shellcode

较小的shellcode可以注入更多的缓冲区,可以用来攻击更多的程序,所以要使shellcode尽量保持简单、紧凑。当攻击问题程序的时候,不仅要把shellcode复制到缓冲区,如果碰到n字节长的缓冲区,不仅要把整个shellcode复制到它里面,还要加上调用shellcode的指令,因此shellcode的长度必须小于n。基于这个原因,shellcode应尽量小。

因为实际环境中,shellcode没有其他指令为它设置参数,所以我们要精心设计。在上面exit()例子里,通过把0放入EBX可以达到设置的目的。所以我们的shellcode应该完成以下任务:

    1. 把0存到EBX
      • 参数
    1. 把1存到EAX
      • 系统调用编号
    1. 执行int 0x80指令来产生系统调用

先用汇编指令实现这3步,的到ELF格式的二进制文件,然后从这个二进制文件中提取操作码。

  • 生成目标文件
  • 链接目标文件
  • 从生成的文件提取操作码

可注入的shellcode

攻击时,最有可能用来保存shellcode的内存区域是为了保存用户输入而分配的缓冲区,甚至可以更进一步将,这个缓冲区就是一个数组。所以如果shellcode中有空值(0x00)的存在,当把shellcode复制到缓冲区(字符数组)的时候会出现异常,因为数组里空值是用来终止字符串的。所以我们要想办法把空值去掉,或把有空值的操作码转换成非空值的操作码。下面介绍一种方法:

  • 直接用其他具有相同功能的指令代替这些产生空值的指令

如果直译,shellcode使用如下汇编指令和对应的操作码:

1
2
3
mov ebx,0       ;\xbb\x00\x00\x00\x00
mov eax,1 ;\xb8\x00\x00\x00\x00
int 0x80 ;\xcd\x80

头两条是产生空值的罪魁祸首,我们可以用如下操作解决:

  • 第一条指令,我们可以用xor指令在不涉及空值的情况下给ebx赋值
    • xor ebx ebx,效果:ebx = ebx XOR ebx = 0000
  • 第二条指令汇编指令看似没有0的参与,为什么会出现空值呢?
    • 因为:这条指令使用了4B寄存器(EAX),而我们复制了1B(1)到寄存器,默认情况下系统会自动用控制填充剩下部分
    • 4B的EAX可以划分为2个2B(AX)和4个1B(AL、AH),所以我们直接使用1B的AL就可以避免
    • mov al,1

派生shell

首先写派生shell的C程序。派生shell最方便、最快捷的方法是创建新进程。在Linux里有两种方法创建新进程:

    1. 通过现有进程创建它,并用它代替现有活动进程
    1. 利用程序生成自己的副本,并在它的位置运行这个进程

下面是一个简单C程序的execve调用

1
2
3
4
5
6
7
8
#include<strio.h>

int main(){
char *cmd[2];
cmd[0] = "/bin/sh";
cmd[1] = NULL;
execve(cmd[0], cmd, NULL);
}

现在把它转换成原始十六进制指令,就像exit()一样。想观察execve的文档,提供的信息很有价值:

  • int execve(const char* filename, char* const argv[], char* const envp[])
  • execve()执行filename(指针)指向的程序
  • argv是字符串数组,用来传递参数,envp是字符串数组,用来传递环境变量。argv和envp都以空指针结束

执行execve()系统调用4个寄存器:1个用来保存系统调用值;3个用来保存系统调用参数。

在shellcode里不可以使用硬编码地址。我们希望shellcode容易移植,因此我们使用相对地址。下面介绍一种相对地址的实现方法

在shellcode里使用相对地址需要一些技巧。我们可以把shellcode在内存中的开始地址或shellcode的重要元素复制到寄存器,然后根据寄存器里的地址设计每条指令:

  • shellcode以一条跳转指令开始,跳过shellcode,跳到调用指令
  • 执行调用指令时,紧跟在调用指令之后的地址将被压入栈
    • 这里把想作为相对地址的基地址直接放在了调用指令之后
    • 需要时可以从栈中找到地址
  • 当调用指令后,我们的的基地址将自动保存在栈上,而我们不必提前知道这个地址
  • 之后,调用指令调用shellcode,执行pop esi把栈上的基地址送入ESI。至此就可以根据ESI的偏移量来引用shellcode里面的代码

伪代码如下:

1
2
3
4
5
6
7
8
9
10
    jmp short GoToCall

shellcode:
pop esi ;把'/bin/sh'送入esi,使用了相对地址
...
...

GoToCall:
call shellcode
db '/bin/sh'

下面用真正的汇编指令替代伪代码。在编写过程中,还需要在字符串尾部保留一些占位符(这里是9B),如下:

  • /bin/shJAAAAKKKK

这些占位符有什么用呢?我们将把系统调用所需的3个参数中的2个(将被载入ECX、EDX)保存在这些占位符里。因为字符串的第一个字节的地址保存在ESI里,所以对于替换和把这些值复制到寄存器来说,很容易就能确定它们所在内存中的位置。另外,可以通过”复制到占位符”方法,用空值有效终止这些字符串。步骤如下:

    1. 用xor EAX EAX的结果(空值)填充EAX
    1. 把AL复制到紧挨着/bin/sh的字符位置(J)来终止/bin/sh字符串。
      • 因为EAX是空值,所以AL也是空值
    1. 得到保存在ESI里的字符串开头地址,把它复制到EBX
    1. 把EBX里的值(字符串开头的地址)复制到AAAA占位符
      • 这是execve系统调用要求的、被执行文件的参数指针
    1. 用正确的偏移量吧保存在EAX中的空值复制到KKKK占位符
    1. 把字符串的地址载入EBX
    1. 把保存在AAAA占位符里的地址(一个指向字符串的指针)载入ECX
    1. 把保存在KKKK占位符里的地址(一个指向空值的指针)载入EDX
    1. 执行int 0x80

得到汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
start:
jmp short GoToCall

shellcode:
pop esi

xor eax, eax ; 1

mov byte [esi+7], al ; 2

lea ebx, [esi] ; 3
mov long [esi+8], ebx ; 4
mov long [esi+12], eax ; 5
mov byte al, 0x0b
mov ebx, esi
lea ecx, [esi + 8]
lea edx, [esi + 12]
int 0x80

GoToCall:
call shellcode
db '/bin/shJAAAAKKKK'

编译并反汇编的到操作码

格式化串漏洞

何为格式化串:

  • printf("%d %x", a, b);
  • printf是一个参数保存在栈上的函数,即a、b从栈中取出

出现格式化串漏洞最常见的原因是,在C语言里没有处理带有可变参数的函数。

什么是格式化串

printf系列函数 的格式化串里包含用户提交的数据时(如用户输入),就可能出现格式化串漏洞。

攻击者可以提交很多格式符(而不给出对应的变量),这样的话,栈上就没有和格式符相对应的参数,从而导致信息泄漏和执行任意代码。

如果我们不给格式符提供变量,将会出现奇怪的事情。例如:

1
2
3
4
5
6
7
8
// fmt.c
#include<stdio.h>

int main(int argc, char* argv[]){
printf(argv[1]);

return 0;
}
  • 用如下形式执行
    • ./fmt "%x %x %x %x"
  • 就相当于用如下形式调用printf
    • printf("%x %x %x %x");
  • 这个语句会透露出一些重要的信息:我们提供了格式符,但没提供对应的参数。printf也没有报错,而是输出一下内容:
    • 4015c98c 4001526c bffff944 bffff8e8
  • 攻击者可以利用它来获取栈上的数据

有许多格式符,下面介绍一个:

  • n,这个参数被视为指向整数指针(或整数变量),在这个参数之前输出的字符的数量被保存到这个参数指针的地址里
    • ./fmt "AAAAAAAAAA%n%n%n%n%n%n%n%n"
    • %n格式符把它的参数作为内存地址,把前面输出的字符的数量写到这个地址
    • 这意味着我们有机会改写某个内存地址的数据,从而控制程序的执行

利用%n格式符把控制的数据写入选择的地址。如果满足一下条件,就可以利用格式化串漏洞执行任意代码:

  • 我们能够控制参数,并可以把输出的字符的数量写入内存的任意区域
  • 宽度格式符允许我们用任意长度填充输出。如:"%23d"。因此可以用选择的值改写单个字节
  • 通常来说,我们可以猜测函数指针的地址,因此我们可以促成系统把提交的字符串当作代码执行

Building

格式化串技术概述

  • 如果格式化串在栈上,当增加字符串的格式符时,可以为格式符提供参数

    • 一旦可以指定参数:
    • 可以用%s从目标进程读取内存数据
    • 可以用%n把输出的字符的数量写入任意地址
    • 可以用宽度修饰符修改输出的字符的数量
    • 可以用%hn修饰符每次写入16位数值
  • 直接参数访问允许多次重用同一格式化串里的栈参数,也允许直接用这些我们感兴趣的参数。直接参数访问使用$修饰符,如:

    • %272$x,将显示栈上第272个参数
  • 利用格式化串漏洞、写内存的技术,可以:

    • 改写保存的返回地址
    • 改写其他特殊程序的函数指针
    • 改写指向异常处理程序的指针
    • 改写GOT(全局偏移表)条目
    • 等等

Web篇

解析应用程序

  • 确定后端使用的技术
      1. 提取版本HTTP消息头中的版本信息,但后端程序员也可以伪造
      1. 文件扩展名
        • 许多Web服务器将特殊的文件扩展名映射到特定的服务器组件中,不同组建处理错误的方式不同
      1. 目录名
        • 一些子目录名常常表示应用程序使用了相关技术
      1. 会话令牌
        • 许多Web服务器和Web应用程序平台默认生成的会话令牌名称会揭示其使用的技术
    • 等等

评论