Trang 1 trên tổng số 2 12 Cuối cùngCuối cùng
Từ 1 tới 10 trên tổng số 11 kết quả

Đề tài: Lập trình API với tập tin PE

  1. #1
    Ngày gia nhập
    02 2014
    Nơi ở
    TP.HCM
    Bài viết
    732

    Mặc định Lập trình API với tập tin PE

    Hôm nay tôi mở đề tài này chia sẻ và cũng để tìm hiểu thêm với các bạn nào vẫn còn yêu thích lập trình WinAPI (nó đã xưa như trái đất).
    Các định nghĩa và khái niệm về tập tin PE các bạn có thể xem ở bài viết của langman tại http://diendan.congdongcviet.com/thr...ile-format.cpp.

    Trên nền kiến thức đó tôi tập trung vào thực hành là chính, vì vậy tôi chỉ diễn giải thêm khi thấy cần thiết. Tôi muốn leo dần tới chương trình Disassembler và xa hơn nữa là chương trình chuyển PE về các định dạng C thân thuộc với chúng ta hơn. Điều đó xa vời quá phải không các bạn, trước khi nghĩ tới cái lớn lao, ta phải làm cái nhỏ trước đã.

    Bài tập đầu tiên : Viết chương trình xuất ra cấu trúc tổng quát của PE.

    C Code:
    1. /*              Cấu trúc phân cấp tổng quát của tập tin PE
    2.     + <Tập tin>
    3.         - IMAGE_DOS_HEADER
    4.         - DosStub
    5.         - RichBlock
    6.         + IMAGE_NT_HEADERS
    7.             - IMAGE_FILE_HEADER
    8.             + IMAGE_OPTION_HEADER
    9.                 IMAGE_DATA_DIRECTORY[16]
    10.         - IMAGE_SECTION_HEADER .text
    11.         - IMAGE_SECTION_HEADER .rsrc
    12.         - ............
    13.  
    14.         + SectionData .text
    15.             ........
    16.         + SectionData .rsrc
    17.             ........
    18.         + ................
    19. */
    Giao diện màn hình thì tôi lựa chọn một TreeView để hiển thị khối comment trên, phần giá trị của từng mục TreeView sẽ hiển thị trên ListView.
    Trong bài viết của langman không thấy đề cập tới khối Rich. Đây là khối thông tin mà có giá trị với Windows hơn là với chính bản thân chương trình. Nó không phải là phần bắt buộc phải có (thường thì chỉ thấy nó xuất hiện khi xây dựng chương trình bằng các ngôn ngữ C\C++\C++.NET). Để xác định nó có mặt trong 1 PE không thì ta quét ngược từ Offset(IMAGE_NT_HEADERS - 8) về Offset(0x80) 1 lần 4 bytes để tìm ký hiệu là chuỗi byte[ 'R', 'i', 'c', 'h' ]. Nếu ta quét bằng 1 con trỏ PDWORD thì có thể so sánh giá trị trong con trỏ như if ( *pDword == 0x68636952 ) // Có ký hiệu Rich. Một vài tài liệu có mô tả cách lấy product id, minor build version và count là xor DWORD theo ngay sau ký hiệu Rich với khối rồi trích xuất tuần tự ra. Tôi chưa quan tâm tới khối này.

    Chương trình pe1 bên dưới chưa xử lý các khối dữ liệu của các SectionData, cũng có lỗi với các PE quá lớn, thao tác vẽ màn hình còn rất tệ (nháy hình), bài tập đầu tiên tới đây thôi.

    Click vào hình ảnh để lấy hình ảnh lớn

Tên:		pe1.png
Lần xem:	8
Size:		27.9 KB
ID:		51921
    Attached Files Attached Files
    Yêu mã hơn yêu em !!!

  2. #2
    Ngày gia nhập
    09 2016
    Bài viết
    1,028

    Theo tôi làm tới mức đọc được mã Asm cũng đủ - phần phân tích ngữ nghĩa, chuyển sang c là cực kỳ phức tạp.

    Các CT đọc netFX hiện nay cũng chưa hoàn thiện chuyển msIL sang c# (dù họ là một tập đoàn lớn)

    fms17

  3. #3
    Ngày gia nhập
    02 2014
    Nơi ở
    TP.HCM
    Bài viết
    732

    Trích dẫn Nguyên bản được gửi bởi [COLOR="#0000FF"
    fms17[/COLOR];889977]Theo tôi làm tới mức đọc được mã Asm cũng đủ - phần phân tích ngữ nghĩa, chuyển sang c là cực kỳ phức tạp.

    Các CT đọc netFX hiện nay cũng chưa hoàn thiện chuyển msIL sang c# (dù họ là một tập đoàn lớn)

    fms17
    @fms17 : Tôi rất ghi nhận lời bạn, nhưng tôi chưa hề nói là tôi sẽ hoàn thành cái mà bạn nói, tôi đang cần sự trợ giúp từ các anh em hiểu vấn đề mà. Thực tâm tôi hướng tới, và tôi rất hiểu những gì bạn nói. Kể cả nhánh .NET mà bạn rất thông thạo tôi vẫn sẽ phân tích thấu đáo theo những gì tôi biết, cái tôi cần là bạn (hay các bạn khác đi trước) chỉ ra những bất hợp lý khi tôi lập luận để giải quyết những khúc mắc. Tôi cũng không duy ý chí rằng mình sánh vai các ông lớn - tôi hiểu mình ở nơi đâu. Tôi mong rằng các bài tập kế tiếp, khi thấy bất cập, bạn nên lên tiếng để người viết biềt mình yếu chỗ nào, tôi không phải người muốn người khác vỗ tay tán thưởng. Ủng hộ những câu nói như : chỗ này không tối ưu, chỗ kia là sai lầm, có cách khác gọn gàng hơn... Được vậy tôi cũng rất vui. Mời các bạn tiếp tục.
    Yêu mã hơn yêu em !!!

  4. #4
    Ngày gia nhập
    09 2016
    Bài viết
    1,028

    Lúc suy lúc thịnh, cuộc chiến sóng - hạt đủ dài, có nhiều công kích, phản bác ..

    Tôi có tiếp xúc với nhiều CT, thấy những điều ngớ ngẩn trong các UD mà thiên hạ ca tụng. Cái quan trọng là nó làm được, đòi chi tất cả đều hoàn hảo.

    Phọt mô xa 2017

  5. #5
    Ngày gia nhập
    02 2014
    Nơi ở
    TP.HCM
    Bài viết
    732

    Cả ngày chủ nhật, hứng chí, viết lại chương trình, chỉ để xuất ra thư mục xuất của PE, lỗi tè le, các bạn xem thử. Phân tích chờ vài ngày sau đã.

    Click vào hình ảnh để lấy hình ảnh lớn

