查看原文
其他

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

七夜安全 七夜安全博客 2021-01-04

微信公众号:七夜安全博客 关注信息安全技术、关注 系统底层原理。问题或建议,请公众号留言。

前言

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

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

  1. 自身.exe -> ntdll.dll -> kernel32.dll ->KERNELBASE.DLL -> NULL


3.定位LDR_DATA_TABLE_ENTRY

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

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

  1. push tebProcessEnvironmentBlock

  2. pop eax

  3. fs mov eax, dword [eax];定位PEB

  4. mov eax, dword [eax + pebLdr] ;找到ldr

  5. mov esi, dword [eax + ldrInLoadOrderModuleList] ;找到ldrInLoadOrderModuleList

  6. lodsd ;不断的向后寻找kernel

  7. xchg eax, esi

  8. lodsd

  9. mov ebp, dword [eax + mlDllBase];kernel dll的基地址

  10. 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里,首先消除上面两条指令的影响,具体内容如下,仔细看注释:

  1. bool overwrite_hdr(BYTE *my_exe, size_t exe_size, DWORD raw)

  2. {

  3. BYTE redir_code[] = "\x4D" //dec ebp

  4. "\x5A" //pop edx

  5. "\x45" //inc ebp

  6. "\x52" //push edx

  7. "\xE8\x00\x00\x00\x00" //call <next_line>

  8. "\x5B" // pop ebx

  9. "\x48\x83\xEB\x09" // sub ebx,9

  10. "\x53" // push ebx (Image Base)

  11. "\x48\x81\xC3" // add ebx,

  12. "\x59\x04\x00\x00" // value

  13. "\xFF\xD3" // call ebx

  14. "\xc3"; // ret


  15. size_t offset = sizeof(redir_code) - 8;


  16. memcpy(redir_code + offset, &raw, sizeof(DWORD));

  17. memcpy(my_exe, redir_code, sizeof(redir_code));

  18. return true;

  19. }

跳转的小技巧

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

  1. "\xE8\x00\x00\x00\x00" //call <next_line>

  2. "\x5B" // pop ebx

  3. "\x48\x83\xEB\x09" // sub ebx,9

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

  1. "\xE8\x00\x00\x00\x00" //call <next_line>

  2. "\x5B" // pop ebx

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

总结

10点多了,设定时发了 。看看书,早点睡觉,迎接上班。。。


推荐阅读:

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

linux无文件执行— fexecve 揭秘

沙盒syscall监控组件:strace and wtrace

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

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

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

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


如果大家喜欢这篇文章的话,请不要吝啬分享到朋友圈,并置顶公众号。

关注公众号:七夜安全博客

回复【8】:领取 python神经网络 教程 

  • 回复【1】:领取 Python数据分析 教程大礼包

  • 回复【2】:领取 Python Flask 全套教程

  • 回复【3】:领取 某学院 机器学习 教程

  • 回复【4】:领取 爬虫 教程

  • 回复【5】:领取编译原理 教程

  • 回复【6】:领取渗透测试教程

  • 回复【7】:领取人工智能数学基础

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存