變形實踐

看雪資料發表於2015-11-15

標 題: 變形的初步方案--兩篇變形文章的讀後感

翻 譯: 月中人

時 間: 2006-08-22 08:06 

鏈 接: http://bbs.pediy.com/showthread.php?threadid=30844 

詳細資訊: 



    變形的初步方案
            --兩篇變形文章的讀後感
  Mental Driller的“Metamorphism in practice or "How I made MetaPHOR and what I've learnt"”一文(以下簡稱M文)所述變形引擎,全部程式碼雖然經過變形,但畢竟已經是機器語言了,有些行為如果處理不好,可能引起懷疑。如果直接使用宏語言編寫程式並且攜帶,只把其中一部分模組翻譯成可直接執行的機器語言,也許更好。宏命令可以相當於一條到三條指令組合,也可以只是個標記;可能有一個或幾個引數,也可能沒有。並且因此可以省略把機器語言反彙編成偽組合語言這一部分,自定義的宏語言程式比較容易控制。這個想法一部分來自Navrhar的“Assembly Language or HLL”一文。
  宏程式包括入口模組、彙編模組、變形模組、傳播模組、X模組。可以放在PE檔案的一個只讀不執行的資料節。宏程式由宏命令組成,宏命令的基本內容有宏代號、引數個數、引數列表、和連線位置(指向下一條命令,其作用與M文中偽指令的Pointer不同)。其中翻譯成機器語言的部分(入口模組和彙編模組)附加到PE檔案的可執行不可寫入的程式碼節(利用空白區或者新節)。
  入口模組取得控制權,並做些準備工作,需要的時候它啟動彙編模組翻譯出其他模組並執行。例如,彙編模組從資料節讀宏程式,先翻譯變形模組。然後執行變形模組把宏程式變形,這樣,新一代的資料節就和上一代的不同,再從中翻譯出的機器語言程式碼也就變形了。
  宏程式變形主要分兩步:置換和收縮/展開。
  置換:把宏程式所有命令打亂重新隨機排列,命令中的連線位置欄位內容必須隨之修改,以保持連續性,並跟蹤程式入口位置。收縮和展開:一條宏命令用幾條宏命令替換、執行相同任務,或者幾條宏命令合併用一條宏命令替換。這與宏語言的設計有密切關係,可以參考M文的指令對和三聯組,只不過改成宏代號之間的對應。
  變形模組的收縮/展開部分隱含地使用一個宏替換表,展開比較容易做,收縮需要判斷在邏輯上連續的幾個宏代號是不是可以用一個宏代號替換。注意,不同之處:在M文中收縮/展開是一對互補操作,在這裡一般情況下是隨機決定把一個宏展開成幾個宏、或者和後面的宏一起、判斷能不能合併(如果後面的宏是引用或跳轉目標就不能合併)。相同之處:能遞迴,所以要設定程式碼大小上下限,不要讓若干代以後宏程式變得太大或太小,接近下限就多傾向於展開一些宏,接近上限就多傾向於收縮一些宏,達到界限就要反向操作或者只使用一條等效的宏代替(如XOR EBX,EBX用SUB EBX,EBX,因為宏可以根據引數不同改變暫存器,所以可用的替換比較多)。應該防止萬一收縮了不是原來展開的宏,造成很多宏不能收縮,因此要設計好宏替換表。
  [注:“隱含地使用表”就是根據它程式設計,但不儲存表。不過如果顯式地使用表,編碼更簡單,可以透過分散存放、混洗、加密等等把表變形。]
  遞迴性與宏的設計有關,如M文的例子,PUSH Reg <--> MOV Mem,Reg/PUSH Mem,而其中MOV Mem,Reg <--> PUSH Reg/POP Mem,又遞迴使用到PUSH Reg。這裡一條指令或一個指令對都可以作為一個宏。
  變形以後,彙編模組就可以從新的宏程式翻譯了。彙編模組的翻譯部分也隱含地使用一個宏命令表,其中每個宏代號對應著一個指令格式串(1至3條機器指令,已填好運算子和部分運算元,留空的運算元將用宏命令的引數填入),如M文中的MakeMOVMemReg()和MakePUSHMem()等等。
  按宏命令的排列順序(已置換)取下一條命令翻譯,根據連線位置欄位用無條件跳轉連線。跳轉指令可以用JMP xxxx或CMP EAX,EAX/JZ xxxx或PUSH xxxx/RETN或PUSH EAX/XOR EAX,EAX/POP EAX/JNC xxxx,等等;跳轉地址xxxx需要計算,實現方法至少有兩個,用最大長度存放各指令組,再根據連線位置就可以計算出xxxx,或者,翻譯命令時用臨時表記錄其地址,最後按表修改跳轉地址,在M文中對此有更多描述。如果有時又按連線位置取下一條命令翻譯(此時不用插入跳轉),這樣可使得程式碼整體架構改變,而實際演算法沒有改變。
  以上只是初步設想,很多問題不可能涉及,下一步詳細設計時合理地設計宏是關鍵,然後編碼首先要保證彙編模組能正確翻譯,之後才變形。
 
                    月中人  原創於2006.8.22.

變形實踐-“我如何做MetaPHOR以及我學會了什麼”

    者:

月中人

    期:

2007-01-11

    布:

Pediy.com

原文標題:

Metamorphism in practice or "How I made MetaPHOR and what I've learnt"

原文作者:

The Mental Driller (#6)

原文日期:

February 2002

關 鍵 詞:

變形,宏,偽彙編,置換,收縮,展開

原文連結:

http://vx.netlux.org/lib/vmd01.html

目錄

0.         基礎知識

a.         什麼是變形作用?

b.         變形程式碼的

1.         規劃

a.         心理演算:我們用宏編碼!

b.         我們要做什麼?

                        i.              簡單化的:置換/代替

                      ii.              “手風琴”模型(收縮/展開)

                    iii.              偽組合語言

2.         編碼

a.         反彙編器/反置換器(反模糊器)

b.         收縮器/模擬器

c.         置換器

d.         展開器(模糊器)

e.         重新彙編器

3.         已知問題(和解決方案)

a.         除錯你的引擎

b.         API呼叫

c.         記憶體

4.         未來

a.         外掛

b.         多平臺交叉感染

c.         針對不同處理器重新彙編

5.         結論

致謝

感謝所有的成員,感謝VecnaZ0MBiE、為了他們作為先行者在變形領域所做每一點探索,再次感謝Vecna、為了他在文章的結構上給我的那些重要建議。

0)      基礎知識

a)        什麼是變形作用?

L 此類文章必不可少的問答J

變形作用是極度變異的人工技術。這意味著,我們變異程式碼中每個東西,而不僅僅變異一個可能存在的解密器。變形作用是多型的自然進化結果-多型象是在逃避病毒掃描器。一個病毒加上變形,檢測它的難度呈指數上升。

那麼,為什麼變形病毒沒有那麼多呢?簡單說:因為製作它們非常困難,如本文中所示(不僅由於所使用的技術,也由於我們編碼會遇到的許多ring0級問題)。不管怎樣,我們將努力理解這一點:可能關鍵是要有好點子(VecnaZ0MBiE等程式碼人具有的那些東西 - 大家好!J

b)        變形程式碼的結構

