Các kỹ thuật lưu trữ payload trong Sections mã độc
Kỹ thuật xây dựng Shellcode Windows x86 trên nền MASM
Thiết kế virus lây file trên Windows x86 MASM

Kỹ thuật xây dựng Shellcode Windows x86 trên nền MASM

Epilogue/Prologue và Calling Conventions

Epilogue và Prologue là 2 cơ chế khởi tạo khung ngăn xếp, và khôi phục khung ngăn xếp trong 1 hàm. Epilogue gọi khi bắt đầu 1 hàm, còn prologue được gọi khi kết thúc hàm đó.

Có nhiều cơ chế calling convention khác nhau, nhưng sử dụng phổ biến nhất là stdcall, stack sẽ tự động được khôi phục sau khi kết thúc hàm. Tham khảo các calling convention trên Windows tại link sau: x86 calling conventions – Wikipedia.

Đoạn mã sau là chương trình asm gọi hàm, sử dụng prologue, epilogue.

.386
.model flat
option casemap:none

.code
_main proc
   push ebp
   mov ebp, esp
   
   push 1
   push 2
   call calc

   mov esp, ebp
   pop ebp
   ret
_main endp

calc proc
   push ebp
   mov ebp, esp
   
   mov eax, [ebp + 8]  ; 1 
   mov edx, [ebp + 12] ; 2
   add eax, edx   

   mov esp, ebp
   pop ebp
   ret
calc endp

.data

end _main

Từ asm tới obj, exe trên Windows và các thành phần quan trọng

Sử dụng ml masm_name.asmlink objname.obj để compile file asm thành file obj và exe.

Dưới đây là mã asm 1 chương trình đơn giản, sử dụng các biến textCode và textData được khai báo ở 2 section là .code và .data.

.386	
.model flat
option casemap:none

.code
_main proc
	push ebp
	mov ebp, esp
	
	mov eax, offset textCode
	mov edx, offset textData
	
	jmp end_label
	call end_label
	call calc	
end_label:
	mov esp, ebp
	pop ebp
	ret	
_main endp
	textCode db "TextCode", 0ah

calc proc
	push ebp
	mov ebp, esp

	
	mov esp, ebp
	pop ebp
	ret
calc endp

.data
	textData db "TextData", 0ah
end _main

.data

Trong mã trên, có 3 thành phần quan trọng là biến, nhãn và hàm. Trong đó, các lệnh call, jmp sẽ nhảy tới label hoặc hàm dựa trên offset tính từ điểm gọi, còn lại sẽ dựa trên địa chỉ tuyệt đối.

Sử dụng dumpbin /all objname.obj để dump các thành phần của file obj.

Section code như dưới đây, 2 phần mã được điền 0 là của textCode và textData, lưu trữ offset cần thay thế ở trong bảng Relocations. Lưu ý bảng Reloc này là của Obj, khác với Reloc trong PE.

Dựa vào Symbol index và Offset, thêm bảng Symbol dưới đây, có thể link được địa chỉ cho các biến chính xác để tạo thành PE.

Sau khi thực hiện link xong, kết quả nhận được file exe. Có thể thấy vị trí của 2 biến ban đầu đã được thay thế bằng địa chỉ tuyệt đối.

Ngoài ra, PE kèm theo bằng Reloc cho riêng nó, gồm 2 biến trên, cần tính toán địa chỉ cần tính lại trong trường hợp Imagebase thay đổi. Hiện tại trong mã được biên dịch đang có imagebase là 0x400000.

Từ đây, có 1 vấn đề xảy ra trong việc xây dựng shellcode. Sau khi copy shellcode vào tiến trình/file đích, địa chỉ tuyệt đối của các biến chắc chắn sẽ không còn đúng nữa. Yêu cầu đặt ra là phải tính toán lại chính xác địa chỉ mới trong shellcode.

Kỹ thuật delta trong shellcode

Sử dụng đoạn code ở phần 2, biên dịch thành file exe và đẩy vào x32dbg. Vì imagebase khi load vào bị thay đổi, nên địa chỉ tương ứng cũng bị thay đổi.

Phương pháp đề ra là tính toán độ lệnh của imagebase, hay nói cách khác là độ lệch của địa chỉ tuyệt đối trước và sau, từ đó tính toán ra địa chỉ mới.