Tên:		ExportDirectory.png
Lần xem:	12
Size:		107.7 KB
ID:		52105
    Attached Files Attached Files
    Yêu mã hơn yêu em !!!

  6. #6
    Ngày gia nhập
    02 2014
    Nơi ở
    TP.HCM
    Bài viết
    732

    Mặc định Đọc dữ liệu thô của các SectionData

    Để đọc được dữ liệu thô của các SectionData, chúng ta phải làm quen với cách phân bổ tập tin PE mà ta truy xuất.
    Khi chúng ta có một thẻ tập tin PE bằng các hàm như HANDLE hFile = CreateFile, OpenFile, và nhận được kích thước là dwSize, ta có thể nhận một con trỏ để đọc tập tin
    C Code:
    1.     //  PBYTE pFile = new BYTE[dwSize];
    2.     //  BOOL bSuccess = ReadFile(hFile, pFile, dwSize, ...);
    3.     //  hay
    4.     //  HANDLE hMapFile = CreateFileMapping(hFile,...); // hoặc
    5.     //  HANDLE hMapFile = OpenFileMapping(...);
    6.     //  PBYTE pMap = (PBYTE)MapViewOfFile(hMapFile,...);
    Cả con trỏ pFile hoặc con trỏ pMap đều trỏ tới vùng nhớ bắt đầu, ta gộp chung cho dễ diễn giải
    C Code:
    1.     PBYTE pBase;
    2. #ifdef USE_MAPPING
    3.     pBase = pMap;
    4. #else
    5.     pBase = pFile;
    6. #endif
    Nếu tất cả trơn chu, ta có một con trỏ pBase trỏ tới vùng bộ nhớ mà PE được nạp lên, khi đó ta có thể nhận được giá trị các trường (giả sử đã kiểm tra là PE)
    C Code:
    1.     PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(pBase + ((PIMAGE_DOS_HEADER)pBase)->e_lfanew);       // Con trỏ NtHeaders
    2.     PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((PBYTE)pNtHeaders + sizeof(DWORD));               // Con trỏ FileHeader
    3.     PIMAGE_OPTIONAL_HEADER pOptionalHeader = (PIMAGE_OPTIONAL_HEADER)((PBYTE)pFileHeader + sizeof(IMAGE_FILE_HEADER));
    4.     WORD uNumberOfSections = pFileHeader->NumberOfSections;
    5.     DWORD dwImageBase = pOptionalHeader->ImageBase;
    6.     DWORD dwFileAlignment = pOptionalHeader->FileAlignment;
    7.     DWORD dwSectionAlignment = pOptionalHeader->SectionAlignment;
    8.     // Mã nhận con trỏ tới SectionHeader đầu tiên có dạng
    9.     DWORD dwNtHeadersOffset = (PBYTE)pNtHeaders - pBase;                // Offset của NtHeaders so với đầu vùng nhớ PE được nạp lên
    10.     DWORD dwFirstSectionHeaderOffset = dwNtHeadersOffset + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER) + pFileHeader->SizeOfOptionalHeader;
    11.     PIMAGE_SECTION_HEADER pFirstSectionHeader = (PIMAGE_SECTION_HEADER)(pBase + dwFirstSectionHeaderOffset);    // Hoặc lệnh dưới
    12.     //PIMAGE_SECTION_HEADER pFirstSectionHeader = (PIMAGE_SECTION_HEADER)((PBYTE)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    13.     // Có được con trỏ SectionHeader đầu tiên, các con trỏ SectionHeader bất kỳ nếu có được truy xuất dễ dàng
    14.     WORD n = 0;         // Ví dụ với chỉ mục 0  
    15.     PIMAGE_SECTION_HEADER pSectionHeaderN = &pFirstSectionHeader[n];    // Hoặc
    16.     //PIMAGE_SECTION_HEADER pSectionHeader = pFirstSectionHeader + n;
    17.     // Lấy giá trị các trường trong SectionHeader
    18.     DWORD dwVirtualAddressN = pSectionHeaderN->VirtualAddress;
    19.     DWORD dwSizeOfRawDataN = pSectionHeaderN->SizeOfRawData;
    20.     DWORD dwPointerToRawDataN = pSectionHeaderN->PointerToRawData;
    Khi chúng ta muốn đọc các khối dữ liệu thô nằm trong các Section, ta phải biết được địa chỉ bắt đầu vùng nhớ của SectionData, sự việc bắt đầu phức tạp.
    C Code:
    1.     // Nơi đây chưa bàn tới trường IMAGE_OPTIONAL_HEADER.ImageBase
    2.     PBYTE pSectionDataN; // Con trỏ dùng để đọc dữ liệu Section N
    3. #ifdef USE_MAPPING
    4.     pSectionDataN = pBase + dwVirtualAddressN;
    5. #else
    6.     pSectionDataN = pBase + dwPointerToRawDataN;
    7. #endif
    Tại sao lại có khác biệt đó ?
    Khi chúng ta nạp PE lên để lấy con trỏ pFile, PE chỉ là một khối dữ liệu tương tự như tập tin đĩa. Đọc byte nào thì byte đó có một Offset tương đối tính từ đầu tập tin.
    "tôi muốn lấy byte thứ 5AB6h thì tôi chỉ việc truy xuất pFile[0x5AB6]", vậy nhận một byte đầu tiên của SectionData thì ta truy xuất
    pFile[<Offset byte đầu tiên của SectionData>] => ta phải tìm Offset của byte đầu tiên ấy. May mắn thay SectionHeader đã lưu trữ Offset này trong PointerToRawData.

    Khi chúng ta tạo bản đồ để nhận con trỏ pMap, thì hàm tạo bản đồ sẽ tính toán vùng nhớ mà sẽ nạp dữ liệu Section từ tập tin lên,
    Nó sẽ lấy số liệu trong trường VirtualAddress và xem đó là Offset tới đầu vùng nhớ bản đồ.
    Như vậy ta có pMap[VirtualAddress] là giá trị byte đầu tiên của SectionData trong vùng nhớ đã Mapping.

    Để rõ hơn cách sắp xếp SectionData giữa đọc tập tin và tạo bản đồ, bạn xem thêm hình bên dưới, nơi đây các trường FileAlignment và SectionAlignment lên tiếng.
    Ở đây tôi giả sử trường hợp ta đọc tập tin vào bộ nhớ được Windows cấp phát tại 100000h và trường hợp ta tạo bản đồ và nhận con trỏ pMap tại 400000h,pA, pB, pC, pD là con trỏ tới SectionData. Bạn chú ý vào kích thước giả định của các SectionData A,B,C,D và giá trị các con trỏ tương ứng.

    Click vào hình ảnh để lấy hình ảnh lớn

