Từ 1 tới 7 trên tổng số 7 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
    641

    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:	5
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
    964

    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
    641

    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
    964

    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
    641

    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:	5
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
    641

    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:	2
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
    641

    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 !!!

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