免杀之Shell混淆

admin 2024年2月24日22:48:11评论20 views字数 4886阅读16分17秒阅读模式

点击蓝字 关注我们

(本文由小貂快跑代发,原作者为“Ting”)

01

背景

所谓杀软的静态扫描 ,就是通过提取磁盘中一个文件的特征码,然后与杀软自身报错的病毒库的特征码匹配,如果满足匹配结果,那么杀软将会将其认定为恶意文件。所有让我们的马面目全非,让杀软匹配不了,不认识我们的马,是过掉静态扫描的本质。所谓让马面目全非也就是——混淆。

02

介绍

现在的杀软不仅仅会对恶意文件进行一个静态扫描、分析其特征以外,杀软还会将文件丢到虚拟沙箱中去运行,挂一个hook去跟踪、监控调用的api函数并判断其行为,有的杀软甚至是可以扫描内存的(比如卡巴斯基),shellcode在内存中解密后就很容易暴露,这加大了与杀软对抗的难度。所以仅仅是shellcode混淆已经很难再bypass掉各种杀软了,但是这并不意外着shellcode混淆就已失去意义了!!
在做免杀过程中,第一步要解决的问题是静态免杀,因为只有先过了静态的免杀才能去考虑后面的的抗沙箱、动态免杀、内存躲避等技术,所以shellcode混淆技术在静态免杀中是由其重要的,也是必须要学习的一门免杀技术。学了混淆之后,下一步再去结合其他动态免杀的技术,才能更加完美的过点杀软!
另外这篇文章是在stagerless的条件下,即非分离的,毕竟stager分离的是自带静态免杀的。
注意:在loader中硬编码的shellcode是加密的,否则会被监测出来并弹窗(杀软报警);shellcode的解密是在memcpy之后开始的,也就是加载进内存之后开始解密,如果在加载之前进行解密,这无异于裸奔。

03

base64编码混淆

免杀之Shell混淆

原理

即对原始的shellcode进行base64编码,让其与原始的shellcode“样貌”不一,掩盖掉原始shellcode的静态特征,防止杀软辨识出来。

使用base64混淆需要注意,编码和解码的算法都需要对应,否则编码之后,再解码就无法还原了,所以这里建议不要使用网上的在线编码平台,建议自己写一个编码、解码算法。
注:编码之后的shellcode大小是原来的3/4倍或者+1~2,目前只进行base64编码是没有什么用处了,需要结合其他的方法进行混淆。
import base64def base64_encode(data):# 将字符串编码为字节流  encoded_bytes = base64.b64encode(data.encode('utf-8'))# 将字节流解码为字符串encoded_string = encoded_bytes.decode('utf-8')return encoded_stringdef base64_decode(encoded_string):# 将字符串编码为字节流  encoded_bytes = encoded_string.encode('utf-8')# 将字节流解码为字符串decoded_bytes = base64.b64decode(encoded_bytes)# 将字节流解码为字符串decoded_string = decoded_bytes.decode('utf-8')return decoded_string# 测试加密和解密data = "shellcode"encoded_data = base64_encode(data)print("编码后:", encoded_data)decoded_data = base64_decode(encoded_data)print("解码后:", decoded_data)
运行结果截图如下,不过现在base64单独使用的话是过不了杀软的静态的,还是要结合其他加密算法的!

免杀之Shell混淆

04

AES加密

免杀之Shell混淆

原理

自己创建一个key,使用这个key对原始shellcode进行加密,修改其“样貌”,修改其静态特征。

使用AES加密最好让密钥是一个动态获取的值,或者可以通过请求来获取,通过密钥协商来获取等待。最好不是硬编码到代码中的,否则杀软容易获取到key自行解密来识别。(比如key=“123456”)
注:编码之后的shellcode大小是(shellcode-size+1)/2
from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpadfrom Crypto.Random import get_random_bytesdef encrypt(plaintext, key):  # 使用ECB模式  cipher = AES.new(key, AES.MODE_ECB)    ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))  return ciphertextdef decrypt(ciphertext, key):  # 使用ECB模式  cipher = AES.new(key, AES.MODE_ECB)    plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)  return plaintext# 生成随机密钥key = get_random_bytes(16)# 要加密的明文plaintext = b"shellcode"# 加密ciphertext = encrypt(plaintext, key)print("加密后:", ciphertext)# 解密decrypted_text = decrypt(ciphertext, key)print("解密后:", decrypted_text)
这里AES加密使用的是最简单的ECB模式,除此之外还有更安全的加密模式,如CBC(Cipher Block Chaining)模式和CTR(Counter),这些模式引入了初始化向量(IV)和密文块之间的链接,以增加加密的安全性和随机性,当然算法也会更复杂一些。
运行后的结果截图如下:

免杀之Shell混淆

强调,key不要直接硬编码 (反例:key=b'123456')

05

异或加密

免杀之Shell混淆

原理

一段二进制代码,在异或两次之后即可还原为原来的二进制码,举个例子就明白咯:

