良好的习惯是人生产生复利的有力助手。
shellcode是一段可以在内存中直接执行的指令,先将指令载入可读可写可执行的缓冲区,将指令指针指向缓冲区的起始位置,依次往下执行即可。
PE文件本身是无法直接在内存中执行的,windows操作系统需要将PE文件按照规则映射到内存中,并将指令指针指向程序入口就可以执行了,这就是PE loader的工作流程。
PE文件本身并不重要,它的执行依赖于PE loader,因此PE如何转化为shellcode的问题,变成了PE loader 如何转化为shellcode的问题。
PE文件功能千奇百怪,不受我们控制,但是PE loader 功能固定,我们只要通过shellcode实现PE loader,就可以达成目标。通过上述的思考,一个PE 转化为shellcode的结构模型就出来了,如下图所示,Stub是shellcode化的PE loader。
在项目的工程目录中,hldr32和hldr64分别是32位和64位的PE loader shellcode实现。
因为项目中有32位和64位的PE loader shellcode实现,我们仅以32位为例进行讲解,由于涉及的知识点过多, 部分的内容一句带过,不进行详细描述,之后会有专题进行讲解,本次只是搭建起整个 PE loader的框架,让大家明白整体流程。
实现PE Loader 需要找到GetProcAddress,LoadLibraryA,VirtualAlloc 三个关键API的地址:
1.定位TEB与PEB
TEB( 线程环境块)中保存频繁使用的线程相关的数据。进程中的每个线程都有自己的一个TEB。一个进程的所有TEB都以堆栈的方式,PEB(进程环境块)存放进程信息,每个进程都有自己的PEB信息。
通过FS寄存器可以获取TEB的基址:在FS存储的是TEB在GDT 中的序号,通过GDT获取TEB的基址。
PEB结构体在TEB偏移0x30处,即FS:[0x30]。
2.定位Ldr
在PEB偏移0x0c处是Ldr,Ldr的类型为PEBLDRDATA结构体指针。Ldr的作用是存储进程已加载的模块(Module)信息。Module是指PE格式的可执行映像,包括EXE映像和DLL映像。Ldr通过3个队列存储进程加载的Module信息,即InLoadOrderModuleList、InMemoryOrderModuleList、和InInitializationOrderModuleList,我们选择的是InLoadOrderModuleList,加载的模块顺序如下:
3.定位LDR_DATA_TABLE_ENTRY
每当为本进程装入一个模块时,就要为其分配、创建一个LDRDATATABLEENTRY数据结构,并将其挂入InLoadOrderModuleList和InMemoryOrderModuleList,完成对这个模块的动态链接以后,就把它挂入InInitializationOrderModuleList队列,以便依次调用模块的初始化函数。由此可见进程加载的每个模块都会有一个LDRDATATABLEENTRY,其作用为存储模块的基本信息,DLL基址在其偏移0x18处。
这三步看似复杂,但最终的汇编代码 很简单:
使用LoadLibraryA循环加载PE文件导入表中的dll
通过上文提到的结构模型和PE Loader的实现,基本上可以完成PE转化为shellcode的功能,但是PE_to_shellcode项目生成的shellcode 不仅可以采用shellcode的加载方式,而且可以双击像PE一样独立运行,这是怎么做到的呢?至少上文的结构模型完成不了,因为Stub已经破坏了PE头,操作系统加载的时候是不会将他识别为PE文件的!!!
之前比较PEtoshellcode项目修改前和修改后的程序发现,修改后的程序是有MZ标识,而且Stub是附加在PE文件后面的,这给了我很大的启发。
通过猜想和比对,对原有的结构模型进行改进,将Stub是附加在PE文件后面,并对PE文件头部进行修改实现跳转,从而实现PE文件一文件两用。
在新模型中,Stub可以不用改变,直接附加在PE文件的最后,对PE文件的头部添加一段跳转shellcode,而且这段shellcode必须以"MZ"开头,这样才能被识别为正常的PE文件。PE文件的头部是DOS头,其结构如下,比较重要的是emagic和elfanew,而其他的位置内容改变可以随意一些:
由于“MZ”必不可少,那需要看一下M和Z对应 ASCII的汇编指令:
在接下来的shellcode里,首先消除上面两条指令的影响,具体内容如下,仔细看注释:
在上面的shellcode中,有三行可能大家不明白:
shellcode如何跳转到Stub,必须要知道Stub在内存中的地址。我们可以先知道整个文件加载到内存中的基地址,然后通过偏移找到Stub。但是如何找到基地址呢?我们可以知道自身指令在内存中的地址,然后减去执行的指令字节数就是基地址,常用的是call-pop方式。