變形病毒就象一輛49cc摩托車載著一艘太空梭的燃料儲備(如果你能想象出來)。事實上,90%以上的程式碼是變形引擎,用來變異那個致力於感染的一小部分程式碼,這有點荒謬。引擎必須能夠變異它本身和附屬程式碼-這些程式碼讓引擎可以獨自地傳播(oh)。完全不同於小巧的多型引擎,它用一個200位元組程式碼長的引擎去變異一個8KB的病毒:這次正相反!因為我們不能用200位元組做一個完整的反彙編器(噢,也許超人可以K。那麼,其結構是:

Engine

Virus

其引擎的(典型)結構是:

Disassembler

Shrinker

Permutator

Expander

Assembler

現在詳細說明:

反彙編器Disassembler

引擎的真正開始。反彙編器將譯碼每條指令以瞭解它的長度、使用的暫存器和所有有關它本身的資訊。同樣,它必須能譯碼那些改變IP的指令,象x86JMPCALL(或者如其他的BSRBranch Subroutine轉移子程式)

收縮器Shrinker

也叫做壓縮器,這部分將壓縮已經過反彙編的程式碼--病毒在這一代的程式碼也就是上一代生成的。即使你使用一個非展開的變異技術,也要這樣做,以避免病毒一代一代地變得越來越大,在很少幾代以後就會變成一個有許多兆位元組的病毒。顯然,這部分依賴你的變形程式碼樣板[注:原文type,或譯“型別”,意指是所使用的宏定義或其他與變形方法相關的東西],而且它也是最難做的:事實上,很少病毒有這部分。基本上,它是把展開器編碼的許多條指令壓縮成一條指令。它其實也是一個模擬器,消除無用和冗餘指令,把多個操作壓縮成一個操作(例如,MOV Reg,1234/ADD Reg,4321 --> MOV Reg,5555)

置換器Permutator

變形引擎的一個基本部分,許多病毒作者製作變形病毒都曾經編寫這部分,雖然它根本不是變形,其實你指令沒有改變。通常結合使用其他變形方法,象指令替換(例如,XOR EAX,EAX 代替 SUB EAX,EAX 等等)。它的原理非常簡單,但是非常有效,因為它打斷所有能被用來檢測病毒的掃描字串。

展開器Expander

這部分僅當有收縮器時才有(嗯,有些病毒有這部分,但是它們的程式碼增長失去控制)。它做的與收縮器做的正相反:它把一條指令重新編碼成許多條指令,但執行相同的功能。

彙編器Assembler

它重新編碼我們用展開器構造出來的指令。它連線那些JMPCALL之類轉移指令,計算指令長度,改變暫存器等等。或者,如果你使用一個內部偽彙編器,那麼它把虛擬碼重新彙編為目標處理器語言。

OK,這些就是常規部分。總之,決定你要做什麼,你需要做一個規劃!別犯傻(對不起 J,不能不規劃你要做的事,就冒然製作一個困難的變形程式碼(容易做的可以 K。否則,最可能的結果就是你永遠完成不了你的程式碼。在編碼MetaPHOR病毒的時候,我為之規劃了大約兩個月,而且我想先在腦子裡把自己想做的事弄清楚。嘿,相信我,它對我幫助很大,我節省了大量的工作。因此,讓我們做一遍我的規劃(就當作幫你規劃你的程式碼)

1)      規劃

a)        心理演算:我們用宏編碼!

這部分非常重要:你要當作不是在機器層次接觸指令。你要表述成“移動這個數值到這個暫存器”或者“把這個暫存器的內容加到這個變數”。這樣你對於編寫變形程式碼會感覺更容易些(事實上,這和你編寫多型程式碼時是一樣的)。所有指令和指令組都是宏操作的真正實現。目的是學會把宏程式碼看作一束指令,宏不是最終讓機器執行的程式碼,而是代表一個操作,必須被執行以製造出一個更重要的操作。

b)        我們要做什麼?

i)          簡單化的:置換/代替

如果你想用這種方法做變形(或者你不想做一個400Kb的引擎那麼辛苦),那麼這就是你的選擇。計劃好怎樣置換程式碼(JMP連結、單指令輪換和NOP填充,等等)。必需的前提是這個引擎只能用於你自己編寫的病毒,因為整個程式碼必須與該引擎的樣板相容。例如,假設你選擇用NOP填充的方法置換程式碼,就必須使所有指令佔用的空間大小相同,並且用NOP填充多出來的空間。這是最簡單的變形方法[注:原文meta應是metamorphism的略寫],同時也是不需要做很多規劃的方法:只要直接編碼,在你完成第一版本編碼後(並且在你測試之前),用NOP填充即可。NOP填充的另一個方法是用宏做:

        db 10h dup (90h) ; NOPs

        org $-10h

        <instruction>

        .align 4

或者類似的東西。如果你是個正常人就編碼一個宏,或者如果你是瘋狂的就直接做 K

NOP填充不同的另一個方法是在執行時取得每條指令的長度,然後在緩衝區弄亂這些指令的排列順序。這需要反彙編以調整JMPsCALLsJccs(條件跳轉),所以它也許不象說的這麼簡單。


ii)          “手風琴”模型(收縮/展開)

多好的技術名稱! XD 這種變形的威力在於,可能用不著置換,而程式碼永遠都不一樣的(而且能與產生“絕對變形”的置換法完美地結合使用)。要做這種變形,你必須先決定,是使用迷你型模擬器,還是僅僅反彙編成偽彙編碼。在MetaPHOR中我採用後者。

對於後者,你首先定義你自己的組合語言(可以基於x86操作碼)。要記住,這種組合語言越象x86操作碼,就越容易操縱。那麼,讓我們結合下一節來學習...

iii)       偽組合語言

...繼續。MetaPHOR的內部偽彙編碼遵守以下規則:

a.          所有指令是16位元組長(但將來操作象Itanium這樣的64位處理器時這可以改)

b.          指令的結構全都是一樣的:

一般結構:

每條指令16位元組,

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

OP *----- instruction data ----* LM *-pointer-*

OP是指令的運算子,它決定我們使用哪種指令資料結構。

LM是“標籤記號”。當有一個標籤指向這條指令時其值為1,它的用處很多,例如用於瞭解兩條指令能不能被收縮(如果第二條指令有標籤那麼就不能收縮)。它在指令中的第0B位元組。[注:16位元組偽指令是從第00到第,下同]

0C位元組的雙字(dword)是一個指標,意即“上次程式碼引用”。在反彙編時它表示EIP,即這條指令用它儲存原始編碼的位置;而我們進一步加工程式碼的時候,用它儲存該指令上次位置的引用。這用於輔助修改標籤表、重新編碼轉移指令(JMP、CALL等),等等。

該引擎用於指令的結構:[注:即偽指令的instruction data部分]

Memory_address_struct:

+01

First index

+02

Second index, bits 7&6 are the multiplicator (00=*1,01=*2,10=*4,11=*8)

+03

DWORD addition to indexes

依據運算子(即所要執行的操作),用下面演算法(判斷指令資料結構中的運算元含義)


l         如果該操作沒有運算元(如NOP、RET等操作),那麼指令資料是沒意義的。

l         如果該操作有一個運算元:

Register operand:

+01: Register

Memory address:

+01: Memory address struct

Immediate value:

+07: DWORD value, zero extended if it's a byte operation

Destiny address (JMP, CALL, etc.)

+01: Label to jump to (DWORD)

l         如果該操作有兩個運算元:

Reg,Imm:

+01: Register

+07: DWORD immediate value, zero extended if it's a 8-bits op.

Reg,Reg:

+01: Source register

+07: Destiny register

Reg,Mem:

+01: Memory address struct

+07: Destiny register

Mem,Reg:

+01: Memory address struct

+07: Source register

Mem,Imm:

+01: Memory address struct

+07: DWORD immediate value, zero extended if it's a 8-bits op.

[注:以上演算法說明:+01表示第01位元組是什麼-冒號後面,如有Register則是暫存器,如有DOWRD則為4位元組立即數或標籤地址,如果是Memory address struct則根據上面那個表。+07與之類似。如果是8位操作而且有立即數,在偽指令中立即數零擴充套件為雙字]

根據這個規則,現在我們使用下面的偽運算子:

00: ADD, 08: OR, 20: AND, 28: SUB, 30: XOR, 38: CMP, 40: MOV, 48: TEST

設定規則:

+00

Reg,Imm

+01

Reg,Reg

+02

Reg,Mem

+03

Mem,Reg

+04

Mem,Imm

+80

8 bits operation

所以,操作碼83表示ADD Mem,Reg使用8位運算元,以此類推。

[注:根據偽運算子和上面規則可知,8300+03+80;以下繼續是偽運算子及其含義]

50

PUSH Reg

51

PUSH Mem

58

POP Reg

59

POP Mem

68

PUSH Imm

70

Conditional jumps

E0

NOT Reg

E1

NOT Mem

E2

NOT Reg8

E3

NOT Mem8

E4

NEG Reg

E5

NEG Mem

E6

NEG Reg8

E7

NEG Mem8

E8

CALL label

E9

JMP label

EA

CALL Mem (used for API calls)

EB

JMP Mem (used for obfuscation in API calls)

EC

CALL Reg (obfuscation of API calls)

ED

JMP Reg (idem)

F0

SHIFT Reg,Imm

F1

SHIFT Mem,Imm

F2

SHIFT Reg8,Imm

F3

SHIFT Mem8,Imm

For all SHIFTs:

+07

Byte with the value of rotation/shifting

+08

Operation performed: 0: ROL, 8: ROR, 20: SHL, 28: SHR

F4

APICALL_BEGIN

Special operation meaning PUSH EAX/PUSH ECX/PUSH EDX that avoids the recoding of these registers, always remaining the same.

