Based on Android's elf plt/GOT symbol redirection process and ELF Hook implementation, androidelf

Source: Internet
Author: User

Based on Android's elf plt/GOT symbol redirection process and ELF Hook implementation, androidelf

The redirection process of elf plt/GOT based on Android and the implementation of ELF Hook -- by low-end code Nong 2014.10.27 Introduction

There are two main reasons for writing this article:

  • First, I found that most of the articles on the Internet describing the PLT/GOT symbol redirection process are for x86. For example, Redirecting functions in shared ELF libraries writes very well. Although the process is very similar to that of ARM, commands vary greatly due to different CPU Systems;
  • Second, most of the ELF file formats on the Internet are based on the Linking View, And the Link View parses ELF Based on the Section. However, during the loading process of the dynamic link library, linker only pays attention to Segment information in ELF. Therefore, the section information in ELF is completely tampered with or even deleted, and does not affect the linker loading process. This prevents static analysis tools (such as IDA and readelf) from analyzing it, generally, ELF files with shells are processed in this way. For such ELF files, if you want to implement the hook function, you must parse the Symbols Based on the Execution View;
Preparation

Before reading this article, make sure you have a rough understanding of the ELF File Format and ARM Assembly. For more information, see:

  • ELF File Format analysis;
  • ARM document;

Preparation tools:

  • Readelf (NDK inclusion)
  • Objdump (NDK included)
  • IDA Pro 6.4 or above
  • Android real machine or Simulator
Symbol redirection

On ARM, there are three common redirection types:R_ARM_JUMP_SLOT,R_ARM_ABS32AndR_ARM_GLOB_DATTo hook the elf function, you must handle the three types of redirection at the same time.

Example

First look at the sample code

typedef int (*strlen_fun)(const char *);strlen_fun global_strlen1 = (strlen_fun)strlen;strlen_fun global_strlen2 = (strlen_fun)strlen;#define SHOW(x) LOGI("%s is %d", #x, x)extern "C" jint Java_com_example_allhookinone_HookUtils_elfhook(JNIEnv *env, jobject thiz){    const char *str = "helloworld";    strlen_fun local_strlen1 = (strlen_fun)strlen;    strlen_fun local_strlen2 = (strlen_fun)strlen;    int len0 = global_strlen1(str);    int len1 = global_strlen2(str);    int len2 = local_strlen1(str);    int len3 = local_strlen2(str);    int len4 = strlen(str);    int len5 = strlen(str);    SHOW(len0);    SHOW(len1);    SHOW(len2);    SHOW(len3);    SHOW(len4);    SHOW(len5);    return 0;}

This Code calls strlen in three different ways: global function pointer, local function pointer, and direct call. In this example, we analyze the three call analyses respectively.

First, read the redirection table through readelf, as shown below:

Relocation section '.rel.dyn' at offset 0x2a48 contains 17 entries: Offset     Info    Type            Sym.Value  Sym. Name0000ade0  00000017 R_ARM_RELATIVE   0000af00  00000017 R_ARM_RELATIVE   0000af0c  00000017 R_ARM_RELATIVE   0000af10  00000017 R_ARM_RELATIVE   0000af18  00000017 R_ARM_RELATIVE   0000af1c  00000017 R_ARM_RELATIVE   0000af20  00000017 R_ARM_RELATIVE   0000af24  00000017 R_ARM_RELATIVE   0000af28  00000017 R_ARM_RELATIVE   0000af30  00000017 R_ARM_RELATIVE   0000aefc  00003215 R_ARM_GLOB_DAT    00000000   __stack_chk_guard0000af04  00003715 R_ARM_GLOB_DAT    00000000   __page_size0000af08  00004e15 R_ARM_GLOB_DAT    00000000   strlen0000b004  00004e02 R_ARM_ABS32       00000000   strlen0000b008  00004e02 R_ARM_ABS32       00000000   strlen0000af14  00006615 R_ARM_GLOB_DAT    00000000   __gnu_Unwind_Find_exid0000af2c  00007415 R_ARM_GLOB_DAT    00000000   __cxa_call_unexpected......Relocation section '.rel.plt' at offset 0x2ad0 contains 48 entries: Offset     Info    Type            Sym.Value  Sym. Name0000af40  00000216 R_ARM_JUMP_SLOT   00000000   __cxa_atexit0000af44  00000116 R_ARM_JUMP_SLOT   00000000   __cxa_finalize0000af48  00001716 R_ARM_JUMP_SLOT   00000000   memcpy...0000afd4  00004c16 R_ARM_JUMP_SLOT   00000000   fgets0000afd8  00004d16 R_ARM_JUMP_SLOT   00000000   fclose0000afdc  00004e16 R_ARM_JUMP_SLOT   00000000   strlen0000afe0  00004f16 R_ARM_JUMP_SLOT   00000000   strncmp......

