静态恶意代码逃逸(第十一课)- 汇编语言编写Shellcode加载器

倾旋
倾旋
技术分享|2022-2-16|最后更新: 2023-6-22|
type
status
date
slug
summary
tags
category
icon
password
URL

0x00 NASM与MASM

NASMMASM是一个汇编器,能够将汇编代码转换为能够被CPU执行的(目标代码)二进制代码,NASM目前是由H. Peter Anvin提供支持,与MASM相对来说较为自由。MASM是由微软推出,但已经许久没有更新,我开始是从MASM开始看,但是有些书籍会举例很多伪汇编的例子,这不能让我很好的锻炼汇编语言,MASM提供了很多的伪汇编语法,这块我直接跳过,伪汇编代码给我的感觉像是在写C语言。
MASM的汇编文件可以直接被Visual Studio编译,这里倒是方便很多,but… 不管是NASM和MASM在学习的过程中都需要乖乖安装环境。很多黑客的Shellcode都是基于NASM环境开发的,Kali Linux中也默认安装了NASM,评判一个Shellcode的好坏就是短小、精炼、干净,NASM是首选。

0x01 程序是如何产生的?

这个问题可能已经非常常见了,我觉得有一本书能够很好的诠释《程序员的自我修养:链接、装载与库》2016年我还买过这本,当时在学习C语言开发,觉得写的真好。
一个程序是由编译器编译代码生成的,编译的过程中经过了一系列步骤,涉及到本文知识的主要是程序的链接。首先,一段C语言代码,经过编译器生成Obj文件,这个文件包含了要执行的所有代码,但是这还不够,还缺少生成复合操作系统平台格式的过程,目前系统中能够跑起来的程序都遵循着COFF规范,Linux下是elf,Windows下是exe。

0x02 从NASM汇编到EXE执行文件

首先,需要写一段测试代码,然后生成目标文件,经过链接器将目标文件进行链接,生成PE文件。
测试代码:
通过nasm编译:
notion image
如果要采用VS自带的链接器,就使用win32格式: nasm -f win32 win.asm
可以看到生成了win.obj,接下来使用链接器进行链接,选择不同的链接器生成的文件会有差异,经过测试w32nasm中提供的alink生成的PE文件和VS自带的Link有很大区别,而且PE的节表名称不能带. ,而Windows平台大部分PE文件的节表名称都是带.的,比较规范,因此这里就采用了VS自带的Link。
notion image
notion image
将win.exe拖入x32dbg进行调试查看:
notion image
汇编生成的文件就是那么简洁….

0x03 Shellcode的通用编写思路