F5

APICALL_END

The complementary of APICALL_BEGIN, it means POP EDX/POP ECX/POP EAX

F6

APICALL_STORE

+01

Memory address struct

This always means: MOV [Mem],EAX <-- Avoiding the recoding of EAX

F7

SET_WEIGHT

+01

Memory address struct

+07

Byte with the weight identificator

+08

Register code structure

+09

Register code structure

F8

MOVZX

Memory address struct is a 8-bits operand, while +07 is a 32 bit reg.

FC

LEA

FE

RET

FF

NOP

這些操作碼會在反彙編輸出中出現。此外,我還定義了另外一些操作碼用於內部操作:

Exists only While shrinking, and means a MOV Mem,Mem, being:

+01

Source memory address struct

+07

Pointer to the instruction that holds the destiny memory address struct, which has the format:

+00

FF (NOP)

+01

Destiny memory address struct

+07

Ptr to source memory address holder (the instruction)

因為只是一個轉換操作碼,在收縮這一步以後可以用來表示其他含義。另外有三個操作碼只用於重新彙編:

4E

INC/DEC Register

+01

Register

+07

0 if INC, 8 if DEC

INC/DEC memory address

+01

Memory address struct

+07

0 if INC, 8 if DEC

FD

Literal byte

+01

Byte to insert directly into the code

以上就是我所做規劃的開始部分。如你所見,操作碼與x86的非常相似,所以很容易記 (事實上,我寫這個列表只是為了回憶它 J

現在,規劃的第二部分:收縮器。我們該如何收縮?我們將在它那部分學習,因為我不想寫兩遍 L

2)      編碼

a)        反彙編器/反置換器

這是引擎的入口點!當然,做任何東西之前,我們必須反彙編。反彙編器從原理上講是很容易的,卻是一個實實在在的苦差事。因為我們的目標是生成絕對變形的程式碼,所以不能使用雜湊表,雖然我們可以編一段程式變異雜湊、把它變得更強。我的解決方案是前者,而且我在反彙編器中隱含地實現反置換。譯碼原理如下:

我們有一個記憶體緩衝區,其大小為容納我們將要反彙編的程式碼,我們把程式碼入口點賦值給ESI。這個緩衝區(其中各地址儲存於變數PathMarks)讓我們容易控制已經反彙編的程式碼。我們還有另外兩個表:LabelTableFutureLabelTable,也已經初始化。這兩個表各有一個計數器變數,其值為表中元素序號。此外我們把指令譯碼到DisassembledCode,這個地址放在EDI

LabelTable表中每個元素是兩個雙字長。第一個雙字儲存指標指向的真實EIP,第二個雙字儲存指標指向相應的已反彙編程式碼。然後,當我們譯碼一個JMP時,我們在該表中設定一個指標作為標籤。這樣,我們移動該標籤的內部指標時,引用該標籤的所有指令就被自動更新。

FutureLabelTable是一個緩衝區表,只用於反彙編。它用來儲存那些跳轉、呼叫等指令的目的地,指向我們還沒有反彙編的程式碼。每譯碼一條指令,我們都要檢查該表中有沒有該指令的地址,如果該地址出現在表中,我們就可以把所有引用該地址的指令弄完整。

有了以上這些,再看看演算法:

1.       初始化PathMarks對映表(即清零),並初始化標籤的個數(LabelTable的計數器)和預測標籤的個數(FutureLabelTable的計數器)。

2.       把當前EIP(在ESI中)直接轉化為PathMarks對映表中一個條目。

¡        如果對映表中已經有該條目,那麼在ESI中儲存為一個JMP,跳轉目的地是這條已反彙編指令[注:然後會被當作“沒有該條目->它是JMP”的情況再次處理](如果LabelTable表中沒有該指令的標籤,就加入這樣一個標籤)。

¡        如果對映表中沒有該條目,那麼標記這個地址為已經反彙編並且譯碼這條指令。接著,我們根據該指令不同處理:

n         如果它是JMP:

²        如果它指向一個已經被譯碼的地址,那麼寫入一個JMP指令,並在LabelTable表中插入一個跳轉目的地的標籤,再從FutureLabelTable表中取出一個新的EIP。如果LabelTable表中已經有這個標籤,那麼這個JMP指令就使用該標籤。

²        如果它指向一個還沒有被譯碼的地址,那麼暫時先寫一條NOP指令(以防萬一某個標籤直接引用這個地址),然後把跳轉目的地賦值給ESI作為新的EIP。這樣,我們消除了一個可能的置換JMP。

n         如果它是Jcc(條件跳轉):

²        如果它指向一個已經被譯碼的地址,那麼寫入這個Jcc,而且如果這個Label還不存在就插入一個跳轉目的地的Label(如果該Label已經插入到表中,就使用該Label)。

²        如果該目的地還沒有被反彙編,那麼把地址儲存到FutureLabelTable表並且繼續(處理下一條指令)。

n         如果它是CALL,那麼採用類似Jcc的處理辦法(只不過寫入CALL而不是寫入Jcc)。

n         如果它是RET、JMP Reg或者是JMP [Mem](程式碼樹中的終端葉子),那麼儲存該指令並且從FutureLabelTable表中取一個新的EIP。

[注:“置換JMP”--在進行置換時插入的跳轉指令JMP。演算法中可能消除置換用的JMP,也可能消除其他作用的JMPPathMarks map意為“執行路線標記對映表”。]

FutureLabelTable表中取出一個新的EIP的時候,我們檢查存於該表的標籤是不是已經譯碼了[注:意即該標籤所指示地址的指令是否已經被反彙編]。如果是,那麼我們把相應的標籤插入到LabelTable表中,並從FutureLabelTable表中去掉該條目。如果不是,我們就處理這個新的EIP(即,我們把新的入口點賦給ESI),在LabelTable表中插入新標籤並繼續。

你可以推理出,當FutureLabelTable表中再沒有條目時,反彙編就結束了,因為沒有條目意味著我們是從程式碼流的終端葉子出發。完成這樣的“模擬過程”以後,我們已經:

1.    消除了置換和置換跳轉(因為我們直接改變ESI中的EIP而消除了那些JMP)

2.    消除了所有絕對無法達到的程式碼。

3.    整個程式碼譯解為我們的偽彙編碼

4.    用指向表條目的指標代替標籤。

反彙編器做好了!我們不必編寫一個反置換器程式碼或一個模擬器程式碼來檢測死碼區,因為我們隱含地消除它們了。一個反置換器執行的例子是:

       CODE         PASSES

      ------        ------

        xxx1        1) Decode xxx1

        xxx2        2) Decode xxx2

        xxx3        3) Decode xxx3

        jmp @A      4) Change EIP to @A (don't store label)

        yyy1        5) Decode xxx7

        yyy2        6) Decode xxx8

    @B: xxx4        7) Decode xxx9

        xxx5        8) Change EIP to @B (don't store label)

        xxx6        9) Decode xxx4

        jmp @C     10) Decode xxx5

        yyy3       11) Decode xxx6

        yyy4       12) Change EIP to @C (don't store label)

    @A: xxx7       13) Decode xxx10

        xxx8       14) Decode xxx11

        xxx9       15) Decode JZ and store @D in FutureLabelTable

        jmp @B     16) Decode xxx12

    @D: xxx13      17) Decode RET, get @D from FutureLabelTable and

        xxx14        complete the JZ at pass 15 (@D = current EIP)

        RET        18) Decode xxx13

        yyy5       19) Decode xxx14

    @C: xxx10      20) Decode RET and get an item from FutureLabelTable.

        xxx11        Since it's empty, we have decoded everything, so finish.

        jz @D

        xxx12

        RET


反彙編結果將會是:

        xxx1

        xxx2

        xxx3

        xxx7

        xxx8

        xxx9

        xxx4

        xxx5

        xxx6

        xxx10

        xxx11

        jz @D

        xxx12

        RET

  @D:   xxx13

        xxx14

        RET

我想現在更清楚了。所有垃圾和填空程式碼(那些yyy?指令)都被隱含地消除了,因此以後不必尋找它們。