In the section ". rel. plt" and ". rel. dyn", we found four strlen in total. We first record their key information, which will be useful for later analysis. They are

. Rel. dyn 116af08 R_ARM_GLOB_DAT

. Rel. dyn 116b004 R_ARM_ABS32.rel.dyn 116b008 R_ARM_ABS32.rel.plt 0000 AFDC R_ARM_JUMP_SLOT

In the code, we called strlen six times in total, but why only four times? In addition, how do they correspond to each other? analyze the assembly code with these problems. Drag the compiled so to IDA. We can see the command of the sample code:

.text:000050BC                 EXPORT Java_com_example_allhookinone_HookUtils_elfhook.text:000050BC Java_com_example_allhookinone_HookUtils_elfhook.text:000050BC.text:000050BC var_40          = -0x40.text:000050BC var_38          = -0x38.text:000050BC var_34          = -0x34.text:000050BC s               = -0x2C.text:000050BC var_28          = -0x28.text:000050BC var_24          = -0x24.text:000050BC var_20          = -0x20.text:000050BC var_1C          = -0x1C.text:000050BC var_18          = -0x18.text:000050BC var_14          = -0x14.text:000050BC var_10          = -0x10.text:000050BC var_C           = -0xC.text:000050BC.text:000050BC                 PUSH            {R4,LR}.text:000050BE                 SUB             SP, SP, #0x38.text:000050C0                 STR             R0, [SP,#0x40+var_34].text:000050C2                 STR             R1, [SP,#0x40+var_38].text:000050C4                 LDR             R4, =(_GLOBAL_OFFSET_TABLE_ - 0x50CA).text:000050C6                 ADD             R4, PC ; _GLOBAL_OFFSET_TABLE_.text:000050C8                 LDR             R3, =(aHelloworld - 0x50CE).text:000050CA                 ADD             R3, PC  ; "helloworld".text:000050CC                 STR             R3, [SP,#0x40+s].text:000050CE                 LDR             R3, =(strlen_ptr - 0xAF34).text:000050D0                 LDR             R3, [R4,R3] ; __imp_strlen.text:000050D2                 STR             R3, [SP,#0x40+var_28].text:000050D4                 LDR             R3, =(strlen_ptr - 0xAF34).text:000050D6                 LDR             R3, [R4,R3] ; __imp_strlen.text:000050D8                 STR             R3, [SP,#0x40+var_24].text:000050DA                 LDR             R3, =(global_strlen1_ptr - 0xAF34).text:000050DC                 LDR             R3, [R4,R3] ; global_strlen1.text:000050DE                 LDR             R3, [R3].text:000050E0                 LDR             R2, [SP,#0x40+s].text:000050E2                 MOVS            R0, R2.text:000050E4                 BLX             R3.text:000050E6                 MOVS            R3, R0.text:000050E8                 STR             R3, [SP,#0x40+var_20].text:000050EA                 LDR             R3, =(global_strlen2_ptr - 0xAF34).text:000050EC                 LDR             R3, [R4,R3] ; global_strlen2.text:000050EE                 LDR             R3, [R3].text:000050F0                 LDR             R2, [SP,#0x40+s].text:000050F2                 MOVS            R0, R2.text:000050F4                 BLX             R3.text:000050F6                 MOVS            R3, R0.text:000050F8                 STR             R3, [SP,#0x40+var_1C].text:000050FA                 LDR             R2, [SP,#0x40+s].text:000050FC                 LDR             R3, [SP,#0x40+var_28].text:000050FE                 MOVS            R0, R2.text:00005100                 BLX             R3.text:00005102                 MOVS            R3, R0.text:00005104                 STR             R3, [SP,#0x40+var_18].text:00005106                 LDR             R2, [SP,#0x40+s].text:00005108                 LDR             R3, [SP,#0x40+var_24].text:0000510A                 MOVS            R0, R2.text:0000510C                 BLX             R3.text:0000510E                 MOVS            R3, R0.text:00005110                 STR             R3, [SP,#0x40+var_14].text:00005112                 LDR             R3, [SP,#0x40+s].text:00005114                 MOVS            R0, R3  ; s.text:00005116                 BLX             strlen.text:0000511A                 MOVS            R3, R0.text:0000511C                 STR             R3, [SP,#0x40+var_10].text:0000511E                 LDR             R3, [SP,#0x40+s].text:00005120                 MOVS            R0, R3  ; s.text:00005122                 BLX             strlen.text:00005126                 MOVS            R3, R0    ...    ....text:000051CA                 ADD             SP, SP, #0x38.text:000051CC                 POP             {R4,PC}.text:000051CC ; End of function Java_com_example_allhookinone_HookUtils_elfhook

