Research on Executing ELF in Memory on Android

Research on Executing ELF in Memory on Android

This article is an excellent piece from the KX forum.

Author ID on KX forum: Ylarod

One

When we first encountered this issue, we found that in the Linux system, we can use memfd_create and execve to execute ELF in memory. However, on Android, we encountered the following problem:

CANNOT LINK EXECUTABLE "/data/local/tmp/payload": library "libicu.so" not found: needed by /system/lib64/libharfbuzz_ng.so in namespace (default)

From this article (https://github.com/5ec1cff/my-notes/blob/master/linker-log.md), we learned that we can enable linker logs by setting the LD_DEBUG environment variable and executingLD_DEBUG=1 /data/local/tmp/memexec, and we captured the following logs through logcat:

W linker  : [ Android dynamic linker (64-bit) ]
W linker  : [ Linking executable "/data/local/tmp/memexec" ]
W linker  : [ Linking "[vdso]" ]
W linker  : [ Reading linker config "/linkerconfig/ld.config.txt" ]
W linker  : [ Using config section "**unrestricted**" ]
W linker  : [ Linking "/data/local/tmp/memexec" ]
W linker  : [ Linking "/system/lib64/liblog.so" ]
W linker  : [ Linking "/apex/com.android.runtime/lib64/bionic/libm.so" ]
W linker  : [ Linking "/apex/com.android.runtime/lib64/bionic/libdl.so" ]
W linker  : [ Linking "/apex/com.android.runtime/lib64/bionic/libc.so" ]
W linker  : [ Linking "/system/lib64/libc++.so" ]
W linker  : [ Linking "/system/lib64/libnetd_client.so" ]
W linker  : [ Linking "/system/lib64/libcutils.so" ]
W linker  : [ Linking "/system/lib64/libbase.so" ]
W linker  : [ CFI add 0x61abb23000 + 0x67000  ]
W linker  : [ CFI add 0x77780d3000 + 0x1000 linux-vdso.so.1 ]
W linker  : [ CFI add 0x7776c48000 + 0x10000 liblog.so ]
W linker  : [ CFI add 0x7773d87000 + 0x37000 libm.so ]
W linker  : [ CFI add 0x7773dc0000 + 0x5000 libdl.so ]
W linker  : [ CFI add 0x7773a1e000 + 0x318000 libc.so ]
W linker  : [ CFI add 0x7776cd2000 + 0xae000 libc++.so ]
W linker  : [ CFI add 0x7773945000 + 0x2b000 libnetd_client.so: 0x777394a000 ]
W linker  : [ CFI add 0x777391b000 + 0x12000 libcutils.so ]
W linker  : [ CFI add 0x77738c4000 + 0x3b000 libbase.so ]
W linker  : [ CFI add 0x777391b000 + 0x12000 libcutils.so ]
W linker  : [ CFI add 0x77738c4000 + 0x3b000 libbase.so ]
W linker  : [ Jumping to _start (0x61abb418e0)... ]
W linker  : [ Android dynamic linker (64-bit) ]
W linker  : [ Linking executable "/memfd:jit-cache (deleted)" ]
W linker  : [ Linking "[vdso]" ]
W linker  : [ Reading linker config "/linkerconfig/ld.config.txt" ]
W linker  : [ Linking "/memfd:/jit-cache (deleted)" ]
F linker  : CANNOT LINK EXECUTABLE "/data/local/tmp/payload": library "libicu.so" not found: needed by /system/lib64/libharfbuzz_ng.so in namespace (**default**)

It seems to be a namespace issue. Our payload’s namespace is default, but why is this the case? Clearly, memexec is unrestricted.

Two

By searching for android linker namespace on Google, I found the official documentation on linker namespaces (https://source.android.com/docs/core/architecture/vndk/linker-namespace?hl=zh-cn#how-does-it-work), which has relevant information configured in the /linkerconfig/ld.config.txt file.

dir.system = /system/bin/......
dir.unrestricted = /data/nativetest64/unrestricted
dir.isolated = /data/local/tmp/isolated
dir.product = /data/local/tests/product
dir.system = /data/local/tests/system
dir.unrestricted = /data/local/tests/unrestricted
dir.vendor = /data/local/tests/vendor
dir.unrestricted = /data/local/tmp......
[system]
additional.namespaces = com_android_adbd,com_android_appsearch,com_android_art,com_android_conscrypt,com_android_i18n,com_android_media,com_android_neuralnetworks,com_android_os_statsd,com_android_resolv,com_android_runtime,com_android_tethering,rs,sphal,vndk,vndk_product
namespace.default.isolated = true
namespace.default.visible = true
namespace.default.search.paths = /system/${LIB}......

First, we need to check if this file is writable. By checking the mount information, we found that the file is not on a read-only filesystem, so we can modify it directly.

# cat /proc/mounts | grep linkerconfig
tmpfs /linkerconfig tmpfs rw,seclabel,nosuid,nodev,noexec,relatime,size=5846680k,nr_inodes=1461670,mode=755 0 0
tmpfs /linkerconfig tmpfs rw,seclabel,nosuid,nodev,noexec,relatime,size=5846680k,nr_inodes=1461670,mode=755 0 0

Next, I used /data/adb/magisk/busybox vi /linkerconfig/ld.config.txt to edit the file.

Next

It turns out that the namespace is determined by the path, so I edited the file and manually added a line at the top:

dir.unrestricted = /memfd:

Then I executed it again, but the problem persisted.

Transformation

Could it be an issue with the lack of a path separator? I added a / to the filename in the code:

memfd_create("/jit-cache", MFD_CLOEXEC);

After compiling and executing again, the same error occurred.

Discovery

I tried to set it to the root directory:

dir.unrestricted = /

This time, an additional warning appeared:

WARNING: linker: /linkerconfig/ld.config.txt:1: warning: property value is empty (ignoring this line)

It seems that simply modifying the configuration file won’t work, so I went to read the linker source code.

Three

Due to the instability of the aospxref service, I couldn’t access it while reading, so I used the official Android source code browsing tool (https://cs.android.com/), but it wasn’t very user-friendly.As we know, the linker source code is located in the bionic/linker directory.By searching for the string “Using config section”, I located the parse_config_file function in linker_config.cpp (https://cs.android.com/android/platform/superproject/+/master:bionic/linker/linker_config.cpp;bpv=1;bpt=1;l=182?gsn=parse_config_file&gs=kythe%3A%2F%2Fandroid.googlesource.com%2Fplatform%2Fsuperproject%3Flang%3Dc%252B%252B%3Fpath%3Dbionic%2Flinker%2Flinker_config.cpp%231A-Ye47njoFuvmSFn4MMAlVSs8FrDCQkXvfwVtzTk7o), and after reading the code, I found the answers to why our previous attempts failed.

Why Setting /memfd: Directory Doesn’t Work

Line 242 (https://cs.android.com/android/platform/superproject/+/master:bionic/linker/linker_config.cpp;l=242;bpv=0;bpt=1)

if (access(value.c_str(), R_OK) != 0) {    if (errno == ENOENT) {        // no need to test for non-existing path. skip.        continue;    }    // If not accessible, don't call realpath as it will just cause    // SELinux denial spam. Use the path unresolved.    resolved_path = value;}

It doesn’t work because it is not a real existing directory. The code checks the path using the access function, which determines that the path does not exist.

Why Setting Root Directory Doesn’t Work

Line 227 (https://cs.android.com/android/platform/superproject/+/master:bionic/linker/linker_config.cpp;l=227;bpv=0;bpt=1)

// remove trailing '/'while (!value.empty() && value.back() == '/') {    value.pop_back();} if (value.empty()) {    DL_WARN("%s:%zd: warning: property value is empty (ignoring this line)",            ld_config_file_path,            cp.lineno());    continue;}

It doesn’t work because the code removes all trailing ‘/’ characters, and when passing the root directory, the string becomes empty, resulting in that warning.

Four

There are two main approaches to modify:

  1. Static replacement of the linker

  2. Dynamic hooking

Although with Magisk, replacing files in the system partition has become easier, replacing the linker is still a complicated task. Dynamic hooking seems to be a more convenient solution for this problem. Due to the special nature of the target code location, existing inline hooks and PLT hooks cannot achieve an earlier execution timing than the linker.

Perhaps I am not well-informed; if there are better methods, please let me know.

Ultimately, I chose to fork a child process, perform ptrace on the child process, modify the logic, and then detach.The modifications mainly involve two steps:

  1. Add “dir.unrestricted = /memfd:/\n” to /linkerconfig/ld.config.txt

  2. Make the access function return 0 for this directory

First Step

Simply edit the file to add the above line.

Second Step

bool mem_load(const std::string&amp; image, char** argv, char** envp){    // Create a memory file, setting this parameter will automatically close the file after exec    int fd = memfd_create("/jit-cache", MFD_CLOEXEC);    ftruncate(fd, (long)image.size()); // Set file length    void *mem = mmap(nullptr, image.size(), PROT_WRITE, MAP_SHARED, fd, 0);    memcpy(mem, image.data(), image.size());    munmap(mem, image.size());    // At this point, the ELF content has been written into the memory file    int pid = fork();    if (pid < 0) {        printf("mem_load failed\n"); // fork failed        return false;    } else if (pid == 0) {        // This is the child process, using PTRACE_TRACEME to establish a connection        ptrace(PTRACE_TRACEME);        fexecve(fd, argv, envp);    }    // This is the parent process    int status;    struct user_regs_struct regs{};    struct iovec iov{};    iov.iov_base = &amp;regs;    iov.iov_len = sizeof(user_regs_struct);    while(true){        wait(&amp;status); // Wait for the child process to pause        if(WIFEXITED(status)){            break; // Child process exits        }        // Read general registers, system call number        ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &amp;iov);        if (regs.regs[8] == __NR_faccessat){ // access function system call number            char path[] = "/memfd:";            long word;            // Note that PTRACE_PEEKDATA reads a fixed length of sizeof(long) bytes            word = ptrace(PTRACE_PEEKDATA, pid, regs.regs[1], NULL);            if (strcmp(path, (char*)&amp;word) == 0){ // Check if it is our added directory                ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL); // Step through to let the system call complete                wait(nullptr); // Wait for the system call to complete                ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &amp;iov); // Read registers                regs.regs[0] = 0; // Modify return value to register                ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &amp;iov); // Modify registers                ptrace(PTRACE_DETACH, pid, NULL, NULL); // Detach process                return true;            }        }        ptrace(PTRACE_SYSCALL, pid, NULL, NULL); // Pause at the next system call    }    return false;}

Research on Executing ELF in Memory on Android

KX ID: Ylarod

https://bbs.pediy.com/user-home-892096.htm

*This article is originally by Ylarod from KX forum, please indicate the source when reprinting from KX community.

Research on Executing ELF in Memory on Android

KX 2022 KCTF Autumn Competition Official Website: https://ctf.pediy.com/game-team_list-18-29.htm

# Previous Recommendations

1. Learning Notes on CVE-2022-21882 Privilege Escalation Vulnerability

2. Wibu Certificate – An Initial Exploration

3. Reverse Engineering APIC Interrupts and Experiments on Win10 1909

4. Analysis and Simulation of EAF Mechanism under EMET

5. SQL Injection Learning Sharing

6. Issues and POCs of V8 Array.prototype.concat Function

Research on Executing ELF in Memory on AndroidResearch on Executing ELF in Memory on Android

Share

Research on Executing ELF in Memory on Android

Like

Research on Executing ELF in Memory on Android

Watching

Research on Executing ELF in Memory on Android

Click “Read the original text” to learn more!

Leave a Comment