網絡安全編程:PE編程實例之添加節區
添加節區在很多場合都會用到,比如在加殼中、在免殺中都會經常用到對PE文件添加一個節區。添加一個節區的方法有4步,第1步是在節表的最后面添加一個IMAGE_SECTI ON_HEADER,第2步是更新IMAGE_FILE_HEADER中的NumberOfSections字段,第3步是更新IMAGE_OPTIONAL_HEADER中的SizeOfImage字段,最后一步則是添加文件的數據。當然,前3步是沒有先后順序的,但是最后一步一定要明確如何改變。
某些情況下,在添加新的節區項以后會向新節區項的數據部分添加一些代碼,而這些代碼可能要求在程序執行之前就被執行,那么這時還需要更新IMAGE_OPTIONAL_HEADER中的AddressOfEntryPoint字段。
1. 手動添加一個節區
先來進行一次手動添加節區的操作,這個過程是個熟悉上述步驟的過程。網上有很多現成的添加節區的工具。這里自己編寫工具的目的是掌握和了解其實現方法,鍛煉編程能力;手動添加節區是為了鞏固所學的知識,熟悉添加節區的步驟。
使用C32Asm用十六進制編輯方式打開測試程序,并定位到其節表處,如圖1所示。
圖1 節表位置信息
從圖1中可以看到,該PE文件有3個節表。直接看十六進制信息可能很不方便,為了直觀方便地查看節表中IMAGE_SECTION_HEADER的信息,那么使用LordPE進行查看,如圖2所示。
圖2 使用LordPE查看該節表信息
用LordPE工具查看的確直觀多了。對照LordPE顯示的節表信息來添加一個節區。IMAGE_SECTION_HEADER結構體定義如下:
- typedef struct _IMAGE_SECTION_HEADER {
- BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
- union {
- DWORD PhysicalAddress;
- DWORD VirtualSize;
- } Misc;
- DWORD VirtualAddress;
- DWORD SizeOfRawData;
- DWORD PointerToRawData;
- DWORD PointerToRelocations;
- DWORD PointerToLinenumbers;
- WORD NumberOfRelocations;
- WORD NumberOfLinenumbers;
- DWORD Characteristics;
- } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
IMAGE_SECTION_HEADER 結構體的成員很多,但是真正要使用的只有 6 個,分別是Name、VirtualSize、VritualAddress、SizeOfRawData、PointerToRawData 和 Characteristics。這 6 項剛好與 LordPE 顯示的 6 項相同。其實 IMAGE_SECTION_HEADER 結構體中其余的成員幾乎不被使用。下面介紹如何添加這些內容。
IMAGE_SECTION_HEADER 的長度為 40 字節,是十六進制的 0x28,在 C32Asm 中占用 2 行半的內容,這里一次把這兩行半的內容手動添加進去。回到 C32Asm 中,在最后一個節表的位置處開始添加內容,首先把光標放到右邊的 ASCII 字符中,輸入“.test”,如圖3所示。
圖3 添加“.test”節名
接下來在00000240位置處添加節的大小,該大小直接是對齊后的大小即可。由于文件對齊是0x1000字節,也就是4096字節,那么采用最小值即可,使該值為0x1000。在C32Asm中添加時,正確的添加應當是“00 10 00 00”,以后添加時也要注意字節順序。在添加后面幾個成員時,不再提示注意字節順序,應時刻清楚這點。在添加該值時,應當將光標定位在十六進制編輯處,而不是剛才所在的ASCII字符處。順便要把VirutalAddress也添加上,VirtualAddress的值是前一個節區的起始位置加上上一個節對齊后的長度的值,上一個節區的起始位置為0x6000,上一個節區對齊后的長度為0x3000,因此新節區的起始位置為0x9000。添加VirtualSize和VirtualAddress后如圖4所示。
圖4 添加VirtualSize和VirtualAddress的值
接下來的兩個字段分別是SizeOfRawData和PointerToRawData,其添加方法類似前面兩個字段的添加方法,這里就不細說了。分別添加“0x9000”和“0x1000”兩個值,如圖5所示。
圖5 添加SizeOfRawData和PointerToRawData
PointerToRawData后面的12字節都可以為0,只要修改最后4字節的內容,也就是Characteristics的值即可。這個值直接使用上一個節區的值即可,實際添加時應根據所要節的屬性給值。這里為了省事而直接使用上一個節區的屬性,如圖6所示。
圖6 添加Characteristics屬性
整個節表需要添加的地方就添加完成了,接下來需要修改該PE文件的節區數量。當前節區數量是3,這里要修改為4。雖然可以通過LordPE等修改工具完成,但是這里仍然使用手動修改。對于修改的位置,請大家自行定位找到,修改如圖7所示。
圖7 修改節區個數為4
除了節區數量以外,還要修改文件映像的大小,也就是SizeOfImage的值。由于新添加了節區,那么應該把該節區的大小加上SizeOfImage的大小,即為新的SizeOfImage的大小。現在的SizeOfImage的大小為0x9000,加上新添加節區的大小為0xa000。SizeOfImage的位置請大家自行查找,修改如圖8所示。
圖8 修改SizeOfImage的值為0xa000
修改PE結構字段的內容都已經做完了,最后一步就是添加真實的數據。由于這個節區不使用,因此填充0值就可以了,文件的起始位置為0x9000,長度為0x1000。把光標移到文件的末尾,單擊“編輯”→“插入數據”命令,在“插入數據大小”文本框中輸入十進制的4096,也就是十六進制的0x1000,如圖9所示。
圖9 “插入數據”對話框的設置
單擊“確定”按鈕,可以看到在剛才的光標處插入了很多0值,這樣工作也完成了。單擊“保存”按鈕進行保存,提示是否備份,選擇“是”。然后用LordPE查看添加節區的情況,如圖10所示。
圖10 添加新的節區信息
對比前后兩個文件的大小,如圖11所示。
圖11 添加節區前后文件的大小
從圖11中可以看出,添加節區后的文件比原來的文件大了4KB,這是由于添加了4096字節的0值。也許大家最關心的不是大小問題,而是軟件添加了大小后是否真的可以運行。其實試運行一下,是可以運行的。
上面的整個過程就是手動添加一個新節區的全部過程,除了特有的幾個步驟以外,要注意新節區的內存起始位置和文件起始位置的值。相信通過上面手動添加節區,大家對此已經非常熟悉了。下面就開始通過編程來完成添加節區的任務。
在C32Asm軟件中可以快速定位PE結構的各個結構體和字段的位置,在菜單欄單擊“查看(V)”->“PE信息(P)”即可在C32Asm工作區的左側打開一個PE結構字段的解析面板,在面板上雙擊PE結構的每個字段則可在C32Asm工作區中定位到十六進制形式的PE結構字段的數據。
2. 通過編程添加節區
通過編程添加一個新的節區無非就是文件相關的操作,只是多了一個對PE文件的解析和操作而已。添加節區的步驟和手動添加節區的步驟是一樣的,只要一步一步按照上面的步驟寫代碼就可以了。在開始寫代碼前,首先修改FileCreate()函數中的部分代碼,具體如下:
- m_hMap = CreateFileMapping(m_hFile, NULL,
- PAGE_READWRITE /*| SEC_IMAGE*/,0, 0, 0);
- if ( m_hMap == NULL )
- {
- CloseHandle(m_hFile);
- return bRet;
- }
這里要把SEC_IMAGE宏注釋掉。因為要修改內存文件映射,有這個值會使添加節區失敗,因此要將其注釋掉或者直接刪除掉。
程序的界面如圖12所示。
圖12 添加節區界面
首先編寫“添加”按鈕響應事件,代碼如下:
- void CPeParseDlg::OnBtnAddSection()
- {
- // 在這里添加驅動程序
- // 節名
- char szSecName[8] = { 0 };
- // 節大小
- int nSecSize = 0;
- GetDlgItemText(IDC_EDIT_SECNAME, szSecName, 8);
- nSecSize = GetDlgItemInt(IDC_EDIT_SEC_SIZE, FALSE, TRUE);
- AddSec(szSecName, nSecSize);
- }
按鈕事件中最關鍵的地方是AddSec()函數。該函數有兩個參數,分別是添加節的名稱與添加節的大小。這個大小無論輸入多大,最后都會按照對齊方式進行向上對齊。看一下AddSec()函數的代碼,具體如下:
- VOID CPeParseDlg::AddSec(char *szSecName, int nSecSize)
- {
- int nSecNum = m_pNtHdr->FileHeader.NumberOfSections;
- DWORD dwFileAlignment = m_pNtHdr->OptionalHeader.FileAlignment;
- DWORD dwSecAlignment = m_pNtHdr->OptionalHeader.SectionAlignment;
- PIMAGE_SECTION_HEADER pTmpSec = m_pSecHdr + nSecNum;
- // 復制節名
- strncpy((char *)pTmpSec->Name, szSecName, 7);
- // 節的內存大小
- pTmpSec->Misc.VirtualSize = AlignSize(nSecSize, dwSecAlignment);
- // 節的內存起始位置
- pTmpSec->VirtualAddress=m_pSecHdr[nSecNum-1].VirtualAddress+AlignSize(m_pSecHdr [nSecNum - 1].Misc.VirtualSize, dwSecAlignment);
- // 節的文件大小
- pTmpSec->SizeOfRawData = AlignSize(nSecSize, dwFileAlignment);
- // 節的文件起始位置
- pTmpSec->PointerToRawData=m_pSecHdr[nSecNum-1].PointerToRawData+AlignSize(m_pSecHdr[nSecNum - 1].SizeOfRawData, dwSecAlignment);
- // 修正節數量
- m_pNtHdr->FileHeader.NumberOfSections ++;
- // 修正映像大小
- m_pNtHdr->OptionalHeader.SizeOfImage += pTmpSec->Misc.VirtualSize;
- FlushViewOfFile(m_lpBase, 0);
- // 添加節數據
- AddSecData(pTmpSec->SizeOfRawData);
- EnumSections();
- }
代碼中每一步都按照相應的步驟來完成,其中用到的兩個函數分別是 AlignSize()和AddSecData()。前者是用來進行對齊的,后者是用來在文件中添加實際的數據內容的。這兩個函數非常簡單,代碼如下:
- DWORD CPeParseDlg::AlignSize(int nSecSize, DWORD Alignment)
- {
- int nSize = nSecSize;
- if ( nSize % Alignment != 0 )
- {
- nSecSize = (nSize / Alignment + 1) * Alignment;
- }
- return nSecSize;
- }
- VOID CPeParseDlg::AddSecData(int nSecSize)
- {
- PBYTE pByte = NULL;
- pByte = (PBYTE)malloc(nSecSize);
- ZeroMemory(pByte, nSecSize);
- DWORD dwNum = 0;
- SetFilePointer(m_hFile, 0, 0, FILE_END);
- WriteFile(m_hFile, pByte, nSecSize, &dwNum, NULL);
- FlushFileBuffers(m_hFile);
- free(pByte);
- }
整個添加節區的代碼就完成了,仍然使用最開始的那個簡單程序進行測試,看是否可以添加一個節區,如圖13所示。
圖13 添加節區
從圖13中可以看出,添加節區是成功的。試著運行一下添加節區后的文件,可以正常運行,而且添加節區的文件比原文件大了4KB,和前面手動添加的效果是一樣的。