PE學習筆記(二) 選擇自 rivershan 的 Blog

來源:互聯網
上載者:User

五、Section Table(節表)

 節表是緊挨著 PE Header 的一結構數組。該數群組成員的數目由 File Header (IMAGE_FILE_HEADER) 結構中 NumberOfSections 域的域值來決定。節表成員結構又命名為 IMAGE_SECTION_HEADER(四十位元組)。其結構定義:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
        DWORD   PhysicalAddress;
        DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

 IMAGE_SECTION_HEADER 結構成員含義:

1.IMAGE_SIZEOF_SHORT_NAME:不超過8位元組的節名。節名僅是個標記,我們選擇任何名字甚至空著也行,不能用null結束。命名不是一個ASCIIZ字串,所以不用null結尾。

2.PhysicalAddress:指定檔案地址。

3.VirtualSize:這個域的意義與程式類型有關。如果是EXE,代表當節被裝入記憶體之後的大小總和,這是在它們被調整為最接近檔案對齊粒度的倍數之前的大小。稍後的SizeOfRawData則是調整後的大小。對於OBJ文檔,這個域沒有意義。

4.VirtualAddress:本節的RVA(相對虛擬位址)。PE裝載器將節映射至記憶體時會讀取本值,因此如果域值是1000h,而PE檔案裝在地址400000h處,那麼本節就被載到401000h。微軟把第一個Section 的此域值設為0x1000h。對於OBJ文檔,此域沒意義,總為0。
 
5.SizeOfRawData:經過檔案對齊處理後節尺寸,PE裝載器提取本域值瞭解需映射入記憶體的節位元組數。 假設一個檔案的檔案對齊尺寸是0x200,如果前面的 VirtualSize 域指示本節長度是0x388位元組,則本域值為0x400,表示本節是0x400位元組長。在obj中,這個與表示有編譯器或 組譯器 指定的真正的section 大小。

6.PointerToRawData:這是本節基於檔案的位移量,PE裝載器通過本域值找到節資料在檔案中的位置。 如果你自己以記憶體映射的方式應設了一個PE程式(而不是由作業系統的裝載器載入),那麼你就必鬚根據此值找到本節的資訊,而不是根據VirtualAddress 中的RVA值。

7.PointerToRelocations:在OBJs中,這是以程式開始為基準的位移量,用來指向section 的重定位資訊。每個OBJ section 的重定位資訊緊跟在section 資訊之後。在EXEs中,這個域(一記下一個域)沒有意義,總是為0。但連結器產生一個EXE,它會決定大部分的待修正紀錄(fixups),只剩下基址的重定位地址以及 imported 函數的重定位地址,留待載入時在解決。兩份相同的資訊放在 base relocation section 和imported function section 之中,所以EXEs 不需要在每一個 section 之後又有重定位資訊。

8.PointerToLinenumbers:行號表的位移量(以程式開始為基準)。行號表與原始碼行號和其被映射到記憶體中的位置有關。在EXE檔案中,行號資訊被放在程式的最尾端。如果沒有COFF行好,設為0。

9.NumberOfRelocations:重定位表格(由PointerToRelocations 指向)中的重定位項目的個數。此域只用於OBJ中。EXE中為0。

10.NumberOfLinenumbers:行號表格(由PointerToLinenumbers 指向)中的行號個數。

11.Characteristics:包含標記以指示節屬性,比如節是否含有可執行代碼、初始化資料、未初始資料,是否可寫、可讀等。 下面是一些標記:

IMAGE_SCN_TYPE_REG      Reserved.
IMAGE_SCN_TYPE_DSECT     Reserved.
IMAGE_SCN_TYPE_NOLOAD    Reserved.
IMAGE_SCN_TYPE_GROUP     Reserved.
IMAGE_SCN_TYPE_NO_PAD     Reserved.
IMAGE_SCN_TYPE_COPY     Reserved.
IMAGE_SCN_CNT_CODE      Section contains executable code.
IMAGE_SCN_CNT_INITIALIZED_DATA   Section contains initialized data.
IMAGE_SCN_CNT_UNINITIALIZED_DATA  Section contains uninitialized data.
IMAGE_SCN_LNK_OTHER     Reserved.
IMAGE_SCN_LNK_INFO      Reserved.
IMAGE_SCN_TYPE_OVER     Reserved.
IMAGE_SCN_LNK_COMDAT     Section contains COMDAT data. 
IMAGE_SCN_MEM_FARDATA     Reserved.
IMAGE_SCN_MEM_PURGEABLE    Reserved.
IMAGE_SCN_MEM_16BIT     Reserved.
IMAGE_SCN_MEM_LOCKED     Reserved.
IMAGE_SCN_MEM_PRELOAD     Reserved.
IMAGE_SCN_ALIGN_1BYTES     Align data on a 1-byte boundary. 
IMAGE_SCN_ALIGN_2BYTES     Align data on a 2-byte boundary. 
IMAGE_SCN_ALIGN_4BYTES     Align data on a 4-byte boundary. 
IMAGE_SCN_ALIGN_8BYTES     Align data on a 8-byte boundary. 
IMAGE_SCN_ALIGN_16BYTES    Align data on a 16-byte boundary. 
IMAGE_SCN_ALIGN_32BYTES    Align data on a 32-byte boundary. 
IMAGE_SCN_ALIGN_64BYTES    Align data on a 64-byte boundary. 
IMAGE_SCN_LNK_NRELOC_OVFL    Section contains extended relocations.
IMAGE_SCN_MEM_DISCARDABLE    Section can be discarded as needed.
IMAGE_SCN_MEM_NOT_CACHED    Section cannot be cached.
IMAGE_SCN_MEM_NOT_PAGED    Section cannot be paged.
IMAGE_SCN_MEM_SHARED     Section can be shared in memory.
IMAGE_SCN_MEM_EXECUTE     Section can be executed as code.
IMAGE_SCN_MEM_READ      Section can be read.
IMAGE_SCN_MEM_WRITE     Section can be written to.

 遍曆節表的步驟:

1.PE檔案有效性校正。
2.定位到 PE Header 的起始地址。
3.從 file Header 的 NumberOfSections域擷取節數。
4.通過兩種方法定位節表: ImageBase+SizeOfHeaders 或者 PE header的起始地址+ PE header結構大小。 (節表緊隨 PE Header)。如果不是使用檔案對應的方法,可以用SetFilePointer 直接將檔案指標定位到節表。節表的檔案位移量存放在 SizeOfHeaders域裡(SizeOfHeaders 是 IMAGE_OPTIONAL_HEADER 的結構成員) 。
5.處理每個 IMAGE_SECTION_HEADER 結構。

六、Import Table(匯入表)

6.1、匯入函數:

 一個匯入函數是被某模組調用的但又不在調用者模組中的函數,因而命名為"import(匯入)"。匯入函數實際位於一個或者更多的DLL裡。調用者模組裡只保留一些函數資訊,包括函數名及其駐留的DLL名。

    PE 程式被載入到記憶體之前,存放在 可攜式執行檔的 .data 中的內容是給裝載器用來決定函數位置並修補它們以便完成image 用的。而在被載入之後,.idata內含有的是指向 EXE/DLL 的匯入函數的指標。

6.2、Data Directory:

 Data Directory 是一個 IMAGE_DATA_DIRECTORY 結構數組,共有16個成員。Data Directory 包含了PE檔案中各重要資料結構的位置和尺寸資訊。 每個成員包含了一個重要資料結構的資訊。

 Data Directory 的每個成員都是 IMAGE_DATA_DIRECTORY 結構類型的,其定義如下所示:

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

 IMAGE_DATA_DIRECTORY 結構成員含義:
 
1.VirtualAddress: 實際上是資料結構的相對虛擬位址(RVA)。比如,如果該結構是關於Import Symbols的,該域就包含指向IMAGE_IMPORT_DESCRIPTOR 數組的RVA。

2.Size: 含有VirtualAddress所指向資料結構的位元組數。

6.3、找尋PE檔案中重要資料結構的一般方法:

1、從 DOS Header 定位到 PE Header。
2、從 Optional Header 讀取 Data Directory 的地址。
3、IMAGE_DATA_DIRECTORY 結構尺寸乘上找尋結構的索引號:比如您要找尋Import Symbols的位置資訊,必須用IMAGE_DATA_DIRECTORY 結構尺寸(8 bytes)乘上1(Import Symbols 在 Data Diectory 中的索引號)。
4、將上面的結果加上 Data Diectory 地址,我們就得到包含所查詢資料結構資訊的 IMAGE_DATA_DIRECTORY 結構項。

6.4、匯入表:

 Data Directory 數組第一項的 VirtualAddress 包含匯入表地址。匯入表實際上是一個 IMAGE_IMPORT_DESCRIPTOR 結構數組。每個結構包含PE檔案匯入函數的一個相關DLL的資訊。該數組以一個全0的成員結尾。

 IMAGE_IMPORT_DESCRIPTOR結構組成:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    };
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date/time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

 IMAGE_IMPORT_DESCRIPTOR 結構成員含義:
 