addr_newB = (addr_newA – addr_oldA) + addr_oldB

addr_old đều có thể lấy được sẵn trong code, vấn đề đặt ra là tìm addr_new. Việc này có thể thực hiện thông qua qua 2 phương pháp dưới đây.

  1. Sử dụng lệnh push 1 nhãn, ngay sau đó pop nhãn đó ra, từ đó có được địa chỉ hiện tại của nhãn đó.
  2. Sử dụng lệnh call 1 nhãn đặt sau lệnh call, lệnh này sẽ push địa chỉ của nhãn vào stack, sau đó thực hiện pop để lấy ra địa chỉ của nhãn này.

Giả sử pop địa chỉ nhãn vào eax, phần sau lệnh trên cần tính hiệu giữa địa chỉ mới của nhãn và cũ của nhãn.

sub eax, offset label

Như đã nói ở phần 2, offset label khi biên dịch sẽ được fix cứng địa chỉ tuyệt đối, vì vậy khi shellcode được sử dụng, eax sẽ là địa chỉ mới, offset label đã được fix cứng địa chỉ cũ, phép tính trên tạo ra delta, độ chênh lệch giữa 2 imagebase, hay nói cách khác là 2 biến.

Để tính toán địa chỉ mới cho 1 biến, chỉ cần cộng delta với địa chỉ cũ.

lea edx, [eax + offset value]

Như vậy có thể tính toán được địa chỉ mới của biến và edx. Dưới đây là mã asm đầy đủ mô tả kỹ thuật delta.

.386
.model flat
option casemap:none

.code
_main proc
	push start_code			; or call start_code
start_code:
	pop ebx
	sub ebx, start_code
	push ebp
	mov ebp, esp
	sub esp, 50h
	
	xor eax, eax
	mov [ebp - 04h], eax 		; create local variable

	lea edx, [ebx + offset msgCode] ; load address msgCode to edx

	mov esp, ebp	
	pop ebp
	ret
_main endp
	msgCode db "HelloCode", 0ah

.data
	mov eax, ebx
	msgData db "HelloData", 0ah

end  _main

PEB và cách truy cập WinAPI thông qua PEB

Giống như các biến, WinAPI cũng gặp vấn đề về việc truy cập địa chỉ để gọi hàm. Thông thường 1 chương trình asm muốn sử dụng WinAPI cần include các thư viện cần thiết, tuy nhiên shellcode chỉ được chứa các opcode mà không bao gồm các thư viện đi kèm.
Giải pháp đề ra là truy cập địa chỉ WinAPI thông qua PEB.

Có thể tham khảo cấu trúc đầy đủ tại link PEB – NtDoc, hoặc với phiên bản rút ngon hơn PEB (winternl.h) – Win32 apps | Microsoft Learn. Phần dưới sẽ sử dụng các cấu trúc của microsoft đưa ra, tuy nhiên để hiểu rõ bản chất nên sử dụng ntDoc.

Cấu trúc PEB và offset để tính toán LDR như sau.

typedef struct _PEB {
  BYTE                          Reserved1[2];    // +0x0
  BYTE                          BeingDebugged;   // +0x2
  BYTE                          Reserved2[1];    // +0x3
  PVOID                         Reserved3[2];    // +0x4
  PPEB_LDR_DATA                 Ldr;             // +0xc
  PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
  PVOID                         Reserved4[3];
  PVOID                         AtlThunkSListPtr;
  PVOID                         Reserved5;
  ULONG                         Reserved6;
  PVOID                         Reserved7;
  ULONG                         Reserved8;
  ULONG                         AtlThunkSListPtr32;
  PVOID                         Reserved9[45];
  BYTE                          Reserved10[96];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE                          Reserved11[128];
  PVOID                         Reserved12[1];
  ULONG                         SessionId;
} PEB, *PPEB;

Cấu trúc LDR như sau.

typedef struct _PEB_LDR_DATA {
    BYTE Reserved1[8];     // 0x0
    PVOID Reserved2[3];    // 0x8
    LIST_ENTRY InMemoryOrderModuleList;  //0x14
} PEB_LDR_DATA, *PPEB_LDR_DATA;

Cấu trúc của LIST_ENTRY

