前言
在构造一个Shellcode载荷时总是存在多种方法,特别是对于Windows平台来说。需要手工编写所有的汇编代码,或者说编译器能够有所帮助吗?需要直接使用syscall,还是需要在内存中搜索函数?因为构造载荷一般来说不会很简单,所以我决定写一篇文章来专门论述相关问题。我习惯于用C语言来完成所有的工作,并使用Visual Studio来对其进行编译:因为C语言的源代码更加优美,编译器能够更好地对其进行优化,并且如果需要的话可以使用LLVM框架实现自己的混淆器。
在本例中,我将针对于x86架构下的Shellcode代码;当然,相关的分析完全可以应用于x86(64位)架构下的Shellcode代码或者别的处理器。
寻找基本DLL
简介
当一个Shellcode载荷加载到Windows系统中的时候,第一步就是要定位需要使用的函数,即搜索存储函数的动态链接库(DLL)。为此,我们需要用到以下各小节所描述的不同的结构。
线程环境块
Windows系统使用TEB结构来描述一个线程,每个线程通过使用FS(x86平台)或GS(x86,64位平台)寄存器来访问其自身的TEB结构。TEB结构具体如下:
因此,若想要访问PEB结构,只需要进行如下操作:
进程环境块
如果说TEB结构给出了一个线程的相关信息,那么PEB结构将告诉我们关于进程自身的信息,其中我们所需要的信息是基本DLL的位置。实际上,在Windows系统加载一个进程到内存中的时候,至少要映射两个DLL:
·ntdll.dll,其中包含执行syscall的函数,它们都以前缀Nt开头(形如Nt*),并调用内核中以Zw开头(形如Zw*)名称相同的函数;
·kernel32.dll,在更高层次上使用NTDLL中的函数。比如,kernel32!CreateFileA函数将调用ntdll!NtCreateFileW函数,而后者又将调用ntoskrnl!ZwCreateFileW函数。
在不同版本的Windows系统下,其他的DLL可能已经存在于内存中,但是是完全可移植的;因此,我们假设以上两个DLL是唯一加载的DLL模块。
让我们看一下PEB结构,如下所示:
可以看到,其中一个成员名为PEB.BeingDebugged,可被IsDebuggerPresent()函数使用;而我们感兴趣的部分是成员PEB.Ldr,其对应于如下结构:
顾名思义,成员PEB.Ldr->In*OrderModuleList是包含所有已加载到内存中的DLL模块的链表(LIST_ENTRY);三个表以不同的顺序指向相同的对象。我更倾向于使用InLoadOrderModuleList,因为可以像使用一个指向_LDR_DATA_TABLE_ENTRY的指针一样直接使用InLoadOrderModuleList.Flink。例如,如果使用InMemoryOrderModuleList,由于InMemoryOrderModuleList.Flink指针指向下一个InMemoryOrderModuleList,所以LDR_DATA_TABLE_ENTRY将位于(_InMemoryOrderModuleList.Flink–0x10)的位置。每个链表成员具有如下结构:
BaseDllName包含DLL模块的名称(比如,ntdll.dll),而DllBase包含DLL模块所加载到内存中的地址。通常,InLoadOrderModuleList中的第一个成员就是可执行程序自身,在之后我们能够找到NTDLL和KERNEL32;然而,我们并不确定在所有版本的Windows系统中都是这个次序,所以最好根据DLL名称(大写/小写)进行搜索。
DJB散列
如前所述,我们并不信任DLL顺序而选择根据DLL名称进行搜索。然而在一段Shellcode代码中,使用ASCII字符串(或更糟的,UNICODE字符串)并不是一个好主意:这将使得我们的Shellcode代码过于臃肿!因此我建议,使用散列机制来比较DLL名称。由于其简洁有效性我选择使用DJB散列算法,具体代码如下所示:
由于DLL名称可能大写也可能小写,因此在散列算法中最好满足如下等式关系:
代码
现在我们已经讨论了如何完成以上工作,是时候来编码实现我们的想法了。具体代码如下所示:
如果想要使用其他的DLL模块,只需要使用LoadLibrary()函数来将其加载。不要着急,我们将在user32.dll相关的Shellcode代码中进行该项工作。
函数地址
简介
现在我们已经找到DLL,下一步需要在DLL内存空间中搜寻所需的函数在哪儿。幸运的是,透彻理解PE文件头部结构的前提下这并不复杂。要牢记的是,当我们讨论PE文件头部时,提到的大部分地址都与可执行程序地址相关。
可移植的执行体头部
执行体的开始位置是一个DOS头部,但它只是为了兼容(DOS系统)而存在。具体结构如下所示:
成员e_lfanew将指示NT头部的位置。由于这是一个相对地址,所以需要进行pFile+e_lfanew操作。NT头部具体结构如下所示:
数据目录中包含了一些有趣成员的地址;而对我们来说,它包含了所有导出函数的地址。
因此,我们可以使用DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress来直接获取导出目录的地址:
可以通过名字或者通过序号来导出一个函数;因此,以下三个数组保持更新:
·AddressOfFunctions,按照序号顺序依次保存函数的地址;
·AddressOfNames,保存函数名称;
·AddressOfNameOrdinals,保存序号,该数组与AddressOfNames数组次序相同。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!