1.結構第一項是一個union子結構。事實上,這個union子結構只是給 OriginalFirstThunk 增添了個別名,您也可以稱其為"Characteristics"。該成員項含有指向一個 IMAGE_THUNK_DATA 結構數組的RVA。

2.TimeDateStamp:程式產生的時刻。此域通常為0。微軟的 BIND 程式可以將此 IMAGE_IMPORT_DESCRIPTOR 所對應的dll的產生時刻寫到這裡來。

3.ForwarderChain:此域涉及到 forwarding(轉交),意味著一個dll 函數在調用另一個 dll。例如,在 WINNT 中,Kernel32.dll 將它的某些輸出函數轉交給 NTDLL.dll。應用程式可能以為它調用 Kernel32.dll,而事實上它調用的事NTDLL.dll。這個域中含有一個索引,指向 FirstThunk 數組。被這個索引所指定的函數就是一個轉交函數。

3.Name:含有指向DLL名字的RVA,即指向DLL名字的指標,也是一個ASCII字串。

4.FirstThunk:與 OriginalFirstThunk 非常相似,它也包含指向一個 IMAGE_THUNK_DATA 結構數組的RVA(當然這是另外一個IMAGE_THUNK_DATA 結構數組)。

 IMAGE_IMPORT_DESCRIPTOR 數組中,最重要的部分是 imported DLL 的名稱以及兩個 IMAGE_THUNK_DATA 數組。每個 IMAGE_THUNK_DATA 對應一個匯入函數。在exe中,兩個數組(分別由 Characteristics 和 FirstThunk 域指向)平行存在,並且都以 NULL 位結束符。

 為什麼需要兩個平行數組?第一個數組(由 Characteristics 指向)從不被修改,有時它被稱為 hint-name table。第二個數組(由 FirstThunk 指向)則被裝載器改寫。裝載器一一檢查每一個 IMAGE_THUNK_DATA 並且找出它所記錄的函數的地址,然後把地址寫入 IMAGE_THUNK_DATA 這個 DWORD 之中。由於這個 IMAGE_THUNK_DATA 數組內容已經被裝載器改寫為輸入函數的地址,所以它又被叫做 Import Address Table(IAT)。IAT 是一個可寫地區。API Hook 就利用到這一特性。PE裝載器載入PE後,FirstThunk 指向的 IMAGE_THUNK_DATA 被改寫,而 Characteristics 所指向的 IMAGE_THUNK_DATA 沒有被改寫。所以若還反過頭來尋找匯入函數名,PE裝載器還能夠根據 Characteristics 所指向的 IMAGE_THUNK_DATA 找尋到函數名。 
 
6.4、IMAGE_THUNK_DATA:

 IMAGE_THUNK_DATA是一個DWORD類型的集合。通常我們將其解釋為指向一個 IMAGE_IMPORT_BY_NAME 結構的指標。注意 IMAGE_THUNK_DATA 包含了指向一個 IMAGE_IMPORT_BY_NAME 結構的指標,而不是結構本身。

 IMAGE_THUNK_DATA 結構定義:
 
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        PBYTE   ForwarderString;
        PDWORD  Function;
        DWORD  Ordinal;
        PIMAGE_IMPORT_BY_NAME  AddressOfData;
    } u1;
} IMAGE_THUNK_DATA32;

 IMAGE_THUNK_DATA 實在PE被載入之後才被決定的。WIN32裝載器使用 IMAGE_THUNK_DATA 的初始內容(可能是函數名稱也可能是函數序號)來尋找輸入函數的位置。然後裝載器就以獲得的地址改寫 IMAGE_THUNK_DATA 的內容。 

6.5、IMAGE_IMPORT_BY_NAME:

 IMAGE_IMPORT_BY_NAME 結構定義:
 
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

1.Hint:指示本函數在其所駐留DLL的匯出表中的索引號。該域被PE裝載器用來在DLL的匯出表裡快速查詢函數。該值不是必須的,一些連接器將此值設為0。