typedef struct _LIST_ENTRY {
   struct _LIST_ENTRY *Flink;   //0x0
   struct _LIST_ENTRY *Blink;   //0x4
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

Và nơi chứa thông tin về DLL được load vào process: LDR_DATA_TABLE_ENTRY

typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];                //0x0
    LIST_ENTRY InMemoryOrderLinks;     //0x8
    PVOID Reserved2[2];                //0x10
    PVOID DllBase;                     //0x18
    PVOID Reserved3[2];                //0x1c
    UNICODE_STRING FullDllName;        //0x24
    BYTE Reserved4[8];         
    PVOID Reserved5[3];
#pragma warning(push)
#pragma warning(disable: 4201) // we'll always use the Microsoft compiler
    union {
        ULONG CheckSum;
        PVOID Reserved6;
    } DUMMYUNIONNAME;
#pragma warning(pop)
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

Dưới đây là sơ đồ thể hiện sự liên kết giữa các struct trên, có bổ sung 1 số tưởng nhưng chỉ cần tập trung vào InMemoryOrderModuleList.

Windows sử dụng double-link list để liên kết các module.

Nhìn vào 2 sơ đồ trên, cần tính toán địa chỉ của LDR_DATA_TABLE_ENTRY. Thông qua LDR có thể tính toán được địa chỉ LIST_ENTRY, từ đó tính ra LDR_DATA_TABLE_ENTRY.

Để lấy địa chỉ PEB trên masm, sử dụng câu lệnh sau.

assume fs:nothing
mov esi, [fs:30h]	;esi: address of PEB
assume fs:error

Tham khảo hàm lấy base dll sau.

GetModuleBase proc      ; arg1: dll name; return base dll
	push ebp
	mov ebp, esp
	sub esp, 50h

	mov edi, [ebp + 8]

	assume fs:nothing
	mov eax, fs:[30h]		;Get PEB
	assume fs:error

	mov eax, [eax + 0ch]		;*LDR
	mov esi, [eax + 14h]		;LIST_ENTRY InMemoryOrderModuleList
	xor ecx, ecx
LoopGetModuleBase:
	mov esi, [esi]		;pointer to flink
	mov ecx, esi		
	sub ecx, 8h			;pointer to _LDR_DATA_TABLE_ENTRY 
	add ecx, 24h		;pointer to UNICODE_STRING FullDllName; 
	mov ecx, [ecx + 4h]	;pointer to PWSTR  Buffer;	

	push ecx
	push edi
	call CompareUnicodeString
	add esp, 8

	test eax, eax
	jz LoopGetModuleBase
	mov eax, [esi - 8h + 18h]	;Pointer to PVOID DllBase;
	mov esp, ebp
	pop ebp
	ret
GetModuleBase endp

Khi đã có base dll, cần truy cập bảng export của PE để lấy địa chỉ API cần thiết. Tham khảo PE format tại PE Format – Win32 apps | Microsoft Learn.

Vì dll đã được load lên mem, nên thay vì đọc trong bảng export của PE, truy cập thẳng vào Data Directory của Export để lấy VA. Data Directories có cấu trúc như sau.

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

Export Directory có index là 1 trong mảng này, chỉ cần truy cập, lấy VA cộng với base dll sẽ trỏ tới Export directory table.

Mã C mô tả quá trình truy cập tương tự như x64 sau.

Cấu trúc mảng IMAGE_EXPORT_DIRECTORY

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD Characteristics;   //0x0
    DWORD TimeDateStamp;     //0x4
    WORD  MajorVersion;      //0x8
    WORD  MinorVersion;
    DWORD Name;              //0xc    // RVA tên DLL
    DWORD Base;              //0x10
    DWORD NumberOfFunctions; //0x14
    DWORD NumberOfNames;     //0x18
    DWORD AddressOfFunctions;//0x1c    // RVA array function RVA
    DWORD AddressOfNames;    //0x20    // RVA array tên hàm
    DWORD AddressOfNameOrdinals; //0x24 // RVA array ordinal
} IMAGE_EXPORT_DIRECTORY;

Có 3 trường quan trọng cần quan tâm là 3 mảng RVA liên quan tới function RVA, tên hàm và oridinal. Cần truy cập và tìm đúng tên hàm cần, sau đó sử dụng index của hàm để tính ra function RVA tương ứng.

Names[i]
      ↓
