Archive for the ‘Tìm hiểu PE file qua các ví dụ cơ bản’ Category


1. PE File Format Offsets

Như các bạn biết, có rất rất nhiều tài liệu viết về định dạng của PE file, các tài liệu này cung cấp các thông tin giải thích chi tiết về cấu trúc cũng như ý nghĩa của từng giá trị trong PE file. Vào thời kỳ đỉnh cao của phong độ, tôi đã dành thời gian dịch một mạch tài liệu “PORTABLE EXECUTABLE FILE FORMAT” của tác giả Goppit đăng tải trên diễn đàn ARTeam. Tài liệu dài 76 trang, không quá dày nhưng cũng là thành quả mà tôi đã dành thời gian và tâm huyết để dịch, sau này thấy cũng có một số bạn trẻ lấy làm “Reference” trong phần tài liệu tham khảo khi làm đồ án kết thúc đời sinh viên :). Giờ nhiều khi đọc lại tài liệu thấy có nhiều chỗ ngây ngô, đúng là cái thủa ban đầu ngơ ngác ấy.

Tài liệu thì nhiều, nhưng đôi khi chúng ta chỉ muốn biết offset của một giá trị đặc biệt trong file, do đó phần đầu tiên của bài viết này tôi xin liệt kê lại các offsets của dữ liệu. Xin nhắc lại thông tin chi tiết về các cấu trúc các bạn có thể xem tài liệu dịch của tôi hoặc các nguồn tài liệu khác trên Internet.

DOS MZ Header:

+00
WORD
e_magic
Magic Number MZ ($5A4D)
+02
WORD
e_cblp
Bytes on last page of file
+04
WORD
e_cp
Pages in file
+06 WORD e_crlc Relocations
+08 WORD e_cparhdr Size of header in paragraphs
+0A  (10) WORD e_minalloc Minimum extra paragraphs needed
+0C  (12) WORD e_maxalloc Maximum extra paragraphs needed
+0E  (14) WORD e_ss Initial (relative) SS value
+10  (16) WORD e_sp Initial SP value
+12  (18) WORD e_csum Checksum
+14  (20) WORD e_ip Initial IP value
+16  (22) WORD e_cs Initial (relative) CS value
+18  (24) WORD e_lfarlc File address of relocation table
+1A  (26) WORD e_ovno Overlay number
+1C  (28) Array[4] of WORD e_res Reserved words
+24  (36) WORD e_oemid OEM identifier (for e_oeminfo)
+26  (28) WORD e_oeminfo OEM information; e_oemid specific
+28  (40) Array[10] of WORD e_res2 Reserved words
+3C  (60) DWORD e_lfanew File address of new exe header

PE Header:

+00
DWORD
Signature ($00004550)
+04 WORD Machine
+06 WORD Number of Sections
+08 DWORD TimeDateStamp
+0C  (12) DWORD PointerToSymbolTable
+10  (16) DWORD NumberOfSymbols
+14  (20) WORD SizeOfOptionalHeader
+16  (22) WORD Characteristics

Optional Header:

  – standard fields-  
+18  (24)
WORD
Magic
+1A  (26) BYTE MajorLinkerVersion
+1B  (27) BYTE MinorLinkerVersion
+1C  (28) DWORD SizeOfCode
+20  (32) DWORD SizeOfInitializedData
+24  (36) DWORD SizeOfUnitializedData
+28  (40) DWORD AddressOfEntryPoint
+2C  (44) DWORD BaseOfCode
+30  (48) DWORD BaseOfData
  -NT additional fields-  