2.Name:含有匯入函數的函數名。函數名是一個ASCII字串。注意這裡雖然將Name的大小定義成位元組,其實它是可變尺寸域,只不過我們沒有更好方法來表示結構中的可變尺寸域。這個結構被提供用於查閱描述名字的結構。

 有些情況下一些函數僅由序數匯出,也就是說不能用函數名來調用它們,只能用它們的位置來調用。此時,調用者模組中就不存在該函數的 IMAGE_IMPORT_BY_NAME 結構。不同的,對應該函數的 IMAGE_THUNK_DATA 值的低位字指示函數序數,而最高二進位 (MSB)設為1。例如,如果一個函數只由序數匯出且其序數是1234h,那麼對應該函數的 IMAGE_THUNK_DATA 值是80001234h。Microsoft提供了一個方便的常量來測試dword值的MSB位,就是 IMAGE_ORDINAL_FLAG32,其值為80000000h。

6.6、列出某個PE檔案的所有匯入函數步驟:

1、校正檔案是否是有效PE。
2、從 DOS Header 定位到 PE Header。
3、擷取位於 OptionalHeader 資料目錄位址。
4、轉至資料目錄的第二個成員提取其VirtualAddress值。
5、利用上值定位第一個 IMAGE_IMPORT_DESCRIPTOR 結構。
6、檢查 OriginalFirstThunk值。若不為0,順著 OriginalFirstThunk 裡的RVA值轉入那個RVA數組。若 OriginalFirstThunk 為0,就改用FirstThunk值。有些連接器產生PE檔案時會置OriginalFirstThunk值為0,這應該算是個bug。不過為了安全起見,我們還是檢查 OriginalFirstThunk值先。
7、對於每個數組元素,我們比對元素值是否等於IMAGE_ORDINAL_FLAG32。如果該元素值的最高二進位為1,那麼函數是由序數匯入的,可以從該值的低位元組提取序數。
8、如果元素值的最高二進位為0,就可將該值作為RVA轉入 IMAGE_IMPORT_BY_NAME 數組,跳過 Hint 就是函數名字了。
9、再跳至下一個數組元素提取函數名一直到數組底部(它以null結尾)。現在我們已遍曆完一個DLL的匯入函數,接下去處理下一個DLL。
10、即跳轉到下一個 IMAGE_IMPORT_DESCRIPTOR 並處理之,如此這般迴圈直到數組見底。(IMAGE_IMPORT_DESCRIPTOR 數組以一個全0域元素結尾)。

6.7、Bound Import:

 當PE裝載器裝入PE檔案時,檢查匯入表並將相關DLLs映射到進程地址空間。然後象我們這樣遍曆IMAGE_THUNK_DATA 數組並用匯入函數的真真實位址替換IMAGE_THUNK_DATAs 值。這一步需要很多時間。如果程式員能事先正確預測函數地址,PE裝載器就不用每次裝入PE檔案時都去修正IMAGE_THUNK_DATAs 值了。Bound import就是這種思想的產物。
 
 Microsoft 出品的類似Visual Studio的編譯器多提供了bind.exe這樣的工具,由它檢查PE檔案的匯入表並用匯入函數的真真實位址替換IMAGE_THUNK_DATA 值。當檔案裝入時,PE裝載器必定檢查地址的有效性,如果DLL版本不同於PE檔案存放的相關資訊,或則DLLs需要重定位,那麼裝載器認為原先計算的地址是無效的,它必定遍曆OriginalFirstThunk指向的數組以擷取匯入函數新地址。

七、Export Table(匯出表)

 當PE裝載器執行一個程式,它將相關DLLs都裝入該進程的地址空間。然後根據主程式的匯入函數資訊,尋找相關DLLs中的真實函數地址來修正主程式。PE裝載器搜尋的是DLLs中的匯出函數。PE 程式把它的匯出函數相關資訊放在.edata 中。

 DLL/EXE要匯出一個函數給其他DLL/EXE使用,有兩種實現方法: 通過函數名匯出或者僅僅通過序數匯出。比如某個DLL要匯出名為"GetSysConfig"的函數,如果它以函數名匯出,那麼其他DLLs/EXEs若要調用這個函數,必須通過函數名,就是GetSysConfig。另外一個辦法就是通過序數匯出。序數是唯一指定DLL中某個函數的16位元字,在所指向的DLL裡是獨一無二的。例如在上例中,DLL可以選擇通過序數匯出,假設是16,那麼其他DLLs/EXEs若要調用這個函數必須以該值作為GetProcAddress調用參數。這就是所謂的僅僅靠序數匯出。 

7.1 匯出表是資料目錄的第一個成員,又可稱為 IMAGE_EXPORT_DIRECTORY。結構定義:
 
 typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

 IMAGE_EXPORT_DIRECTORY 結構成員含義:

1.Characteristics:此域沒有用途,總是為0。

2.TimeDateStamp:程式被產生的時刻。

3.MajorVersion/MinorVersion:無實際用途,0。

4.Name:一個 RVA 值,指向一個 ASCIIZ 字串(dll 名稱,如MYDLL.dll)。模組的真實名稱。本域是必須的,因為檔案名稱可能會改變。這種情況下,PE裝載器將使用這個內部名字。

3.Base:基數,加上序數就是函數地址數組的索引值了。

4.NumberOfFunctions:模組匯出的函數/符號總數。

5.NumberOfNames:通過名字匯出的函數/符號數目。該值不是模組匯出的函數/符號總數,這是由上面的NumberOfFunctions給出。本域可以為0,表示模組可能僅僅通過序數匯出。如果模組根本不匯出任何函數/符號,那麼資料目錄中匯出表的RVA為0。

6.AddressOfFunctions:模組中有一個指向所有函數/符號的RVAs數組,本域就是指向該RVAs數組的RVA。簡言之,模組中所有函數的RVAs都儲存在一個數組裡,本域就指向這個數組的首地址。

7.AddressOfNames:類似上個域,模組中有一個指向所有函數名的RVAs數組,本域就是指向該RVAs數組的RVA。

9.AddressOfNameOrdinals:RVA,指向包含上述 AddressOfNames數組中相關函數之序數的16位元組。

 匯出表的設計是為了方便PE裝載器工作。
 
 首先,模組必須儲存所有匯出函數的地址以供PE裝載器查詢。模組將這些資訊儲存在AddressOfFunctions域指向的數組中,而數組元素數目存放在NumberOfFunctions域中。 因此,如果模組匯出40個函數,則AddressOfFunctions指向的數組必定有40個元素,而NumberOfFunctions值為40。

 現在如果有一些函數是通過名字匯出的,那麼模組必定也在檔案中保留了這些資訊。這些名字的RVAs存放在一數組中以供PE裝載器查詢。該數組由AddressOfNames指向,NumberOfNames包含名字數目。考慮一下PE裝載器的工作機制,它知道函數名,並想以此擷取這些函數的地址。至今為止,模組已有兩個模組: 名字數組和地址數組,但兩者之間還沒有聯絡的紐帶。因此我們還需要一些聯絡函數名及其地址的東東。PE參考指出使用到地址數組的索引作為聯結,因此PE裝載器在名字數組中找到匹配名字的同時,它也擷取了指向地址表中對應元素的索引。而這些索引儲存在由AddressOfNameOrdinals域指向的另一個數組(最後一個)中。由於該數組是起了聯絡名字和地址的作用,所以其元素數目必定和名字數組相同,比如,每個名字有且僅有一個相關地址,反過來則不一定: 每個地址可以有好幾個名字來對應。因此我們給同一個地址取"別名"。為了起到串連作用,名字數組和索引數組必須並行地成對使用,譬如,索引數組的第一個元素必定含有第一個名字的索引,以此類推。

7.2 如果我們有了匯出函數名並想以此擷取地址,可以這麼做:

1、定位到PE Header。
2、從資料目錄讀取匯出表的虛擬位址。
3、定位匯出表擷取名字數目(NumberOfNames)。
4、並行遍曆AddressOfNames和AddressOfNameOrdinals指向的數組匹配名字。如果在AddressOfNames 指向的數組中找到匹配名字,從AddressOfNameOrdinals 指向的數組中提取索引值。例如,若發現匹配名字的RVA存放在AddressOfNames 數組的第77個元素,那就提取AddressOfNameOrdinals數組的第77個元素作為索引值。如果遍曆完NumberOfNames 個元素,說明當前模組沒有所要的名字。
5、從AddressOfNameOrdinals 數組提取的數值作為AddressOfFunctions 數組的索引。也就是說,如果值是5,就必須讀取AddressOfFunctions 數組的第5個元素,此值就是所要函數的RVA。

7.3 假設我們只有函數的序數,那麼怎樣擷取函數地址呢,可以這麼做:

1、定位到PE Header。
2、從資料目錄讀取匯出表的虛擬位址。
3、定位匯出表擷取nBase值。
4、減掉nBase值得到指向AddressOfFunctions 數組的索引。
5、將該值與NumberOfFunctions作比較,大於等於後者則序數無效。
6、通過上面的索引就可以擷取AddressOfFunctions 數組中的RVA了。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.