pusofalse 发表于 2013-1-9 05:14:08

出题:AU3缓冲区溢出攻击测试(一) RtlMoveMemory函数解析

本帖最后由 pusofalse 于 2013-1-9 05:24 编辑

准备发2~3篇关于AU3程序安全的话题,献给那些跟我一样,想继续深入学习AU3(或各种知识),但不知从何入手的朋友。话题的最后,将会给出实际的题目。取材是我比较感兴趣,但在ACN比较冷门的软件安全领域的话题。其中涉及到的对AU3内置函数的调用(主要是DllCall和DllStruct*),相信对那些还不熟悉这些函数功能的朋友有些许帮助。文中如有错谬,请不吝赐教,不胜感谢之情。

第一部分:实施攻击前,必须要具备的知识

Ntdll.dll中的RtlMoveMemory函数,它的作用是将某处内存中的数据复制到另一个内存块中,AU3原型定义如下:

RtlMoveMemory($pDestination, $pSource, $iLength)

$pDestination - 目标缓冲区地址
$pSource - 源缓冲区地址
$iLength - 指定要复制的长度,以字节为单位
比如,我们用如下代码创建了两个相同的缓冲区:

; 代码1
$a = DllStructCreate("long Data")
$b = DllStructCreate("long Data")

; 填充$a缓冲区中的内容为 12345678
DllStructSetData($a, "Data", 12345678)


现在,我们要将$a中的内容复制进$b中,我们可以这样做:

; 代码2
DllStructSetData($b, "Data", DllStructGetData($a, "Data"))


这种方法看起来简洁明了,也符合逻辑习惯。然而,上面的示例中,两个缓冲区只有一个相同的成员Data。但在实际应用中,我们时常会创建比这复杂许多的结构,比如:


; 代码3
$a = DllStructCreate("long Data;char szMessage;dword Alignment;hwnd hWnd")
$b = DllStructCreate("long Data;char szMessage;dword Alignment;hwnd hWnd")


此刻,如果我们要将$a中的内容复制到$b中,我们不得不这样做:

; 代码4
DllStructSetData($b, "Data", DllStructGetData($a, "Data"))
DllStructSetData($b, "szMessage", DllStructGetData($a, "szMessage"))
DllStructSetData($b, "Alignment", DllStructGetData($a, "Alignment"))
DllStructSetData($b, "hWnd", DllStructGetData($a, "hWnd"))
; ...


如果结构中有100个成员,那么我们就要写100行这样的代码。程序的空间复杂度将上升为o(n)(n代表数据规模)。不过现在好了,我们已经知道有RtlMoveMemory函数的存在,这个函数可以帮助我们减少代码行数:


; 代码5
DllCall("ntdll.dll", "none", "RtlMoveMemory", _
        "ptr", DllStructGetPtr($b), _        ; 目标缓冲区地址
        "ptr", DllStructGetPtr($a), _        ; 源缓冲区地址
        "int", DllStructGetSize($a))        ; 要复制的长度
; 注,这段代码存在隐患。


不管结构中有多少个成员,我们都可以按照上面的方法,一次性复制完成,空间复杂度为o(1)。很明显地,在其他语言当中,类似C,ASM,VB等等,RtlMoveMemory函数是一个调用频率极高的函数,作为AU3的程序员,理应知道这个函数的功能和调用方法。

由于AU3的高度封装性,在使用内置函数DllCall调用系统函数的同时,我们可以指定参数的类型。使用AU3,我们只需要知道一个缓冲区的地址,便可以向这个缓冲区中写入数据,而不必创建一个DllStruct结构,例如下面的代码,功能是将一个32位数1234写入$a缓冲区中。


; 代码6
DllCall("ntdll.dll", "none", "RtlMoveMemory", _
        "ptr", $pa, _                        ; 目标缓冲区地址,$pa 为$a结构的指针
        "long*", 1234, _
        "int", 4)                        ; 要复制的长度,long型数值为4个字节。


这段调用之所以能够成功,关键就是一个星号*在起作用,传递星号,便是要告诉AU3解析器,让它在内部自动分配一个缓存区(暂且将这个缓存区命名为c),并将参数1234写入这个c中,最后,将c的地址传递给RtlMoveMemory。

如果不借助RtlMoveMemory,必须这样做:

; 代码7
$tTemp = DllStructCreate("long Value", $pa) ; 使用$a的地址,创建一个临时缓冲区
DllStructSetData($tTemp, "Value", 1234) ; 填充内容

显然,在只知道缓冲区地址的情况下,如果要向这个缓冲区写入数据,代码7的效率不如代码6。

有人可能会问,为何一定要用缓冲区指针,而不直接用DllStructCreate函数返回的结构作为操作的对象?其实很简单,一是因为“调用约定”,在调用者与被调用者之间,必须要遵守同一种协议,程序才能正常工作。第二个原因,在涉及到某些稍复杂的应用时,指针是唯一选择,应该注意到,DllStructCreate只是AU3内置的一种缓存区操作模型,创建缓冲区的主要作用是调用系统函数,而系统函数根本不认识AU3的结构,系统函数只认指针。“为何一定要用缓冲区指针,而不直接用DllStructCreate函数返回的结构作为操作的对象?”,这个问题应该这样问:“为何DllStructCreate函数不直接返回缓冲区指针,而是一个结构?这样就多了一步DllStructGetPtr函数的调用,牺牲了一点效率”。这个问题大概只有AutoIt的编写者才知道如何回答,如果你知道,一定要告诉我是为何。