+34  (52) DWORD ImageBase
+38  (56) DWORD SectionAlignment
+3C (60) DWORD FileAlignment
+40  (64) WORD MajorOperatingSystemVersion
+42  (66) WORD MinorOperatingSystemVersion
+44  (68) WORD MajorImageVersion
+46  (70) WORD MinorImageVersion
+48  (72) WORD MajorSubsystemVersion
+4A  (74) WORD MinorSubsystemVersion
+4C  (76) DWORD Reserved1
+50  (80) DWORD SizeOfImage
+54  (84) DWORD SizeOfHeaders
+58  (88) DWORD CheckSum
+5C  (92) WORD Subsystem
+5E  (94) WORD DllCharacteristics
+60  (96) DWORD SizeOfStackReserve
+64  (100) DWORD SizeOfStackCommit
+68  (104) DWORD SizeOFHeapReserve
+6C  (108) DWORD SizeOfHeapCommit
+70  (112) DWORD LoaderFlags
+74  (116) DWORD NumberOfRvaAndSizes
+78  (120) DWORD ExportDirectory VA
+7C  (124) DWORD ExportDirectory Size
+80  (128) DWORD ImportDirectory VA
+84  (132) DWORD ImportDirectory Size
+88  (136) DWORD ResourceDirectory VA
+8C  (140) DWORD ResourceDirectory Size
+90  (144) DWORD ExceptionDirectory VA
+94  (148) DWORD ExceptionDirectory Size
+98  (152) DWORD SecurityDirectory VA
+9C  (156) DWORD SecurityDirectory Size
+A0  (160) DWORD BaseRelocationTable VA
+A4  (164) DWORD BaseRelocationTable Size
+A8  (168) DWORD DebugDirectory VA
+AC  (172) DWORD DebugDirectory Size
+B0  (176) DWORD ArchitectureSpecificData VA
+B4  (180) DWORD ArchitectureSpecificData Size
+B8  (184) DWORD RVAofGP VA
+BC  (188) DWORD RVAofGP Size
+C0  (192) DWORD TLSDirectory VA
+C4  (196) DWORD TLSDirectory Size
+C8  (200) DWORD LoadConfigurationDirectory VA
+CC  (204) DWORD LoadConfigurationDirectory Size
+D0  (208) DWORD BoundImportDirectoryinheaders VA
+D4  (212) DWORD BoundImportDirectoryinheaders Size
+D8  (216) DWORD ImportAddressTable VA
+DC  (220) DWORD ImportAddressTable Size
+E0  (224) DWORD DelayLoadImportDescriptors VA
+E4  (228) DWORD DelayLoadImportDescriptors Size
+E8  (232) DWORD COMRuntimedescriptor VA
+EC  (236) DWORD COMRuntimedescriptor Size
+F0  (240) DWORD 0
+F4  (244) DWORD 0

Section Header:
Section đầu tiên sẽ bắt đầu ngay sau Optional Header (+F8: tức là ta có thể tìm tới các section header bắt đầu tại offset F8 kể từ PE header). Section thứ hai sẽ bắt đầu liền sau Section thứ nhất và cứ tiếp tục cho đến Section cuối cùng. Số Section được định nghĩa tại giá trị NumberOfSections tại offset 06 kể từ PE header.

+0
Array[8] of BYTE
Name
+08 DWORD PhysicalAddress / Virtual Size
+0C DWORD VirtualAddress
+10  (16) DWORD SizeOfRawData
+14  (20) DWORD PointerToRawData
+18  (24) DWORD PointerToRelocations
+1C  (28) DWORD PointerToLineNumbers
+20  (32) WORD NumberOfRelocations
+22  (34) WORD NumberOfLineNumbers
+24  (36) DWORD Characteristics

Export Directory:

+0
DWORD
Characteristics
+04 DWORD TimeDateStamp
+08 WORD MajorVersion
+0A WORD MinorVersion
+0C DWORD Name
+10  (16) DWORD Base
+14  (20) DWORD NumberOfFunctions
+18  (24) DWORD NumberOfNames
+1C  (28) DWORD *AddressOfFunctions
+20  (32) DWORD *AddressOfNames
+24  (36) DWORD *AddressOfNameOrdinals

Import Directory:

+0
DWORD
OriginalFirstThunk
+04 DWORD TimeDateStamp
+08 DWORD ForwarderChain
+0C DWORD Name
+10 DWORD FirstThunk

2. Thêm một section mới vào PE file

Sau khi tổng hợp thông tin về các offset như trên, phần này tôi sẽ trình bày cách làm thế nào để thêm một section mới vào trong một PE file, đồng thời chỉnh sửa lại một số giá trị quan trọng tại PE Header để đảm bảo cho file vẫn hợp lệ và section mới sẽ được nạp vào bộ nhớ. Có thể nói, việc thêm được một section mới rất hữu ích và quan trọng khi chúng ta không đủ không gian để thêm một đoạn code của chúng ta ở một nơi nào đó trong PE file. Các bạn có thể tìm thấy nhiều công cụ có khả năng cho phép bạn thực hiện điều này, tuy nhiên việc thực hiện bằng tay luôn đem lại nhiều điều thú vị, nó giúp ta học và hiểu được nhiều hơn về định dạng PE file, đặc biệt trong trường hợp cụ thể này là về các sections. Các công cụ và kiến thức cần thiết để chúng ta có thể thực hiện được công việc này bao gồm:

  • Một PE file để thực hành: trong bài này tôi sử dụng target nhỏ là AddSection.exe.
  • Công cụ Hex editor: có thể sử dụng bất kỳ chương trình nào mà bạn thông thạo. Tôi sử dụng Hex Workshop
  • Công cụ hỗ trợ xem các sections table: ví dụ LordPE, wARK v.v.., trong bài tôi dùng PEditor cho nó đang dạng hóa.
  • Kiến thức về PE Header, Section Header.

Quá trình thực hiện thêm một section vào PE file như sau:

  • Bổ sung thêm các bytes cho section mới:

Việc bổ sung các bytes này chính là xác định kích thước cho section mới sẽ được thêm vào PE File. Đầu tiên, ta sẽ xem thông tin về các section hiện có của AddSection.exe. Load file vào trong PEditor:

Như hình, ta thấy file AddSection.exe hiện đang có 3 sections cùng với các thông tin liên quan tới từng section này. Tiếp theo ta dùng Hex Workshop để mở file AddSection.exe. Giả dụ, tôi cần section mới có độ dài 110h bytes thì phải làm thế nào. Rất dễ dàng, cuộn chuột xuống cuối cùng của file ta sẽ thấy có rất nhiều vùng 00 xuất hiện bắt đầu từ offset AE0h. Để có được 110h bytes, tôi sẽ lựa chọn các bytes từ offset AE0h cho đến khi tại Status Bar thông tin Sel hiển thị 000110h thì dừng, lúc đó điểm kết thúc là offset BEFh:


Sau khi chọn xong, nhấn chuột phải và chọn Copy. Sau khi Copy xong, nhấn chuột vào phần cuối của file, chuột phải và chọn Paste:

Nhấn Yes để chấp nhận, kết quả có được như sau:

Như vậy chúng ta đã có thông tin cho section mới, nó sẽ được bắt đầu tại offset C00h và kết thúc tại offset D0Fh. Độ dài của nó (hay kích thước) là 110h bytes.

  • Sửa PE Header:

Bước tiếp theo ta sẽ phải sửa thông tin tại PE Header. Có 3 thứ ta cần phải chỉnh tại PE Header bao gồm:

  • Tăng số sections (tại offset 06 bắt đầu từ PE Header).
  • Tăng Image Size.
  • Thêm section mới vào section table.

Như chúng ta thấy, dấu hiệu PE (Signature) bắt đầu tại offset B0. Thông tin Number of Sections bắt đầu tại offset 06 tính từ PE Header, tức là tại B0h + 06h = B6h. Và tại offset B6 ta sẽ thấy giá trị sau: 03 00 (đảo ngược lại trật tự các bytes ta sẽ có 00 03), tương ứng với số lượng section ban đầu của file là 3. Như vậy, để bổ sung section mới ta phải tăng giá trị này thêm 1, tức là thành 04 00, như hình minh họa dưới đây:

Bước tiếp theo ta sẽ tăng image size. Quay trở lại PEditor, ta thấy trường Section Alignment có giá trị 1000h và trường Image Size là 4000h.

Vì Section Alginment là 1000h nên section mới của chúng ta ít nhất cũng phải có độ dài 1000h. Do đó, chúng ta sẽ cộng thêm 1000h vào Image Size, tức là 4000h + 1000h = 5000h. Theo thông tin về offset ở trên thì Image Size nằm tại offset 50h trong PE Header, mà PE Header bắt đầu tại B0 nên Image Size sẽ nằm tại B0h + 50h = 100h. Ta chuyển tới offset 100h và sửa các bytes 0040 thành 0050, tương tự như hình:


Vậy là xong bước tăng Image Size, bước cuối chúng ta phải thêm section mới vào trong Section table. Ta cũng biết Section table bắt đầu tại offset F8h trong PE Header. Và một section sẽ có độ dài 28h bytes, vậy section mới của chúng ta sẽ có thông tin như sau:

+0 Array[8] of BYTE Name; Tên của Section là .REA --> 2E 52 45 41 00 00 00 00
+08 DWORD PhysicalAddress / Virtual Size; Virtual size là 110h-> 10 01 00 00
+0C DWORD VirtualAddress; Bắt đầu tại 4000 do .data là 3000 -> 00 40 00 00
+10 (16) DWORD SizeOfRawData; là 110h-> 10 01 00 00
+14 (20) DWORD PointerToRawData; Section mới bắt đầu tại C00 -> 00 0C 00 00
+18 (24) DWORD PointerToRelocations; -> 00 00 00 00
+1C (28) DWORD PointerToLineNumbers; -> 00 00 00 00
+20 (32) WORD NumberOfRelocations; -> 00 00
+22 (34) WORD NumberOfLineNumbers; -> 00 00
+24 (36) DWORD Characteristics; -> C0000040 (tương tự .data section) -> 40 00 00 C0

Với thông tin có được như trên, ta sẽ thêm các dự liệu mới này đằng sau section cuối cùng (là .data) vào trong section table, địa chỉ offset nơi ta bắt đầu thêm sẽ là B0h + F8h + 3*28h = 220h. Thực hiện chỉnh sửa ta sẽ có được như hình minh họa dưới đây:

Sau khi chỉnh sửa xong, lưu lại file đã chỉnh sửa và kiểm tra lại kết quả bằng PEditor:

Như vậy là xong, mọi việc có vẻ không quá khó 🙂 .

3. Tìm hiểu RVAs và Import Tables

Ở phần trên, ta đã tìm hiểu về Section Table cũng như cách thêm một Section vào PE file. Phần này, ta sẽ tìm hiểu về Import Table. Trong Import Table sẽ lưu trữ các hàm từ các DLLs được sử dụng bởi chương trình. Phần này khá thú vị và sẽ phức tạp hơn phần Section Table bởi vì chúng ta sẽ phải làm quen và sử dụng tới RVA. Công cụ sử dụng vẫn chủ yếu là một trình Hex Editor (tôi dùng Hex Workshop như phần trước). Tôi sẽ mô tả qua về Import Table và sau đó sẽ có phần thực hành nhỏ để trực quan.

  • RVA

RVA, tên tiếng anh đầy đủ là Relative Virtual Offset, dịch tiếng Việt nôm na là địa chỉ ảo tương đối. RVA được sử dụng để mô tả một memory offset nếu không biết địa chỉ base address, vì vậy nó không giống như là một file offset. Nếu như bạn biết thông tin về Section Table thì sẽ dễ dàng để tính được một file offset từ một địa chỉ RVA. Để dễ hiểu tôi mở file ví dụ là Utility.exe trong PEditor và tìm thông tin về Section table:

Đầu tiên chúng ta sẽ phải tìm hiểu section nào chứa thông tin RVA, từ đó ta sẽ tính toán ra file offset theo công thức sau:

File Offset := RVA - Virtual Offset + Raw Offset

Lấy ví dụ như sau: Giả sử chúng ta có một RVA là 0x11A0h. Theo quan sát thì ta có thể thấy RVA này nằm trong section .text (vì  1000h < 0x11A0h < 2000h). Raw Offset của section .text là 0x400h. Vì vậy theo công thức trên thì File Offset là 0x11A0 – 0x1000 + 0x400 = 0x5A0.

Ví dụ khác, giả sử RVA là 0x30D2h. Vậy RVA sẽ nằm trong section .data. Do đó, File Offset là 0x30D2 – 0x3000 + 0xA00 = 0xAD2.

Để đỡ phải mất công tính toán bằng tay, ta có thể sử dụng các công cụ hỗ trợ tính toán RVA. Bản thân các trình PE Editor như LordPE hay PEditor cũng có. Ví dụ, đối với PEditor:

  • Import Table:

Như đã biết, tại Offset +0x80 sau PE Signature là một RVA tới Import Directory. Import Directory là một mảng của cấu trúc còn được gọi là IMAGE_IMPORT_DESCRIPTORs. Mỗi một DLL sẽ có một IMAGE_IMPORT_DESCRIPTOR tương ứng được sử dụng bởi PE file. Một cấu trúc IMAGE_IMPORT_DESCRIPTOR sẽ bao gồm:

+0

DWORD

OriginalFirstThunk

+04

DWORD

TimeDateStamp

+08

DWORD

ForwarderChain

+0C

DWORD

Name

+10

DWORD

FirstThunk

  • OriginalFirstThunk: là một RVA trỏ tới một mảng của cấu trúc IMAGE_THUNK_DATAs. Đây cũng là những RVAs, mỗi cấu trúc tương ứng với một imported function. Mảng này không bao giờ thay đổi. Chú ý: Có một số trình linker thiết lập OriginalFirstThunk là các giá trị 0, do vậy ta sẽ sử dụng FirstThunk.
  • TimeDateStamp và ForwarderChain: Chúng ta sẽ không quan tâm đến hai giá trị này.
  • Name: là một RVA trỏ tới tên của DLL.
  • FirstThunk: là một RVA trỏ tới một mảng của cấu trúc IMAGE_THUNK_DATAs. Có thể gọi là một bản sao của mảng đầu tiên. Mảng này sẽ thay đổi!

Chi tiết các thông tin mô tả các bạn có thể xem lại tài liệu mà tôi đã dịch hoặc các nguồn tài liệu khác. Giờ quay trở lại với ví dụ của chúng ta, sử dụng Hex Workshop để mở file Utility.exe. Quan sát sẽ thấy PE signature bắt đầu tại offset 0xB0h. Tìm tới Import Table bằng cách cộng thêm 0x80h ta có 0xB0h + 0x80 = 0x130h, như vậy tại offset 0x130h là một RVA trỏ tới Import Directory:

Như thấy trên hình, ImportDirectory VA có giá trị 0x44200000, đảo ngược lại ta có 0x00002044. Với RVA 0x2044 ta tính ra file offset là 0x844.

Tại Hex Workshop ta tìm tới offset này:

Như vậy, Import Table của chúng ta bắt đầu tài offset 0x0844. Như đã biết, một IMAGE_IMPORT_DESCRIPTOR có độ dài 0x14 bytes (tức là 20 bytes). Do vậy, IMAGE_IMPORT_DESCRIPTOR đầu tiên sẽ bắt đầu từ 0x844 cho tới 0x858 và IMAGE_IMPORT_DESCRIPTOR thứ hai sẽ bắt đầu từ 0x858 cho tới 0x86B. Tới lúc nào bạn thấy có 0x14 bytes có giá trị 0x00 thì đó chính là kết thúc của mảng IMAGE_IMPORT_DESCRIPTORs. Với thông tin như vây, ta biết rằng file của chúng ta import các hàm từ 2 file DLL khác nhau.