Ordinals[i]
      ↓
Functions[ Ordinals[i] ]

Hàm lấy địa chỉ API bao gồm các bước chính.

  • Tìm VA tới _IMAGE_EXPORT_DIRECTORY
  • Tìm VA tới AddressOfFunctions, AddressOfNames, AddressOfNameOrdinals
  • Duyệt mảng AddressOfNames, so sánh với tên API cần tìm, sử dụng 1 biến tăng dần để tìm index mảng.
  • Duyệt mảng AddressOfNameOrdinals thông qua index tìm được ở trước để lấy ra oridinal.
  • Duyệt mảng AddressOfFunctions để lấy địa chỉ thông qua index ở oridinal.

Dưới đây là mã asm tham khảo hàm tìm kiếm địa chỉ.

GetFuncAddr proc	;arg1: base dll ;arg2: winapiname ;return address of api
	push ebp
	mov ebp, esp
	sub esp, 50h
	xor eax, eax

	mov [ebp - 4], eax		;base dll
	mov [ebp - 8], eax		;RVA to AddressOfFunctions
	mov [ebp - 12], eax		;RVA to AddressOfNameOridinal

	mov esi, [ebp + 8]		;base dll
	mov [ebp - 4], esi
	add esi, [esi + 3ch]	;pointer to nt header
	mov esi, [esi + 78h]
	mov eax, [ebp - 4]
	add esi, eax			;pointer to _IMAGE_EXPORT_DIRECTORY 
	lea eax, [esi + 20h]	;RVA to AddressOfNames array
	lea edx, [esi + 1ch]	;RVA to AddressOfFunctions
	mov [ebp - 8], edx
	lea edx, [esi + 24h]	;RVA to AddressOfNameOridinal
	mov [ebp - 12], edx	

	mov esi, [eax]			;RVA AddressOfNames[0]
	add esi, [ebp - 4]		;VA AddressOfNames[0]
	xor ecx, ecx

	mov edi, [ebp + 12]		;arg2 winapi name
LoopFindNameFunc:
	mov eax, [esi]
	add eax, [ebp - 4]
	
	push edi
	push eax
	call CompareString
	add esp, 8

	add esi, 4				;AddressOfNames[i + 1]
	inc ecx					;inc index
	test eax, eax			;Check result compare
	jz LoopFindNameFunc

	mov edx, [ebp - 12]		;RVA to AddressOfNameOridinal
	mov eax, [ebp - 4]		;base dll
	add eax, [edx]			;VA AddressOfNameOridinal

	lea edx, [ecx * 2]		;calc index of oridinals - word
	add eax, edx
	xor edx, edx
	mov dx, [eax]			;store oridinal to dx

	dec edx				;dec index
	lea edx, [edx * 4]	;calc offset (dword)
	mov eax, [ebp - 8]	;RVA address func
	mov eax, [eax]
	add eax, [ebp - 4]	;VA address func
	add eax, edx		;VA function find

	mov eax, [eax]		;RVA function find
	add eax, [ebp - 4]	;VA function

	mov esp, ebp
	pop ebp
	ret
GetFuncAddr endp

Dựa vào các kĩ thuật trên, có thể tải và gọi bất kì API thuộc thư viện nào để xây dựng shellcode với chức năng tùy chỉnh.

Dump shellcode từ exe và thử nghiệm inject

Lệnh build nhanh asm -> exe

ml main.asm

Lệnh dump section text

dumpbin /RAWDATA /SECTION:.text main.exe > raw.txt

Mã python tách riêng phần opcode ra file bin.

# dump_shellcode.py
from pathlib import Path

EXE_PATH = "main.exe"
OUT_PATH = "text.bin"

POINTER_TO_RAW = 0x400
SIZE_OF_RAW = 0x400

data = Path(EXE_PATH).read_bytes()
chunk = data[POINTER_TO_RAW:POINTER_TO_RAW + SIZE_OF_RAW]
Path(OUT_PATH).write_bytes(chunk)

print(f"[+] Wrote {len(chunk)} bytes to {OUT_PATH}")

Viết thử 1 chương trình Alloc -> WriteMem -> CreateThread để test shellcode. Kết quả nhận được MessageBox của shellcode.

Add a comment

Leave a Reply

Your email address will not be published. Required fields are marked *