Tên:		Sections.png
Lần xem:	5
Size:		17.4 KB
ID:		52137

    Sau khi nắm một số ý trên, bạn có suy nghĩ gì thảo luận cho:
    . Có trường hợp nào mà FileAlignment > SectionAlignment không. Nếu có thì ảnh hưởng các trường khác trên các cấu trúc khác ra sao.
    . Theo bạn thì giá trị thấp nhất cho 2 trường trên có thể đạt được là bao nhiêu. Bạn có thể giải thích không (khó đấy).

    Phần sau : Đi sâu vào thư mục xuất.
    Yêu mã hơn yêu em !!!

  7. #7
    Ngày gia nhập
    02 2014
    Nơi ở
    TP.HCM
    Bài viết
    732

    Mặc định Đi sâu vào thư mục xuất

    Như vậy là ta đã có con trỏ và kích thước của SectionData bất kỳ, dùng con trỏ này ta có thể lấy toàn bộ dữ liệu trong nó. Nhưng nó chỉ là dữ liệu "thô",những bytes đọc được chưa nói lên bất kỳ công dụng nào. Dữ liệu bên trong các SectionData có thể là mã, là tài nguyên, v.v... Hôm nay ta sẽ xem thử thứ đầu tiên mà nó có thể chứa trong SectionData - Đó là thư mục xuất.

    Quay trở lại với phân cấp PE, bên trong IMAGE_OPTIONAL_HEADER có một mảng IMAGE_DATA_DIRECTORY[16], mỗi cấu trúc có 2 trường VirtualAddress và Size. VirtualAddress là RVA của đối tượng nào đó và Size là kích thước của đối tượng. Windows đã định chuẩn cho 15 kiểu đối tượng mà nó lấp đầy 15 chỉ mục đầu tiên trong mảng IMAGE_DATA_DIRECTORY, chỉ mục cuối cùng (index 15) tới hiện thời còn trống và được lấp đầy bằng 0. Các chỉ mục đã được "hằng hóa" trong các tập tin
    #include tương tự như bên dưới.
    #define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Thư mục xuất - phân tích trong ngày nay
    #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Thư mục nhập
    #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Thư mục tài nguyên
    #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Bảng ngoại lệ
    #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Bảo mật
    #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Bảng dùng để định vị lại
    #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Thư mục Debug
    #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture
    #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // GlobalPtr
    #define IMAGE_DIRECTORY_ENTRY_TLS 9 // Dùng cho lưu giữ cục bộ các luồng việc
    #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 //
    #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11
    #define IMAGE_DIRECTORY_ENTRY_IAT 12 // Bảng IAT (cực kỳ quan trọng nếu bạn muốn thọc gậy)
    #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13
    #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // Thường dùng bởi .NET

    Trước khi vào mục chính ta thử xem RVA là gì, và RVA trong Loader khi Windows nạp khởi chạy chương trình, ta quy đổi nó ra Offset tập tin như thế nào.
    RVA là Offset của một byte bộ nhớ bất kỳ so với pBase là điểm bắt đầu mà bộ nạp (Loader) nạp lên vùng nhớ.
    Khi Loader nạp chương trình, tất cả cấu trúc PE được nạp lên từ Offset 0 cho tới cuối SectionHeader cuối cùng đều được giữ nguyên khoảng cách như nó ở trong
    tập tin, do vậy lúc này : File Offset = RVA.

    Do cách thức canh chỉnh biên đoạn của các trường FileAlignment và SectionAlignment là khác nhau, do vậy một RVA từ một SectionData nào đó có thể không có một
    Offset tập tin tương ứng.

    Nếu muốn nhận Offset tập tin từ một RVA ta có thể làm như sau ( ví dụ có một RVA là dwRVA), giả sử ta lấy tên hàm là OffsetFromRVA(DWORD dwRVA)
    + Duyệt từng SectionHeader để lấy con trỏ pSectionHeaderN
    + DWORD dwAddress = pSectionHeaderN->VirtualAddress;
    + DWORD dwSizeData = pSectionHeaderN->SizeOfRawData;
    + DWORD dwPointerToRawData = pSectionHeaderN->PointerToRawData;
    + if (dwRVA >= dwAddress && dwRVA < (dwAddress + dwSizeData))
    + {
    return (dwRVA + dwPointerToRawData - dwVirtualAddress);
    + }
    + Nếu đã duyệt qua hết các SectionHeader mà biểu thức điều kiện không thỏa lần nào thì có nghĩa RVA này không có Offset tập tin tương ứng

    Giờ vào mục chính ta xét tới IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_EXPORT].
    Thông thường thì các tập tin dạng dll (trường IMAGE_FILE_HEADER.Characteristics có cờ IMAGE_FILE_DLL được bật) mới có thư mục xuất nhưng lâu lâu cũng thấy các chương trình dạng exe cũng có.
    Trường VirtualAddress (nếu khác 0) là RVA tới một cấu trúc IMAGE_EXPORT_DIRECTORY có các trường sau:
    . Characteristics (4 bytes, không sử dụng thiết lập 0)
    . TimeDateStamp (4 bytes, thời gian)
    . MajorVersion (2 bytes, Major Version)
    . MinorVersion (2 bytes, Minor Version)
    . Name (4 bytes, RVA của chuỗi kết thúc NULL chứa tên của pe)
    . Base (4 bytes, Thứ thự bắt đầu của những hàm được xuất)
    . NumberOfFunctions (4 bytes, Số phần tử trong mảng AddressOfFunctions)
    . NumberOfNames (4 bytes, Số phần tử trong mảng AddressOfNames)
    . AddressOfFunctions (4 bytes, RVA trỏ tới một mảng những địa chỉ hàm)
    . AddressOfNames (4 bytes, RVA trỏ tới một mảng các chuỗi tên hàm)
    . AddressOfNameOrdinals ( 4 byte, RVA trỏ tới một mảng WORDs là các tên thứ tự của hàm)

    Từ những thông tin trên ta có thể nhận những con trỏ tới các mảng chứa địa chỉ hàm, mảng chứa con trỏ tên hàm và mảng số thứ tự.
    Ở đây ta cần chú ý hai mảng AddressOfNames và AddressOfNameOrdinals là có số phần tử như nhau = NumberOfNames và nó có sự tương ứng giữa các chỉ mục, nghĩa là AddressOfNames[0] và AddressOfNameOrdinals[0] là cùng nắm giữ thông tin về 1 hàm còn AddressOfNames[2] và AddressOfNameOrdinals[4] là nắm giữ thông tin về 2 hàm khác nhau

    Từ 1 chỉ mục index trong AddressOfNames và AddressOfNameOrdinals, ta nhận một tên hàm như sau:
    DWORD dwOffset = OffsetFromRVA(pAddressOfNames[index]); // Hàm OffsetFromRVA là ví dụ ở trên
    PCHAR pszName = (PCHAR)(pBase + dwOffset);
    Nhận tên thứ tự như sau:
    WORD uNameOrdinal = pAddressOfNameOrdinals[index];
    Cuối cùng nhận địa chỉ hàm như sau:
    DWORD dwFunctionRVA = pAddressOfFunctions[uNameOrdinal];

    Nhưng sự việc chưa hết rắc rối, một PE có thể có (NumberOfFunctions != NumberOfNames), đây là trường hợp có các hàm chỉ được xuất bởi thứ tự toàn cục. Nghĩa là trong các mảng AddressOfNames và AddressOfNameOrdinals không chứa các dẫn hướng đưa tới hàm này. Và như vậy khi ta duyệt mảng AddressOfFunctions ta sẽ gặp một số hàm có địa chỉ RVA nhưng không tên tuổi không thứ tự chi cả, nhưng một hàm xuất thì phải có thứ tự xuất mà các chương trình khác biết chứ.

    Một thư viện sẽ có nhiều thứ để xuất chứ không chỉ có riêng các hàm, tất cả các thành phần xuất đó được lên danh sách, riêng các hàm sẽ nằm ở một vùng liên tiếp mà thứ tự bắt đầu được xác định bởi trường Base. Chúng ta tìm được uNameOrdinal ở trên thì nó chỉ là chỉ mục trong mảng các địa chỉ hàm mà thôi, muốn nhận thứ tự của hàm trên toàn bộ các thứ tự xuất thì ta phải cộng thêm Base vào.
    Ví dụ : uNameOrdinal = 3; dwBase = 50; thì DWORD dwOrdinal = 53; // Thứ tự toàn cục là 53, bên ngoài sẽ thấy được hàm này qua dwOrdinal

    Như vậy các hàm chỉ được xuất bởi thứ tự toàn cục, thì dwOrdinal = dwBase + chỉ mục trong mảng pAddressOfFunctions.

    Phần tiếp theo : Tìm hiểu về thư mục nhập
    Attached Files Attached Files
    Yêu mã hơn yêu em !!!

  8. #8
    Ngày gia nhập
    02 2014
    Nơi ở
    TP.HCM
    Bài viết
    732

    Mặc định Tìm hiểu về các hàm nhập từ bên ngoài vào tập tin PE

    Các liên kết tới các hàm nhập ngoại được bắt đầu từ 4 thư mục dữ liệu :
    1. IMAGE_DATA_DIRECTOTY[IMAGE_DIRECTORY_ENTRY_IMPORT]
    2. IMAGE_DATA_DIRECTOTY[IMAGE_DIRECTORY_ENTRY_IAT]
    3. IMAGE_DATA_DIRECTOTY[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT]
    4. IMAGE_DATA_DIRECTOTY[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT]
    Hôm nay ta sẽ xem qua các liên kết ở 2 trong 4 mục ở trên.
    Đây là hình ảnh tượng trưng cho các liên kết đó
    Click vào hình ảnh để lấy hình ảnh lớn