這裡的問題是,一旦被收縮,程式碼構架(在此已經反置換了)會被檢測病毒的利用。或許不會,但是我們還是想讓構架每代都不相同(西班牙語是“rizar el rizo”,或者西英混合語叫做“to loop the loopJ。因此,我們這一步插入三維指令,我把它叫作“3D instructions”。什麼?嗯哼...好,這不是譁眾取寵,它只是形象化表述:想象一下你在各維做的變形:第一維是當前程式碼(你正在反彙編的東西)。第二維是下面馬上要做出的編碼(這次變形的結果)。第三維是將來下一代所譯出的程式碼。

那麼,第一維和第二維是明確的,但是...第三維是什麼呢?簡單說:例如,把一些JMP編碼為CMP EAX,EAX/JZ @xxx。你看出關鍵了嗎?收縮器(我們在下一部分馬上要講到)必須能夠把“CMP EAX,EAX/JZ @xxx”對壓縮成“JMP @xxx”,但是這要在下一代才發生,而在這一代並不壓縮它(在更遠的後代有一些結構將被完全壓縮掉)。我們唯一必須要注意的是,我們不能在這類跳轉的後面放垃圾指令,因為它們也將會被譯碼。讓我們再用上面的程式碼為例,這次先去掉那些yyy?,並用CMP/JZ對代替其中第一個JMP,如下面左邊程式碼:

[注:CMP/JZ將在這一代的收縮器中被壓縮成JMP @A,然後在下一代的反彙編中被隱式消除。這裡CMP/JZ就是所謂的3D instruction,在第三維被消除,所以它的主要作用是讓程式碼在這一代的處理過程中永遠保持一定的變形複雜性。]


          xxx1       Result of the disassembly:

          xxx2          xxx1

          xxx3          xxx2

          CMP X,X       xxx3

          JZ @A         CMP X,X

      @B: xxx4          JZ @A    --> This will be compressed to JMP @A on this

          xxx5      @B: xxx4         generation and then eliminated implicitly

          xxx6          xxx the disassembly of next generation.

          jmp @C        xxx6

      @A: xxx7          xxx10

          xxx8          xxx11

          xxx9          jz @D

          jmp @B        xxx12

      @D: xxx13         RET

          xxx14     @A: xxx7

          RET           xxx8

      @C: xxx10         xxx9

          xxx11         jmp @B

          jz @D     @D: xxx13

          xxx12         xxx14

          RET           RET

這意味著絕對變形。構架改變,但是程式碼演算法沒變。必需要經歷若干代才能變回到最初的程式碼,但是因為其他JMPs同樣被變異,實際上你永遠無法達到,而且你需要經歷若干代來消除一個置換JMP,而其時另外一些新的又被插入。好在這種插入不是無限的,達到極致時這個程式碼穩定了,但是之前那些代的程式碼是變化的,我們會看到我們的程式碼因為不斷插入這些跳轉而體積增長。

這裡還可以做的另一件事,就是用這個反彙編器把一些指令譯碼成易懂形式:例如,INC Reg --> ADD Reg,1。這樣,我們只須處理一種指令,不用處理它的所有變體-即該處理器中所有執行相同操作的指令(雖然這也可以用收縮器來做,但是在這裡做我們就可以少設計一些偽操作碼)

b)        收縮器/模擬器(反模糊器)

在此,我們要模擬還是壓縮?模擬更先進更強大,但是它意味著編碼非常複雜,而且有很多問題,比如迴圈後數值的控制。比較容易且有更好的質量/數量壓縮比的方法,是壓縮已知的指令對和指令三聯組:和展開器部分所做的正好相反。

規劃必須有這部分。你必須列出所有可能的單條指令、指令對、指令三聯組,並且決定哪些是要壓縮/展開以及哪些是不壓縮/不展開。收縮器可以被用來消除“智慧垃圾”:在演算法中插入一些似乎成為演算法的一部分但實際上無用的程式碼。現在我列出一些單條//三聯組指令,它們在MetaPHOR中有機會被壓縮,同時它們也是展開器可以用一條指令生成的那些指令組合。

圖注:

Reg    暫存器

Mem    記憶體地址

Imm    立即數

當一條指令是Reg,Reg這種型別的,兩個Reg表示同一暫存器。(舉個例子)如果不是同一暫存器,我就會寫成Reg,Reg2

對一條指令的變形:

XOR Reg,-1

--> NOT Reg

XOR Mem,-1

--> NOT Mem

MOV Reg,Reg

--> NOP

SUB Reg,Imm

--> ADD Reg,-Imm

SUB Mem,Imm

--> ADD Mem,-Imm

XOR Reg,0

--> MOV Reg,0

XOR Mem,0

--> MOV Mem,0

ADD Reg,0

--> NOP

ADD Mem,0

--> NOP

OR Reg,0

--> NOP

OR Mem,0

--> NOP

AND Reg,-1

--> NOP

AND Mem,-1

--> NOP

AND Reg,0

--> MOV Reg,0

AND Mem,0

--> MOV Mem,0

XOR Reg,Reg

--> MOV Reg,0

SUB Reg,Reg

--> MOV Reg,0

OR Reg,Reg

--> CMP Reg,0

AND Reg,Reg

--> CMP Reg,0

TEST Reg,Reg

--> CMP Reg,0

LEA Reg,[Imm]

--> MOV Reg,Imm

LEA Reg,[Reg+Imm]

--> ADD Reg,Imm

LEA Reg,[Reg2]

--> MOV Reg,Reg2

LEA Reg,[Reg+Reg2]

--> ADD Reg,Reg2

LEA Reg,[Reg2+Reg2+xxx]

--> LEA Reg,[2*Reg2+xxx]

MOV Reg,Reg

--> NOP

MOV Mem,Mem

--> NOP (result of a compression of PUSH Mem/POP Mem, with pseudoopcode )

被消去的指令[注:指箭頭左邊的](等效於NOP)被當作垃圾指令使用,與可執行程式碼放在一起。由於每個NOP指令都能被展開(例如,MOV Reg,Reg可以被做成PUSH Reg/POP Reg,而每一個PUSHPOP也都可以被展開,以此類推),所以你無法知道哪些是垃圾,哪些不是,除非你把全部都壓縮了。

MetaPHOR能壓縮的指令對有:

PUSH Imm / POP Reg

--> MOV Reg,Imm

PUSH Imm / POP Mem

--> MOV Mem,Imm

PUSH Reg / POP Reg2

--> MOV Reg2,Reg

PUSH Reg / POP Mem

--> MOV Mem,Reg

PUSH Mem / POP Reg

--> MOV Reg,Mem

PUSH Mem / POP Mem2

--> MOV Mem2,Mem (codificated with pseudoopcode )

MOV Mem,Reg/PUSH Mem

--> PUSH Reg

POP Mem / MOV Reg,Mem

--> POP Reg

POP Mem2 / MOV Mem,Mem2

--> POP Mem

MOV Mem,Reg / MOV Reg2,Mem

--> MOV Reg2,Reg

MOV Mem,Imm / PUSH Mem

--> PUSH Imm

MOV Mem,Imm / OP Reg,Mem

--> OP Reg,Imm

MOV Reg,Imm / ADD Reg,Reg2

--> LEA Reg,[Reg2+Imm]

MOV Reg,Reg2 / ADD Reg,Imm

--> LEA Reg,[Reg2+Imm]

MOV Reg,Reg2 / ADD Reg,Reg3

--> LEA Reg,[Reg2+Reg3]

ADD Reg,Imm / ADD Reg,Reg2

--> LEA Reg,[Reg+Reg2+Imm]

ADD Reg,Reg2 / ADD Reg,Imm

--> LEA Reg,[Reg+Reg2+Imm]

OP Reg,Imm / OP Reg,Imm2

--> OP Reg,(Imm OP Imm2) (must be calculated)

OP Mem,Imm / OP Mem,Imm2

--> OP Mem,(Imm OP Imm2) (must be calculated)

LEA Reg,[Reg2+Imm] / ADD Reg,Reg3

--> LEA Reg,[Reg2+Reg3+Imm]

LEA Reg,[(RegX+)Reg2+Imm] / ADD Reg,Reg2

--> LEA Reg,[(RegX+)2*Reg2+Imm]

POP Mem / PUSH Mem

--> NOP

MOV Mem2,Mem / MOV Mem3,Mem2

--> MOV Mem3,Mem

MOV Mem2,Mem / OP Reg,Mem2

--> OP Reg,Mem

MOV Mem2,Mem / MOV Mem2,xxx

--> MOV Mem2,xxx

MOV Mem,Reg / CALL Mem

--> CALL Reg

MOV Mem,Reg / JMP Mem

--> JMP Reg

MOV Mem2,Mem / CALL Mem2

--> CALL Mem

MOV Mem2,Mem / JMP Mem2

--> JMP Mem

