# Mở đầu Chào mừng các bạn đến với bài viết thứ 6 trong chuỗi bài viết về phát triển mã độc của mình! Trong bài viết này, chúng ta sẽ nói về file Portable Executable (PE), đây là định dạng file thực thi chủ yếu được sử dụng trong hệ điều hành Windows. Việc hiểu rõ về định dạng file PE đóng vai trò quan trọng trong việc nghiên cứu và phát triển mã độc. Yah, bắt đầu bài viết thôi! :face_with_cowboy_hat: <div style="text-align:center;"> <img src="https://steamuserimages-a.akamaihd.net/ugc/954101135156565426/21D9841F8E03ED30D91A7720388E1E8D3A464FC0/?imw=5000&imh=5000&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=false" alt=""> </div> # Portable Executable File Như các bạn đã biết, các file như Word, PowerPoint và Excel đều có định dạng riêng của nó. Định dạng này quy định cách thức dữ liệu được lưu trữ trong file. Thông thường, cấu trúc của một file được xác định bởi các phần đầu được gọi là **header**. Để lấy dữ liệu từ một file cụ thể, ta thường phải phân tích các giá trị trong header của file đó. Trong phần header của một file, thường sẽ chứa các giá trị được gọi là **magic byte**. Về cơ bản, magic byte là một dãy các giá trị byte, đóng vai trò như **signature** (chữ ký) để nhận diện loại file đó. Hình dưới là mô tả tổng quát về định dạng của một file. :alien: ![image](https://hackmd.io/_uploads/H1kyMUXcT.png) Để thực tế, mình sử dụng công cụ `CFF Explorer` để thử mở một file ZIP. Như các bạn có thể thấy, file ZIP này bắt đầu bằng hai kí tự **PK**, đây chính là hai magic byte của file ZIP. ![image](https://hackmd.io/_uploads/BJaVVIQ9p.png) Một ví dụ khác, mình thử mở một file PNG. Dựa vào hình dưới, có thể thấy các magic byte của file PNG là chuỗi **PNG** và chuỗi này bắt đầu từ kí tự thứ 2 tính từ đầu file. ![image](https://hackmd.io/_uploads/r1kMr8Qqa.png) Và hiển nhiên, cấu trúc của file PE cũng khá giống với cấu trúc của các file mà mình đã đề cập trước đó :grin:. Hình ảnh dưới đây minh họa tổng quan về cấu trúc của file PE, nó bao gồm hai thành phần chính là **header** và **section**. Header được thiết kế để lưu trữ thông tin siêu dữ liệu (meta information), trong khi các section được tạo ra để chứa mã nguồn (code), dữ liệu (data) và tài nguyên (resource) cần thiết cho quá trình thực thi file PE. Một số thông tin siêu dữ liệu được có trong các header bao gồm ngày, giờ, và phiên bản của file PE. Các header cũng chứa các con trỏ/offset để xác định vị trí nơi mã nguồn và dữ liệu được lưu trữ. ![image](https://hackmd.io/_uploads/S1mhylE5a.png) ## DOS Header và DOS Stub Bắt đầu của mỗi file PE đều là DOS header và tiếp theo là DOS stub. Mặc dù trong hệ điều hành Windows hiện đại, DOS header và DOS stub không còn đóng vai trò gì quan trọng trong quá trình thực thi file PE, nhưng nếu chúng không còn tác dụng, tại sao chúng vẫn được giữ lại trong file PE? :thinking_face: Để giải thích cho việc này, cùng nhắc lại thời kỳ đầu của thập kỷ 90, khi các dòng hệ điều hành DOS của Microsoft, đặc biệt là MS-DOS, đang phổ biến. Trong thời kỳ đó, hệ điều hành MS-DOS sử dụng định dạng file DOS làm file thực thi. Tuy nhiên, vào năm 1993, Microsoft đã giới thiệu hệ điều hành Windows NT và chuyển sang định dạng file PE cho các ứng dụng Windows. :smiling_imp: DOS header và DOS stub, tuy đã không còn tác dụng trong hệ điều hành Windows hiện đại nhưng nó vẫn được giữ lại trong file PE vì mục đích tương thích. Trong giai đoạn chuyển đổi từ MS-DOS sang Windows NT, việc này giúp đảm bảo rằng file PE có thể hiển thị thông điệp phù hợp khi cố gắng chạy trên hệ điều hành không tương thích, như MS-DOS. DOS header chứa thông tin cơ bản về tập tin PE, và DOS stub chứa một phần mã máy chạy được trên MS-DOS để thực hiện các công việc nhỏ. Nhìn vào hình ảnh dưới đây, ta có thể nhận ra vị trí bắt đầu của DOS header được đánh dấu bằng magic byte MZ, tương ứng với hai giá trị hexa 4D và 5A. Tiếp theo, chúng ta có thể thấy chuỗi "This program cannot be run in DOS mode" xuất hiện trong DOS stub. Chuỗi này được hiển thị để thông báo cho người dùng rằng nếu họ cố gắng chạy PE file trên hệ điều hành MS-DOS thì sẽ không thành công. ![image](https://hackmd.io/_uploads/rJWKxsmqT.png) Một trường quan trọng khác trong DOS header cần chú ý là e_lfanew, có vị trí tại offset 3C từ đầu file PE. Giá trị của nó là vị trí bắt đầu của PE header trong file PE. Thông tin này đóng vai trò trong việc xác định vị trí bắt đầu của PE header. Trong hình dưới, bạn có thể thấy giá trị của e_lfanew là F8. ![image](https://hackmd.io/_uploads/Hk08xxE9T.png) Thử kiểm tra offset tại vị trí F8, chúng ta thấy nó bắt đầu bằng hai kí tự **PE**, đây chính là signature của PE header. ![image](https://hackmd.io/_uploads/rJ_pgeV9p.png) ## PE Header/NT Header Tiếp theo, ta sẽ tìm hiểu các thành phần quan trọng của PE Header, hay còn được gọi là NT Header, dựa vào hình dưới có thể thấy PE Header bao gồm ba phần chính: Signature, File Header, và Optional Header. Bây giờ, hãy cùng mình xem xét chi tiết về từng thành phần này. ![image](https://hackmd.io/_uploads/SJJIbg4qT.png) ### Signature PE header bắt đầu bằng trường Signature, nó có độ lớn 4 byte và mang giá trị là **PE** (giá trị hexa là 0x5045). Nhưng bởi vì tool CFF Explorer hiển thị giá trị ở định dạng little-endian cho nên nó mới hiển thị ngược giá trị trường Signature là 0x4550. ![image](https://hackmd.io/_uploads/rJzqUx456.png) ### File Header Về File Header, nó có tổng cộng 7 trường thông tin, nhưng mình chỉ đề cập đến 5 trường quan trọng trong ngữ cảnh của mã độc. #### Machine Trong File Header, trường Machine giữ giá trị chỉ định loại bộ xử lý mà file PE này được thiết kế để chạy trên. Ở hình dưới, ta có thể thấy giá trị 0x014C sẽ tương ứng với kiểu bộ xử lý Intel i386. Ngoài ra, còn có các bộ xử lý phổ biến khác bao gồm AMD64 (giá trị hexa là 0x8664) cho kiến trúc 64-bit và ARM (giá trị hexa là 0x01C0) cho thiết bị di động và IoT. ![image](https://hackmd.io/_uploads/SJDyO6Xcp.png) #### NumberOfSections Trường NumberOfSections lưu trữ số lượng section có trong file PE. Như ở hình dưới, ta có thể thấy file PE đang được xét hiện tại có 5 section. ![image](https://hackmd.io/_uploads/BkE75pmcp.png) Các section thông thường xuất hiện trong file PE bao gồm: .text, .rdata, .data, .rsrc, .reloc. Dưới đây là chức năng chính của mỗi section: * **.text:** chứa mã thực thi của file PE. * **.rdata:** chứa các dữ liệu hằng (constant) trong file PE. * **.data:** chứa dữ liệu đã được khai báo và định nghĩa. * **.bss:** chứa dữ liệu đã được khai báo nhưng chưa được định nghĩa. * **.rsrc:** chứa các tài nguyên của file PE, có thể là icon, file âm thanh, hình ảnh, ... * **.reloc:** chứa thông tin cần thiết cho quá trình tái định vị (relocation). ![image](https://hackmd.io/_uploads/HkKVc6mcT.png) #### TimeDateStamp Trường TimeDateStamp, với độ dài là 4 byte, được sử dụng để ghi lại thời điểm mà file PE được tạo hoặc chỉnh sửa. Điều này là một phần quan trọng của header, giúp xác định thời điểm cuối cùng khi file được biên dịch hoặc sửa đổi. Tuy nhiên ta cũng phải lưu ý rằng trong khía cạnh bảo mật, các hacker có thể thay đổi giá trị của trường này để làm cho quá trình phân tích tĩnh của file PE trở nên khó khăn hơn. Hành động này có thể dẫn đến sự hiểu lầm về thời gian và gây khó khăn trong việc theo dõi sự thay đổi và cập nhật của file. ![image](https://hackmd.io/_uploads/rJ_v96X5p.png) #### SizeOfOptionalHeader Trường SizeOfOptionalHeader được sử dụng để lưu trữ kích thước của section Optional Header. Nguyên nhân của việc này là do Optional Header không có kích thước cố định; thay vào đó, kích thước của nó có thể thay đổi tùy thuộc vào kiến trúc của các tệp tin PE khác nhau và theo các tính năng cụ thể mà tệp tin PE có hoặc không có. ![image](https://hackmd.io/_uploads/r1-dsTmq6.png) #### Characteristics Trường thông tin cuối cùng của File Header là Characteristics với kích thước là 2 byte. Trường này đại diện cho một số thuộc tính của file PE. Để xem và hiểu rõ các thuộc tính này, ta có thể sử dụng công cụ như CFF Explorer. Công cụ này không chỉ giúp xem tất cả các thuộc tính có thể xuất hiện mà còn cho phép kiểm tra các thuộc tính mà file PE hiện tại đang sở hữu. ![image](https://hackmd.io/_uploads/S1BYhaX56.png) Ngoài ra, ta còn có thể chỉnh sửa các thuộc tính của file PE bằng cách nhấn vào các checkbox bằng công cụ CFF Explorer. Dưới đây là một số thuộc tính quan trọng về file PE: * **File is executable:** thuộc tính này chỉ ra rằng file PE hiện tại là một file thực thi. * **File is a DLL:** thuộc tính này chỉ ra rằng file PE hiện tại là một dynamic-link library (DLL). * **32 bit word machine:** thuộc tính này ta xác định xem file PE hiện tại là 32-bit hay 64-bit. ### Optional Header Còn về Optional Header, nó chứa nhiều trường thông tin quan trọng cho Windows loader khi thực hiện quá trình sao chép và ánh xạ file PE lên bộ nhớ của process. Mình sẽ đề cập đến một số trường quan trọng có trong Optional Header sau đây. #### Magic Trường đầu tiên là Magic có kích thước 2 byte, nếu giá trị của nó là 0x010B thì file PE là 32-bit còn nếu giá trị của nó là 0x020B thì file PE là 64-bit. ![image](https://hackmd.io/_uploads/Hke2hTX9a.png) #### ImageBase Khi Windows loader tạo một tiến trình, nó sẽ sao chép và tải file PE và các section của nó từ đĩa vào bộ nhớ ảo của process. Nhưng trước tiên, nó cần cấp phát không gian trong bộ nhớ ảo. Nhưng làm sao để Windows loader biết nên phân bổ không gian trong bộ nhớ ảo ở vị trí nào để sao chép file PE và các section của nó? Việc này xuất phát từ trường ImageBase trong file PE trong Optional Header. Như ở hình dưới, có thể thấy giá trị ImageBase của chương trình là 0x400000. ![image](https://hackmd.io/_uploads/SJg4Efr56.png) Nhưng khi mình chạy chương trình này lên và kiểm tra image base của nó thì kết quả lại là 0x710000. Hmm, tại sao giá trị ImageBase hiển thị là 0x400000 trong khi giá trị image base thật sự lại là 0x710000? Liệu rằng có sự nhầm lẫn gì ở đây không? :thinking_face: ![image](https://hackmd.io/_uploads/rJovVMrqT.png) Câu trả lời là không! Bản thân giá trị của trường ImageBase chỉ là một giá trị tham khảo thôi, Windows loader khi nạp file PE lên bộ nhớ của process có thể sử dụng giá trị này hoặc không tùy thuộc vào tình huống cụ thể. Vậy thì trong tình huống nào thì Windows loader không sử dụng giá trị của trường ImageBase? :thinking_face: Giả sử Windows loader muốn nạp một file PE lên bộ nhớ, giá trị ImageBase của file PE này là 0x400000. Tuy nhiên, trong quá trình tạo một process hoàn chỉnh, không chỉ Windows loader nạp module tương ứng với file PE mà còn phải nạp các module khác, chẳng hạn như DLL và mapped file. Điều này làm cho địa chỉ 0x400000 **có thể đã được sử dụng bởi một module khác**, khiến cho Windows loader phải tìm một vị trí khác để nạp file PE lên. Vì lý do này, giá trị của trường ImageBase chỉ được tham khảo bởi Windows loader và có thể được điều chỉnh để tránh xung đột địa chỉ khi nạp các module khác vào không gian bộ nhớ. #### AddressOfEntryPoint AddressOfEntryPoint là một trường trong Optional Header, với kích thước 4 byte. Nó chứa địa chỉ Relative Virtual Address (RVA) của điểm bắt đầu thực thi mã máy, đó là vị trí mà quá trình thực thi chương trình bắt đầu trong không gian bộ nhớ ảo. ![image](https://hackmd.io/_uploads/HyaJ6p7q6.png) Vừa rổi mình có nhắc đến từ khóa Relative Virtual Address (RVA), vậy thì nó là gì ? :thinking_face: Ý nghĩa của Relative Virtual Address (RVA) đúng như tên gọi của nó, đó là một địa chỉ ảo tương đối. Nó được tính toán từ vị trí Image Base của module trong không gian địa chỉ ảo của process. Ngoài ra, có một loại địa chỉ khác được gọi là Virtual Address (VA). Khác với RVA, VA là địa chỉ ảo tuyệt đối, được tính từ vị trí bắt đầu của không gian địa chỉ ảo. Hình dưới mô tả ý nghĩa của RVA cùng với VA một cách trực quan. ![image](https://hackmd.io/_uploads/SJZONeE5a.png) Dựa vào hình trên, ta có thể dễ dàng suy ra mối liên hệ giữa VA và RVA như sau: $$ VA = Image Base + RVA $$ #### SizeOfImage SizeOfImage là một trường trong Optional Header. Nó thể hiện độ lớn của module (file PE) khi được nạp vào bộ nhớ của process. Lưu ý rằng độ lớn của module tương ứng với file PE khi nó được ánh xạ vào bộ nhớ của process sẽ khác với kích thước của file PE trên đĩa. ![image](https://hackmd.io/_uploads/rJHWDCQ96.png) #### SizeOfHeaders Trường SizeOfHeaders có kích thước 4 byte, dùng để lưu trữ độ lớn của tất cả các header có trong file PE. ![image](https://hackmd.io/_uploads/BJ_1vRQca.png) #### Data Directory Data Directory chứa thông tin về độ lớn (size) và Relative Virtual Address (RVA) của một vị trí cụ thể trên bộ nhớ, thường là các bảng hoặc thư mục quan trọng trong file PE. File PE thường có tổng cộng 16 Data Directory. Trong trường hợp một Data Directory không tồn tại trong file PE, kích thước và RVA của nó được thiết lập là 0. Một số Data Directory quan trọng bao gồm Export Directory và Import Directory, có vai trò quan trọng trong quá trình loader của Windows khi nạp file PE lên bộ nhớ. Ngoài ra, Resource Directory cũng là một Data Directory quan trọng, chứa thông tin về tài nguyên như icon và dữ liệu ẩn trong file PE, là những phần mà chúng ta cũng nên theo dõi. Khi sử dụng công cụ CFF Explorer để xem thông tin của Data Directory, ngoài việc cung cấp độ lớn và Relative Virtual Address (RVA), nó cũng giúp xác định Data Directory đó thuộc về phần (section) nào trong tệp tin. Điều này làm cho quá trình tra cứu trở nên thuận tiện hơn, giúp ta dễ dàng định vị và theo dõi thông tin liên quan đến các Data Directory trong file PE. ![image](https://hackmd.io/_uploads/Hy_iEAXca.png) ### Section Table Section Table là một thành phần cực kỳ quan trọng trong file PE. Nhờ có nó, Windows loader mới xác định được vị trí của các section trong file PE, và cũng nhờ vào nó, Windows loader mới có thể biết được nên nạp các section vào những vị trí cụ thể nào trên bộ nhớ. Section Table bao gồm nhiều entry, mỗi entry chứa các trường thông tin mô tả một section cụ thể. Bây giờ, mình sẽ giải thích chi tiết các trường thông tin quan trọng có trong một entry của Section Table. #### Name Trường đầu tiên là Name, nó lưu trữ tên của section. Trường này có độ lớn đối đa là 8 byte cho nên việc này cũng đồng nghĩa là tên của section cũng chỉ có tối đa là 8 kí tự. #### Virtual Size & Virtual Address Virtual Size và Virtual Address lần lượt là độ lớn và RVA của section trên không gian địa chỉ ảo của process. RVA là gì thì mình đã có giải thích ở trên rồi :grin:. Hình dưới đây mô tả một cách trực quan về trường Virtual Size và Virtual Address của Section 2. ![image](https://hackmd.io/_uploads/r1w7AkN9a.png) #### Raw Size & Raw Address Thông tin tiếp theo là Raw Size và Raw Address. Raw Size là độ lớn của section và Raw Address là offset của section tính từ đầu file PE trên đĩa. Hình dưới mô tả ý nghĩa của Raw Size và Raw Address. ![image](https://hackmd.io/_uploads/Bye50kEqp.png) Dựa vào tính chất của Virtual Address và Raw Address, ta có thể suy ra mối liên hệ giữa chúng. Để hình dung dễ hơn, mình đã chuẩn bị sẵn hình minh họa dưới đây. ![image](https://hackmd.io/_uploads/HyGY37BqT.png) Nếu mình gọi Denta chính là **khoảng cách của một điểm dữ liệu nào đó trong Section 2 tính từ đầu Section 2** thì dựa vào hình vẽ ta có thể tính Denta theo công thức: $$ Denta = Offset - Raw\:Address $$ Hoặc $$ Denta = RVA - Virtual\:Address $$ Ở đây, Offset là offset của điểm dữ liệu trong Section 2, Raw Address là offset của Section 2. RVA là RVA của điểm dữ liệu, Virtual Address là RVA của Section 2. Bằng cách đặt hai biểu thức này bằng nhau, ta thu được mối liên hệ giữa Offset và RVA: $$ Offset - Raw\:Address = RVA - Virtual\:Address $$ Công thức trên được sử dụng để chuyển đổi giữa giá trị offset của một điểm dữ liệu và RVA, hoặc ngược lại. Công thức này sẽ xuất hiện thường xuyên trong các bài viết tiếp theo, vì vậy bạn đọc nên lưu ý đến nó. #### Characteristics Và cuối cùng là trường Characteristics, trường này sẽ giúp ta xác định các thuộc tính của một section. Các thuộc tính này có thể là các quyển truy cập của một section. Hình dưới mô tả tất cả các tính chất có sẵn của một section bằng công cụ CFF Explorer. ![image](https://hackmd.io/_uploads/SJXVQ4Hcp.png) Trong ngữ cảnh về mã độc, ta cần phải quan tâm một số thuộc tính như sau: * **Is shareable:** section có thể được chia sẻ giữa nhiều process. * **Is executable:** section chứa mã thực thi. * **Is readable:** section có thể đọc. * **Is writeable:** section có thể ghi. * **Contains code:** section chứa mã máy. * **Contains initialized data:** section chứa dữ liệu được khởi tạo. * **Contains Uninitialized data:** section chứa dữ liệu chưa được khởi tạo. ## Kết luận :::warning :zap: Lưu ý rằng, bài viết chỉ mang tính chất giáo dục và không khuyến khích việc sử dụng thông tin để thực hiện các hoạt động xấu hay bất hợp pháp. Nếu có thắc mắc hay ý kiến, đừng ngần ngại chia sẻ với mình để làm cho bài viết trở nên tốt hơn. ::: ## Tham khảo * [https://0xrick.github.io/win-internals/pe5/](https://0xrick.github.io/win-internals/pe5/) * [https://learn.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)?redirectedfrom=MSDN](https://learn.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)?redirectedfrom=MSDN) * [https://en.wikipedia.org/wiki/MS-DOS](https://en.wikipedia.org/wiki/MS-DOS) * [https://en.wikipedia.org/wiki/Windows_NT](https://en.wikipedia.org/wiki/Windows_NT)