Tên:		PE_Import.png
Lần xem:	4
Size:		22.3 KB
ID:		54859
    Từ IMAGE_DIRECTORY_ENTRY_IMPORT ta sẽ nhận được một con trỏ tới 1 mảng các cấu trúc IMAGE_IMPORT_DESCRIPTOR, mảng này được kết thúc bằng 1 phần tử có tất cả các trường mang giá trị 0. Ta xem qua các trường của IMAGE_IMPORT_DESCRIPTOR.
    . Characteristics/OriginalFirstThunk : 4 Bytes - RVA tới một mảng các DWORD (x86) hay các ULONGLONG (x64). Mảng này được đặt tên là INT (Import Names Table), mỗi phần tử trong mảng thì hoặc là chỉ dẫn về thứ tự nhập (Ordinal) của hàm trong Dll nhập hoặc là RVA tới 1 cấu trúc IMAGE_IMPORT_BY_NAME.
    . TimeDateStamp : 4 Bytes
    . ForwarderChain : 4 Bytes - Thông tin chưa hoàn chỉnh về tham chiếu lồng nhau, nói thêm sau.
    . Name : 4 Bytes - RVA to một chuỗi Asciiz là tên của thư viện có hàm nhập vào.
    . FirstThunk : 4 Bytes - RVA tới 1 mảng các IMAGE_THUNK_DATA32 (x86) hay các IMAGE_THUNK_DATA64 (x64). Mảng này được đặt tên là IAT (Import Address Table), mỗi phần tử trong mảng có thể là chỉ dẫn về thứ tự nhập (Ordinal) hoặc RVA tới IMAGE_IMPORT_BYTE_NAME hoặc địa chỉ bộ nhớ hoặc là chỉ mục của hàm đầu tiên trong bảng hàm nhập mà được dẫn xuất từ 1 Dll khác nữa.

    Mã để nhận con trỏ tới từng cấu trúc IMAGE_IMPORT_DESCRIPTOR có thể như sau:
    C++ Code:
    1.     // Giả sử tập tin đã được kiểm tra là PE và pFile != NULL
    2.     // là con trỏ bộ nhớ mà tập tin được đọc vào từ hàm ReadFile()
    3.     void HandleAllImportDescriptor(PBYTE pFile)
    4.     {
    5.         PIMAGE_NT_HEADERS       pNtHeaders = (PIMAGE_NT_HEADERS)(pFile + ((PIMAGE_DOS_HEADER)pFile)->e_lfanew);
    6.         BOOL                    is64 = (pNtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) ? TRUE : FALSE;
    7.         PIMAGE_DATA_DIRECTORY   pDataDirectory;
    8.         if(is64)
    9.             pDataDirectory = (PIMAGE_DATA_DIRECTORY)&((PIMAGE_NT_HEADERS64)pNtHeaders)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    10.         else
    11.             pDataDirectory = (PIMAGE_DATA_DIRECTORY)&((PIMAGE_NT_HEADERS32)pNtHeaders)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    12.  
    13.         DWORD       dwRVA = pDataDirectory->VirtualAddress;
    14.         if (!dwRVA)
    15.             return;         // Không xét tiếp
    16.         PIMAGE_IMPORT_DESCRIPTOR    pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pFile + OffsetFromRVA(dwRVA));
    17.         while (pImport->Name)
    18.         {
    19.             // Làm gì đó với con trỏ cấu trúc này, ví dụ : Gọi HandleINT() hay HandleIAT() ở dưới
    20.             pImport++;
    21.         }
    22.     }

    Mã để nhận từng phần tử trong bảng INT có thể như sau:
    C++ Code:
    1.     void HandleINT(PBYTE pFile, BOOL is64, PIMAGE_IMPORT_DESCRIPTOR pImport)
    2.     {
    3.         DWORD   dwRVA = pImport->OriginalFirstThunk;
    4.         if (!dwRVA)
    5.             return;
    6.         if (is64)
    7.         {
    8.             PULONGLONG  pUlonglong = (PULONGLONG)(pFile + OffsetFromRVA(dwRVA));    // Ulonglong đầu tiên
    9.             while (*pUlonglong)
    10.             {
    11.                 // Làm gì đó với con trỏ 64 này
    12.                 pUlonglong++;
    13.             }
    14.         }
    15.         else
    16.         {
    17.             PDWORD  pDword = (PDWORD)(pFile + OffsetFromRVA(dwRVA));    // Dword đầu tiên
    18.             while (*pDword)
    19.             {
    20.                 // Làm gì đó với con trỏ 32 này
    21.                 pDword++;
    22.             }
    23.         }
    24.     }

    Tương tự, mã để nhận từng phần tử trong bảng IAT có thể :
    C++ Code:
    1.     void HandleIAT(PBYTE pFile, BOOL is64, PIMAGE_IMPORT_DESCRIPTOR pImport)
    2.     {
    3.         DWORD   dwRVA = pImport->FirstThunk;
    4.         if (!dwRVA)
    5.             return;
    6.         if (is64)
    7.         {
    8.             PIMAGE_THUNK_DATA64     pThunk64 = (PIMAGE_THUNK_DATA64)(pFile + OffsetFromRVA(dwRVA)); // Dword đầu tiên
    9.             while (pThunk64->u1.AddressOfData)
    10.             {
    11.                 // Làm gì đó với con trỏ 64 này
    12.                 pThunk64++;
    13.             }
    14.         }
    15.         else
    16.         {
    17.             PIMAGE_THUNK_DATA32     pThunk32 = (PIMAGE_THUNK_DATA32)(pFile + OffsetFromRVA(dwRVA)); // Dword đầu tiên
    18.             while (pThunk32->u1.AddressOfData)
    19.             {
    20.                 // Làm gì đó với con trỏ 32 này
    21.                 pThunk32++;
    22.             }
    23.         }
    24.     }

    Chú ý :
    _ Bảng INT hay IAT của mỗi thư viện có hàm được nhập sẽ được kết thúc bởi 1 phần tử mà được lấp đầy bằng 0.
    _ Thứ tự xuất hiện của các IMAGE_IMPORT_DESCRIPTOR và các khối trong các bảng INT và IAT là không liên quan nhau. Nghĩa là IMAGE_IMPORT_DESCRIPTOR xuất hiện trước có thể trỏ tới khối INT hay IAT sau hoặc ngược lại - Do sở thích và tính toán của TBD.

    Rắc rối nhất là sự liên kết giữa bảng INT và bảng IAT tới bảng IMAGE_IMPORT_BY_NAME (gọi là bảng Hint/Name) mà chúng ta sẽ xem tới.
    Theo sơ đồ bên trên thì mỗi phần tử của bảng INT hay bảng IAT là 1 RVA tới 1 phần tử trong bảng Hint/Name nhưng thực tế không phải vậy.

    Quay trở lại với trường IMAGE_FILE_HEADER.Characteristics, nếu cờ IMAGE_FILE_DLL là được bật chúng ta có PE theo định dạng Dll, ngược lại là định dạng exe.
    Đuôi gợi nhớ exe : (*.exe), (*.scr), ...
    Đuôi gợi nhớ dll : (*.dll), (*.ocx), (*.cpl), (*.ax), ...

    Trong định dạng exe :
    _ Các phần tử trong bảng INT-IAT hoặc là chỉ dẫn về 1 Ordinal toàn cục hoặc là RVA tới một IMAGE_IMPORT_BY_NAME (1 phần tử của bảng Hint/Name). Để biết chính xác nó là gì thì ta xem bit có trọng số lớn nhất có được bật hay không (hay chúng ta dùng toán tử & giữa giá trị với IMAGE_ORDINAL_FLAG). Nếu bit này = 1, số bít còn lại là giá trị Ordinal cho hàm nhập. Nếu bit này = 0, giá trị của phần tử này chính là RVA tới một phần tử của bảng Hint/Name.

    Trong định dạng dll:
    C++ Code:
    1. //      if (bit có trọng số lớn nhất == 1)
    2. //      {
    3. //          Nó dẫn ra giá trị Ordinal toàn cục;
    4. //      }
    5. //      else
    6. //      {
    7. //          Xem nó như một RVA;
    8. //          if (chuyển đổi ra Offset tập tin thành công)
    9. //          {
    10. //              Nó chính là RVA tới 1 phần tử trong bảng Hint / Name;
    11. //          }
    12. //          else
    13. //          {
    14. //              if (Giá trị của nó lớn hơn kích thước IMAGE_OPTIONAL_HEADER.SizeOfImage)
    15. //                  Nó là địa chỉ bộ nhớ;
    16. //              else
    17. //                  Giá trị của nó là Index chuỗi Forward.Không tìm thấy tài liệu nào chỉ dẫn thêm.
    18. //          }
    19. //      }

    Chúng ta xem cấu trúc Hint/Name (IMAGE_IMPORT_BY_NAMEs)
    . Hint : 2 Bytes -> Ordinal của hàm
    . Name[1] : Byte đầu tiên của chuỗi tên hàm.
    Chú ý : Độ dài của Name là tùy thuộc vào tên hàm nên bảng Hint/Name chúng ta không thể xem là một mảng các IMAGE_IMPORT_BY_NAME được. Trong tập tin nó được canh chỉnh theo WORD (kể cả byte NULL kết chuỗi) và nối tiếp nhau
    Ví dụ (Hex): 23 00 'R' 'e' 'a' 'd' 'F' 'i' 'l' 'e' 00 00 09 00 'G' 'e' 't' 'D' 'C' 00
    IMAGE_IMPORT_BY_NAME đầu tiên có Hint = 0x0023 có tên hàm là ReadFile. Vì chuỗi có 8 ký tự + NULL = 9 nên phải được chèn thêm 1 byte NULL cho đủ canh chỉnh 2 Byte
    IMAGE_IMPORT_BY_NAME thứ hai có Hint = 0x0009 có tên hàm là GetDC. Vì chuỗi có 5 ký tự + NULL = 6 đã đủ canh chỉnh nên không chèn gì cả.

    Phần thứ 2 của hôm nay là dẫn hướng từ thư mục IMAGE_DATA_DIECTORY[IMAGE_DIRECTORY_ENTRY_IAT]
    Chỉ mục này chỉ là dẫn hướng tới bảng IAT tổng của tất cả các bảng IAT con mà được dẫn xuất từ các IMAGE_IMPORT_DESCRIPTOR.
    Do nó rất quan trọng khi Windows nạp nó lên và thực thi trên vùng nhớ nên nó được dành riêng 1 điểm nhập.
    Để duyệt nó ta không thể cứ chạy con trỏ PIMAGE_THUNK_DATA cho đến khi giá trị trong con trỏ = 0 được, nguyên do là các bảng IAT con đã có các đánh dấu cuối bảng riêng của nó bằng phần tử lấp đẩy 0 như đã biết ở trên. Ở đây chúng ta có thể lấy số phần tử trong bảng IAT tổng bằng cách :

    if (is64)
    cElement = pDataDirectory->Size / sizeof(IMAGE_THUNK_DATA64);
    else
    cElement = pDataDirectory->Size / sizeof(IMAGE_THUNK_DATA32);

    pe4.exe từ bên dưới mô tả kỹ hơn về các thư mục trong ngày hôm nay.
    Attached Files Attached Files
    Yêu mã hơn yêu em !!!

  9. #9
    Ngày gia nhập
    02 2014
    Nơi ở
    TP.HCM
    Bài viết
    732

    Mặc định Xem qua thư mục tài nguyên của PE

    Mặc dù Windows có cung cấp một số API để làm việc với tài nguyên như : FindResource, LoadResource, UpdateResource,... nhưng vẫn hữu hạn, để biết chính xác vị trí của các tài nguyên trong tập tin cho mục đích riêng, chúng ta phải đi theo cấu trúc của PE.

    Thư mục tài nguyên được bắt đầu từ IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCE].
    Nếu trường Address và Size lớn hơn 0, nó là RVA tới một cấu trúc IMAGE_RESOURCE_DIRECTORY đầu tiên (RootResource), chúng ta có thể nhận con trỏ tới cấu trúc này
    PIMAGE_RESOURCE_DIRECTORY pRootResource = pFile + OffsetFromRVA(DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Address);
    và các trường tổng quát cấu trúc này :
    . DWORD Characteristics - Luôn luôn là 0
    . DWORD TimeDateStamp
    . WORD MajorVersion
    . WORD MinorVersion
    . WORD NumberOfNamedEntries - Số phần tử có tên của mảng IMAGE_RESOURCE_DIRECTORY_ENTRY[] theo ngay sau cấu trúc này (RootEntry).
    . WORD NumberOfIdEntries - Số phần tử sử dụng ID của mảng IMAGE_RESOURCE_DIRECTORY_ENTRY[] theo ngay sau cấu trúc này.
    Như vậy tổng số phần tử của mảng IMAGE_RESOURCE_DIRECTORY_ENTRY[] theo ngay sau là :
    WORD cElements = pRootResource->NumberOfNamedEntries + pRootResource->NumberOfIdEntries;

    Cấu trúc tổng quát của 1 IMAGE_RESOURCE_DIRECTORY_ENTRY gồm :
    . DWORD Name - Là ID của tài nguyên hoặc là Offset tới cấu trúc IMAGE_RESOURCE_DIR_STRING_U chứa kích thước chuỗi và chính chuỗi tên theo Unicode
    . DWORD OffsetToData - Offset tới IMAGE_RESOURCE_DIRECTORY hoặc IMAGE_RESOURCE_DATA_ENTRY.

    Nếu cấu trúc này đang là RootEntry thì OffsetToData là Offset tới IMAGE_RESOURCE_DIRECTORY cấp 2 (StyleResource)
    và theo ngay sau từng IMAGE_RESOURCE_DIRECTORY cấp 2 cũng là những IMAGE_RESOURCE_DIRECTORY_ENTRY cấp 2 (StyleEntry)

    Trường OffsetToData của mỗi IMAGE_RESOURCE_DIRECTORY_ENTRY cấp 2 sẽ là Offset tới IMAGE_RESOURCE_DIRECTORY cấp 3 (LastResource)
    và theo sau từng IMAGE_RESOURCE_DIRECTORY cấp 3 cũng là những IMAGE_RESOURCE_DIRECTORY_ENTRY cấp 3 (LastEntry)

    Ở phân cấp cuối cùng, trường OffsetToData của mỗi IMAGE_RESOURCE_DIRECTORY_ENTRY cấp 3 sẽ là Offset tới IMAGE_RESOURCE_DATA_ENTRY,
    IMAGE_RESOURCE_DATA_ENTRY này chứa các thông tin :
    . DWORD OffsetToData - RVA of Data - Dữ liệu thực sự của tài nguyên
    . DWORD Size - Size of Data
    . DWORD CodePage
    . DWORD Reserved

    Ghi nhớ :
    _ Các Offset bàn tới ở trên là Offset tính từ RootResource.
    _ Trong trường Name : Nếu bít có trọng số lớn nhất là 1, các bits còn lại là Offset tới IMAGE_RESOURCE_DIR_STRING_U, ngược lại nó là ID số.
    _ Trong trường OffsetToData : Nếu bit cao được thiết lập, nó là Offset tới IMAGE_RESOURCE_DIRECTORY cấp dưới, ngược lại các bits còn lại chỉ tới IMAGE_RESOURCE_DATA_ENTRY

    Mã tổng quát để duyệt hết thư mục tài nguyên có thể :
    C++ Code:
    1.     // pFile : Con trỏ bộ nhớ mà PE được đọc lên bằng ReadFile
    2.     // pResourceDataDirectory : Con trỏ tới IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCE]
    3.     // OffsetFromRVA : Hàm được đề cập trong các ngày trước
    4.     void ViewResource(PBYTE pFile, PIMAGE_DATA_DIRECTORY pResourceDataDirectory)
    5.     {
    6.         DWORD                           dwRootOffset = OffsetFromRVA(pResourceDataDirectory->VirtualAddress);
    7.         // Thư mục gốc
    8.         PIMAGE_RESOURCE_DIRECTORY       pRootResource = (PIMAGE_RESOURCE_DIRECTORY)(pFile + dwRootOffset);
    9.         WORD                            cRootEntries = pRootResource->NumberOfNamedEntries + pRootResource->NumberOfIdEntries;
    10.         // Điểm nhập gốc
    11.         PIMAGE_RESOURCE_DIRECTORY_ENTRY pRootEntry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pRootResource + 1);
    12.  
    13.         for (WORD wRoot = 0; wRoot < cRootEntries; wRoot++, pRootEntry++)
    14.         {
    15.             // Thư mục kiểu : Icon, bitmap, cursor, ...,
    16.             PIMAGE_RESOURCE_DIRECTORY       pStyleResource = (PIMAGE_RESOURCE_DIRECTORY)((PBYTE)pRootResource + (pRootEntry->OffsetToData & 0x7FFFFFFF));
    17.             WORD                            cStyleEntries = pStyleResource->NumberOfNamedEntries + pStyleResource->NumberOfIdEntries;
    18.             // Điểm nhập kiểu
    19.             PIMAGE_RESOURCE_DIRECTORY_ENTRY pStyleEntry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pStyleResource + 1);
    20.  
    21.             for (WORD wStyle = 0; wStyle < cStyleEntries; wStyle++, pStyleEntry++)
    22.             {
    23.                 // Thư mục cuối
    24.                 PIMAGE_RESOURCE_DIRECTORY       pLastResource = (PIMAGE_RESOURCE_DIRECTORY)((PBYTE)pRootResource + (pStyleEntry->OffsetToData & 0x7FFFFFFF));
    25.                 // Điểm nhập cuối
    26.                 PIMAGE_RESOURCE_DIRECTORY_ENTRY pLastEntry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pLastResource + 1);
    27.                 // Điểm nhập dữ liệu
    28.                 PIMAGE_RESOURCE_DATA_ENTRY      pDataEntry = (PIMAGE_RESOURCE_DATA_ENTRY)((PBYTE)pRootResource + (pLastEntry->OffsetToData & 0x7FFFFFFF));
    29.                 // Dữ liệu dòng của tài nguyên
    30.                 PBYTE                           pRawData = pFile + OffsetFromRVA(pDataEntry->OffsetToData);
    31.             }
    32.         }
    33.     }

    Mã cụ thể hơn đã có trong pe4.exe của ngày trước.

    Phần sau : Làm việc với Base Relocation
    Yêu mã hơn yêu em !!!

  10. #10
    Ngày gia nhập
    02 2014
    Nơi ở
    TP.HCM
    Bài viết
    732

    Mặc định Làm việc với Base Relocation

    Tại sao lại có thư mục Base Relocation ?

    TBD : Anh Windows ơi, tôi tạo ra cái exe, anh làm ơn nạp image của nó lên từ địa chỉ 0x400000 mà tôi đã cho trong trường IMAGE_OPTIONAL_HEADER.ImageBase nhé.
    Windows : Bộ chú mày tưởng địa chỉ đó của mình chú mày chắc, anh đây còn phải lo hàng trăm tiến trình rồi dịch vụ này nọ, đứa nào cũng nhè chỗ đó đòi nạp, hì không bảo đảm đâu nhé.
    TBD : Vậy chết em, mã đã biên dịch lấy các địa chỉ ảo của rất nhiều thành phần theo mốc địa chỉ đó, giờ anh nạp nó lên chỗ khác thì các địa chỉ ảo đó cũng chỉ tới chỗ khác thì làm sao đây. Anh xem lại giùm em giúp.
    Windows : Thì anh đã quy định, tất cả các địa chỉ ảo mà có thể bị chỉ tới chỗ khác đó, chú mày tập hợp hết lại - thông báo cho anh trong Base Relocation. Anh bảo đảm sẽ sửa lại gía trị trong nó cho phù hợp, chạy OK.

    Khi trình biên dịch-liên kết tạo ra tập tin EXE, nó giả định nơi mà tập tin sẽ được tạo bản đồ trong bộ nhớ. Trên cơ sở này, bộ liên kết đặt địa chỉ thực của mã và những phần dữ liệu vào trong tập tin thực thi. Nếu tập tin thực thi cuối cùng được nạp lên ở nơi khác trong vùng đĩa chỉ ảo, địa chỉ ảo mà bộ liên kết cài vào hình ảnh là không còn đúng. Thông tin được cất giữ trong phân đoạn Base Relocation bảo cho bộ nạp PE sửa lại những địa chỉ này trong hình ảnh đã nạp vì vậy mà chúng phù hợp hơn.
    Mặt khác, nếu bộ nạp PE có khả năng nạp tập tin tại địa chỉ cơ sở đúng theo trình liên kết, thì lúc này Base Relocation là không cần thiết nữa.

    Những điểm nhập trong phân đoạn Base Relocation được gọi là địa chỉ cấp phát lại và khi đó sử dụng chúng phụ thuộc vào địa chỉ cơ sở của hình ảnh được nạp.

    Ví dụ : Trong mã đã biên dịch có 1 lệnh nhảy tới : 0x401000. Bộ nạp đã nạp image lên địa chỉ 0x600000, nếu vẫn giữ nguyên lệnh nhảy tới 0x401000, thì rõ ràng lệnh đã nhảy tới một địa chỉ nằm trước cả image, thật vô tiền khoáng hậu. Lệnh nhảy này phải được là nhảy tới 0x601000 thì mới đúng theo ý định chương trình. Chênh lệch này là một khoảng 0x600000 - 0x400000 = 0x200000. Tất cả các mã và dữ liệu cần được định vị lại sẽ được cộng thêm khoảng này (khoảng delta).

    Thư mục Base Relocation được bắt đầu bằng 1 cấu trúc IMAGE_BASE_RELOCATION có các trường sau:
    . DWORD VirtualAddress : RVA tới một khối (mỗi khối thường là 0x1000) mà có các Relocations.
    . DWORD SizeOfBlock : Kích thước tổng của IMAGE_BASE_RELOCATION này và các WORD Relocations theo sau.

    Mã nhận IMAGE_BASE_RELOCATION đầu tiên có thể như sau :
    C++ Code:
    1.     PIMAGE_BASE_RELOCATION GetBaseRelocation()
    2.     {
    3.         PIMAGE_DATA_DIRECTORY   pDataDirectory = (PIMAGE_DATA_DIRECTORY)GetDataDirectory(IMAGE_DIRECTORY_ENTRY_BASERELOC);
    4.         DWORD                   dwAddress = pDataDirectory->VirtualAddress;
    5.         DWORD                   dwSize = pDataDirectory->Size;
    6.         if (dwAddress && dwSize)
    7.             return (PIMAGE_BASE_RELOCATION)(pFile + OffsetFromRVA(dwAddress));
    8.         return NULL;
    9.     }

    Ví dụ : PIMAGE_BASE_RELOCATION pReloc = GetBaseRelocation();
    Giả sử pReloc->VirtualAddress = 0x00061000 và pReloc->SizeOfBlock = 0x00000010.
    Thì mảng các WORDs theo là : (0x10 - sizeof(IMAGE_BASE_RELOCATION)) / 2 = (16 - 8)/2 = 4 => Có 4 WORDs theo sau IMAGE_BASE_RELOCATION này.
    Giả sử 4 WORD này là :
    _ 0xA258
    _ 0xA270
    _ 0xA278
    _ 0x0000
    Trong các WORDs này, 4 bits đầu là kiểu relocation, 12 bits sau là địa chỉ ảo của relocation, ta có :
    _ relocation 1 : 0x00061000 + 0x258 = 0x61258, kiểu IMAGE_REL_BASED_DIR64 (kiểu A)
    _ relocation 2 : 0x00061000 + 0x270 = 0x61270, kiểu IMAGE_REL_BASED_DIR64 (kiểu A)
    _ relocation 3 : 0x00061000 + 0x278 = 0x61278, kiểu IMAGE_REL_BASED_DIR64 (kiểu A)
    _ relocation 4 : 0x00061000 + 0x000 = 0x61000, kiểu IMAGE_REL_BASED_ABSOLUTE (kiểu 0) - Đây chỉ là relocation chèn vào để toàn khối là bội số của DWORD.

    Theo sau một khối IMAGE_BASE_RELOCATION + WORDs là một khối tương tự nối tiếp cho đến hết, ta nhận tuần tự như sau :
    C++ Code:
    1.     PIMAGE_BASE_RELOCATION GetBaseRelocation(PIMAGE_BASE_RELOCATION pPrevRelocation)
    2.     {
    3.         if (pPrevRelocation && pPrevRelocation->VirtualAddress)
    4.             return (PIMAGE_BASE_RELOCATION)((PBYTE)pPrevRelocation + pPrevRelocation->SizeOfBlock);
    5.         return NULL;
    6.     }

    Mã duyệt cụ thể hơn các Base Relocation có trong pe4.exe trong các ngày trước.

    Phần sau : Xem qua thư mục Security.
    Yêu mã hơn yêu em !!!

Quyền hạn của bạn

  • Bạn không thể gửi đề tài mới
  • Bạn không thể gửi bài trả lời
  • Bạn không thể gửi các đính kèm
  • Bạn không thể chỉnh sửa bài viết của bạn