MOV Mem,Reg / MOV Mem2,Mem

--> MOV Mem2,Reg

OP Reg,xxx / MOV Reg,yyy

--> MOV Reg,yyy

Jcc @xxx / !Jcc @xxx

--> JMP @xxx (this applies to (Jcc & 0FEh) with (Jcc | 1)

NOT Reg / NEG Reg

--> ADD Reg,1

NOT Reg / ADD Reg,1

--> NEG Reg

NOT Mem / NEG Mem

--> ADD Mem,1

NOT Mem / ADD Mem,1

--> NEG Mem

NEG Reg / NOT Reg

--> ADD Reg,-1

NEG Reg / ADD Reg,-1

--> NOT Reg

NEG Mem / NOT Mem

--> ADD Mem,-1

NEG Mem / ADD Mem,-1

--> NOT Mem

CMP X,Y / != Jcc (CMP without Jcc)

--> NOP

TEST X,Y / != Jcc

--> NOP

POP Mem / JMP Mem

--> RET

PUSH Reg / RET

--> JMP Reg

CALL Mem / MOV Mem2,EAX

--> CALL Mem / APICALL_STORE Mem2

MOV Reg,Mem / CALL Reg

--> CALL Mem

XOR Reg,Reg / MOV Reg8,[Mem]

--> MOVZX Reg,byte ptr [Mem]

MOV Reg,[Mem] / AND Reg,0FFh

--> MOVZX Reg,byte ptr [Mem]

也許有更多,但是至少對我們的命題來說這套足夠了。我們必需領悟的是,對於此等情形,掃描程式碼然後用等效指令替換指令對的第一條指令,再用NOP覆蓋第二條,這樣就把指令對壓縮了。

但有更多:三聯組:

MOV Mem,Reg
OP Mem,Reg2
MOV Reg,Mem

--> OP Reg,Reg2

MOV Mem,Reg
OP Mem,Imm
MOV Reg,Mem

--> OP Reg,Imm

MOV Mem,Imm
OP Mem,Reg
MOV Reg,Mem

--> OP Reg,Imm (it can't be SUB)

MOV Mem2,Mem
OP Mem2,Reg
MOV Mem,Mem2

--> OP Mem,Reg

MOV Mem2,Mem
OP Mem2,Imm
MOV Mem,Mem2

--> OP Mem,Imm

CMP Reg,Reg
JO/JB/JNZ/JA/JS/JNP/JL/JG @xxx
!= Jcc

--> NOP

CMP Reg,Reg
JNO/JAE/JZ/JBE/JNS/JP/JGE/JLE @xxx
!= Jcc

--> JMP @xxx

MOV Mem,Imm
CMP/TEST Reg,Mem
Jcc @xxx

--> CMP/TEST Reg,Imm
Jcc @xxx

MOV Mem,Reg
SUB/CMP Mem,Reg2
Jcc @xxx

--> CMP Reg,Reg2
Jcc @xxx

MOV Mem,Reg
AND/TEST Mem,Reg2
Jcc @xxx

--> TEST Reg,Reg2
Jcc @xxx

MOV Mem,Reg
SUB/CMP Mem,Imm
Jcc @xxx

--> CMP Reg,Imm
Jcc @xxx

MOV Mem,Reg
AND/TEST Mem,Imm
Jcc @xxx

--> TEST Reg,Imm
Jcc @xxx

MOV Mem2,Mem
CMP/TEST Reg,Mem2
Jcc @xxx

--> CMP/TEST Reg,Mem
Jcc @xxx

MOV Mem2,Mem
AND/TEST Mem2,Reg
Jcc @xxx

--> TEST Mem,Reg
Jcc @xxx

MOV Mem2,Mem
SUB/CMP Mem2,Reg
Jcc @xxx

--> CMP Mem,Reg
Jcc @xxx

MOV Mem2,Mem
AND/TEST Mem2,Imm
Jcc @xxx

--> TEST Mem,Imm
Jcc @xxx

MOV Mem2,Mem
SUB/CMP Mem2,Imm
Jcc @xxx

--> CMP Mem,Imm
Jcc @xxx

PUSH EAX
PUSH ECX
PUSH EDX

--> APICALL_BEGIN

POP EDX
POP ECX
POP EAX

--> APICALL_END

也可能有更多,上面這些只是我使用的。我們採取和指令對相同的機制:我們檢查指標所指的三條指令是否構成一個已定義的三聯組,然後我們壓縮它、用兩組NOP覆蓋後兩條指令。

我們定義好單條指令、指令對和指令三聯組,接著就看壓縮演算法了。如果我們是遞迴展開的(就是說,假如我們編碼PUSH Imm/POP Reg,而PUSH Imm可以被進一步編碼為MOV Mem,Imm/PUSH Mem,所以就變成是“MOV Mem,Imm/PUSH Mem/POP Reg”,而POP Reg也可以被進一步展開),那麼這裡也不能使用定義直接收縮,所以壓縮演算法如下:

          CurrentPointer = FirstInstruction

       @@Loop:

          if ([CurrentPointer] == MATCHING_SINGLE)

          {

             Convert it

             if (CurrentPointer != FirstInstruction) call DecreasePointer

             if (CurrentPointer != FirstInstruction) call DecreasePointer

             if (CurrentPointer != FirstInstruction) call DecreasePointer

             goto @@Loop

          }

          if ([CurrentPointer] == MATCHING_PAIR)

          {

             Convert it

             if (CurrentPointer != FirstInstruction) call DecreasePointer

             if (CurrentPointer != FirstInstruction) call DecreasePointer

             if (CurrentPointer != FirstInstruction) call DecreasePointer

             goto @@Loop

          }

          if([CurrentPointer] == MATCHING_TRIPLET)

          {

             Convert it

             if (CurrentPointer != FirstInstruction) call DecreasePointer

             if (CurrentPointer != FirstInstruction) call DecreasePointer

             if (CurrentPointer != FirstInstruction) call DecreasePointer

             goto @@Loop

          }

          do (CurrentPointer++) while [CurrentPointer] == NOP

          if(CurrentPointer != LastInstruction) goto @@Loop

      DecreasePointer:

          do (CurrentPointer--) while (([CurrentPointer] == NOP) &&

                                       ([CurrentPointer.Label == FALSE))

          return

我們不必使用一些NOP填充後續指令,這樣才能去掉由前面反彙編產生的那些不受歡迎的垃圾。這是因為壓縮演算法沒有處理那種以RETJMP之類指令作為指令對/三聯組第一條指令的情況,而且我們非常肯定程式碼是以這些指令中的某一個作為結束的。並請注意我們總是忽略掉那些NOP

也要小心有標籤的那些指令:如果有一個標籤指向一組指令中的某一條(第一條除外),我們絕不壓縮這組指令(這就是為什麼我們要在規劃中定義的指令結構中設定一個LM位元組)。有標籤的指令意味著,有某一個跳轉或呼叫等等指向這條指令,如果我們將它與其上一條指令合併,就會把程式碼破壞了。

壓縮時消除的記憶體地址只不過是臨時變數,在重新彙編中預留它們用來存放數值以執行該操作[注:只在實現該操作的一組指令內使用]。因此,我們絕不能在任何指令對或三聯組中使用真正的變數(儲存重要資料的記憶體地址),否則該變數將會被消除而導致引擎損壞。只要稍微注意到這一點,我們就不用擔心記憶體變數了,而且不必檢查他們是重要變數還是過渡變數,我們可以根據程式碼隨意地重新定義這些變數的位置、並且可以和臨時變數混放在一起。

一個收縮器執行的例子:

  Original code:

  MOV [Var1], ESI  * PUSH ESI       * MOV EAX,ESI

  PUSH [Var1]      * nop              nop

  POP EAX            POP EAX        * nop

  PUSH EBX           PUSH EBX         PUSH EBX        =====>

  POP [Var2]         POP [Var2]       POP [Var2]

  ADD EAX,[Var2]     ADD EAX,[Var2]   ADD EAX,[Var2]

             MOV EAX,ESI        MOV EAX,ESI   * LEA EAX,[ESI+EBX]

             nop                nop             nop

             nop                nop             nop

  ====>    * MOV [Var2],EBX   * ADD EAX,EBX   * nop

           * nop                nop             nop

             ADD EAX,[Var2]   * nop             nop

把上面這個演算法傳給偽彙編器以後,我們得到的最終程式碼將雜有大量NOP指令。但是不要緊,因為展開的時候,我們就忽略掉這些NOP指令,做一個真正的最最佳化(雖然實現的最最佳化又被展開器削弱,但誰會在意呢...)

c)        置換器

使用一個內部彙編器使得我們在這一步很容易做,而且我們不必關照所有指令相同尺寸以及類似問題,因為重新彙編器會給我們計算。

置換程式碼最容易的方法是定義“程式碼框架”:我們構造一個表,在其中定義程式碼各分割槽,給每個分割槽指定一個起始偏移量和一個結束偏移量,這樣:

    ESI = Initial address of instructions

    EDI = Address of last instruction

    Given ESI = 00000000h,

          EDI = 00000060h

    while(ESI < EDI)

       Store ESI

       ESI += Random(8)+8

       Store ESI

       if((ESI+ > EDI)

          Store ESI,EDI

          break;

       end if

    end while

      

    Result (for example):

          DD    00000000h,0000000Ah

          DD    0000000Ah,00000017h

          DD    00000017h,00000023h

          DD    00000023h,00000032h

          DD    00000032h,0000003Dh

          DD    0000003Dh,00000049h

          DD    00000049h,00000052h

          DD    00000052h,00000060h

現在我們混洗這個陣列的元素。混洗很容易(有很多演算法可用)。混洗以後我們得到:

[注:原文shuffle意即“混洗”,即弄亂排列順序]

          DD    00000032h,0000003Dh

          DD    00000023h,00000032h

          DD    0000000Ah,00000017h

          DD    00000000h,0000000Ah

          DD    00000017h,00000023h

          DD    00000052h,00000060h

          DD    0000003Dh,00000049h

          DD    00000049h,00000052h

混洗時,我們跟蹤第一個框架,這是程式碼的入口點。如果第一個框架不是入口點,我們在該框架中插入一個JMP。因為所有跳轉指令的目的地未知,所以我們先把它們存入一個表中,在複製所有的指令之後,完成它們。所以,我們首先要做的是插入一個跳轉到入口點的JMP,然後我們開始複製指令。根據第一個框架的定義,我們必須複製地址偏移量從32h3Dh的那些指令,然後插入一個跳轉到下一個框架的JMP,以此類推。

經過以上處理,我們就得到一置換過的程式碼。此時我們只須根據先前的儲存表完成所有的JMP,這一步就圓滿完成了。

可以在這裡嘗試別的做法:舉個例子,假如你留著收縮器生成的那些NOP指令,會導致更隨機的程式碼分佈,因為那些NOP指令也參予置換,而下一步程式碼處理又把這些NOP消除了。

d)       展開器(模糊器)

展開器的工作與收縮器是相反的。它準確地執行反操作(當然,以隨機方式)。只取已定義的單條指令、指令對和指令三聯組,並且把指令用等效編碼代替(每條指令除了它們的直接編碼之外都有一個可選的等效替代)

在此,我們將把全部程式碼遞迴地編碼。例如,見到一個50h操作碼(PUSH Reg)的時,我們將呼叫MakePUSHReg(),它將隨機決定是直接編碼成PUSH,還是使用一個已定義的指令對或三聯組繞著彎實現。如此,該函式決定用MOV Mem,Reg/PUSH Mem,所以呼叫MakeMOVMemReg()MakePUSHMem()。可這樣太沒有規律了!函式MakeMOVMemReg() 呼叫可能換成MakePUSHReg()+MakePOPMem(),於是又呼叫MakePUSHReg()了。這會讓程式碼增長太多,因此我們給它加一個遞迴控制,即一個變數,當遞迴呼叫一個函式時增量,離開時減量。我們檢查該變數,看它是不是達到一定值,如果是,就直接編碼該指令(在本例中,也就是使用50h+Reg 並且 EIP++ 繼續處理下一條指令)

如果我們使用內部的組合語言,展開器就不必處理最終編碼了,它會更好。最終編碼的任務就由重新彙編器完成(下一節)。為了保證程式碼展開結果的品質,我們要生成儘可能類似最終程式碼的偽彙編碼(這就是為什麼我在操作碼列表中使用象4E這樣的操作碼來編碼INCDEC)。所以,如果我們在展開器中見到CMP EAX,0,我們把它編成偽彙編碼OR EAX,EAX或者TEST EAX,EAX,如此,重新彙編器只須直接編碼指令,除了操作碼生成的某些隨機性之外,不必解決其他任何東西。

展開器還要做另外一些操作,如:

換掉暫存器

我們選擇一個新的暫存器換掉我們一直用到現在的那個暫存器。較容易的方法是在一個列表中放入0,1,2,3,5,6,7序列並打亂它,然後根據那些數字換掉每個暫存器。這樣,執行那些操作時暫存器永遠是不同的,甚至使得記憶體地址也不同,因為如果我們把ESI換成EBP(舉個例子),這些記憶體引用的方法被彙編成x86是不同的(或者說可能是不同的)。我們可以在VecnaRegswap中看到這一點。

重新選擇變數

為了使用記憶體變數,我們必須擁有一個記憶體緩衝區(VirtualAlloc預留,或用宿主本身的.bss節,等等)。如果有變數,我們習慣在一個固定位置儲存重要資料,忘了它吧!把它們全部一起放在一個緩衝區。這樣,我們只須明白一個記憶體地址是一個變數(就象它是[DeltaRegister+12345678h]形式的一個記憶體地址),把它儲存在一個緩衝區,就象我們對程式碼標籤所做的那樣(但這次是記憶體地址),弄亂它們的排列順序,併為每個變數重新賦值一個地址。我們這樣做,而不使用固定變數。唯一不方便的是,我們必須由其他地方提供某些值,比如由解密器(如果我們使用某個)

也舉個LEA的例子:如果我們把LEA僅僅用來重新編碼MOV Reg,ValueADD Reg,ValueMOV Reg,RegADD Reg,Reg,會比較好。為什麼?因為如果你看到一個LEA,意味著到了這裡,因為它是更多一些指令的收縮,象MOV EAX,EBX / ADD EAX,12345678h。所以,如果我們不用LEA編碼,把這個任務分解成更簡單的操作來實現(隨機地),那麼我們相當於內含一個交換器模組。一個例子:

     MOV EAX,ECX                                             

     ADD EAX,3   -> (shrink) -> LEA EAX,[ECX+3] -> (expand) -> MOV EAX,3

                                                               ADD EAX,ECX

                                                   (expand) -> MOV EAX,ECX

                                                               ADD EAX,3

如果我們花力氣在引擎某部分編寫一個交換器,我們會發現這些情況正是所期望的,所以對於最終結果而言再編寫一個顯式的交換器是多餘的。不管怎樣,都能在展開時發生一個交換,但是小心,因為很多情況下-比如象下面這些-兩個元素間的交換如果沒有控制好就會破壞程式碼:

     MOV EAX,1234

     MOV EBX,2345  <-- check if a label is pointing to this!

 

     MOV EAX,1234

     MOV EBX,[EAX] <-- check if the second instruction uses the elements of

                       the first instruction

 

     MOV EAX,[EBX]

     MOV EBX,1234  <-- check if the first instruction uses the elements of

                       the second instruction

 

     MOV EAX,[EBX]

     MOV [ECX],EDX <-- EBX and ECX has the same value? We can't know this

                       without total emulation

還有很多很多。我的經驗是,有太多因素使得,即使交換器只做了最微小簡單的改變都能讓程式碼崩潰,雖然它看上去完全正確。我們唯一可以安全地變異的是那些用LEA插入的及其後來解出來的東西。而且,我們又避免了一個大程式的編碼 J[注:這裡mutate“變異”是指swap“交換”]

e)        重新彙編器

重新彙編器是引擎的結束邊緣。這塊程式碼將生成能被處理器所理解的指令。如果我們能理解把程式碼展開成偽組合語言的基本原理,這就是小菜一碟,因為我們在編寫多型引擎時已經做過很多次了。而且,想象著自己就要完工的引擎會令你士氣大增,相信我 J

重新彙編器是一個友好的模組,容易得到其他模組輔助,比如展開器。我們可以把它編成照字面意義讀取偽彙編碼,直接把它的表示寫出來,而不用管它的含意。

但是我們編碼它的時候,發現有些東西不是看上去那麼容易的,例如那些EIP轉移指令。我們該如何編碼那些向前的JMPCALLJcc呢?我們必須(再一次)使用一個表,並且把所有指令儲存進去,我們必須在偽碼全部被彙編後再完成那些目的地地址。

但是真正棘手的是那些向前短跳轉JMP/Jcc。我們可以不用它,但這不合理 K。我的解決方案(非常簡潔)是看指令是否指向前面1112條指令的最大長度,所以如果低於那個分數,我們可以任意決定編碼成短或長(當然,隨機地)。對於向後的跳轉我們沒有問題,因為我們無例外地知道長度。那些CALL不必由我們決定,但是當它們向前的時候我們也必須解決它們,因為那裡的程式碼還沒被彙編。

引擎在這部分的隨機性是由跳轉(當我們能決定我們是使用短跳轉還是長跳轉)和操作碼重新彙編(對於同一操作碼我們有幾個可能選擇時)實現的。下面的列表顯示一些指令,可以被這樣處理:

  B0+Reg

  C0+Reg   --> MOV Reg8,Value

  B8+Reg

  C0+Reg   --> MOV Reg,Value

  50+Reg

  FF F0+Reg   --> PUSH Reg

  58+Reg

  C0+Reg   --> POP Reg

  40+Reg

  FF C0+Reg   --> INC Reg

  48+Reg

  FF C8+Reg   --> DEC Reg

這是一個例子。其他的就是那些能夠使用EAX的指令(獨佔使用EAX的操作碼或普通的操作碼),還有那些採用符號擴充套件位元組到雙字運算元(運算子83,並使用-80之間的數值)的操作碼,等等。所有它們只是在操作碼層次上的隨機,因為所有操作的可選替代(複雜的移動之類)由展開器處理。

嘿,完工了!我們得到一段重新彙編過的程式碼!現在開始難的部分:除錯。

3)      已知問題(和解決方案)