免杀之Shell混淆
所以可以自定义一段二进制码,然后异或加密之后,再memcpy加载进内存之后再进行异或一遍,解密,然后执行即可。
#include <stdio.h>#include <string.h>char shellcode[] = "shellcode";void xor_encrypt_decrypt(size_t shellcodeSize) {    for (int i = 0; i < shellcodeSize; i++)    {        shellcode[i] ^= 0x5b;    }}int main() {    //获取shellcode大小    size_t shellcodeSize = strlen(shellcode);    //第一次异或    xor_encrypt_decrypt(shellcodeSize);    printf("第一次异或后:%sn", shellcode);    //第二次异或,即复原    xor_encrypt_decrypt(shellcodeSize);    printf("第二次异或后:%sn", shellcode);    return 0;}
由于异或实现起来比较简单这里就直接用C来写咯,结果的截图如下:

免杀之Shell混淆

06

偏移量混淆

免杀之Shell混淆

原理

这个混淆方式挺巧妙的。它不会直接存shellcode(不论加密与否)在loader中,而是存shellcode对应的字符在所有通用设备都固有的文件中的位置,生成一个存放位置的数组,再利用存放位置的数组里的元素缩影,一个个取出来去编码后的文件里面寻找对于字符,将他们重新组装成shellcode。如果还是不理解的话试试看流程图呢:

免杀之Shell混淆

import codecs#获取win.ini的二进制编码with open("C:Windowswin.ini", 'rb') as file:    content = file.read()new_file = codecs.encode(content, 'hex').decode()#获取shellcode的二进制编码with open("shellcode.bin", 'rb') as file:    shellcode = file.read()new_shellcode = codecs.encode(shellcode, 'hex').decode()#遍历查找对应的位置,并添加到数字list中list = []for a in new_file:    offset = new_file.find(a)    list.append(offset)print(list)
获取到的list(存在位置索引的数组)部分截图如下,之后把数组放在loader中之后在自己写对应的还原算法即可。

免杀之Shell混淆

07

uuid混淆

免杀之Shell混淆

原理

将shellcode的字节数用空字节填充为16的倍数,然后对没连续的16个字节的shellcode进去uuid加密,然后再在memcpy进去内存之后,进行解密执行即可

import  uuid#多写几个shellcode,体会一下uuid加密的前提是位数为16的倍数shellcode = b'shellcode,shellcode,shellcode,shellcode' def convertToUUID(shellcode):    #如果shellcode的长度不足16位数,则需要用空字节填充shellcode为16的倍数    if len(shellcode)%16 !=0:        #计算出需要填充空字节的数量        addNullbyte = b"x00" * (16-(len(shellcode)%16))        #拼接需要填充空字节的数量        shellcode += addNullbyte    uuids = []    # 每次取出shellcode的16个字节进行uuid转换    for i in range(0,len(shellcode),16):        uuidString = str(uuid.UUID(bytes_le=shellcode[i:i+16]))        uuids.append(uuidString.replace("'","""))    return uuidsu = convertToUUID(shellcode)print(str(u).replace("'","""))
以上代码运行结果截图如下:

免杀之Shell混淆

小白可能会遇到的困惑:在使用对shellcode进行混淆的时候,要注意将加密的代码与加载器分开,不能放在一个代码文件中,否则就没有作用了,这样静态是过不了的,需要在自己的电脑上,额外运行加密算法对shellcode进行加密,然后把加密后shellcode放进加载器(不分离的情况下)进去加载,流程图如下:

免杀之Shell混淆

08

总结

了解了以上的混淆方式后,可能有的师傅会想,既然要让杀软认不出来,那就多加密,结合多种加密算法,甚至自研一种更加复杂的加密算法不就更能过掉静态扫描了吗?当然不是,因为杀软有个东西叫做熵,当加密算法复杂了以后,程序的熵会增大,这有可能会引起杀软的嫌疑,另外算法的复杂度讲究够用就行,不需要太过于复杂,有时候一个异或就足以bypass某绒。
以上方式单独使用已经很难过杀软了,但是为什么要学习呢,因为过静态免杀还是有用的,除此之外可以相互结合着使用,让加密的算法更加复杂,比如我先base64加密了再去uuid等,让自己的算法复杂起来,提高静态免杀通过率。
另外要说的是,虽然加密算法有的是用的python,有的是用的c写的,但是你的解密算法可以用任意的语言,比如用python加密,我用c来解密,用c加密,用c来解密都可以,只要加密解密算法是对应的加密后可以还原回来就行!
以上就是这篇文章的全部了,之后不出意外会更新一篇偏移混淆的免杀实战篇,因为感觉偏移混淆挺有意思的,相信肯定还有更多、更具技巧性、更有意思的混淆技术,等我学会了给各位分享!

  //  作者:Ting

一名大学本科在校生,参加多次演练、众测项目,一次地级市演练红队第三,i春秋特聘作家。喜欢挖洞、渗透。目前在研究代码审计和免杀,梦想做一名资深红队~

原文始发于微信公众号(赛博游民营):免杀之Shell混淆

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月24日22:48:11
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   免杀之Shell混淆https://cn-sec.com/archives/2499273.html

发表评论

匿名网友 填写信息