大部分通用的Shellcode编写思路都是从FS寄存器去寻找PEB,然后遍历PEB中的模块列表,从模块列表中寻找Kernel32.dll和ntdll.dll的基址,最终不断从模块中找到API的地址进行调用。
找一个Exploit-db上的Shellcode举例:
其中mov ebx, [fs:ebx+0x30] ; EBX = Address_of_PEB [fs:0x30] 指向的就是PEB地址。
进程环境块 (Process Environment Block) 是一种用户模式数据结构,应用程序(以及恶意软件扩展)可以使用它来获取加载模块列表、进程启动参数、堆地址、检查程序是否正在运行等信息。
汇编代码解读:
EBX+0xC 意味着从PEB的地址开始增加0xC个字节的地址就是指向PPEB_LDR_DATA Ldr结构体成员的地址,0xC来源于PEB第一个成员到LDR结构体成员的数据宽度,PVOID Reserved3[2]这样实际上是占用了8个字节,一个指针占用4个字节,2个就是8字节。
EBX+0x1C意味着从LDR开始增加0x1C个字节的地址,就是指向ntdll.dll的LIST_ENTRY结构体成员LIST_ENTRY InInitializationOrderModuleList的地址,由于LIST_ENTRY 是一个链表结构,它有两个成员,因此整个结构体占用8个字节,后续取地址2次,都是为了遍历这个链表结构。
0x1C = ULONG Length + BOOLEAN Initialized + PVOID SsHandle + sizeof(LIST_ENTRY InLoadOrderModuleList) + sizeof(LIST_ENTRY InLoadOrderModuleList)
EBX+0x8 是从InInitializationOrderLinks开始增加8字节,获取指向到DllBase成员的地址,而这个地址就是DLL加载到内存中的基址,有了基址,就可以从DLL的导出表寻找导出函数进行调用了。
InInitializationOrderModuleList中的DLL加载顺序是固定的,第一个DLL是NTDDLL.dll,第二个是KERNELBASE.dll、第三个是KERNEL32.DLL。
notion image
DLL顺序:0x77ca0000(C:\WINDOWS\SYSTEM32\ntdll.dll)、0x76d20000(C:\WINDOWS\System32\KERNELBASE.dll) 、76b30000(C:\WINDOWS\System32\KERNEL32.DLL)。
汇编代码解读:
[eax+0x3C] 此时EAX中保存着DLL的基地址,指向DOS头
notion image
EAX+0x3C指向DOS头的e_lfanew成员,e_lfanew保存的是从PE文件第一个字节到PE头的文件偏移。mov ebx, [eax+0x3C] 的含义就是将PE头的文件偏移地址保存到ebx。
notion image
notion image
add ebx, eax 的含义是将PE头的地址保存到ebx中。从PE头向下偏移0x78就是PE文件相对于PE头的导出表偏移地址,后续就是从PE文件寻找导出函数地址了,根据公开的一些Shellcode,可以封装出一些常用的过程,比如从Kernel32.dll中寻找GetProcAddress API的地址,以便后续传入API的名字自动帮助我们去寻找API的地址。
OK,接下来,写一个执行操作系统命令的Shellcode,并使用NASM编译,VS Link进行链接。
首先,执行操作系统命令的API是WinExec
notion image
在Kernel32.dll中导出,那这就容易多了。
使用NASM汇编器进行汇编:
notion image
生成Obj文件后,使用VS Link 链接器进行链接:
notion image

0x03 汇编传递字符串的方式

在写Shellcode或者汇编程序的过程中,难免需要将大量的字符串当作参数传递给函数。不能以高级语言的认知去理解汇编语言传递参数,这里涉及到堆栈平衡、字符串在内存中的形态、计算机存储模式(大端、小端)、函数参数入栈方式等,由于本文篇幅与重点有限,不展开讲解了。
从字符串入栈开始进行调试:
notion image
这一段是调用GetProcAddress这个API,它有两个参数:
Windows操作系统的API传递参数都是从右向左传递,因此第一个参数就是要获取的函数地址的函数名,这个参数是一个字符串。字符串是一个不固定长度的一段连续的值,所以函数接收的参数类型大部分是一个字符串指针,这个指针指向了字符串的开始位置,函数在读取字符串的适合遇到0x00作为字符串的结尾。
当栈中压入两个值后,代表了字符串本身已经入栈,再次将指向这个字符串起始位置的内存地址压入栈来完成传入第一个参数的操作,这时刚好ESP栈顶指向的就是字符串的起始位置,所以PUSH ESP就是传递的字符串指针。大部分Shellcode中的字符串传递方式都是如此。
notion image

0x04 Shellcode加载器实现

思路其实与C语言实现的过程并无任何差异,只不过是用更加灵活的汇编语言完成,汇编语言生成的目标文件经过链接后的体积非常小,使得杀毒软件定位特征变得棘手。
这个项目的开端是用编写Shellcode的视角去做的,可以生成加载器版本的Shellcode去替换到各类软件中,字节码就是它的灵魂。上一个案例中的程序编译完毕的体积仅仅只有1.5KB,由于对齐的因素实际占用空间4KB。
notion image
思路:寻找PEB →定位Kernel32.DLL的PE基址→定位GetProcAddress API的地址,获取GetFileSize、CreateFileA、ReadFile、CreateFileMappingA、MapViewOfFileA、VirtualAlloc、VirtualProtect这类常用的API地址对Shellcode进行读取,放入可执行的内存,最终call一下执行。
notion image
生成的样本只有2KB,能够完成Shellcode的执行,我还增加了一系列的反调试功能,预防一些自动分析沙箱,延长样本存活时间。
 
©2021-2024 倾旋. All rights reserved.