First, find out several important addresses. They are

  • GLOBAL_OFFSET_TABLE: 0x0000AF34
  • Strlen_ptr: 0x0000AF08
  • _ Imp_strlen: 0x0000B0C8
  • Global_strlen1_ptr: 0x0000AF0C
  • Global_strlen1: 0x0000B004
  • Global_strlen2_ptr: 0x0000AF10
  • Global_strlen2: 0x0000B008
Global function pointer calls external Function

The call of global_strlen1 and global_strlen2 corresponds to the BLX commands at 0x000050E4 and 0x000050F4. After calculation, the final R3 values are * struct and * global_strlen2 respectively, while the values of global_strlen1 and global_strlen2 exactly correspond. rel. two R_ARM_ABS32 relocation items of dyn, So we come to the conclusion:The global function pointer is used to call an external function. Its relocation type is R_ARM_ABS32 and is located in the. rel. dyn section..

We only analyze the call process of global_strlen1. First, locate global_strlen1_ptr (0x0000AF0C). The address is located in the. got section,GLOBAL_OFFSET_TABLE. Then, use global_strlen1_ptr to locate 0x0000B004 (in the. data Section), and then use 0x0000B004 to locate the final function address,Therefore, the Offset of the R_ARM_ABS32 relocation item points to the address of the final called function address (that is, the pointer to the function pointer)The entire relocation process is first implemented in. got and then located in. date from. got. The following is the hexadecimal representation of the. got segment:

...0000AF0C  04 B0 00 00 08 B0 00 00  DC B0 00 00 B4 87 00 000000AF1C  F4 84 00 00 60 5B 00 00  58 5B 00 00 50 5B 00 000000AF2C  EC B0 00 00 FC 8C 00 00  00 00 00 00 00 00 00 00...0000B004  C8 B0 00 00 C8 B0 00 00  ?? ?? ?? ?? ?? ?? ?? ??0000B014  ?? ?? ?? ?? ?? ?? ?? ??  ?? ?? ?? ?? ?? ?? ?? ??0000B024  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00...0000B0C8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 000000B0D8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00...

Finally, it is found that the instruction for 0x0000B0C8 address slices is all 0. When the link is dynamic, the linker will overwrite the value of 0x0000B004 address, pointing to the real address of strlen (instead of the current 0x0000B0C8, a little round ).

Partial function pointer calls external Function

The call of local_strlen1 and local_strlen2 corresponds to the BLX command at 0x00005100 and 0x0000510C. After calculation, the final R3 value is * strlen_prt, that is, 0x0000AF08. rel. r_ARM_GLOB_DAT relocation item in dyn, So we come to the conclusion:Call an external function by using a local function pointer. Its relocation type is R_ARM_GLOB_DAT and is located in the. re. dyn section..

We only analyze the call process of local_strlen1. First, locate strlen_prt (0x0000AF08), which is located in the. got section,GLOBAL_OFFSET_TABLEAnd then use strlen_prt to locate 0x0000B0C8, which is the same as the analysis result above,So R_ARM_GLOB_DAT's reset Offset points to the address of the final called function address (that is, the pointer to the function pointer)The following is the hexadecimal representation of the. got segment:

0000AF08  C8 B0 00 00 04 B0 00 00  08 B0 00 00 DC B0 00 000000AF18  B4 87 00 00 F4 84 00 00  60 5B 00 00 58 5B 00 000000AF28  50 5B 00 00 EC B0 00 00  FC 8C 00 00 00 00 00 00...0000B0C8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 000000B0D8  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00...

Note that the 0x000050D8 command "STR R3, [SP, #0x40 + var_24]" has saved the real address of the function to the stack,Therefore, even if we modify the GOT table, the stack value will not be affected. Therefore, this type of relocation cannot be hook by modifying the address..

Directly call external functions

Finally, let's take a look at the direct call of strlen, which corresponds to the BLX commands at 0x00000000a and 0x00005122. Finally, they all point to the. plt section commands, as shown below:

.plt:00002E38                 ADR             R12, 0x2E40.plt:00002E3C                 ADD             R12, R12, #0x8000.plt:00002E40                 LDR             PC, [R12,#(strlen_ptr_0 - 0xAE40)]! ; __imp_strlen...0000AFDC  C8 B0 00 00 CC B0 00 00  D0 B0 00 00 D4 B0 00 00 0000AFEC  D8 B0 00 00 DC B0 00 00  E0 B0 00 00 E4 B0 00 00 0000AFFC  E8 B0 00 00 00 00 00 00  C8 B0 00 00 C8 B0 00 00 ...

Finally, the PC points to * strlen_ptr_0, that is, 0x0000AFDC, the address of strlen_ptr_0, which is located in the. got section, and 0x0000AFDC is exactly 0x0000B0C8. Therefore, the conclusion is as follows,Directly call an external function. Its relocation type is R_ARM_JUMP_SLOT, which is located in the. re. plt section. Its Offset points to the address of the final called function address (that is, the pointer to the function pointer). The whole process is first to. plt, then to. got, and finally to locate the real function address.

The analysis of this part shows that there are some differences between the decompilation results of IDA and objdump. The following is the Assembly command from objdump:

00002e38 <strlen@plt>:    2e38:   e28fc600    add ip, pc, #0, 12    2e3c:   e28cca08    add ip, ip, #8, 20  ; 0x8000    2e40:   e5bcf19c    ldr pc, [ip, #412]! ; 0x19c......    afd8:   00002c50    andeq   r2, r0, r0, asr ip    afdc:   00002c50    andeq   r2, r0, r0, asr ip    afe0:   00002c50    andeq   r2, r0, r0, asr ip    afe4:   00002c50    andeq   r2, r0, r0, asr ip

See the address at afdc, pointing to 0x00002c50, and 0x00002c50 happens to be PLT [0]. The command is as follows:

00002c50 <__cxa_atexit@plt-0x14>:    2c50:   e52de004    push    {lr}        ; (str lr, [sp, #-4]!)    2c54:   e59fe004    ldr lr, [pc, #4]    ; 2c60 <__cxa_atexit@plt-0x4>    2c58:   e08fe00e    add lr, pc, lr    2c5c:   e5bef008    ldr pc, [lr, #8]!    2c60:   000082d4    ldrdeq  r8, [r0], -r4

After executing the 2c5c command, the pc finally points to 0x0000af3c, Which is exactlyGLOBAL_OFFSET_TABLE+ 8, that is, GOT [2]. We can see at 0x0000af3c:

0000AF3C  00 00 00 00 28 B0 00 00  24 B0 00 00 2C B0 00 000000AF4C  30 B0 00 00 34 B0 00 00  38 B0 00 00 3C B0 00 00

The result shows that the function address pointed to in GOT [2] is actually 0, because the symbol binding on android does not support lazy binding, so when so is loaded, linker will find out all the functions corresponding to GOT [n] (n> = 2) in advance, so the code of GOT [2] will not be executed here, therefore, the complete PLT/GOT link process does not exist in the current Android system. I guess this is mainly due to stability considerations.

Summary

Although the commands decomcompiled by IDA and obudump are somewhat different in the PLT \ GOT process, this difference does not affect Android because lazy binding is not supported on Android. At the same time, we come to a very important conclusion:Although the relocation items of R_ARM_ABS32, R_ARM_GLOB_DAT, and R_ARM_JUMP_SLOT are different in code usage, their offset is a pointer to a function.This is very useful for the following elfhook.

Parsing ELF Based on execution View

The example provided in Redirecting functions in shared ELF libraries is to parse ELF Based on the Link View. Compared with parsing Based on the execution view, the following logic is basically the same. The key is to find it through segment. dynsym ,. dynstr ,. rel. plt and rel. dyn and the number of their items.

For the first time, locate the segment of the PT_DYNAMIC type through the Program Header Table. The content of this segment corresponds to. dynamic, and the content of this segment corresponds to the Elf32_Dyn array. Its structure is as follows:

/* Dynamic structure */typedef struct {    Elf32_Sword d_tag;      /* controls meaning of d_val */    union {        Elf32_Word  d_val;  /* Multiple meanings - see d_tag */        Elf32_Addr  d_ptr;  /* program virtual address */    } d_un;} Elf32_Dyn;

By traversing this array, We can find all the required information. I will list their mappings:

  • DT_HASH->. hash
  • DT_SYMTAB & DT_SYMENT->. dynsym
  • DT_STRTAB & DT_STRSZ->. dynstr
  • PLTREL (determine whether REL or RELA) & (DT_REL | DT_RELA) & (DT_RELSZ | DT_RELASZ) & (DT_RELENT | DT_RELAENT)->. rel. dyn
  • DT_JMPREL & DT_PLTRELSZ & (DT_RELENT | DT_RELAENT)->. rel. plt
  • FINI_ARRAY & FINI_ARRAYSZ->. fini_array
  • INIT_ARRAY & INIT_ARRAYSZ->. init_array

Here is the code for searching:

void getElfInfoBySegmentView(ElfInfo &info, const ElfHandle *handle){    info.handle = handle;    info.elf_base = (uint8_t *) handle->base;    info.ehdr = reinterpret_cast<Elf32_Ehdr *>(info.elf_base);    // may be wrong    info.shdr = reinterpret_cast<Elf32_Shdr *>(info.elf_base + info.ehdr->e_shoff);    info.phdr = reinterpret_cast<Elf32_Phdr *>(info.elf_base + info.ehdr->e_phoff);    info.shstr = NULL;    Elf32_Phdr *dynamic = NULL;    Elf32_Word size = 0;    getSegmentInfo(info, PT_DYNAMIC, &dynamic, &size, &info.dyn);    if(!dynamic){        LOGE("[-] could't find PT_DYNAMIC segment");        exit(-1);    }    info.dynsz = size / sizeof(Elf32_Dyn);    Elf32_Dyn *dyn = info.dyn;    for(int i=0; i<info.dynsz; i++, dyn++){        switch(dyn->d_tag){        case DT_SYMTAB:            info.sym = reinterpret_cast<Elf32_Sym *>(info.elf_base + dyn->d_un.d_ptr);            break;        case DT_STRTAB:            info.symstr = reinterpret_cast<const char *>(info.elf_base + dyn->d_un.d_ptr);            break;        case DT_REL:            info.reldyn = reinterpret_cast<Elf32_Rel *>(info.elf_base + dyn->d_un.d_ptr);            break;        case DT_RELSZ:            info.reldynsz = dyn->d_un.d_val / sizeof(Elf32_Rel);            break;        case DT_JMPREL:            info.relplt = reinterpret_cast<Elf32_Rel *>(info.elf_base + dyn->d_un.d_ptr);            break;        case DT_PLTRELSZ:            info.relpltsz = dyn->d_un.d_val / sizeof(Elf32_Rel);            break;        case DT_HASH:            uint32_t *rawdata = reinterpret_cast<uint32_t *>(info.elf_base + dyn->d_un.d_ptr);            info.nbucket = rawdata[0];            info.nchain = rawdata[1];            info.bucket = rawdata + 2;            info.chain = info.bucket + info.nbucket;            break;        }    }    //because .dynsym is next to .dynstr, so we can caculate the symsz simply    info.symsz = ((uint32_t)info.symstr - (uint32_t)info.sym)/sizeof(Elf32_Sym);}

However, there is a value that I cannot get through the PT_DYNAMIC segment, that is, the number of. dynsym items, and I finally get through a work ing method. Because. dynsym and. the two sections of dynstr are adjacent. Therefore, the two addresses are subtracted to obtain them. the total length of dynsym can be obtained in addition to sizeof (Elf32_Sym. the number of dynsym items. If you have a better method, please let me know.

ELF Hook

With the above introduction, it is very easy to write an ELF Hook. I will post the key code:

#define R_ARM_ABS32 0x02#define R_ARM_GLOB_DAT 0x15#define R_ARM_JUMP_SLOT 0x16int elfHook(const char *soname, const char *symbol, void *replace_func, void **old_func){    assert(old_func);    assert(replace_func);    assert(symbol);    ElfHandle* handle = openElfBySoname(soname);    ElfInfo info;    getElfInfoBySegmentView(info, handle);    Elf32_Sym *sym = NULL;    int symidx = 0;    findSymByName(info, symbol, &sym, &symidx);    if(!sym){        LOGE("[-] Could not find symbol %s", symbol);        goto fails;    }else{        LOGI("[+] sym %p, symidx %d.", sym, symidx);    }    for (int i = 0; i < info.relpltsz; i++) {        Elf32_Rel& rel = info.relplt[i];        if (ELF32_R_SYM(rel.r_info) == symidx && ELF32_R_TYPE(rel.r_info) == R_ARM_JUMP_SLOT) {            void *addr = (void *) (info.elf_base + rel.r_offset);            if (replaceFunc(addr, replace_func, old_func))                goto fails;            //only once            break;        }    }    for (int i = 0; i < info.reldynsz; i++) {        Elf32_Rel& rel = info.reldyn[i];        if (ELF32_R_SYM(rel.r_info) == symidx &&                (ELF32_R_TYPE(rel.r_info) == R_ARM_ABS32                        || ELF32_R_TYPE(rel.r_info) == R_ARM_GLOB_DAT)) {            void *addr          = (void *) (info.elf_base + rel.r_offset);            if (replaceFunc(addr, replace_func, old_func))                goto fails;        }    }    fails:    closeElfBySoname(handle);    return 0;}

Finally, the test code is as follows:

typedef int (*strlen_fun)(const char *);strlen_fun old_strlen = NULL;size_t my_strlen(const char *str){    LOGI("strlen was called.");    int len = old_strlen(str);    return len * 2;}strlen_fun global_strlen1 = (strlen_fun)strlen;strlen_fun global_strlen2 = (strlen_fun)strlen;#define SHOW(x) LOGI("%s is %d", #x, x)extern "C" jint Java_com_example_allhookinone_HookUtils_elfhook(JNIEnv *env, jobject thiz){    const char *str = "helloworld";    strlen_fun local_strlen1 = (strlen_fun)strlen;    strlen_fun local_strlen2 = (strlen_fun)strlen;    int len0 = global_strlen1(str);    int len1 = global_strlen2(str);    int len2 = local_strlen1(str);    int len3 = local_strlen2(str);    int len4 = strlen(str);    int len5 = strlen(str);    LOGI("hook before:");    SHOW(len0);    SHOW(len1);    SHOW(len2);    SHOW(len3);    SHOW(len4);    SHOW(len5);    elfHook("libonehook.so", "strlen", (void *)my_strlen, (void **)&old_strlen);    len0 = global_strlen1(str);    len1 = global_strlen2(str);    len2 = local_strlen1(str);    len3 = local_strlen2(str);    len4 = strlen(str);    len5 = strlen(str);    LOGI("hook after:");    SHOW(len0);    SHOW(len1);    SHOW(len2);    SHOW(len3);    SHOW(len4);    SHOW(len5);    return 0;}

From the printed results, we can see that local_strlen1 and local_strlen2 are not affected, but if the function is called again, it takes effect and the reason is not resolved. The test results will not be sent. Let's try it.

GitHup address

Complete code, see https://github.com/boyliang/AllHookInOne.git




Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

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.