a)        除錯你的引擎

如果你從未編寫過這樣一個引擎,你會覺得除錯變形程式碼是地獄。在你除錯第一代程式碼的時候感覺很好,因為你瞭解你做什麼,但是當你必須除錯生成的程式碼時問題就來了。彙編程式是如此如此地混亂,以致你都快要發瘋了,因此解決辦法是:從初期開始除錯!直到所有其他部分完全工作以後才編碼收縮器/展開器。

你必須考慮到的另一件事是,偵錯程式使用INT 3補丁從那些CALL返回。如果你編寫一個單獨函式的反彙編碼,在跟蹤/單步時要留神,因為:

  MOV EDI,[VirusEntry]

  CALL Disassembly

  MOV EAX,12345678  ;--> The disassembler will see: 

                                                    MOV EDI,[VirusEntry]

                                                    CALL Disassembly

                                                    INT 3

                                                    JS @xxx

                                                    ...

你會發現,偵錯程式把程式碼破壞了。這是好的一面,因為程式碼隱含地反除錯 J,但是不好的一面是如果你不知道這種情況,你會以為是某些地方出錯,並且白花時間檢查。解決辦法是:

1.         跟進到呼叫裡面,並且跟蹤所有的除錯(覺得不可接受的那些地方),或者

2.         使用硬體斷點(~!噼o啪啦-鼓掌聲)

