zoukankan      html  css  js  c++  java
  • windows:shellcode 原理

      shellcode,一段短小精干的代码,放在任何地方都能执行,不依赖当前所处环境,那么就有这么几点要求:

    •     不能有全局变量:函数里的局部变量在栈空间,地址是执行的时候动态分配的;但全局变量在编译时,会由编译器分配好固定的存储空间。编写shellcode的程序肯定会为这个全局变量预留空间,但执行shellcode的目标进程未必会预留,可能已经被其他全局变量占用
    •     不能有字符串:和上个类似,字符串会被编译器放在文字常量区,地址也是编译时固定写死的,目标进程的同一地址可能已经被占用,导致shellcode出错;
    •     能动态获取所需系统API的地址,代码才能不写死;

            针对以上要求,解决的思路:

    •     所有变量都在函数内,写成局部变量,地址就能动态分配了;
    •     字符串用字符数组替代,也就变成了局部变量,地址也能动态分配,比如 char *s = "hello" 可以改成char  q1[] = {'h','e','l','l','o',''},q1的地址也会在栈的局部空间分配;
    •     最难的就是这个:动态获取所需系统API地址;shellcode的很多操作涉及系统底层,必然调用windows的API。正常情况下,windows针对3环用户态程序提供LoadLibrary加载dll,返回dll的基址。然后通过GetProcAddress从dll种获取函数基址;但这两个函数本身也是windwos 的API,其地址依然需要动态获取,该怎么做了?

      1、动态获取LoadLibraryA的地址

            windwos 32位下,每个进程都有PEB结构体,记录了进程各种信息;在0xc处是PEB_LDR_DATA结构体,该结构体记录了顺序加载的模块链表,说明如下:

      

       

       核心代码如下:

      (1)找到链表的头指针:(注意:不同版本windows的PEB结构体可能有细微差别,需要适配)

    __asm{
            mov eax,fs:[0x30]
            mov eax,[eax+0x0c]
            add eax,0x0c
            mov pBeg,eax
            mov eax,[eax]
            mov pPLD,eax 
    }

      (2)遍历链表,查找kernerl32.dll

    while(pPLD!=pBeg)
    {
        pLast=(WORD*)pPLD->BaseDllName.Buffer;
        pFirst=(WORD*)szKernel32;
        while(*pFirst && (*pFirst-32==*pLast||*pFirst==*pLast))
            {pFirst++,pLast++;}
        if(*pFirst==*pLast)
            {
            dwKernelBase=(DWORD)pPLD->DllBase;
            break;
            }
        pPLD=(LDR_DATA_TABLE_ENTRY*)pPLD->InLoadOrderModuleList.Flink;
    }

      2、kernerl32.dll 基址找到后就好办了:根据PE的导出表能找到GetProcAddr的地址,核心代码如下:

    MAGE_DOS_HEADER *pIDH=(IMAGE_DOS_HEADER *)dwKernelBase; 
    IMAGE_NT_HEADERS *pINGS=(IMAGE_NT_HEADERS *)((DWORD)dwKernelBase+pIDH->e_lfanew);
    IMAGE_EXPORT_DIRECTORY *pIED=(IMAGE_EXPORT_DIRECTORY*)((DWORD)dwKernelBase+pINGS
    ->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    
    DWORD *pAddOfFun_Raw=(DWORD*)((DWORD)dwKernelBase+pIED->AddressOfFunctions);
    WORD *pAddOfOrd_Raw=(WORD*)((DWORD)dwKernelBase+pIED->AddressOfNameOrdinals);
    DWORD *pAddOfNames_Raw=(DWORD*)((DWORD)dwKernelBase+pIED->AddressOfNames);
    DWORD dwCnt=0;
    
    char *pFinded=NULL,*pSrc=szGetProcAddr;
    for(;dwCnt<pIED->NumberOfNames;dwCnt++)
    {
        pFinded=(char *)((DWORD)dwKernelBase+pAddOfNames_Raw[dwCnt]);
        while(*pFinded &&*pFinded==*pSrc) 
        {pFinded++;pSrc++;}
        if(*pFinded == *pSrc)
            {
            pGetProcAddress=(PGETPROCADDRESS)((DWORD)dwKernelBase+pAddOfFun_Raw[pAddOfOrd_Raw[dwCnt]]);
            break;
            }
        pSrc=szGetProcAddr;
    }

      有了GetProcAddr,又能继续查找LoadLibrary(也在kernerl32.dll里面)的地址,如下:

    pLoadLibrary=(PLOADLIBRARY)pGetProcAddress((HMODULE)dwKernelBase,szLoadLibrary);

      这下有了LoadLibrary和GetProcAddr两大函数地址,任何dll的任何函数入口都能找到了,比如MessageBox,如下:

    pMessageBox=(PMESSAGEBOX)pGetProcAddress(pLoadLibrary(szUser32),szMessageBox);
    char szTitle[]={'S','h','e','l','l','C','o','d','e',0};
    char szContent[]={0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x20,0x21,0};
    pMessageBox(NULL,szContent,szTitle,0);

      完整代码如下:(所有代码都写入main,方便下一步提取)

    #include<windows.h>
    
    int main()
    {
        typedef DWORD (WINAPI *PGETPROCADDRESS) (HMODULE hModule,LPCSTR lpProcName);
        typedef int (WINAPI * PMESSAGEBOX) (HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType);
        typedef HMODULE (WINAPI * PLOADLIBRARY) (LPCTSTR lpFileName);
    
        typedef struct UNICODE_STRING
        {
            USHORT Length;
            USHORT MaximumLength;
            PWSTR Buffer;
        }UNICODE_STRING;
    
        typedef struct PEB_LDR_DATA{
            DWORD Length;
            BYTE initialized;
            PVOID SsHandle;
            LIST_ENTRY InLoadOrderModuleList;
            LIST_ENTRY InMemoryOrderModuleList;
            LIST_ENTRY InInitializationOrderModuleList;
            VOID * EntryInProgress;
        }PEB_LDR_DATA;
    
        typedef struct LDR_DATA_TABLE_ENTRY
        {
            LIST_ENTRY InLoadOrderModuleList;
            LIST_ENTRY InMemoryOrderModuleList;
            LIST_ENTRY InInitializationOrderModuleList;
            void* DllBase;
            void* EntryPoint;
            ULONG SizeOfImage;
            UNICODE_STRING FullDllName;
            UNICODE_STRING BaseDllName;
            ULONG Flags;
            SHORT LoadCount;
            SHORT TlsIndex;
            HANDLE SectionHandle;
            ULONG CheckSum;
            ULONG TimeDateStamp;
        }LDR_DATA_TABLE_ENTRY;
    
        LDR_DATA_TABLE_ENTRY *pPLD=NULL,*pBeg=NULL;
        PGETPROCADDRESS pGetProcAddress=NULL;
        PMESSAGEBOX pMessageBox=NULL;
        PLOADLIBRARY pLoadLibrary=NULL;
        WORD *pFirst =NULL,*pLast=NULL;
        DWORD ret =0,i=0;
        DWORD dwKernelBase=0;
    
        char szKernel32[]={'k',0,'e',0,'r',0,'n',0,'e',0,'l',0,'3',0,'2',0,'.',0,'d',0,'l',0,'l',0,0,0};
        char szUser32[]={'U','S','E','R','3','2','.','d','l','l',0};
        char szGetProcAddr[]={'G','e','t','P','r','o','c','A','d','d','r','e','s','s',0};
        char szLoadLibrary[]={'L','o','a','d','L','i','b','r','a','r','y','A',0};
        char szMessageBox[]={'M','e','s','s','a','g','e','B','o','x','A',0};
    
        __asm{
            mov eax,fs:[0x30]
                mov eax,[eax+0x0c]
                add eax,0x0c
                mov pBeg,eax
                mov eax,[eax]
                mov pPLD,eax 
        }
        // 遍历找到kernel32.dll
        while(pPLD!=pBeg)
        {
            pLast=(WORD*)pPLD->BaseDllName.Buffer;
            pFirst=(WORD*)szKernel32;
            while(*pFirst && (*pFirst-32==*pLast||*pFirst==*pLast))
            {    pFirst++,pLast++;}
            if(*pFirst==*pLast)
            {
                dwKernelBase=(DWORD)pPLD->DllBase;
                break;
            }
            pPLD=(LDR_DATA_TABLE_ENTRY*)pPLD->InLoadOrderModuleList.Flink;
        }
    
        // 遍历kernel32.dll的导出表,找到GetProcAddr函数地址
    
        IMAGE_DOS_HEADER *pIDH=(IMAGE_DOS_HEADER *)dwKernelBase; 
        IMAGE_NT_HEADERS *pINGS=(IMAGE_NT_HEADERS *)((DWORD)dwKernelBase+pIDH->e_lfanew);
        IMAGE_EXPORT_DIRECTORY *pIED=(IMAGE_EXPORT_DIRECTORY*)((DWORD)dwKernelBase+
            pINGS->OptionalHeader.
            DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    
        DWORD *pAddOfFun_Raw=(DWORD*)((DWORD)dwKernelBase+pIED->AddressOfFunctions);
        WORD *pAddOfOrd_Raw=(WORD*)((DWORD)dwKernelBase+pIED->AddressOfNameOrdinals);
        DWORD *pAddOfNames_Raw=(DWORD*)((DWORD)dwKernelBase+pIED->AddressOfNames);
        DWORD dwCnt=0;
    
        char *pFinded=NULL,*pSrc=szGetProcAddr;
        for(;dwCnt<pIED->NumberOfNames;dwCnt++)
        {
            pFinded=(char *)((DWORD)dwKernelBase+pAddOfNames_Raw[dwCnt]);
            while(*pFinded &&*pFinded==*pSrc) 
            {pFinded++;pSrc++;}
            if(*pFinded == *pSrc)
            {
                pGetProcAddress=(PGETPROCADDRESS)((DWORD)dwKernelBase+pAddOfFun_Raw[pAddOfOrd_Raw[dwCnt]]);
                break;
            }
            pSrc=szGetProcAddr;
        }
        // 有了GetProcAddr 可以获得任何api
        pLoadLibrary=(PLOADLIBRARY)pGetProcAddress((HMODULE)dwKernelBase,szLoadLibrary);
        pMessageBox=(PMESSAGEBOX)pGetProcAddress(pLoadLibrary(szUser32),szMessageBox);
    
        // 使用函数
        char szTitle[]={'S','h','e','l','l','C','o','d','e',0};
        char szContent[]={0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x20,0x21,0};
        pMessageBox(NULL,szContent,szTitle,0);
    
        return 0;
    }

      2、shellcode提取:从IDA看,main函数从401000开始,

      

           在4012E0结束,总长度2e0;

               

      这里可以直接在hexview查看二进制编码,可以直接从401000复制到4012E0,这段代码插入其他exe代码的入口点

       

       3、随变找个exe,比如下面这个变量位置测试的exe,查看发现其入口点是4796:

      

      12bc入口+2e0(shellcode长度)=159c,先删除这部分代码:

       

      再把shellcode复制过来:

       

      再次运行exe:能看到弹窗,达到注入代码的目的:

       

    -----------------------------------------------------------分割线------------------------------------------------------------------------------------------------------------------------------

    内存和数据相关区域的分工:

    (1)栈(stack):由编译器进行管理,自动分配和释放,存放函数调用过程中的各种参数、局部变量、返回值以及函数返回地址;

    (2)堆(heap):用于程序动态申请分配和释放空间。C语言中的malloc和free,C++中的new和delete均是在堆中进行的,还有windows驱动编程常用的ExAllocatePool;正常情况下,程序员申请的空间在使用结束后应该释放,若程序员没有释放空间,则程序结束时系统自动回收。堆内存的好处:只要程序员不主动释放,且程序运行不结束,这块数据会一直存在;这个特性可以用来隐藏驱动(https://www.cnblogs.com/theseventhson/p/13170445.html);

    (3)全局(静态)存储区:分为DATA段和BSS段。DATA段(全局初始化区)存放初始化的全局变量和静态变量;BSS段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0;

    (4)文字常量区:存放常量字符串,程序结束后由系统释放;

    其中,栈内存存放的数据仅仅在函数调用过程使用,结束后就没用了,所以编译器会增加分配(esp-xxx)和释放(esp+xxx)的代码; 但全局变量和静态变量要求在任何函数都能使用,所以不能存放在栈,只能放在DATA和BSS段,等程序运行结束后由操作系统回收;代码实验如下:

    第一次的代码(就是上面用来做注入测试的storPosition.exe):

    #include <stdio.h>
    #include <stdlib.h>
    
    int k1 = 1;
    int k2;
    static int k3 = 2;
    static int k4;
    
    int main( )
    {   
        static int m1=2, m2;
        int i=1;
        char*p;
        char str[10] = "hello";
        char* q = "hello";
        p= (char *)malloc( 100 );
        free(p);
        printf("栈区-变量地址  i:%p
    ", &i);
        printf("                p:%p
    ", &p);
        printf("              str:%p
    ", str);
        printf("                q:%p
    ", &q);
        printf("堆区地址-动态申请:%p
    ", p);
        printf("全局外部有初值 k1:%p
    ", &k1);
        printf("    外部无初值 k2:%p
    ", &k2);
        printf("静态外部有初值 k3:%p
    ", &k3);
        printf("    外静无初值 k4:%p
    ", &k4);
        printf("  内静态有初值 m1:%p
    ", &m1);
        printf("  内静态无初值 m2:%p
    ", &m2);
        printf("文字常量地址    :%p, %s
    ",q, q);
        printf("程序区地址      :%p
    ",&main);
        return 0;
    }

         各种变量地址的分布:

          

       第二次的代码:和第一次比增加了3个局部变量:m、j和q1,并打乱了顺序:

    #include <stdio.h>
    #include <stdlib.h>
    
    int k1 = 1;
    int k2;
    static int k3 = 2;
    static int k4;
    
    int main( )
    {   
        char*p;
        int m=3;
        static int m1=2, m2;
        int j=2,i=1;
        char str[10] = "hello";
        char* q = "hello";
        char  q1[] = {'h','e','l','l','o',''};
        p= (char *)malloc( 100 );
        free(p);
        printf("栈区-变量地址  i:%p
    ", &i);
        printf("栈区-变量地址  j:%p
    ", &j);
        printf("栈区-变量地址  m:%p
    ", &m);
        printf("                p:%p
    ", &p);
        printf("              str:%p
    ", str);
        printf("                q:%p
    ", &q);
        printf("                q1=%s:%p
    ", q1, &q1);
        printf("堆区地址-动态申请:%p
    ", p);
        printf("全局外部有初值 k1:%p
    ", &k1);
        printf("    外部无初值 k2:%p
    ", &k2);
        printf("静态外部有初值 k3:%p
    ", &k3);
        printf("    外静无初值 k4:%p
    ", &k4);
        printf("  内静态有初值 m1:%p
    ", &m1);
        printf("  内静态无初值 m2:%p
    ", &m2);
        printf("文字常量地址    :%p, %s
    ",q, q);
        printf("程序区地址      :%p
    ",&main);
        return 0;
    }

      各种变量地址的分布:

        

        通过对比可以发现:静态变量、全局变量只要有初值,地址都是固定的;下面是更详细的说明:同样都是字符串,s1存放在栈,函数执行完返回后该区域被收回;s2存放在字符常量,程序结束后才会被释放

       

    最后:参考别人的文章如下:

    https://b0ldfrev.gitbook.io/note/windows_operating_system/windows-xia-tong-yong-shellcode-yuan-li

    https://www.bilibili.com/video/BV1y4411k7ch?p=6

    https://blog.csdn.net/yangquanhui1991/article/details/51786380

    https://www.anquanke.com/post/id/168276

  • 相关阅读:
    stack2
    xctf 实时数据监测
    note-service2
    stack pivot学习
    sctf_2019_easy_heap 利用off-by-null构造出double free来向任一地址写入(经典)
    ciscn_2019_s_1 unlink或者of-by-null
    starctf_2019_babyshell 绕过循环检测注入shellcode
    sublime 安装package control
    windows上安装nodejs,升级npm,安装webpack指南
    mysql 常见语句
  • 原文地址:https://www.cnblogs.com/theseventhson/p/13194646.html
Copyright © 2011-2022 走看看