Ta hãy xem mảng đầu tiên. Giá trị RVA tới tên của file DLL là tại offset 0x844 + 0x0C = 0x850. Tại offset 0x850 ta có được giá trị 0x0000218E, tính ra file offset sẽ là 0x98E.

Với thông tin trong Hex WorkShop cung cấp, ta có được DLL đầu tiên là USER32.DLL. Giá trị RVA của OriginalFirstThunk là tại offset 0x844, nó có giá trị là 0x2090 -> file offset là 0x890.

Như trên hình, ở đây chúng ta có tới 12 RVA giữa offset 0x890 và 0x8C0 (điều này có nghĩa là file của chúng ta sử dụng 12 hàm của USER32.DLL). Giá RVA tại offset 0x8C0 là 0x0, điều này có nghĩa là đây là kết thúc của mảng. Địa chỉ RVA đầu tiên là 0x2118 -> file offset là 0x918. Chuyển tới offset 0x918 ta có:

Như trên hình, giá trị Hint là 0x019B và tên của hàm là LoadIconA. Đó chính là hàm đầu tiên được chương trình import từ user32.dll. Địa chỉ RVA thứ hai là tại offset 0x894, có giá trị là 0x2124 -> file offset là 0x924. Theo trên hình thì Hint có giá trị 0x01DD và tên của hàm là PostQuitMessage. Cứ tương tự như vậy ta sẽ có được thông tin của 10 hàm còn lại được chương trình Import từ user32.dll.

Thực hiện tương tự như đã làm với DLL tiếp theo.

Giá trị Name sẽ nằm tại offset 0x844 + 0x14 + 0x0C = 0x864. Địa chỉ RVA tại đây là 0x21CE -> file offset có giá trị 0x9CE. Trong Hex Workshop ta sẽ có được DLL thứ hai là KERNEL32.DLL.

Giá trị OriginalFirstThunk kernel32.dll là tại offset 0x844 + 0x14 = 0x858. Giá trị RVA tại đây là 0x2080 -> file offset sẽ là 0x880.

Như trên hình, có 3 RVAs nghĩa là có 3 hàm được sử dụng từ kernel32.dll. Làm tương tự như đã làm với user32.dll ta sẽ có được tên của các hàm:

Tóm tắt các bước thực hiện:

  1. Tìm tới offset 0x80 trong Optional Header để lấy thông tin về địa chỉ RVA của Import Directory.
  2. Tại đó tìm ra mảng của IMAGE_IMPORT_DESCRIPTORs, mỗi mảng có độ dài là 0x14 bytes. Dấu hiệu kết thúc của mảng là 0x14 bytes giá trị 0x00. Số mảng IMAGE_IMPORT_DESCRIPTORs tương đương với số dll được import.
  3. Tại ví trí 0x0C của mỗi mảng ta sẽ có được thông tin RVA, từ địa chỉ RVA này trỏ tới tên của dll đã được import.
  4. Tại vị trí bắt đầu của mỗi mảng IMAGE_IMPORT_DESCRIPTOR, ta có được thông tin về OriginalFirstThunk RVA. Nếu giá trị này là 0x00, ta sẽ sử dụng giá trị FirstThunk RVA tại offset 0x10 trong mảng để thay thế.
  5. Tìm tới đó ta sẽ có được thông tin về mảng các DWORDS, mỗi giá trị trong mảng này sẽ trỏ tới giá trị Hint (WORD) và tên của hàm đã được import. Dấu hiệu kết thúc của mảng này là 8 giá trị 0x00.

Nguồn tham khảo:

Best Regards

m4n0w4r