因此,按下“執行到這裡”鍵的時候要留神,因為如果你在執行反彙編器以後這樣,它是OK的,但是如果在反彙編以前、而你指向的程式碼要在反彙編以後執行,那麼它就會崩潰。在“抵達指令”上使用硬體斷點將沒有這個問題,但是更讓人生氣(需要按更多次鍵,等等)。把它做成一個宏。

b)        API呼叫

檢測API呼叫真是苦不堪言。因為所有呼叫的引數個數都不同,我們不知道引數在什麼點上開始(但是透過這個CALL我們知道API的結束點,啊,現在我們安詳地休息了 L)。這類呼叫是脆弱的,在它們附近我們不能隨意改變暫存器,因為返回值總是放在EAX中,而且它們總是修改ECXEDX。所以,在一個正常程式碼中,我們恰當地設定暫存器數值,沒有使用這三個,但是當我們“自由變形”改變暫存器的時候[注:原文“a go-go],我們沒辦法不用這三個暫存器(否則,我們只能用其他暫存器玩玩,而這樣使得可能選擇的範圍受到侷限)。另外我們還必須檢測什麼時候EAX被使用以便及時儲存返回值,限制使用這些暫存器引起許多問題,同我們的引擎格格不入。

容易的解決方案是使用一個“程式碼標記”:它們代表一個特定指令序列,用特定的運算元;它們在程式碼中不重複,除非它們標記的東西出現。我使用的那些指令序列名為APICALL_BEGINAPICALL_ENDAPICALL_STORE

APICALL_BEGIN只是PUSH EAX/PUSH ECX/PUSH EDX。它會被收縮器發現,因為這個結構的每條指令和其他指令一樣都能被展開。收縮器會檢測到這個指令系列,並且把它們改成偽操作碼F4,這是一個標記,指示展開器編碼PUSH EAX/PUSH ECX/PUSH EDX。暫存器必須總是EAXECXEDX。這樣,我們確保在API呼叫前儲存這些暫存器內容。

同樣地,APICALL_ENDPOP EDX/POP ECX/POP EAX。這也被收縮器翻譯成一個偽操作碼F5,作為標記指示展開器編碼POP EDX/POP ECX/POP EAX。這標誌一個API呼叫結束後恢復儲存的暫存器內容。因此,對這些暫存器來說,就好象從來沒有執行過API呼叫一樣。

APICALL_STORE是另一個偽結構。收縮器會檢測它,在API呼叫之後馬上取得一個MOV [Mem],EAX。這樣做是為了避免轉換EAX,所以這條指令將總是編碼成MOV [Mem],EAX,這樣就不用管暫存器EAX必須被轉換成什麼。記憶體地址是儲存返回數值的變數,可以在處理APICALL_END時取出。


用下一個例子舉例說明該技術的使用:

  PUSH EAX

  PUSH ECX

  PUSH EDX            -------------------> APICALL_BEGIN

  MOV EAX,[EBP+AddressOfNewDirectory]

  PUSH EAX

  CALL DWORD PTR [EBP+RVA_SetCurrentDirectoryA]

  MOV [EBP+ReturnValue],EAX -------------> APICALL_STORE [ReturnValue]

  POP EDX

  POP ECX

  POP EAX             -------------------> APICALL_END

  MOV EAX,[EBP+ReturnValue]

  CMP EAX,EDX          ; Get the return value and check it

  JZ @X

  ...

現在改變暫存器:

  PUSH EAX

  PUSH ECX

  PUSH EDX            -------------------> APICALL_BEGIN

  MOV ESI,[EBX+AddressOfNewDirectory]

  PUSH ESI

  CALL DWORD PTR [EBX+SetCurrentDirectoryA]

  MOV [EBX+ReturnValue],EAX   -----------> APICALL_STORE [ReturnValue]

  POP EDX

  POP ECX

  POP EAX             -------------------> APICALL_END

  MOV ESI,[EBX+ReturnValue]

  CMP ESI,ECX          ; Get the return value and check it

  JZ @X

  ...

出於明顯的原因,你絕對不能使用EAXECXEDX作為病毒重新編碼時的增量暫存器,因為如果你用了,那麼API函式呼叫將會覆蓋它,然後返回值也將不知被寫入什麼地方,因此有99%的可能丟擲一個異常。在此前提下(增量不是用EAXECXEDX)我們可以編碼展開CALL DWORD PTR [Mem],我們打算用MOV Reg,Mem/CALL Reg:我們使用EAXECXEDX完美代替Reg,不儲存任何東西,因為暫存器數值將被API呼叫銷燬。


至於Linux(對於想在這個系統下變形的那些)API呼叫就更復雜了,因為我們在暫存器(EAXEBXECX)中傳遞引數。好,我們可以這樣定義一個結構:

  MOV [EBP+Parameter1], XXX

  MOV [EBP+Parameter2], YYY

  MOV [EBP+Parameter3], ZZZ

  PUSH EAX

  PUSH EBX

  PUSH ECX

  PUSH EDX

  PUSH EBP       --------------> LINUX_SYSCALL_BEGIN DeltaReg

  MOV EAX,[EBP+Parameter1]

  MOV EBX,[EBP+Parameter2]

  MOV ECX,[EBP+Parameter3]

  MOV EDX,[EBP+Parameter4]  ---> LINUX_SYSCALL_LOADPARAM

  INT 80h

  POP EBP

  MOV [EBP+ReturnValue],EAX  --> LINUX_SYSCALL_STORE DeltaReg,[ReturnValue]

  POP EDX

  POP ECX

  POP EBX

  POP EAX        --------------> LINUX_SYSCALL_END