在知道了某些情况下 必须使用指针,而不是结构的前提下,再来看RtlMoveMemory函数的另一个作用——读取缓存区——这也是AU3缓冲区溢出隐患的根本所在。

如果要读取上面定义的$a缓冲区中的内容,可以这样做:

; 代码8
$data = DllStructGetData($a, "Data") ; 读取$a缓冲区中的"Data"
MsgBox(0, "", $data)


但同样地,我们只知道$a的地址$pa,也同样不想创建一个临时结构,所以就有了下面这种写法:

; 代码9
$data = DllCall("Ntdll.dll", "none", "RtlMoveMemory", "long*", 0, "ptr", $pa, "long", 4)
MsgBox(0, "", $data)


同样是利用了星号的作用,AU3将会在内部创建一个缓冲区(命名为d)。代码9与代码6的区别在于,代码6中的内建缓存区c,它的内容不会被RtlMoveMemory函数修改,因为它用作了RtlMoveMemory函数的$pSource参数,RtlMoveMemory只是从这个参数中读取要复制的内容,注意“读取”操作,是不会令这个缓冲区中的内容发生改变的。而代码9的内建缓存区d,会用作$pDestination参数,RtlMoveMemory从$pa中读取数据,并写入到d中,只有“写入”操作,才会令缓冲区中的内容有所改变。RtlMoveMemory调用完成之后,AU3解析器便将d中的内容传出,放在DllCall函数返回的数组里面。注意DllCall函数(或AU3解析器),并不知道RtlMoveMemory函数(或每个系统函数)的作用为何、特性为何,它只是机械地完成这一调用过程。它并不会因为 我们调用的系统函数是RtlMoveMemory,所以才在内部自动创建一个缓冲区;也不会因为 我们调用的系统函数是RtlMoveMemory,所以才将我们传递的参数先读入这个内建缓冲区之中,之后系统函数执行完成,再将缓冲区更新的数据传出,它之所以这样做,都是因为我们指定了星号的缘故。

至此,我们离缓冲区溢出攻击又近了一步。

注意到只有在我们指定了星号的前提下,AU3才在内部自动创建缓冲区,而这个缓冲区的长度,则取决于“星号之前”的参数类型,例如我们指定为"long*",解析器就会自动创建一个长度为4字节的缓冲区,因为long型数值占用4个字节;指定为"int64*",则内建缓冲区的长度为8字节,因为int64型数值占用8个字节;指定为"dword*"、"ptr*"、"hwnd*"或"handle*”,内建缓冲区都同是4字节,因为long、dword、ptr、hwnd、handle这些类型的数值都是占用4个字节(在32位系统中),更多的长度占用,参考DllStructCreate函数帮助。

RtlMoveMemory的第3个参数$iLength,指定要复制的长度,以字节为单位。假如我们指定的$iLength参数超出了内建缓存区的实际长度,这时就将发生溢出,例如下面的代码将发生溢出:

; 代码10
$data = DllCall("Ntdll.dll", "none", "RtlMoveMemory", _
        "long*", 0, _                ; 解析器将在内部创建一个长度为4字节的缓冲区
        "ptr", $pa, _                ; $a缓冲区指针
        "long", 8)                ; 从$a中复制8个字节到内建缓冲区中
MsgBox(0, "", $data)


我们指定复制8个字节,而缓冲区只有4个字节,后面的4个字节没地方放,只好复制到内建缓冲区的后面了。注意RtlMoveMemory可不会知道会溢出,它只是老老实实地按照我们指定的参数进行工作,而AU3解析器也不知会发生溢出,因为它不知道我们指定第3个参数为8是什么含义。一切都在机械地执行。

代码10有无限的可能会执行成功,而且程序依旧能正常运行,这是因为溢出的数据没有覆盖掉重要的东西。试想,假如攻击者构造一串特殊的数据,发送给我们用AU3(或别的语言)编写的一个服务器程序,而服务器程序中,恰好有一段如代码10那般编写不严谨的代码,并且,恰好处理客户端发送来的数据时调用了这段代码,那我们的服务器就要拱手让人了。

以上。

第二部分:线程堆栈,与函数的调用链。

annybaby 发表于 2013-1-9 10:47:53

好久不见P大出作品了~~
深夜发帖,辛苦了~~

3mile 发表于 2013-1-9 12:52:53

坐学P神科普
膜拜中。。。

xiaowo 发表于 2013-1-9 13:21:27

基础知识缺失,造成理解上的困难,好难消化……

xms77 发表于 2013-1-9 21:55:34

回复 1# pusofalse
学习第一部分,等待第二部分!

happytc 发表于 2013-1-9 21:58:15

回复 1# pusofalse

p侠搞科普,不错呀,写这么多,花了不少功夫,支持一下。
不过,估计没有学过别的语言,如C之类的,可能看起来有点困难的,虽然这本来是基础中的基础。

BTW:在我用了无数次RtlMoveMemory函数后,某一天猛然才知道Rtl是什么意思呢
对了,建议既然你讲解了这个api函数,应该随口提一下它跟RtlCopyMemory函数的区别

ceoguang 发表于 2013-1-9 23:56:04

可以学习C/C++的安全函数,溢出就终止进程
或者调用MS的API
IsBadReadPtr
IsBadWritePtr
IsBadxxxxPtr
页: [1]
查看完整版本: 出题:AU3缓冲区溢出攻击测试(一) RtlMoveMemory函数解析