标 题:
Windows Shell Code编写中级拔高
时 间: 2013-05-28,23:59:54
一、基础知识&基本原理&一些思想方法
关于Windows Shell Code的文章论坛里的介绍也不少了,不过授之以鱼不如授之以渔,虽然有部分文章也做了大量讲解,但是几乎都是对照一份现成的代码来介绍为何那样写,为何这样写。所以如果Windows系统原理的知识相对薄弱的同学看起来还是云里雾里,也没弄懂,干脆把代码照抄走了,结果会遇到各种意想不到的异常,这篇文章叫中级拔高,而没有叫高级,因为ShellCode编写的高级技术是需要从OPCODE这个层次去研究的,本文暂时不介绍OPCODE这座高山。
废话不多说,本文的目的:转变一下对待ShellCode的态度,从“为什么写成这样那样”转变为“如何去写”。
ShellCode是什么以及名称的来历我就不赘述了,大家翻阅典籍吧什么比如《0DAY安全:软件漏洞分析技术(第x版)》什么什么的,里面有写故事讲的不错。ShellCode的要利用一般是在漏洞中,比如你发现了一个权限提升EoP(Elevation of Privilege)漏洞能够执行任意代码,那这里的“任意代码”当然不是如字面意思所说的任意了,而是经过精心构造的一段二进制数据,把着这段数据写入到某一地址,然后漏洞又能顺着你的代码执行下去,这就达到了通过ShellCode实现的漏洞利用。还有一种就是感染性病毒,为什么叫感染,因为是他通过在非运行时状态下永久改变一个目标程序的功能,就是把一段ShellCode插入到一个目标程序的载体文件中。
通过上面的简单介绍,有一点应该很清楚了,ShellCode是一段代码,并且这段代码将来要运行在一个非本地编译的程序的内存空间,这样一来写我们就要想:这种特性对我们写一个ShellCode会产生什么样的影响呢?首先最重要的一点:通过绝对地址定位的东西在ShellCode中都是不可直接实现的,比如数据偏移。这点还需要详细的解释一下:在16位的程序中,还是通过段基址+段偏移的方式去寻址,而且我们可以控制段寄存的值,这种情况下写ShellCode要方便很多,因为我们可以做到在ShellCode的第一条指令就放一个调整段寄存器值的指令,到我们的ShellCode的基址中,然后ShellCode中的所有数据都可以很方便的被访问了。但是在32位的windows程序中,保护模式中,程序结构采用Flat模式,DS,ES,CS等寄存器的值都是固定且一样的,并且不是简单的一个基址,而是一个段描述符,所以想在程序中去更改段寄存器的值是吃力不讨好的,32位保护模式下的windows程序的所有数据偏移都是相对于DS的,而DS实际上一个段描述符,这里可以简单的认为DS就是0,这就是说我们在32位保护模式下的所有的数据地址都是相对于DS基址的偏移地址。
既然数据偏移不能直接使用了,那代码呢?比如jmp,Call指令等跳转指令?其实除了ret指令之外所有的代码牵扯到的地址都是相对的,举个例子:
Jmp解释:
可以看到jmp指令的操作码是EB,而后紧跟了一个操作数02,就是说跳转指令的功能是直接把EIP的值(00981004)加上jmp指令后的操作数(02),然后继续执行,下一条指令就是(00981006)。
Call解释:
同JMP一样,call指令的也是把当前EIP的值加上操作数然后继续执行,不同的是call会压栈返回值,所以call指令可以分解为:push和jmp两条指令。[题外话:这个分解还是有点小内涵的,心细的同学想一想如果分解成这两条指令那这两条指令应该跟什么操作数?]
而ret指令则是直接把栈中的一个数据放入EIP然后继续执行。
讲到这里会不会有人问那我们怎么知道我们程序跟我们要Call的那个函数到底差了多少呢?这是问到点子上了,正因为代码与代码之间的关系都是相对的,我们才能在一个程序总写代码,然后让这个代码在另一个程序中运行,如果没有这一点支持,那ShellCode这种东西就不复存在了。而我们知道一个函数的地址之后如何去Call他呢?当然不能直接写立即数,而是要把目标地址放入寄存器,然后去call寄存器,这样cpu就会自动计算指令偏移从而正确的跳转了。
这就解决了call一个系统函数的问题了,那数据怎么办?在ShellCode中肯定会用到数据,一般指的是字符串,当ShellCode遇到字符串就两个字蛋疼,但是这个问题是不可避免的。方法有三个:
1. 在ShellCode中hardcode一些数据偏移,然后在程序外把数据写入到那些固定的地址中,很简单的方法,缺点就不需要说了,计算繁琐,还不能保证写入的地址就一定不会有程序的原有数据占用。
2. 转化法,比如我要写一个查找函数名的函数,那我就要传入一个字符串的地址作为参数,字符串就是要写的数据,如何转化?假如我需要查找一个名为LoadLibrary的字符串,肯定要用到字符串比较,真的是这样么?想一想,程序中有一个字符串数组,能不能最这些字符串进行加工一下,转化成一个寄存器可以容纳的数据呢?当然是要HASH了,因为我们可以很容易拿到我们要比较的字符串的地址,这样我们就可以对齐进行计算hash,然后在跟我们需要查找的目标字符串的hash值进行对比,就达到了查找字符串的目的了。以执行时间来置换代码体积。这不仅仅能让ShellCode的体积减小,更重要的是能把一次寻址转换成数值对比,这种方法也有缺点对字符串的长度有限制。现成的HASH算法是很多的,我比较喜欢用的就是循环移位的方法,冲突概率没计算过,不过一般使用中很少出现,实现方法见下文。
3. 栈!越来越觉得栈是计算机原理的中最最最有创意的一个概念和构件了!比如对比字符串,那我们就Push..push..push..然后push esp,这就把一个字符串的地址压栈了~只要没超过程序的栈大小就百试不爽啊。缺点是代码体积增大了。
关于ShellCode的一些基本知识和原理就说这么多,下面来点干货。
二、结合Windows系统,实践代码
关于Windows的ShellCode,如何写?要找到敲门砖,先来了解一下Windows系统的大致架构吧,windows的分层架构如下:
我们一般写应用层程序的时候都是使用的Win32子系统层次上封装导出的系统API,而Native层的API很少使用,但是不代表他不可用,ntdll.dll这个模块就是windows中应用层和内核层的接口。只要是运行在windows系统下的程序都必须去加载这个模块,并且是第一个加载的模块。既然这样,敲门砖找到了么?我认为找到了。
一般的ShellCode教程中都说去找Kernel32.dll这个模块,为什么,因为他是第二个加载的,因为它里面有我们需要的函数LoadLibraryA/W,找的方法似乎都是去读取:
pPeb->pLdr->InInitializationOrderModuleList
这个链表中的第二项,说是在9X和XP下都能工作,我读到这些ShellCode的时候在想,第一个写出这种代码的人是不是不知道Ntdll.dll的地位和其中包含的丰富的函数资源!为什么不直接先找到Ntdll.dll这个模块呢?
可能有人发现了,通过那种找链表第二项去找Kernel32.dll模块的方法在windows 7上面就悲剧了。为什么?因为他们找到的是KernelBase.dll模块,这个模块是干什么的?这就是上述方法的缺点了,不稳定,windows系统更新换代中层次越高的东西被修改替换的可能性越大。KernelBase.dll这个模块是因为Windows 7 引入了一种全新的机制—Api Set Schema,这个机制不是本文的话题,以后再发帖详解,这个资料相对比较少的,可查的只有两处介绍的比较好的,还需要翻墙。
话题回到敲门砖,上面说了Ntdll.dll这个模块的特殊性,所以毫无疑问这个模块应该是所有ShellCode的敲门砖!因为他处在初始化顺序模块列表中的位置固定,并且处在系统架构中应用层的最底层,一般不会做很大的改动,相对稳定,更重要的是这个模块中包含了所有的C语言运行时的字符串处理函数,所以找到了这个模块,那我们就能使用这个模块中的绝大部分函数来达到简化shellcode编码的作用。
那如何拿到这个敲门转呢?思路很简单:
1. 封装一个get_ntdll_base函数,通过pPeb->pLdr->InInitializationOrderModuleList,这个链表,取出第一项就是包含了ntdll模块信息的结构体,从而可以找到这个模块的基址;
2. 封装一个遍历导出表获取函数地址的函数,然后就可以查找ntdll.dll中导出的任意函数了get_proc_address_by_digest,有了这个函数后面就不需要去找GetProcAddress这个函数了,这个函数的参数就是函数名的hash;
3. 使用2中的封装的函数在ntdll中查找_wcsnicmp这个函数,因为有了这个函数和我们已经找到的初始化顺序模块列表,那我们就能自己封装一个获取任意已经加载的模块机制的函数get_module_base;
4. 使用3中封装的函数这个get_module_base去获取Kernel32.dll的基址,然后获取函数LoadLibraryA的地址,有了LoadLibraryA函数,基本就没有做不到的事情了。
贴出代码供各位拜读,这段ShellCode功能就是显示一个MessageBox:
上面的代码生成的exe文件在xp和windows 7下的运行情况:
三、ShellCode编写经验谈
上面的代码是在VC++中内联汇编编写的,这样就要经过一个编写,编译,提取的过程,有点麻烦,难道没有捷径么?
很多年以前一直在寻找有没有所写即所得的纯净的汇编程序,不会给我的代码添加额外的任何信息,3年前找到了,Flat Assembler。我在这里还要再一次夸赞这个强大的汇编程序,他能让目前所有的汇编程序都感到汗颜,因为这个汇编程序使用的时候总共只有四个参数,其中三个是默认参数,一个必选参数就是源文件。然而FASM却并不简单,他能汇编16位,32位,MZ,PE,ELF,COFF等各种格式的目标程序,并且提供了windows SDK的接口库,对于不同格式的目标程序,只需要在源码中指定一两条宏指令就可以了,这些都不重要,重要的是FASM是一个支持所写即所得的汇编程序。
比如你用masm或者nasm这些汇编程序去编译一个只有一条push eax指令的源文件,首先编译过不过就是个问题了,在一个即使过了,目标程序也被添加了几K自己的“垃圾代码”了,但是看看FASM:
Fasm就是这么简洁而又强大的汇编程序,这里也给出上面的那段ShellCode用Fasm来写的源码,语法只有很少量的改变,关于详细的不同大家直接去看Flat Assembler的文档了,我上篇帖子打包发出来的资料中有。
然后说一下调试ShellCode,或许用VS这些东西就够方便的了,但是我这里想说如果你采用了我推荐的FASM来写ShellCode的话,那调试还是个比较头疼的事情的,不过还好,这种小问题,花5分钟写个程序就能搞定的事了,在这提供一个调试辅助工具,ShellCodeSuite,真的是一拍脑袋花了5分钟写出来的,WTL框架(MFC作为初期学习windows编程还是很有必要的,但是学到一定程度该丢掉的尽早丢掉),为了省时间全部写成英文了,见谅。
程序中包含了一个字符串HASH功能,方便计算函数名的HASH,之后会添加一个PE感染功能。
额,写这么多吧,还有一篇关于DLL劫持的文章,本来打算发,但是发现劫持漏洞涉及的厂家比较多,就一直压箱底了。
时 间: 2013-05-28,23:59:54
关于Windows Shell Code的文章论坛里的介绍也不少了,不过授之以鱼不如授之以渔,虽然有部分文章也做了大量讲解,但是几乎都是对照一份现成的代码来介绍为何那样写,为何这样写。所以如果Windows系统原理的知识相对薄弱的同学看起来还是云里雾里,也没弄懂,干脆把代码照抄走了,结果会遇到各种意想不到的异常,这篇文章叫中级拔高,而没有叫高级,因为ShellCode编写的高级技术是需要从OPCODE这个层次去研究的,本文暂时不介绍OPCODE这座高山。
废话不多说,本文的目的:转变一下对待ShellCode的态度,从“为什么写成这样那样”转变为“如何去写”。
ShellCode是什么以及名称的来历我就不赘述了,大家翻阅典籍吧什么比如《0DAY安全:软件漏洞分析技术(第x版)》什么什么的,里面有写故事讲的不错。ShellCode的要利用一般是在漏洞中,比如你发现了一个权限提升EoP(Elevation of Privilege)漏洞能够执行任意代码,那这里的“任意代码”当然不是如字面意思所说的任意了,而是经过精心构造的一段二进制数据,把着这段数据写入到某一地址,然后漏洞又能顺着你的代码执行下去,这就达到了通过ShellCode实现的漏洞利用。还有一种就是感染性病毒,为什么叫感染,因为是他通过在非运行时状态下永久改变一个目标程序的功能,就是把一段ShellCode插入到一个目标程序的载体文件中。
通过上面的简单介绍,有一点应该很清楚了,ShellCode是一段代码,并且这段代码将来要运行在一个非本地编译的程序的内存空间,这样一来写我们就要想:这种特性对我们写一个ShellCode会产生什么样的影响呢?首先最重要的一点:通过绝对地址定位的东西在ShellCode中都是不可直接实现的,比如数据偏移。这点还需要详细的解释一下:在16位的程序中,还是通过段基址+段偏移的方式去寻址,而且我们可以控制段寄存的值,这种情况下写ShellCode要方便很多,因为我们可以做到在ShellCode的第一条指令就放一个调整段寄存器值的指令,到我们的ShellCode的基址中,然后ShellCode中的所有数据都可以很方便的被访问了。但是在32位的windows程序中,保护模式中,程序结构采用Flat模式,DS,ES,CS等寄存器的值都是固定且一样的,并且不是简单的一个基址,而是一个段描述符,所以想在程序中去更改段寄存器的值是吃力不讨好的,32位保护模式下的windows程序的所有数据偏移都是相对于DS的,而DS实际上一个段描述符,这里可以简单的认为DS就是0,这就是说我们在32位保护模式下的所有的数据地址都是相对于DS基址的偏移地址。
既然数据偏移不能直接使用了,那代码呢?比如jmp,Call指令等跳转指令?其实除了ret指令之外所有的代码牵扯到的地址都是相对的,举个例子:
Jmp解释:
Code:
ShellCode: 00981000 8B 00 mov eax,dword ptr [eax] 00981002 EB 02 jmp shellcode_start (981006h) 00981004 90 nop 00981005 90 nop shellcode_start: 00981006 55 push ebp
可以看到jmp指令的操作码是EB,而后紧跟了一个操作数02,就是说跳转指令的功能是直接把EIP的值(00981004)加上jmp指令后的操作数(02),然后继续执行,下一条指令就是(00981006)。
Call解释:
Code:
ShellCode: 01121000 8B 00 mov eax,dword ptr [eax] 01121002 E8 02 00 00 00 call shellcode_start (1121009h) 01121007 90 nop 01121008 90 nop shellcode_start: 01121009 55 push ebp
而ret指令则是直接把栈中的一个数据放入EIP然后继续执行。
讲到这里会不会有人问那我们怎么知道我们程序跟我们要Call的那个函数到底差了多少呢?这是问到点子上了,正因为代码与代码之间的关系都是相对的,我们才能在一个程序总写代码,然后让这个代码在另一个程序中运行,如果没有这一点支持,那ShellCode这种东西就不复存在了。而我们知道一个函数的地址之后如何去Call他呢?当然不能直接写立即数,而是要把目标地址放入寄存器,然后去call寄存器,这样cpu就会自动计算指令偏移从而正确的跳转了。
这就解决了call一个系统函数的问题了,那数据怎么办?在ShellCode中肯定会用到数据,一般指的是字符串,当ShellCode遇到字符串就两个字蛋疼,但是这个问题是不可避免的。方法有三个:
1. 在ShellCode中hardcode一些数据偏移,然后在程序外把数据写入到那些固定的地址中,很简单的方法,缺点就不需要说了,计算繁琐,还不能保证写入的地址就一定不会有程序的原有数据占用。
2. 转化法,比如我要写一个查找函数名的函数,那我就要传入一个字符串的地址作为参数,字符串就是要写的数据,如何转化?假如我需要查找一个名为LoadLibrary的字符串,肯定要用到字符串比较,真的是这样么?想一想,程序中有一个字符串数组,能不能最这些字符串进行加工一下,转化成一个寄存器可以容纳的数据呢?当然是要HASH了,因为我们可以很容易拿到我们要比较的字符串的地址,这样我们就可以对齐进行计算hash,然后在跟我们需要查找的目标字符串的hash值进行对比,就达到了查找字符串的目的了。以执行时间来置换代码体积。这不仅仅能让ShellCode的体积减小,更重要的是能把一次寻址转换成数值对比,这种方法也有缺点对字符串的长度有限制。现成的HASH算法是很多的,我比较喜欢用的就是循环移位的方法,冲突概率没计算过,不过一般使用中很少出现,实现方法见下文。
3. 栈!越来越觉得栈是计算机原理的中最最最有创意的一个概念和构件了!比如对比字符串,那我们就Push..push..push..然后push esp,这就把一个字符串的地址压栈了~只要没超过程序的栈大小就百试不爽啊。缺点是代码体积增大了。
关于ShellCode的一些基本知识和原理就说这么多,下面来点干货。
二、结合Windows系统,实践代码
关于Windows的ShellCode,如何写?要找到敲门砖,先来了解一下Windows系统的大致架构吧,windows的分层架构如下:
我们一般写应用层程序的时候都是使用的Win32子系统层次上封装导出的系统API,而Native层的API很少使用,但是不代表他不可用,ntdll.dll这个模块就是windows中应用层和内核层的接口。只要是运行在windows系统下的程序都必须去加载这个模块,并且是第一个加载的模块。既然这样,敲门砖找到了么?我认为找到了。
一般的ShellCode教程中都说去找Kernel32.dll这个模块,为什么,因为他是第二个加载的,因为它里面有我们需要的函数LoadLibraryA/W,找的方法似乎都是去读取:
pPeb->pLdr->InInitializationOrderModuleList
这个链表中的第二项,说是在9X和XP下都能工作,我读到这些ShellCode的时候在想,第一个写出这种代码的人是不是不知道Ntdll.dll的地位和其中包含的丰富的函数资源!为什么不直接先找到Ntdll.dll这个模块呢?
可能有人发现了,通过那种找链表第二项去找Kernel32.dll模块的方法在windows 7上面就悲剧了。为什么?因为他们找到的是KernelBase.dll模块,这个模块是干什么的?这就是上述方法的缺点了,不稳定,windows系统更新换代中层次越高的东西被修改替换的可能性越大。KernelBase.dll这个模块是因为Windows 7 引入了一种全新的机制—Api Set Schema,这个机制不是本文的话题,以后再发帖详解,这个资料相对比较少的,可查的只有两处介绍的比较好的,还需要翻墙。
话题回到敲门砖,上面说了Ntdll.dll这个模块的特殊性,所以毫无疑问这个模块应该是所有ShellCode的敲门砖!因为他处在初始化顺序模块列表中的位置固定,并且处在系统架构中应用层的最底层,一般不会做很大的改动,相对稳定,更重要的是这个模块中包含了所有的C语言运行时的字符串处理函数,所以找到了这个模块,那我们就能使用这个模块中的绝大部分函数来达到简化shellcode编码的作用。
那如何拿到这个敲门转呢?思路很简单:
1. 封装一个get_ntdll_base函数,通过pPeb->pLdr->InInitializationOrderModuleList,这个链表,取出第一项就是包含了ntdll模块信息的结构体,从而可以找到这个模块的基址;
2. 封装一个遍历导出表获取函数地址的函数,然后就可以查找ntdll.dll中导出的任意函数了get_proc_address_by_digest,有了这个函数后面就不需要去找GetProcAddress这个函数了,这个函数的参数就是函数名的hash;
3. 使用2中的封装的函数在ntdll中查找_wcsnicmp这个函数,因为有了这个函数和我们已经找到的初始化顺序模块列表,那我们就能自己封装一个获取任意已经加载的模块机制的函数get_module_base;
4. 使用3中封装的函数这个get_module_base去获取Kernel32.dll的基址,然后获取函数LoadLibraryA的地址,有了LoadLibraryA函数,基本就没有做不到的事情了。
贴出代码供各位拜读,这段ShellCode功能就是显示一个MessageBox:
Code:
void _declspec(naked)ShellCode() { #define LoadLibraryExA_Digest 0xc0d83287 #define LoadLibraryA_Digest 0x0C917432 #define MessageBoxA_Digest 0x1E380A6A #define FreeLibrary_Digest 0x30BA7C8C __asm { shellcode_start: push ebp // 保存栈帧 mov ebp, esp // 在栈中构造UNICODE字符串 KERNEL32.DLL[K\0E\0 R\0N\0 E\0L\0 3\02\0 .\0D\0 L\0L\0 \0\0\0\0 push 0000000h //0000 push 004C004Ch //0L0L push 0044002Eh //0D0. push 00320033h //0203 push 004c0045h //0L0E push 004E0052h //ONOR push 0045004Bh //0E0K push esp call get_module_base add esp, 7*4 test eax, eax jz _shellcode_return push eax // [ebp-4] push LoadLibraryA_Digest push eax call get_proc_address_by_digest test eax, eax jz _shellcode_return // 在栈中构造ANSI字符串 USER32.DLL push 00004C4Ch //'00LL' push 442E3233h //'D.23' push 52455355h //'RESU' push esp call eax // LoadLibraryA add esp, 3*4 test eax, eax jz _shellcode_return push eax // [ebp-8] hModule push MessageBoxA_Digest push eax call get_proc_address_by_digest test eax, eax jz _shellcode_return // 在栈中构造ANSI字符串 Shell Code. Shion [Shel l_Co de._ Shio n000 mov edi, '000n' and edi, 000000ffh push edi push 'oihS' push ' .ed' push 'oC l' push 'lehS' mov edi, esp push 00000040h push 0 push edi push 0 call eax add esp, 5*4 push FreeLibrary_Digest mov edi, [ebp-4] push edi call get_proc_address_by_digest test eax, eax jz _shellcode_return push [ebp-8] call eax _shellcode_return: mov esp, ebp pop ebp // 恢复栈帧 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 /************************************************************************/ get_module_base: push ebp mov ebp, esp call get_ntdll_base test eax, eax jz _find_modulebase_done push 548b2e5fh push eax call get_proc_address_by_digest test eax, eax // _wcsnicmp jz _find_modulebase_done push eax // [ebp-04h]_wcsnicmp mov eax, fs:[30h] // eax = ppeb test eax, eax jz _find_modulebase_done mov eax, [eax+0ch] // eax = pLdr pLdr:[PEB_LDR_DATA] test eax, eax jz _find_modulebase_done 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 ptr[esi+1ch] // length push edi push [esi+20h] // pLdrDataTableEntry.DllBaseName.Buffer [WCHAR] push [ebp+08h] mov edi, [ebp-04h] call edi test eax, eax jnz _find_modulebase_loop mov eax, [esi+08h] // eax = pLdrDataTableEntry.DllBase //mov ecx, [esi+20h] _find_modulebase_done: mov esp, ebp pop ebp ret 4 /************************************************************************/ /* Get base address of ntdll.dll module * tishion * 2013-05-26 13:45:20 * * OUT: * eax = ntdll.base /************************************************************************/ get_ntdll_base: mov eax, fs:[30h] // eax = ppeb test eax, eax jz _find_ntdllbase_done mov eax, [eax+0ch] // eax = pLdr pLdr:[PEB_LDR_DATA] test eax, eax jz _find_ntdllbase_done mov eax, [eax+1ch] // eax = pLdr->InInitializationOrderModuleList test eax, eax jz _find_ntdllbase_done 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 /************************************************************************/ 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) /************************************************************************/ get_proc_address_by_digest: push ebp mov ebp, esp mov eax, [ebp+8] cmp word ptr [eax], 5a4dh // 'MZ' jnz _return_null add eax, [eax+3ch] // eax = ImageNtHeader IMAGE_NT_HEADERS cmp dword ptr [eax], 00004550h // 'PE' jnz _return_null 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地址表 mov eax, [ecx+10h] // ordinals base push eax // [ebp-14h] mov eax, [ecx+14h] // NumberOfFunctions push eax // [ebp-18h] mov eax, [ecx+18h] // NumberOfNames push eax // [ebp-1ch] 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] //add edx, [ebp-14h] //Ordinal base 处理, 蛋疼?找微软! //sub edx, [ebp-14h] // 以上两条同样是让程序流程清晰,实际运用中,如果不需要输出Ordinal,则不需要进行该操作 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: mov esp, ebp pop ebp ret 8 //jmp_to_oep: // //jmp $ // jmp _tWinMain } }
三、ShellCode编写经验谈
上面的代码是在VC++中内联汇编编写的,这样就要经过一个编写,编译,提取的过程,有点麻烦,难道没有捷径么?
很多年以前一直在寻找有没有所写即所得的纯净的汇编程序,不会给我的代码添加额外的任何信息,3年前找到了,Flat Assembler。我在这里还要再一次夸赞这个强大的汇编程序,他能让目前所有的汇编程序都感到汗颜,因为这个汇编程序使用的时候总共只有四个参数,其中三个是默认参数,一个必选参数就是源文件。然而FASM却并不简单,他能汇编16位,32位,MZ,PE,ELF,COFF等各种格式的目标程序,并且提供了windows SDK的接口库,对于不同格式的目标程序,只需要在源码中指定一两条宏指令就可以了,这些都不重要,重要的是FASM是一个支持所写即所得的汇编程序。
比如你用masm或者nasm这些汇编程序去编译一个只有一条push eax指令的源文件,首先编译过不过就是个问题了,在一个即使过了,目标程序也被添加了几K自己的“垃圾代码”了,但是看看FASM:
Fasm就是这么简洁而又强大的汇编程序,这里也给出上面的那段ShellCode用Fasm来写的源码,语法只有很少量的改变,关于详细的不同大家直接去看Flat Assembler的文档了,我上篇帖子打包发出来的资料中有。
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 ;// 在栈中构造UNICODE字符串 KERNEL32.DLL[K\0E\0 R\0N\0 E\0L\0 3\02\0 .\0D\0 L\0L\0 \0\0\0\0 push 00000000h ;//0000 push 004C004Ch ;//0L0L push 0044002Eh ;//0D0. push 00320033h ;//0203 push 004c0045h ;//0L0E push 004E0052h ;//0N0R push 0045004Bh ;//0E0K push esp call get_module_base add esp, 7*4 test eax, eax jz ._shellcode_return push eax ;// [ebp-4] push LoadLibraryA_Digest push eax call get_proc_address_by_digest test eax, eax jz ._shellcode_return ;// 在栈中构造ANSI字符串 USER32.DLL ;push 00004C4Ch ;//'00LL' ;push 442E3233h ;//'D.23' ;push 52455355h ;//'RESU' push 'LL' push '32.D' push 'USER' push esp call eax ;// LoadLibraryA add esp, 3*4 test eax, eax jz ._shellcode_return push eax ;// [ebp-8] hModule push MessageBoxA_Digest push eax call get_proc_address_by_digest test eax, eax jz ._shellcode_return ;// 在栈中构造ANSI字符串 Shell Code. Shion [Shel l_Co de._ Shio n000 push 'ion' push 'Tish' push 'de. ' push 'l Co' push 'Shel' mov edi, esp push 00000040h push 0 push edi push 0 call eax add esp, 5*4 push FreeLibrary_Digest push dword [ebp-4] call get_proc_address_by_digest test eax, eax jz ._shellcode_return push dword [ebp-8] call eax ._shellcode_return: mov esp, ebp pop ebp ;// 恢复栈帧 ret jmp jmp_to_oep ;/************************************************************************/ ;/* 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 ;/************************************************************************/ get_module_base: push ebp mov ebp, esp call get_ntdll_base test eax, eax 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 mov eax, 30h mov eax, [fs:eax] ;// eax = ppeb test eax, eax jz ._find_modulebase_done mov eax, [eax+0ch] ;// eax = pLdr pLdr:[PEB_LDR_DATA] test eax, eax jz ._find_modulebase_done 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 ;//mov ecx, [esi+20h] ._find_modulebase_done: mov esp, ebp pop ebp ret 4 ;/************************************************************************/ ;/* Get base address of ntdll.dll module ;* tishion ;* 2013-05-26 13:45:20 ;* ;* OUT: ;* eax = ntdll.base ;/************************************************************************/ get_ntdll_base: mov eax, 30h mov eax, [fs:eax] ;// eax = ppeb test eax, eax jz ._find_ntdllbase_done mov eax, [eax+0ch] ;// eax = pLdr pLdr:[PEB_LDR_DATA] test eax, eax jz ._find_ntdllbase_done mov eax, [eax+1ch] ;// eax = pLdr->InInitializationOrderModuleList test eax, eax jz ._find_ntdllbase_done 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 ;/************************************************************************/ 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) ;/************************************************************************/ get_proc_address_by_digest: push ebp mov ebp, esp mov eax, [ebp+8] cmp word [eax], 5a4dh ;// 'MZ' jnz ._return_null add eax, [eax+3ch] ;// eax = ImageNtHeader IMAGE_NT_HEADERS cmp dword [eax], 00004550h ;// 'PE' jnz ._return_null 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] ;//add edx, [ebp-14h] ;//Ordinal base 处理, 蛋疼?找微软! ;//sub edx, [ebp-14h] ;// 以上两条同样是让程序流程清晰,实际运用中,如果不需要输出Ordinal,则不需要进行该操作 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: mov esp, ebp pop ebp ret 8 jmp_to_oep: jmp $
然后说一下调试ShellCode,或许用VS这些东西就够方便的了,但是我这里想说如果你采用了我推荐的FASM来写ShellCode的话,那调试还是个比较头疼的事情的,不过还好,这种小问题,花5分钟写个程序就能搞定的事了,在这提供一个调试辅助工具,ShellCodeSuite,真的是一拍脑袋花了5分钟写出来的,WTL框架(MFC作为初期学习windows编程还是很有必要的,但是学到一定程度该丢掉的尽早丢掉),为了省时间全部写成英文了,见谅。
程序中包含了一个字符串HASH功能,方便计算函数名的HASH,之后会添加一个PE感染功能。
额,写这么多吧,还有一篇关于DLL劫持的文章,本来打算发,但是发现劫持漏洞涉及的厂家比较多,就一直压箱底了。