它更大,但我不是上帝J

有個東西雖然不是一個API呼叫,但也在這裡用上了:SET_WEIGHT偽指令。這條指令對應下列程式碼結構:

  PUSH Reg1

  MOV Reg1,WEIGHT_IDENT

  MOV Reg2,xxyyzztt

  MOV [Mem],Reg2

  POP Reg1

這被壓縮成SET_WEIGHT [Mem],IDENT,Reg1,Reg2。這條指令被用於把其中資料從一代傳送到另一代。我們只能用它傳送這個,因為我們在全部重新彙編之前獲取這些數值,所以我們不能用它取代棧傳遞引數。

其中weights代表一個小遺傳演算法的成分,讓病毒成為“適應的”,因為“自然選擇”將使倖存者做出最好的選擇(感染的型別、解密器的結構、感染率,等等)以能生存。演算法不是非常先進,但是確實有效。

c)        記憶體

是的,變形作用確實附帶產生大量記憶體需求,用來儲存表、反彙編、臨時改變的程式碼、執行路線標記、區域性變數,以及很多等等東西。使用ESP是一個十分遺憾的技巧,對於儲存不太大的東西可能有用,但是當你認識到需要大約34Mb來做一份象樣的工作(只要看看z0mbieMistfall:預留了32Mb),情況就大大地不同了。唯一的解決辦法是使用VirtualAlloc而且預留你需要的數量。

因為我們預留了記憶體,複製程式碼到記憶體再無阻礙,如同動態地生成程式碼一樣。而且,(如果你在複製時解密),在反病毒模擬器看來它是複製,而不是解密。因此,我們必須編碼一個多型引擎至少生成一個“複製器”(僅當你不想加密程式碼時)

複製到記憶體可以讓我們做得更好:我們可以把程式碼複製到該記憶體框架內的任何地方,而且把記憶體變數放在我們希望的偏移地址。因為我們使用一個增量(Delta)暫存器,所以我們不需要關心程式碼在哪裡!它的問題是取得我們的內部偏移量(另外只要關注透過作業系統動態取得的記憶體塊首地址)。我們可以在重新彙編時重新編碼所有緩衝區和表的地址,於是甚至連最終程式碼的“外形”都是不同的!這裡的問題是我們必須提供程式碼被存放的地址,在預留的虛擬分配的記憶體中資料分割槽,等等,我們必須從唯一來源(解密器/複製器,但我們又可以隨心所欲地變化)提供這些,例如我們製造一些數值入棧然後在引擎中一次把它們全部彈出棧(看看MetaPHOR原始碼)

4)      未來

這部分是可選的。表達一些想法--我正在考慮提高病毒MetaPHOR的功能性,對你的實現也許有用。

a)        外掛

有一天我肯定要在我的病毒中實現(我希望!)。我瞭解這類變形引擎的外掛是很容易實現的(雖然“容易”不意味著“編碼又少又快”)。外掛能用“程式碼標記”實現,同樣的方法我們用來表示API呼叫。我們的做法是製作一個資訊頭(它使用的變數等等)和一段用偽組合語言寫的程式(我們的組合語言)。一個程式碼標記,表示一個外掛,可以是“PUSH Reg/MOV Reg,12345678/POP Reg(這是一段無用程式碼,但我們不讓收縮器消除它)。事實上,透過那個暫存器傳遞的數字是一個外掛ID號和一個版本號,(使用高字和低字),這使我們可以比較當前安裝的外掛版本和一個新外掛的版本,並且決定新外掛是否必須代替舊的或者被加入執行程式碼。標記會被重複兩次(一次用於開始,一次用於結束),後一次其中數值的高位設定為1,所以我們能用新的外掛代替當前外掛。收縮器必須檢測這個結構,而且設定兩個新指令,名為“PLUGIN_BEGIN Version”和“PLUGIN_END Version”。當收縮器做完它的任務,緊接在置換器之後,外掛注入器馬上開始工作:它搜尋新外掛,無論何處,解密它們並且檢查它們的簽名(Vecna在他的Hybris中所做的)。如果你使用公-私鑰簽名系統,外掛不需要被加密,也幾乎不可能修改或放入一個不屬於你的外掛。

依據外掛的型別和版本,我們能直接替換外掛入口(如果型別一致而且版本更高),覆蓋對前一外掛的呼叫。這是一個3D-編碼技術[注:原文3D-coding,請參考前面的3D instructions],我在文章開始不久提到過:舊外掛的程式碼保留著,而且它被彙編成新病毒的組成部分,但是在下一代,病毒被反彙編時,反彙編器到不了那部分,所以舊外掛被消除(同時,程式碼被混淆並與絕對無用的程式碼混合一起J

整個病毒全部能用外掛組成,或者至少在所有程式中,甚至在外掛裝入器中,可以有外掛ID號。既然那樣,如果有一天我支援Linux(我想在外掛完成前就能J,只須編碼外掛並把它放到相應地方。

而且,我們應該必須有一個欄位指明我們想放一個新外掛的地方(只是以防萬一我們想在置換器之後展開器之前馬上插入一個外掛搞點新花樣,或者只是我們忘記編碼一個函式,現在我們想放入它)。這個欄位就是外掛ID號,而且它是簡單的:僅僅是為原始病毒中的每個外掛做一個唯一標識號,前後兩個值分得很開,例如:反彙編器:10000001,收縮器:20000001,等等。(所有它們都有ID號和版本)。如果我們想把一個新函式放在反彙編器與收縮器之間,我們使用ID18000001,以此類推。

拭目以待吧,也許某一天我把它做出來J

b)        多平臺交叉感染

MetaPHOR 1.0版我沒有實現,因為要趕上#6發行就沒時間做了。但它是很容易的(事實上,我們已經在BennyWinux中看到其原理的實證)。我們只是必須根據執行它的系統呼叫不同的API函式,並且準備一個函式用於把我們的已變異程式碼附加到一個PE檔案或是ELF檔案。API函式能以同樣的方式被使用(例如,我編碼一個叫MapFile的函式,它返回給我一個檔案映像,在Windows作業系統平臺上是使用CreateFileMappingMapViewOfFile,在Linux平臺上是使用相關的int 80h功能)

c)        針對不同處理器重新彙編

我不知道是否要在外掛之前編寫它(這種情況下我寧可編一個外掛也不重新寫一個病毒)

希望你已經認識到,偽組合語言是非常一般化的,所以它能被改編以適應我們想要的任何處理器!當然,僅當它們使用32位體系架構才行。64位體系架構要求改變內部偽彙編器所有的處理手段,但這不會很複雜(我只須擴充指令長度從16位元組到32位元組,讓我有足夠空間用於QWORDs和一些必被使用的新欄位)

我確實考慮過,並認為一定能做到,而且第一步可能是使用Alpha彙編器和Alpha處理器下的WinNT。為什麼?因為除了我使用的反彙編器和彙編器之外,我什麼都不必改變,因為感染方法和演算法將是完全相同的!(因為我們使用ring 3)。收縮器、置換器、展開器等等,都將保持原狀。

我們必須做的唯一一件事是,重新定義一些指令和這個彙編器下的展開,因為例如TEST指令不是所有的處理器都有(我想Itaniums中沒有它)

真正要做的第一件事是,編碼一個新的反彙編器(但保留當前的)。所以我們只須知道,我們的程式碼是Alpha還是x86,以便呼叫相應的程式。此外,因為我們只使用我們的內部組合語言,所以到達再彙編器之前都是處在共同部分(演算法方面,而非編碼方面),可以根據PE頭中指明的目標處理器選擇再彙編器(用於Alpha的還是用於x86)。我們能使用的處理器型別很多(這就意味著大量的工作!)x86AlphaItanium680x0PowerPCPA-RISC,等等。(所有能執行WinNTUnix/Linux、甚至MacOS的平臺)

最主要的是,程式碼完全被改變了:它不是象Mr. SandmanEsperanto那樣,一個病毒由幾個部分組成、每部分用於一種處理器(Sandy致敬J,他的病毒是另一類程式碼)。整個病毒將重新彙編為新處理器的組合語言,所以一個反病毒軟體想為x86處理器攔截病毒,必須處理可能的到Alpha彙編器的轉化,而另一方面,對於Alpha處理器又要轉化到x86,完全跳過反病毒軟體(例如,在一個被傳染的伺服器上)。因為它是變形的,他們不能用字串檢測它。咂咂!J

5)      結論

變形:古往今來最強的病毒技術。

還要補充嗎?
 

相關文章