shellcode编写指南
前言
linux的shellcode就不用说了,直接通过一个int 0x80系统调用,指定想调用的函数的系统调用号(syscall),传入调用函数的参数,即可,懂的都懂。
在windows中,没有像int 0x80系统调用功能来找相应的函数,但是也有syscall这样的系统调用,过AV奇效,这里主要介绍的是如何手动去通过GetProcAddress去查找某个函数的地址,然后进行调用,这里需要一丢丢c基础和汇编基础。
原理
在windows中,我们需要如下步骤去找到相应的函数,进行调用:
1)找到PEB表,获取Kernel32.dll base地址
2)通过kernel32.dll PE文件格式找到导出表的地址
3)通过导出表定位GetProcAddress的RVA
4)通过GetProcAddress函数找到LoadLibraryA函数地址
5)通过这GetProcAddress和LoadLibraryA两个函数来加载dll文件和查找函数以供使用
先了解几个基本概念,api函数,动态链接库文件。
2.1 kernel32.dll
定义:kernel32.dll是windows中非常重要的32位动态链接库文件,工作在ringo,属于内核级文件,它控制着系统的内存管理、数据的输入输出操作和中断处理,当Windows启动时,kernel32.dll就驻留在内存中特定的写保护区域,使别的程序无法占用这个内存区域,提供了954个可供调用api。
2.2 GetProcAddress
定义:GetProcAddress是一个计算机函数,功能是检索指定的动态链接库(DLL)中的输出库函数地址
FARPROC GetProcAddress(
HMODULE hModule, // DLL模块句柄,可通过LoadLibrary、AfxLoadLibrary 或者GetModuleHandle函数返回此句柄。
LPCSTR lpProcName // 函数名
);
如果函数查找成功,返回值是DLL中的输出函数地址,如果函数调用失败,返回值是NULL
动态链接库DLL的进程会调用GetProcAddress来获取DLL中导出函数的地址。
2.3 LoadLibrary
将指定的模块加载到调用进程的地址空间中。
HMODULE LoadLibraryA( LPCSTR lpLibFileName );//模块的名称,可以是库模块(.dll文件)或可执行模块(.exe文件)。
如果函数成功,则返回值是模块的句柄,如果函数失败,则返回值为NULL。
2.4 PE文件格式
PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件
不多讲了,一张图就行,PE文件格式内容要说多,专门有一本书来讲PE文件结构,要说简单,一张图就能概括,如果不是为了去写壳,脱壳.......根本不需要知道那么多。
2.5 PEB
在Windows操作系统中,PEB是一个位于所有进程内存中固定位置的结构体,此结构体包含关于进程的有用信息,如可执行文件加载到内存的位置,模块列表(DLL),指示进程是否被调试的标志,还有许多其他的信息。
微软定义:
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;//偏移4+2*4
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;
在64位上的定义:
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[21];
PPEB_LDR_DATA LoaderData;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
BYTE Reserved3[520];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved4[136];
ULONG SessionId;
} PEB;
我们重点关注结构体PEB_LDR_DATA里面的内容,包含如下信息(该结构包含有关进程的已加载模块的信息):
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8];//保留供操作系统内部使用。
PVOID Reserved2[3];//保留供操作系统内部使用
LIST_ENTRY InMemoryOrderModuleList;//双向链接列表的头部,该列表包含该过程的已加载模块。列表中的每个项目都是指向LDR_DATA_TABLE_ENTRY结构的指针,偏移为8+3x4
} PEB_LDR_DATA, *PPEB_LDR_DATA;
LIST_ENTRY结构是一个简单的双向链表,包含指向下一个元素(Flink)的指针和指向上一个元素的指针(Blink)
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
我们需要通过LDR_DATA_TABLE_ENTRY结构体来获取已加载DLL的信息结构体为:
typedef struct _LDR_DATA_TABLE_ENTRY
LIST_ENTRY InLoadOrderLinks; /* 0x00 */
LIST_ENTRY InMemoryOrderLinks; /* 0x08,这里是Flink指向的地方 */
LIST_ENTRY InInitializationOrderLinks; /* 0x10 */
PVOID DllBase; /* 0x18 */
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName; /* 0x24 */
UNICODE_STRING BaseDllName; /* 0x28 */
ULONG Flags;
WORD LoadCount;
WORD TlsIndex;
union
LIST_ENTRY HashLinks;
struct
PVOID SectionPointer;
ULONG CheckSum;
union
ULONG TimeDateStamp;
PVOID LoadedImports;
_ACTIVATION_CONTEXT * EntryPointActivationContext;
PVOID PatchInformation;
LIST_ENTRY ForwarderLinks;
LIST_ENTRY ServiceTagLinks;
LIST_ENTRY StaticLinks;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
InMemoryOrderModuleList字段是一个指针,指向LDR_DATA_TABLE_ENTRY 结构体上的LIST_ENTRY字段,但是它不是指向LDR_DATA_TABLE_ENTRY 起始位置的指针,而是指向这个结构的InMemoryOrderLinks字段。
编写shellcode
3.1 c++库文件配合内联汇编
先来针对指定系统的shellcode的编写,指定系统的,我们首先通过LoadLibraryA函数导入相应的dll文件,获得一个dll句柄,在把这个dll句柄当作参数传入GetProcAddress 搜索查找指定函数,返回该函数的地址,然后通过函数的地址来调用函数,用c++代码内联汇编实现
#include<Windows.h>
#include<iostream>
#include<string.h>
using namespace std;
int main()
string cmd = "dir";
HINSTANCE Libhandle = LoadLibraryA("msvcrt.dll"); //加载dll文件
if (Libhandle == NULL)
return 0;
//system("dir");
cout <<"msvcrt Address = "<< Libhandle << endl;
LPTSTR getaddr = (LPTSTR)GetProcAddress(Libhandle, "system");//获得system函数的地址
cout << "system Address = " << getaddr << endl;
//通过汇编代码来调用函数
_asm{
//system("dir"); //64 69 72
pushad
pushfd
xor ebx, ebx
mov ebx,0x726964 ;这里需要注意数据入栈的顺序
push ebx
push esp
mov ebx, getaddr
call ebx
add esp,8
popfd
popad
return 0;
}
会得到这个效果
但是这是c++代码去加载相应头文件,直接调用LoadLibraryA来加载,具有局限性,无法移植的shellcode,且那段内联汇编代码必须在导入相应链接库得情况才能执行,不然会报错。
这里补充一点知识:
其实程序最开始加载并不是从main函数的,main函数也是别的函数调用执行的。
1.首先操作系统必须的创建进程,然后jmp到这个进程的入口函数
2.然后经过一系列的初始化
3.完成初始化之后,调用main函数,开始执行程序主体。
所以说,我们这里我们直接用BinaryNinja打开,找到可执行文件的入口点并不是main函数入口点
他会先从初始化函数开始(这里是编译器给我加的),一直执行到我们mian函数
3.2 从PEB表查找LoadLibraryA,GetProcAddress
在上面讲了,虽然我们通过c++库拿到了LoadLibraryA和GetProcAddress函数,但是在实际的情况下并不实用,因为实际情况下并没有c++库给我们调用,所以这时候就体现了PEB表的优势在,因为PEB表是位于所有进程内存中固定位置的结构体,所以我们在任意进程里都能找到PEB表,通过PEB表找到kernel32.dll,从Kernel32.dll中找到LoadLibraryA,和GetProcAddress这个两个函数,这样就解决了可移植性的问题。
这里首先还得了解一个非常重要的概念,FS段寄存器,在我们介绍Kernel32.dll时候,说了工作在ringo,属于内核级文件,与之相对应的User32.dll 工作在ring3,属于用户级文件,这里就涉及了内核态和用户态,不讲深了,你只需要知道我们的程序虽然在用户层里运行,但是有时候也需要切换到内核状态。
而FS寄存器的改变,就意味着程序在R3和R0之间进行切换(都是在R0下给FS赋不同值的),在R3下:FS段寄存器的值是0x3B,在R0下:FS段寄存器的值是0x30,注意这里0x30和0x3B 是代表指向GDT表中的不同段。
当运行在R3下时,FS指向的段是GDT中的0x3B段.该段的长度为4K,基地址为当前线程的线程环境块(TEB),所以该段也被称为“TEB段”
当运行在R0下时, FS指向的段是GDT中的0x30段.该段的长度也为4K,基地址为0xFFDFF000.该地址指向系统的处理器控制区域(KPCR)
从以上可得知,我们如何去找PEB的基地址?
在R3状态下的FS寄存器存的值就是PEB表基地址,加上偏移量0X30,就得到了PEB的地址,上汇编代码。
mov eax,fs:[ecx + 0x30];PEB
找到了PEB表,然后通过PEB加上偏移0xC得到PPEB_LDR_DATA [Ldr]结构体的地址
mov eax, [eax + 0xc] ;PEB->Ldr
再偏移0x14,找到InMemoryOrderLinks
mov esi,[eax+0x14];PEB->Ldr->InMemoryOrderModuleList
现在我们的寄存器放的值就是InMemoryOrderModuleList地址哟,但是我们想要的kernel32.dll处于第三个模块(固定位置,第三),我们前面讲了在LIST_ENTRY结构体中,可以通过Flink和Blink指针进行模块的切换,而InMemoryOrderModuleList便是LIST_ENTRY结构体指针,而InMemoryOrderModuleList指向的就是LIST_ENTRY结构体中的InMemoryOrderLinks(Flink指针)字段,我们通过InMemoryOrderLinks(Flink指针)字段来遍历到Kernel32.dll模块。
lodsd指令:会把esi寄存器指向的地址读取双字,然后把结果存放在eax寄存器
xchg指令:交换寄存器中的值
lodsd; 读取第二个模块的地址
xchg eax,esi
lodsd;读取第三模块的地址
mov ebx,[eax+0x10];获得kernel32.dllbase地址
push ebx
因为InMemoryOrderLinks在LIST_ENTRY偏移为0x8,而dllbase为0x18,所以InMemoryOrderLinks到dllbase只需要偏移0x10,这里我们就找到了kernel32.dll base地址。
如何通过kernel32.dll地址找到 GetProcAddress函数地址?
这里需要解析kernel32.DLL文件的PE头找到导出表(前面就说了,dll文件也是PE文件格式),需要找到PE头,在PE文件结构中,是用IMAGE_DOS_HEADER结构体来定义DOS文件头
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // 00000000 4D 5A,Magic number
WORD e_cblp; // 00000002 90 00,Bytes on last page of file
WORD e_cp; // 00000004 03 00,Pages in file
WORD e_crlc; // 00000006 00 00,Relocations
WORD e_cparhdr; // 00000008 04 00,Size of header in paragraphs
WORD e_minalloc; // 0000000A 00 00,Minimum extra paragraphs needed
WORD e_maxalloc; // 0000000C FF FF,Maximum extra paragraphs needed
WORD e_ss; // 0000000E 00 00,Initial (relative) SS value
WORD e_sp; // 00000010 B8 00,Initial SP value
WORD e_csum; // 00000012 00 00,Checksum
WORD e_ip; // 00000014 00 00,Initial IP value
WORD e_cs; // 00000016 00 00,Initial (relative) CS value
WORD e_lfarlc; // 00000018 40 00,File address of relocation table
WORD e_ovno; // 0000001A 00 00,Overlay number
WORD e_res[4]; // 0000001C 00 00 00 00,Reserved words
// 00000020 00 00 00 00
WORD e_oemid; // 00000024 00 00,OEM identifier (for e_oeminfo)
WORD e_oeminfo; // 00000026 00 00,OEM information; e_oemid specific
WORD e_res2[10]; // 00000028 00 00 00 00,Reserved words
// 0000002C 00 00 00 00
// 00000030 00 00 00 00
// 00000034 00 00 00 00
// 00000038 00 00 00 00
LONG e_lfanew; // 0000003C F8 00 00 00,File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
其中有用的两个字段,分别是e_magic和e_lfanew,一个是dos 签名:标志这是dos头,一个记载PE头的在文件中偏移,我们要拿到e_lfanew值,通过它来找到NT文件头,这里偏移量为0x3c
mov edx,[ebx+0x3c];e_lfanew
add edx,ebx;加上基地址,得到了PEheader地址
来看看NT文件头
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;//Signature PE文件标识,被定义为00004550
IMAGE_FILE_HEADER FileHeader;//FileHeader,该结构指向IMAGE_FILE_HEADER。
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;//OptionalHeader,该结构指向_IMAGE_OPTIONAL_HEADER32,Windows操作系统可执行文件的大部分特性均在这个结构里面呈现
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
#ifdef _WIN64
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#else
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#endif
这里有两种,一种是32位的,一种是64位的,因为我这里是拿的c++中里面的库里面的结构体定义,直接复制粘贴过来了,结构上大体一样,这里OptionalHeader 指向着选项头
来看看选项头:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
/*Magic字段 :说明文件的类型,如果为010Bh,表面文件为PE32;如果为0107h,表明文件为ROM映像;如果为20Bh,表面文件为PE64.*/
BYTE MajorLinkerVersion; // 链接程序的主版本号
BYTE MinorLinkerVersion; // 链接程序的次版本号
DWORD SizeOfCode; // 所有含代码的节的总大小
DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小
DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
DWORD AddressOfEntryPoint; // 程序执行入口RVA
DWORD BaseOfCode; // 代码的区块的起始RVA
DWORD BaseOfData; // 数据的区块的起始RVA
DWORD ImageBase; // 程序的首选装载地址
/*ImageBase字段 指出文件的优先装入地址。也就是说当文件被执行时,如果可能的话,Windows优先将文件装入到由ImageBase字段指定的地址中。只有指定的地址已经被**模块
使用时,文件才被装入到**地址中。链接器产生可执行文件的时候对应这个地址来生成机器码,所以当文件被装入这个地址时不需要进行重定位操作,装入的速度最快。如果文件
被装载到**地址的话,将不得不进行重定位操作,这样就要慢一点。对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被**模块占据,所以EXE
总是能够按照这个地址装入。这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被**的
DLL使用,所以DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED
位总是为0,而EXE文件的这个标志位总是为1。在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认
优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。*/
DWORD SectionAlignment; // 内存中的区块的对齐大小
DWORD FileAlignment; // 文件中的区块的对齐大小
WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
WORD MajorImageVersion; // 本PE文件映像的主版本号
WORD MinorImageVersion; // 本PE文件映像的次版本号
WORD MajorSubsystemVersion; // 运行所需要的子系统的主版本号
WORD MinorSubsystemVersion; // 运行所需要的子系统的次版本号
DWORD Win32VersionValue; // 子系统版本号,暂时保留未用。必须设置为0
DWORD SizeOfImage; // 映像装入内存后的总尺寸 +54h
DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小
DWORD CheckSum; // 映像的校检和 +5Ch
WORD Subsystem; // 可执行文件期望的子系统 表3-4
WORD DllCharacteristics; // Dll文件属性 +60h 表3-6
DWORD SizeOfStackReserve; // 初始化时保留的栈大小
DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小 +68h
DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小 +70h
/*SizeOfHeapCommit字段:初始化时提交的堆大小,在进程初始化时设定的堆所占用的内存空间。默认值为1页。*/
DWORD LoaderFlags; // 加载标志 与调试有关,默认为 0
DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表
/*DataDirectory字段 这个字段可以说是最重要的字段之一,它由16个相同的IMAGE_DATA_DIRECTORY结构组成。虽然PE文件中的数据是按照装入内存后的页属性归类而被放在不同
的节中的,但是这些处于各个节中的数据按照用途可以被分为导出表、导入表、资源、重定位表等数据块,这16个IMAGE_DATA_DIRECTORY结构就是用来定义多种不同用途的
数据块的。*/
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
这里的DataDirectory字段是我们需要注意的,因为在这个字段中有导出表的数据块,算算该字段的偏移,0x78
mov edx, [edx + 0x78]
add edx, ebx;export table addr
我们来看看数据目录,IMAGE_DATA_DIRECTORY结构体:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;//RVA
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
定义非常简单,但它仅仅指出了某种数据块的位置和长度,看一下索引所代表的含义:
索 引 |
索引值在Windows.inc中的预定义值 |
对应的数据块 |
---|---|---|
0 |
IMAGE_DIRECTORY_ENTRY_EXPORT |
导出表 |
1 |
IMAGE_DIRECTORY_ENTRY_IMPORT |
导入表 |
2 |
IMAGE_DIRECTORY_ENTRY_RESOURCE |
资源 |
3 |
IMAGE_DIRECTORY_ENTRY_EXCEPTION |
异常(具体资料不详) |
4 |
IMAGE_DIRECTORY_ENTRY_SECURITY |
安全(具体资料不详) |
5 |
IMAGE_DIRECTORY_ENTRY_BASERELOC |
重定位表 |
6 |
IMAGE_DIRECTORY_ENTRY_DEBUG |
调试信息 |
7 |
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE |
版权信息 |
8 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR |
具体资料不详 |
9 |
IMAGE_DIRECTORY_ENTRY_TLS |
Thread Local Storage |
10 |
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG |
具体资料不详 |
11 |
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT |
具体资料不详 |
12 |
IMAGE_DIRECTORY_ENTRY_IAT |
导入函数地址表 |
13 |
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT |
具体资料不详 |
14 |
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR |
具体资料不详 |
15 |
未使用 |
|
可以看到,我们要的导出表,就在第一个索引,我们必须得获得获得导出表的地址,但是前面的数据目录是记录着导出表的虚拟地址,而实际的结构如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp; //时间戳. 编译的时间. 把秒转为时间.可以知道这个DLL是什么时候编译出来的.
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; //指向该导出表文件名的字符串,也就是这个DLL的名称
DWORD Base; // 导出函数的起始序号
DWORD NumberOfFunctions; //所有的导出函数的个数
DWORD NumberOfNames; //以名字导出的函数的个数
DWORD AddressOfFunctions; // 导出的函数地址的地址表 RVA 也就是 函数地址表
DWORD AddressOfNames; // 导出的函数名称表的 RVA 也就是 函数名称表
DWORD AddressOfNameOrdinals; // 导出函数序号表的RVA 也就是 函数序号表
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
但是通过加偏移0x24,当然这是相对于DataDirectory这个基地址来说的,这里我们需要通过AddressOfNames这个指针数组来遍历kernel32.dll加载载的函数名称,来找到GetProcAddress等函数。
mov esi, [edx + 0x20] ; AddressOfNames 偏移
add esi, ebx; Names table addr
xor ecx,ecx;ecx清零
Get_Function_ProcAddress:
inc ecx;这里来计数,我们需要知道GetProcAddress的索引
lodsd
add eax,ebx
cmp dword ptr[eax],0x50746547;比较前四个字节GetP字符串的ascill
jnz Get_Function_ProcAddress;没有找到就继续执行
cmp dword ptr[eax + 0x4], 0x41636f72 ; 比较中间四个字节rocA字符串的ascill
jnz Get_Function_ProcAddress
cmp dword ptr[eax + 0x8], 0x65726464 ; 比较中间四个字节ddre字符串的ascill
jnz Get_Function_ProcAddress;这里差不多就稳了
此时,我们只是找到GetProcAddress函数的索引,必须找到索引相对应的序号,但是注意序号都是从0开始的,这个后面会注意到,会自减1。
此时,我们只是通过索引来找到GetProcAddress函数的序号,然后我们可以利用序号来找到函数的实际地址:
mov esi, [edx + 0x24]; AddressOfNameOrdinals字段的偏移
add esi, ebx; AddressOfNameOrdinals虚拟地址
mov cx, [esi + ecx * 2] ; 名称序号数组以2字节大小为单位的数字
dec ecx;自减,因为实际是以0开始的
mov esi, [edx + 0x1c] ; AddressOfFunctions 字段偏移
add esi, ebx ;AddressOfFunctions地址
mov edx, [esi + ecx * 4] ;取出GetProcAddress RVA偏移
add edx, ebx ; GetProcAddress地址
总结一下在导出表查找函数的顺序:
通过导出表AddressOfNames找到函数名称对应的索引 =》在AddressOfNameOrdinals表中找到索引对应的序号》通过序号在AddressOfFunctions找到对应的RVA偏移》加上基地址dll文件基地址和函数的RVA地址就得到了函数的RVA
如何获取LoadLibraryA函数地址?
因为我们已经获得了GetProcAddress地址,我们可以利用GetProcAddress(kernel32, “LoadLibraryA”)这样的方式来查找LoadLibraryA函数的地址,但是在着之前,我们需要保存我们刚找到的地址,在栈上保存数据是最明智的选择。
push edx;GetProcAddress of addr
;然后进行函数调用
push 0x0
push 0x41797261 ; aryA
push 0x7262694c ; Libr
push 0x64616f4c ; Load 入栈函数参数
push esp ; "LoadLibrary"
push ebx ;kernel32.dll of addr
call edx;函数调用GetProcAddress(kernel32, “LoadLibraryA”)
注意:函数调用会把结果输出到eax寄存器中,那么eax中存储的就是LoadLibraryA函数的地址了
3.3 获取system函数地址
因为我们已经获取到LoadLibraryA函数地址,所以我们可以利用他来导入相应的库,然后通过GetProcAddress来获取system函数地址,但是我们也得先保持栈平衡,再来保存eax里的地址
add esp, 0x10 ; pop "LoadLibraryA"
pop ecx ;
push eax ; EAX = LoadLibraryA of addr
push 0x00
push 0x00006c6c ;"ll"
push 0x642e7472 ; "rt.d"
push 0x6376736d ; "msvc"
push esp ;
"msvcrt.dll"
call eax ; LoadLibrary("msvcrt.dll")
add esp,0x10
注意:现在eax中将保存着msvcrt.dll动态链接库的基地址
然后我们通过GetProcAddress来获取system函数的地址:
mov edx,[esp+4];现在esp上是LoadLibraryA_addr esp+4是GetProcAddress esp+8是Kernel32.dll of addr,eax是msvcrt.dll addr
push 0x00006d65;"em"
push 0x74737973;"syst"
push esp;"system"
push eax;
call edx;
add esp,0x10
注意:现在eax中保存着system函数的地址
然后进行函数调用:
xor ebx, ebx
mov ebx,0x726964;"dir"
push ebx
push esp
call,eax
add esp,8
这里就基本上把shellcode完成了,但是还得把所有shellcode加在一起,还得调试更改:
;通过PEB表找到kernel32.dll base地址
xor ecx, ecx
mov eax, fs: [ecx + 0x30]
mov eax, [eax + 0xc]
mov esi, [eax + 0x14]
lodsd
xchg eax, esi
lodsd
mov ebx, [eax + 0x10]
push ebx ;kernel32.dll of 入栈
;通过kernel32.dll 的PE文件结构,找到AddressOfNames
mov edx, [ebx + 0x3c]
add edx, ebx
mov edx, [edx + 0x78]
add edx, ebx
mov esi, [edx + 0x20]
add esi, ebx
xor ecx, ecx
;通过addressofNames数组遍历得到GetProcAddress的索引
Get_Function_GetProcAddress:
inc ecx;
lodsd
add eax,ebx
cmp dword ptr[eax],0x50746547
jnz Get_Function_GetProcAddress
cmp dword ptr[eax + 0x4], 0x41636f72
jnz Get_Function_GetProcAddress
cmp dword ptr[eax + 0x8], 0x65726464
jnz Get_Function_GetProcAddress
;通过AddressOfNameOrdinals加上索引,获得序号
mov esi, [edx + 0x24]
add esi, ebx
mov cx, [esi + ecx * 2]
dec ecx
;通过AddressOfFunctions遍历序号获得GetProcAddress函数的地址
mov esi, [edx + 0x1c]
add esi, ebx
mov edx, [esi + ecx * 4]
add edx, ebx
push edx ;GetProcAddress of addr入栈
;通过Kernel32.dll of addr 和 Get ProcAddress of addr 获得LoadLibraryA函数地址
push 0x0
push 0x41797261
push 0x7262694c
push 0x64616f4c
push esp
push ebx
call edx
add esp,0xc
pop ecx
push eax;LoadLibraryA of addr 入栈
;导入msvcrt.dll文件
add esp, 0x10
push eax
xor ecx,ecx
push ecx
mov cx, 0x6c6c
push ecx
push 0x642e7472
push 0x6376736d
push esp
call eax
;通过GetProcAddress函数,传入msvcrt.dll地址,找到system地址
add esp, 0x10;
mov edx, [esp + 0x4]
xor ecx, ecx
push ecx
mov ecx, 0x616E6F74
push ecx
push 0x6d65
push 0x74737973
push esp;
push eax;
call edx;
add esp,0x10
;然后就是利用system函数去执行命令了
xor ebx, ebx
mov ebx, 0x726964
push ebx
push esp
call eax
;注意栈平衡就行
add esp, 0x8
popfd
popad
这里我只是去调用system函数,其他函数可以类推,我们只要拿到了GetProcAddress和LoadLibraryA这两个函数的地址,然后就天高任鸟飞了,实际的shellcode可能实际就封装成一个函数调用了
通过调试,需要去除push 0x0 这样的汇编会出现\x00这样的空字节,会截断字符串,所以用了一个push 寄存器来代替,再加上一个推出函数。
调试......................完整代码如下
#include<iostream>
int main()
_asm {
xor ecx, ecx
mov eax, fs: [ecx + 0x30]
mov eax, [eax + 0xc]
mov esi, [eax + 0x14]
lodsd
xchg eax, esi
lodsd
mov ebx, [eax + 0x10]
push ebx
mov edx, [ebx + 0x3c]
add edx, ebx
mov edx, [edx + 0x78]
add edx, ebx
mov esi, [edx + 0x20]
add esi, ebx
xor ecx, ecx
Get_Function_GetProcAddress:
inc ecx
lodsd
add eax, ebx
cmp dword ptr[eax], 0x50746547
jnz Get_Function_GetProcAddress
cmp dword ptr[eax + 0x4], 0x41636f72
jnz Get_Function_GetProcAddress
cmp dword ptr[eax + 0x8], 0x65726464
jnz Get_Function_GetProcAddress
mov esi, [edx + 0x24]
add esi, ebx
mov cx, [esi + ecx * 2]
dec ecx
mov esi, [edx + 0x1c]
add esi, ebx
mov edx, [esi + ecx * 4]
add edx, ebx
xor ecx, ecx
push edx
push ecx
push 0x41797261
push 0x7262694c
push 0x64616f4c
push esp
push ebx
call edx
add esp, 0x10
push eax
xor ecx,ecx
push ecx
mov cx, 0x6c6c
push ecx
push 0x642e7472
push 0x6376736d
push esp
call eax;
add esp, 0x10;
mov edx, [esp + 0x4]
xor ecx, ecx
push ecx
mov ecx, 0x616E6F74
push ecx
push 0x6d65
push 0x74737973
push esp;
push eax;
call edx;
add esp,0x10
xor ebx, ebx
mov ebx, 0x726964
push ebx
push esp
call eax
add esp, 0xc
pop edx
pop ebx
mov ecx, 0x61737365
push ecx
sub dword ptr[esp + 0x3], 0x61
push 0x636f7250
push 0x74697845
push esp
push ebx
call edx
xor ecx, ecx
push ecx
call eax
}
从我们编写的汇编来看,代码逻辑很简单,就是要注意其中的堆栈平衡即可,从整个shellcode来看,前面找GetProcAddress和LoadLibraryA这两个函数是固定的,只要找到这两个,我们就能利用它们来查找任意的函数来执行,所以这里就总结出一个shellcode编写框架:
xor ecx, ecx
mov eax, fs: [ecx + 0x30]
mov eax, [eax + 0xc]
mov esi, [eax + 0x14]
lodsd
xchg eax, esi
lodsd
mov ebx, [eax + 0x10]
push ebx
mov edx, [ebx + 0x3c]
add edx, ebx
mov edx, [edx + 0x78]
add edx, ebx
mov esi, [edx + 0x20]
add esi, ebx
xor ecx, ecx
Get_Function_GetProcAddress:
inc ecx
lodsd
add eax, ebx
cmp dword ptr[eax], 0x50746547
jnz Get_Function_GetProcAddress
cmp dword ptr[eax + 0x4], 0x41636f72
jnz Get_Function_GetProcAddress
cmp dword ptr[eax + 0x8], 0x65726464
jnz Get_Function_GetProcAddress
mov esi, [edx + 0x24]
add esi, ebx
mov cx, [esi + ecx * 2]
dec ecx
mov esi, [edx + 0x1c]
add esi, ebx
mov edx, [esi + ecx * 4]
add edx, ebx
xor ecx, ecx
push edx
push ecx
push 0x41797261
push 0x7262694c
push 0x64616f4c
push esp
push ebx
call edx
add esp, 0x10
push eax
xor ecx,ecx
push ecx
mov cx, 0x6c6c; ll
push ecx
push 0x642e7472
push 0x6376736d
push esp
call eax;
add esp, 0x10;
;这里填入想要执行的一些shellcode,这时候GetProcAddress和LoadLibraryA都在栈上,都可供调用。
;esp LoadLibraryA_addr
;esp+4 GetProcAddress
;esp+8 Kernel32.dll
add esp, 0x8
pop edx
pop ebx
mov ecx, 0x61737365
push ecx
sub dword ptr[esp + 0x3], 0x61
push 0x636f7250
push 0x74697845
push esp