无文件执行:一切皆是shellcode (中)

2020-03-12 15:49:01 浏览数 (1)

前言

良好的习惯是人生产生复利的有力助手。

2020年第四篇文章,继续今年的flag:每周至少更新一篇文章。

PE to Shellcode原理

在上一篇文章中,介绍了PE_to_shellcode这个项目,并简单提了一句原理,本节就详细讲解一下PE是如何转化为shellcode的。

好奇心

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实现。

PE loader的实现

因为项目中有32位和64位的PE loader shellcode实现,我们仅以32位为例进行讲解,由于涉及的知识点过多, 部分的内容一句带过,不进行详细描述,之后会有专题进行讲解,本次只是搭建起整个 PE loader的框架,让大家明白整体流程。

(1)定位kernel基地址

实现PE Loader 需要找到GetProcAddress,LoadLibraryA,VirtualAlloc 三个关键API的地址:

  1. LoadLibraryA 用来加载动态链接库
  2. GetProcAddress 用来获取动态链接库中的API函数地址
  3. VirtualAlloc 用于为PE文件分配内存空间

这三个API位于kernel32.dll中,而每个进程启动都会自动加载kernel32.dll,因此需要先找到进程中的kernel32.dll基址,然后再通过偏移量找到API的入口地址。寻找进程中的kernel32.dll基地址,分为三个步骤:

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,加载的模块顺序如下:

代码语言:javascript复制
自身.exe -> ntdll.dll -> kernel32.dll ->KERNELBASE.DLL -> NULL

3.定位LDR_DATA_TABLE_ENTRY

每当为本进程装入一个模块时,就要为其分配、创建一个LDRDATATABLEENTRY数据结构,并将其挂入InLoadOrderModuleList和InMemoryOrderModuleList,完成对这个模块的动态链接以后,就把它挂入InInitializationOrderModuleList队列,以便依次调用模块的初始化函数。由此可见进程加载的每个模块都会有一个LDRDATATABLEENTRY,其作用为存储模块的基本信息,DLL基址在其偏移0x18处。

这三步看似复杂,但最终的汇编代码 很简单:

代码语言:javascript复制
        push    tebProcessEnvironmentBlock        pop     eax        fs mov  eax, dword [eax];定位PEB        mov     eax, dword [eax   pebLdr] ;找到ldr        mov     esi, dword [eax   ldrInLoadOrderModuleList] ;找到ldrInLoadOrderModuleList        lodsd    ;不断的向后寻找kernel        xchg    eax, esi        lodsd        mov     ebp, dword [eax   mlDllBase];kernel dll的基地址        call    parse_exports
(2) 定位API

我们找到了kernel32.dll的基地址,接下来通过偏移量找到kernel32.dll的导出表,最后通过导出表找到api的入口地址。导出表的结构如下,最终要找到AddressOfFunctions。

(3) 映射PE文件到内存
  1. 使用VirtualAlloc分配内存
  2. 映射map MZ header, NT Header, FileHeader, OptionalHeader, all section headers
  3. 映射sections data
(4) 导入dll 并重定向地址

使用LoadLibraryA循环加载PE文件导入表中的dll

(5) 设置入口点并执行

猜想

通过上文提到的结构模型和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”开头的shellcode

由于“MZ”必不可少,那需要看一下M和Z对应 ASCII的汇编指令:

  • M对应的值是x4D,汇编指令是 dec ebp
  • Z对应的值是x5A,汇编指令是 pop edx

在接下来的shellcode里,首先消除上面两条指令的影响,具体内容如下,仔细看注释:

代码语言:javascript复制
bool overwrite_hdr(BYTE *my_exe, size_t exe_size, DWORD raw){    BYTE redir_code[] = "x4D" //dec ebp        "x5A" //pop edx        "x45" //inc ebp        "x52" //push edx        "xE8x00x00x00x00" //call <next_line>        "x5B" // pop ebx        "x48x83xEBx09" // sub ebx,9        "x53" // push ebx (Image Base)        "x48x81xC3" // add ebx,        "x59x04x00x00" // value        "xFFxD3" // call ebx        "xc3"; // ret
    size_t offset = sizeof(redir_code) - 8;
    memcpy(redir_code   offset, &raw, sizeof(DWORD));    memcpy(my_exe, redir_code, sizeof(redir_code));    return true;}

跳转的小技巧

在上面的shellcode中,有三行可能大家不明白:

代码语言:javascript复制
"xE8x00x00x00x00" //call <next_line>"x5B" // pop ebx"x48x83xEBx09" // sub ebx,9

shellcode如何跳转到Stub,必须要知道Stub在内存中的地址。我们可以先知道整个文件加载到内存中的基地址,然后通过偏移找到Stub。但是如何找到基地址呢?我们可以知道自身指令在内存中的地址,然后减去执行的指令字节数就是基地址,常用的是call-pop方式。

代码语言:javascript复制
"xE8x00x00x00x00" //call <next_line>"x5B" // pop ebx

此时ebx中存储的是就是当前指令的地址。

推荐阅读:

无文件执行:一切皆是shellcode (上)

linux无文件执行— fexecve 揭秘

沙盒syscall监控组件:strace and wtrace

无"命令"反弹shell-逃逸基于execve的命令监控(上)

APT组织武器:MuddyC3泄露代码分析

Python RASP 工程化:一次入侵的思考

教你学木马攻防 | 隧道木马 | 第一课

0 人点赞