zoukankan      html  css  js  c++  java
  • ShellCode瘦身的艺术0_HASH

    写在前面的话:

    前面几篇文章,我们介绍了如何获取kernerl32.dll导出函数地址的方法;

    并在此基础上,编写了ShellCode,实现了动态加载DLL以及解析API地址;

    但是,似乎还称不上Perfect,我们能够获取到LoadLibrary和GetProcAddress,事情就结束了吗?

    我们仍然需要给他们push一些个参数,那些API的名字,占用了我们ShellCode的大部分空间;(如果API较多的话)

    这使得我们的ShellCode看上去不那么美妙,因此,对API做HASH势在必行;

    那也许有朋友会问:做了HASH,总有一处还原的地方吧,如果不还原,那程序里就一定有字符串存在;否则,GetProcAddress怎么玩呢?

    也因此,我们对Kernel32.dll导出表的解析,就需要一般化一下了;让它不止适应于kernel32.dll,而是windows下的任何32位的PE文件;

    (64位类似,解析PE,都一样,笔者就拿32位举例了,有兴趣的朋友也可以自行解析)

    如果能够做到,那我们的HASH才会有意义,因为,ENT里就有API名字了;

    因此,在开始HASH运算前,我们先来搞一下之前的那部分程序;

    零:导出表一般化解析

    0. 先来看下PE的DOS头结构

    typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
        WORD   e_magic;                     // Magic number
        WORD   e_cblp;                      // Bytes on last page of file
        WORD   e_cp;                        // Pages in file
        WORD   e_crlc;                      // Relocations
        WORD   e_cparhdr;                   // Size of header in paragraphs
        WORD   e_minalloc;                  // Minimum extra paragraphs needed
        WORD   e_maxalloc;                  // Maximum extra paragraphs needed
        WORD   e_ss;                        // Initial (relative) SS value
        WORD   e_sp;                        // Initial SP value
        WORD   e_csum;                      // Checksum
        WORD   e_ip;                        // Initial IP value
        WORD   e_cs;                        // Initial (relative) CS value
        WORD   e_lfarlc;                    // File address of relocation table
        WORD   e_ovno;                      // Overlay number
        WORD   e_res[4];                    // Reserved words
        WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
        WORD   e_oeminfo;                   // OEM information; e_oemid specific
        WORD   e_res2[10];                  // Reserved words
        LONG   e_lfanew;                    // File address of new exe header
      } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

    我们需要关注的是最后一个字段,这个里的内容是NT头的偏移,首先,看下,这个字段在本结构体的偏移60(0x3C)

    也就是说,[BaseAddr+0x3C]就是e_lfanew的值,因此,NT头的首地址BaseAddr+e_lfanew;

    那再看下NT头的结构:

    typedef struct _IMAGE_NT_HEADERS {
        DWORD Signature;
        IMAGE_FILE_HEADER FileHeader;
        IMAGE_OPTIONAL_HEADER32 OptionalHeader;
    } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
    
    // NT头中的文件头20Byte
    typedef struct _IMAGE_FILE_HEADER {
        WORD    Machine;
        WORD    NumberOfSections;
        DWORD   TimeDateStamp;
        DWORD   PointerToSymbolTable;
        DWORD   NumberOfSymbols;
        WORD    SizeOfOptionalHeader;
        WORD    Characteristics;
    } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
    
    // NT头中的扩展头
    typedef struct _IMAGE_OPTIONAL_HEADER {
        //
        // Standard fields.
        //
    
        WORD    Magic;
        BYTE    MajorLinkerVersion;
        BYTE    MinorLinkerVersion;
        DWORD   SizeOfCode;
        DWORD   SizeOfInitializedData;
        DWORD   SizeOfUninitializedData;
        DWORD   AddressOfEntryPoint;
        DWORD   BaseOfCode;
        DWORD   BaseOfData;
    
        //
        // NT additional fields.
        //
    
        DWORD   ImageBase;
        DWORD   SectionAlignment;
        DWORD   FileAlignment;
        WORD    MajorOperatingSystemVersion;
        WORD    MinorOperatingSystemVersion;
        WORD    MajorImageVersion;
        WORD    MinorImageVersion;
        WORD    MajorSubsystemVersion;
        WORD    MinorSubsystemVersion;
        DWORD   Win32VersionValue;
        DWORD   SizeOfImage;
        DWORD   SizeOfHeaders;
        DWORD   CheckSum;
        WORD    Subsystem;
        WORD    DllCharacteristics;
        DWORD   SizeOfStackReserve;
        DWORD   SizeOfStackCommit;
        DWORD   SizeOfHeapReserve;
        DWORD   SizeOfHeapCommit;
        DWORD   LoaderFlags;
        DWORD   NumberOfRvaAndSizes;
        IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
    } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
    
    
    typedef struct _IMAGE_DATA_DIRECTORY {
        DWORD   VirtualAddress;
        DWORD   Size;
    } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
    
    #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

    我们要找什么呢,导出表的RVA,导出表是扩展头里的第0号元素;因此,计算出的数据目录表[导出表]相对NT头的偏移,就是0x78;

    至此,我们通过分析DOS头和NT头结构,得到了下面的信息:

    0、e_lfanew = [BaseAddr+0x3C]

    1、NTStartVA:BaseAddr + e_lfnew

    2、ExportStartRVA:[NTStartVA + 0x78]

    3、ExportStartVA:BaseAddr + ExportStartRVA

    到这一步,接下来就需要看下导出表的结构了

    typedef struct _IMAGE_EXPORT_DIRECTORY {
        DWORD   Characteristics;
        DWORD   TimeDateStamp;
        WORD    MajorVersion;
        WORD    MinorVersion;
        DWORD   Name;
        DWORD   Base;
        DWORD   NumberOfFunctions;
        DWORD   NumberOfNames;
        DWORD   AddressOfFunctions;     // RVA from base of image
        DWORD   AddressOfNames;         // RVA from base of image
        DWORD   AddressOfNameOrdinals;  // RVA from base of image
    } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

    需要的字段,EAT/ENT/EOT,我们在上边的分析中,其实已经得到了这个导出表结构体的首地址了,就是ExportStartVA,那就简单了

    4、EATRVA = [ExportStartVA + 0x1C]  -> EAT = BaseAddr + EATRVA

    5、ENTRVA = [ExportStartVA + 0x20]  -> ENT = BaseAddr + ENTRVA

    6、EOTRVA = [ExportStartVA + 0x24]  -> EOT = BaseAddr + EOTRVA

    至此,分析结束,开始编写代码;

    一、代码(为了便于理解,咱们封装成一些裸函数)

    0、获取基址

    void __declspec(naked) GetKernelBase() {
        _asm {
            push ebp;
            mov ebp, esp;
            sub esp, 0x0C;
            mov eax, fs:[0x30]; // PEB
            mov eax, [eax + 0xC]; // LDR
            mov eax, [eax + 0xC]; // InLoadOrderModuleList, exe
            mov eax, [eax]; // nt.dll
            mov eax, [eax]; // kernel32.dll
            mov eax, dword ptr ds : [eax + 0x18]; // BaseAddr;
            mov esp, ebp;
            pop ebp;
            ret;
        }
    }

    1、解析导出表,部分关键代码(全部代码,考虑下,还是在我们写完HASH算法后贴出来)

    _asm{
            push ebp;
            mov ebp, esp;
            sub esp, 0x10;
            push ebx;
            push ecx;
            push esi;
            push edi;
            
            ...
        
            mov [ebp - 0x4], eax; // [ebp - 0x4] -> BaseAddr
            mov eax, [eax + 0x3C]; // e_lfanew
            add eax, [ebp - 0x4]; // NTStartVA
            mov eax, [eax + 0x78]; // ExportStartRVA
            add eax, [ebp - 0x4]; // ExportStart_VA
            mov ebx, [eax + 0x1C]; // EATRVA
            add ebx, [ebp - 0x4]; // EAT
            mov [ebp - 0x8], ebx; // [ebp - 0x8] -> EAT
            mov ebx, [eax + 0x20]; // ENTRVA
            add ebx, [ebp - 0x4]; // ENT
            mov [ebp - 0xC], ebx; // [ebp - 0xC] -> ENT
            mov ebx, [eax + 0x24]; // EOTRVA
            add ebx, [ebp - 0x4]; // EOT
            mov [ebp - 0x10], ebx; // [ebp - 0x10] -> EOT
            
            ...
    
            pop edi;
            pop esi;
            pop ecx;
            pop ebx;
            mov esp, ebp;
            pop ebp;
            ret;
    }

    2、接下来就要考虑如何实现HASH算法了

    要求:尽量简单,又不失功能;(不同的API的HASH碰撞几率越小越好,同时ShellCode里,要兼顾体积)

    /*
    * @1 API
    * @2 Length
    */
    void __declspec(naked) ApiHash() {
        _asm {
            push ebp;
            mov ebp, esp;
            sub esp, 0x8;
            mov dword ptr[ebp - 0x4], 0x6B821B17; // Init Hash Value
            mov dword ptr[ebp - 0x8], 0; // Init Local Var
            jmp short _begin;
    
        _loop:
            mov eax, [ebp + 0x8]; // eax = srcApi
            add eax, 0x1; // eax = srcApi + 1
            mov[ebp + 0x8], eax; // srcApi++
            mov ecx, [ebp - 0x8]; // ecx = i
            add ecx, 0x1; // ecx += 1
            mov[ebp - 0x8], ecx; // i++
    
        _begin:
            mov edx, [ebp - 0x8]; // edx = i
            cmp edx, [ebp + 0xC]; // edx vs len
            jnb short _end; // if (edx >= len) exit;
            mov eax, [ebp - 0x4]; // eax = Hash
            shl eax, 0x5; // eax = Hash << 5
            mov ecx, [ebp + 8]; // ecx = srcApi
            movsx edx, byte ptr[ecx]; // edx = *srcApi
            add eax, edx; // eax = Hash << 5 + *srcApi
            mov ecx, [ebp - 0x4]; // ecx = Hash
            shr ecx, 0x2; // ecx = Hash >> 2
            add eax, ecx; // eax = Hash << 5 + *srcApi + Hash >> 2
            xor eax, [ebp - 0x4];
            mov[ebp - 0x4], eax; // Hash ^= (Hash << 5 + *srcApi + Hash >> 2);
            jmp short _loop;
    
        _end:
            mov eax, [ebp - 0x4]; // eax = Hash
            mov esp, ebp;
            pop ebp;
            ret 0x8;
        }
    }

    3、既然HASH算法也有了,在开始编写获取API的函数之前,先实现一个获取字符串长度的函数;

    /*
     * @ String
     */
    void __declspec(naked) asmstrlen() {
        _asm {
            push ebp;
            mov ebp, esp;
            sub esp, 0x4;
            mov dword ptr [ebp - 0x4], 0;
            jmp short _begin;
    
        _loop:
            mov eax, [ebp + 0x8]; // eax = String
            add eax, 0x1; // eax = String + 1
            mov [ebp + 0x8], eax; // String++
            mov ecx, [ebp - 0x4]; // ecx = i
            add ecx, 0x1; // ecx += 1
            mov [ebp - 0x4], ecx; // i++
    
        _begin:
            mov ecx, [ebp + 8]; // ecx = String
            movsx edx, byte ptr [ecx]; // edx = *String
            cmp edx, 0;
            je _end;
            jmp _loop;
    
        _end:
            mov eax, [ebp - 0x4]; // eax = len
            mov esp, ebp;
            pop ebp;
            ret 0x4;
        }
    }

    4、接下来,就要编写通过HASH获取API地址的函数了

    /* 
     * @1 BaseAddr
     * @2 HASH
     */
    void __declspec(naked) GetHASHAPIAddr() {
        _asm {
            push ebp;
            mov ebp, esp;
            sub esp, 0x14;
            push esi;
            push edi;
    
            mov eax, [ebp + 8]; // BaseAddr
            mov [ebp - 0x4], eax;
            mov eax, [eax + 0x3C]; // e_lfanew
            add eax, [ebp - 0x4]; // NTStartVA
            mov eax, [eax + 0x78]; // ExportStartRVA
            add eax, [ebp - 0x4]; // ExportStart_VA
            mov ebx, [eax + 0x1C]; // EATRVA
            add ebx, [ebp - 0x4]; // EAT
            mov [ebp - 0x8], ebx; // [ebp - 0x8] -> EAT
            mov ebx, [eax + 0x20]; // ENTRVA
            add ebx, [ebp - 0x4]; // ENT
            mov [ebp - 0xC], ebx; // [ebp - 0xC] -> ENT
            mov ebx, [eax + 0x24]; // EOTRVA
            add ebx, [ebp - 0x4]; // EOT
            mov [ebp - 0x10], ebx; // [ebp - 0x10] -> EOT
    
            xor ebx, ebx;
            mov eax, [eax + 0x18]; // NumOfNames
            mov [ebp - 0x14], eax;
            cld;
    
        _ENT_FIND:
            mov esi, [ebp - 0xC]; // ENTStartVA
            mov esi, [esi + 4 * ebx]; // ENTContentRVA
            add esi, [ebp - 0x4]; // ENTContentVA
            push esi;
            push esi;
            call asmstrlen;
            pop esi;
            push eax;
            push esi;
            call ApiHash;
            mov edi, [ebp + 0xC]; // HASH
            cmp eax, edi;
            je _ENT_OK;
            inc ebx;
            mov eax, [ebp - 0x14];
            dec eax;
            mov [ebp - 0x14], eax;
            cmp eax, 0;
            jg _ENT_FIND;
            jmp _ENT_END;
    
        _ENT_OK:
            mov ecx, [ebp - 0x10]; // EOTStartVA
            mov ecx, [ecx + 2 * ebx];
            and ecx, 0xFFFF;
            mov esi, [ebp - 0x8]; // EATStartVA
            mov eax, [esi + 4 * ecx]; // EAT Address RVA
            add eax, [ebp - 0x4]; // EAT Address VA
    
        _ENT_END:
            pop edi;
            pop esi;
            mov esp, ebp;
            pop ebp;
            ret 0x8;
        }
    }

     我们只需要事先准备好需要的API的HASH值,就可以了,下面让我们来测试下;

    5、测试

    int main(int argc, char** argv) {
        DWORD LoadLibAddr = 0;
        _asm {
            call GetKernelBase;
            push 0x28182EF6; // LoadLibrayA HASH
            push eax;
            call GetHASHAPIAddr;
            mov LoadLibAddr, eax;
        }
    
        printf("LoadLibrary[0x%X]
    ", LoadLibAddr);
    
        getchar();
    
        return 0;
    }

    我们在调试器中输入这个地址:

    可以看到,获取到了这个函数的地址;

    获取有同学会说,这个是在kernel32.dll里的,其他dll里的函数也可以吗;当然了,看我们的GetHashAPIAddr参数就知道了;

    来代码吧,搞一个MessageBox的函数,这个是在user32.dll里的,见代码,运行后会弹框,证明就成功了;

    int main(int argc, char** argv) {
        char srcDll[] = "user32.dll";
        DWORD LoadLibAddr = 0;
    
        _asm {
            call GetKernelBase;
            push 0x28182EF6; // LoadLibrayA HASH
            push eax;
            call GetHASHAPIAddr;
            mov LoadLibAddr, eax;
    
            push esi;
            mov esi, eax;
            lea eax, srcDll;
            push eax;
            call esi;
            push 0x564B6854; // MessageBoxA HASH
            push eax;
            call GetHASHAPIAddr;
    
            push 0;
            push 0;
            push 0;
            push 0;
            call eax;
        }
    
        printf("LoadLibraryA[0x%X]
    ", LoadLibAddr);
    
        getchar();
    
        return 0;
    }

    至此,我们的API算是都准备好了,通过实现HASH算法,我们去掉了占用体积过大的API字符串,瘦身的目的达到了;

    在后续的文章中,笔者将带领大家一起分析ShellCode中的截断问题,敬请期待;

  • 相关阅读:
    概率论
    Python3爬虫爬取淘宝商品数据
    利用Python数据分析基础
    Linux安装MATLAB2016a
    python3爬取高清壁纸(2)
    python3爬取高清壁纸(1)
    Git使用基础
    Python3基础
    正则表达式的使用基础
    Nginx配置多域名代理
  • 原文地址:https://www.cnblogs.com/Reginald-S/p/8797585.html
Copyright © 2011-2022 走看看