The story of how a linux kernel vulnerability helps me collect data for a dissertation
A couple of years ago I decided to find out whether a person can be identified by the gestures that he enters on the screen of a smartphone. A kind of "keyboard style", but only for the touch screen. To understand this, you need to analyze hundreds of thousands of gestures from many different users. But ... How to collect this data on a smartphone?
I will talk about my way to solving this problem. He was long, thorny, but damn fascinating! I hope you will be interested to follow him and learn for yourself something new about linux, android, their security and their insides. I am not a guru in a linux device, so some explanations will seem to someone obvious and unnecessarily detailed, but again, this is my way and I describe in detail everything that I learned in the process. I hope this does not alienate experienced linuxoids and slightly lower the entry threshold for everyone else. So. How to implement touch logger for android?
Choosing a path
The simplest and most obvious solution is to write a separate application and collect the parameters of gestures only in it, as was done in
[1] . But this is a completely uninteresting task. And not only because it is too simple. By limiting data collection to a single application, there is a risk of missing some important behavioral characteristics of user gestures, which appear only under
special conditions. Therefore, I would like to collect gestures regardless of the application in which the user works, although it is not so easy to do it.
Mobile operating systems are much better than desktop ones. They, unlike the latter, were already being developed in those times when serious thought was taken of the security and isolation of processes, and the same Android simply does not allow you to track user gestures outside the view of your application. I suspect that in other mobile OS the same situation. But is it really so well protected that you had to resort to using linux kernel vulnerabilities to get touch coordinates?
')
Many now, for example, remembered about the android accessibility service - a universal access service. A handy thing for helping people with disabilities or writing various kinds of Trojans for Android. You can subscribe to universal access events in your application and receive information about user gestures. It would seem that! Take it and use it, everything is already written for you. But alas, I don’t extract the parameters of gestures I need from this data.
Well, there are not so many options. Try, for example, to include the item "display touches" in the developer menu.

After that, the traces of your gestures will remain on the screen, no matter what application you are. So, this code at least has access to information about touch coordinates. I wonder where he took this data and is it still there? In search of an answer, we dive into the android source and discover
this :

The
mPointerLocationView
line allows the
mPointerLocationView
object
mPointerLocationView
handle all input events from the touch screen.
If you examine the class hierarchy and call graph, it becomes clear that in the end it all comes down to calling the
registerPointerEventListener
method on the
WindowManagerService
system service. The latter, through the
WindowManager
interface, allows ordinary applications to call their methods remotely. It seems that it is enough to get access to
WindowManager
in the application, call this method, and the application will be able to handle the coordinates coming from the system. But there is a catch: in Android, access to system services is provided through
Binder IPC , and WindowManager can call only those methods that are in the corresponding
AIDL file . Unfortunately, there is no our method in AIDL, and it will not be possible to call it remotely from the user application. But if you are a vendor, and you don’t know how else to monitor users, you can change the android sources and add gesture logging directly to the device’s firmware, which I did for the sake of interest
here . However, in our case, this solution is not suitable. You can flash one, two data collection devices, but dozens of unfamiliar test users will not take this step. A more versatile way is needed.
Well, here it is, a solution to which I tried with all my might to find an alternative, but I could not. So, Android is based on the linux kernel, and the input system architecture is the same as in any linux distribution. Input device drivers, including touchscreens, transmit data to the userspace via character devices located in
/dev/input/
, from where they can be read. And directly from the driver, you can get all the data that you can wish for: coordinates, time stamps, touch area, gesture type ... Enough for more than one study. True, here, too, there is a catch: in Android, access to these devices has either root, or a user who is in the input group. Android applications are launched on behalf of users who cannot brag of such rights.
The way out of this situation is to launch an application to collect data on rooted devices. It is for this purpose that I wrote the
second version of my touch logger.
I used this version for data collection. There were plenty of people willing to help, but only some devices were ruled, so I didn’t collect so much. If you find a way to collect data without a root installed, there will be a few more suitable test devices, which will give us hundreds and hundreds of megabytes of valuable data for analysis. One great vulnerability in the linux kernel will help us in this.
CVE-2016–5195 aka DirtyCOW
HimselfThe history of finding and fixing this vulnerability is in itself very entertaining. She was accidentally noticed
11 years ago , but never closed. Once again, it was discovered and corrected only in 2016, after the discovery of a ready exploit that used it. That's how the vulnerability with an 11-year history turned into a 0-day. Remarkably, it is present on all versions of the kernel, starting from 2.6.22! That is, on all versions of android. In the android security bulletins, this vulnerability
appeared in December 2016, which means that it was fixed only on those devices that received security updates after this date. Nexus 5, for example, remained susceptible to it, like hundreds of other android devices.
What does dirtyCOW do? In short, it allows you to overwrite any file that the user can only read. By “any,” I mean ANY, even if the file system itself is mounted only for reading (in android, this is exactly the way of the / system partition, where the most delicious system files are stored). You can read about the mechanism of the vulnerability, for example,
here . We should remember only two things. First: changes to the read-only file system will disappear after a reboot, and second: you cannot change the file size, that is, write to it more than it takes up disk space.
Well, with the power given to us by the vulnerability, we can temporarily overwrite any system file that is readable. It would seem that this gives us a free hand, you can do whatever you want! But again, android is well protected, and will have to try to get around this protection and achieve their goals.
Idea implementation
Our task is to start a process from a normal application that can read data from input devices. Let me remind you that only the process started from the user with UID = 0 (root) or in the input group has access to the input device (in android it has GID = 1004).
Consider, for example, the file
/system/bin/ping
. It can be found in almost all android-devices, and it is available for reading and execution for everyone. Remarkably, this file has a SUID bit. SUID (
S et owner
U ser
ID up on execution - set owner UID during execution) is one of the attributes of a file in Linux. Usually the process inherits the rights of the user who started it. The presence of the SUID bit allows you to start the process with the rights of the owner of the executable file, as well as with its UID and GID. Accordingly, since the owner of the ping is root, then this file will be executed as root. Do you understand what I'm getting at? If we rewrite ping with dirtyCOW and run it, our code will execute as root and be able to read input devices! Well, gentlemen, congratulations, we did it, all the great lads, we disagree ... No.
SELinux
Yes, our plan will work amazingly on all versions of android up to 4.2. But on more recent versions may arise, but starting from 5.0 some difficulties will arise. The whole blame for SELinux (Security-Enhanced Linux) is a kernel module that implements a mandatory security policy in addition to an existing discretionary one. Briefly on how it works. Each user, process, file, network port, character device, and so on has a so-called security context — some label, an additional attribute. There is a set of policies in the system that describes the operations allowed for users and processes based on their contexts. All operations that are not in the policy set are prohibited and strictly suppressed by the system.
Third-party applications run in processes with a untrusted_app context. Already from the name of the context it follows that the rights of such processes are few. The
/system/bin/ping
file has a system_file context, and yes, SELinux does not allow running system_file from the untrusted_app context. Therefore, we will not be able to start pinging from an application on android 5.0+, like other system files with a SUID bit.
In fact, contexts in SELinux consist of several parts, for example, in ping, the context has the full form u:object_r:system_file:s0
, and the user has u:r:untrusted_app:s0
. In our case it doesn't matter, so I will use a shorter recording.
other methods
So, in the system there are no files with SUID-bits available to run from third-party applications. But what if you overwrite a file and make it so that the system launches it as root? In android there is such a thing as a service. Not the one that runs from your application and does something in the background. Service is a daemon that starts at system startup.
Examples of services described in the file init.rcRemarkably, android restarts services if they crash. What is even more interesting, many services are running as root. In android 6.0.1, this is vold, healthd, debuggerd, installd, zygote and many others. And yes, the file
/system/bin/app_process
, aka zygote, is readable by all users!
The zygote service, described in a separate fileLittle by little, the outlines of the new plan. If you overwrite
/system/bin/app_process
on our payload and drop the zygote service, android restarts it and our code in the app_process file runs as root.
The question arises how to drop a zygote. In fact, this is the most unreliable moment in the implementation of the touch-logger. Now I rely on the fact that when overwriting
/system/bin/app_process
our payload, zygote will fall “by itself”. But there is no guarantee that this will always work. Let's see what happens when the app_process file is overwritten and why this may or may not lead to a crash.
Virtual memory and mmap
In Linux, each process has its own virtual address space. It stores all the data needed by the process: its stack, heap, environment variables and much, much more. This space is translated by the kernel into physical addresses in RAM.
Good visualization of this concept.However, in the same space is the executable file of the process itself, and its interpreter, and libraries from its dependencies, and many more different files. It would be too wasteful to store all this in RAM, so linux has a rather elegant mechanism for loading files into the process memory without using RAM at all. It is called a memory-mapped file, or a file reflected in memory. The kernel creates in the address space of the process a page with the contents of the file, but in fact this virtual page is translated by the kernel not into RAM, but directly onto the disk. That is, the process works with the contents of the file as if with a large chunk of allocated memory, although in reality it reads or writes bytes on the disk. (by the way, if you haven’t read about dirtyCOW’s work principle yet, it’s just based on bugs with multi-thread access to the memory-mapped file).
It is in this “reflected” form that its executable file is stored in the process memory. Partially, it is loaded into memory, but most of it remains on the disk. Therefore, changes to the file immediately appear in the memory of the process. Suppose that after replacing the executable file, the process loads the following instruction from memory. With a high probability, it may be “inappropriate” and lead the process to a fall, or it may even turn out to be rubbish, with the same ending. However, it may happen that the process during the replacement of the executable file will “hang” in the blocking call, executing some select (), read () or waitpid (). In this case, nothing will happen to him until the end of the call and he will continue to exist. There are probably other scenarios for the survival of the process after replacing the executable file, but I am not familiar with them due to some experience in linux.
We found out what happens when a file is overwritten and how it can cause (or not) a drop in the process. I never came up with a more reliable way to restart, so let's leave everything as it is. Moreover, this method still works quite well (the exception is samsung devices that have their own, Samsung's magic does not allow zygote to fall after rewriting its executable file. I did not find out how it works).
Let's go back to the zygote. What kind of insignificant service is this, that I can easily replace it with some other binary file? Zygote is a process that spawns all android applications. When you click on the icon of the same clash of clans, zygote creates a copy of its process using fork (), and in that copy launches the desired application. This is done to save resources and speed up the launch. If you kill zygote, all android applications will fall after it. This state of affairs deprives our touch logger of any meaning. Why collect custom gestures from bricks? Fortunately, this problem is easy to get around.
Demons
First of all, why all of a sudden all android applications should terminate if zygote crashes? After all, at the completion of the parent process in linux, the child continues to work fine, and its new parent becomes init. But the fact is that when starting up, zygote spawns a new group of processes, or a new session. In this session, all android applications are launched, all child processes of android applications, their child processes, and so on. When the process that generated the session is completed, the entire branching tree of its descendants in the same session is completed with it. So when you close the terminal emulator, all processes running in it will end. That is why a zygote in case of a fall will necessarily drag all android applications to hell.
As mentioned above, this causes some problems. The first. Who will return the app_process to the site after the payload has been launched, and our application will be killed? And the second. How to leave our payload running after returning app_process?
In order for the process to continue to work after the session is completed, it must be untied from the current session ... Having created a new one! This is done using the setsid () system call. But you can do more cunning. In one call, you can start a new process, untie it from the current session and untie it from the standard input / output streams. This magic call is called daemon () and it combines the power of fork () and setsid (), creating a new demon process that will work even if the parent disappears in hell along with his entire session, for the demon itself is a creature of hell.
With the help of daemon () both problems mentioned above are perfectly solved. First we create a daemon inside the application. It will survive the zygote crash, so it can rewrite the app_process to payload, wait for it to start, and then return the app_process back.
The solution to the first problem in picturesThe second is solved in a similar way: when the system launches our payload, we will create another daemon in it, which will work even after the app_process recovery. But will it? The fall of the process when overwriting its executable file has not been canceled, even demonization does not help. From this save perhaps that replacing the executable file on the fly. So why not do this?
Execve ()
Essence execve ()This system call replaces the current process with a new one by running the binary file specified in the arguments. You can read more about this in the corresponding
man .
By the way, almost all linux processes are started with fork () + execve ().
As a result, the system launches our payload. We inside it do daemon () and execve () to run another binary, let's call it exec_payload. Then we return the app_process to the site, but exec_payload continues to work.
The solution to the second problem in picturesNow we know how to start the process as root from a normal application. More precisely, how, after killing half of the system, make it execute our code as root and return everything “as it was”. However, the difficulties do not end there. Remember about SELinux? So, he has not gone away and still does not allow the execution of many operations.
SELinux [2]
Consider what situation our exec_payload will be in after starting from the point of view of SELinux contexts. In zygote, the context of the same name is zygote. Exec_payload inherits when launched. Character devices in
/dev/input
also have their context: input_device. And, as expected, SELinux does not allow processes with a zygote context to access files with an input_device context. Have we really come all this way just to rest on SELinux again?
Not really. This time, the situation is still a little better, and we have more rights. Remember that zygote runs all android applications? So, if a process with a zygote context has a descendant with a untrusted_app context (or platform_app for embedded applications, or isolated_app ... who would know for what), this means that zygote has the right to change the selinux context. It remains to find the necessary context that has access to input devices. And it is easy to find it: it is a shell.
Shell is a user (with the same context), on whose behalf all commands are executed, if you access the Android device remotely via
ADB (Android debug bridge). It has less rights than root with an init or kernel context, but significantly more than android applications. In particular, the shell user has access to input devices for reading and writing. By changing the context from the zygote to the shell and adding ourselves to the input group, our exec_payload can finally get access to the desired input devices.
Getting the shell context from zygote #ifdef __aarch64__ void * selinux = dlopen("/system/lib64/libselinux.so", RTLD_LAZY); #else void* selinux = dlopen("/system/lib/libselinux.so", RTLD_LAZY); #endif if (selinux) { void* getcon = dlsym(selinux, "getcon"); const char* error = dlerror(); if (!error) { getcon_t* getcon_p = (getcon_t*) getcon; char* secontext; int ret = (*getcon_p)(&secontext); void* setcon = dlsym(selinux, "setcon"); const char* error = dlerror(); if (!error) { setcon_t* setcon_p = (setcon_t*) setcon; if ((*setcon_p)("u:r:shell:s0") != 0) { LOGV("Unable to set context: %s!", strerror(errno)); } (*getcon_p)(&secontext); LOGV("Current context: %s", secontext); } } dlclose(selinux); } else { LOGV("SELinux not found."); }
And finally, it remains to solve the last problem. How to exchange data between our (newly launched after the fall of half of the system) application and the daemon, reading the data from /dev/input/event[X]
? Perhaps several options. Unfortunately, the most adequate of them (UNIX socket, FIFO) are not available to us (insidious SELinux!). It remains to write data to the SD card by one process and read them from there by another process. Not the coolest option, but still. In addition, you can send data to the application via special intent using the built-in am utility (in fact, this is the command interface for the Activity Manager). Perhaps there are other ways that I did not think of."Manual" way to collect data
Using exploits to achieve different goals is very unreliable. It is rarely possible to implement a universal solution based on an exploit that will work on all devices with any versions of Android. My implementation of the touch logger is no exception. Not everywhere will zygote fall after rewriting app_process. Probably, not all SELinux devices will allow changing the context from zygote to the shell to our payload. And surely there are other pitfalls, while unknown to me.However, our goal is to collect input events, and if test users agree to provide their devices for this, you can implement another solution, simple and 100% universal. True, to launch it will have to do several manual operations. If to throw exec_payload on adb in a directory/data/local/tmp
(where the shell has access), and from there start it - the daemon will start working in the same way as if it was launched by our application. The only thing will be different UID (2000 instead of 0), but it will not affect access to input devices. So in the third version of my touch-logger (which is now being brought to mind) I am going to provide such an option as a last resort, while leaving the possibility of launching on rooted devices as another backup option.Other DirtyCOW Applications on Android
It is clear that exploits are not always (almost never) used for such noble purposes as science. And you are probably wondering what else the use of DirtyCOW can be found using the above method of elevating privileges. As you understand, this is not a full-fledged root, so our capabilities are very limited. However, it is possible to mess things up with these rights. First, you can use access to input devices to steal passwords (I made a “pause” button in the touch logger to prevent this data from falling into my hands and potentially into the wrong hands). Second: you can use built-in utilities like am and pm. This allows you to secretly install and uninstall applications. Am, in turn, allows you to get information about the activity that is currently running on top of the rest. Perfect functionality for a banking trojan,which, when launching a banking application, will draw over its own login form. In general, dirtyCOW, like any tool, can be used for different purposes, both in good and not so much.One last trick
Finally, I want to talk about the most interesting: one more way to execute your code from a more privileged user. It will allow you to run code with the privileges of mediaserver, netd, debuggerd, or other services, if you suddenly need it.In fact, it is not necessary to rewrite the executable file, so that when it starts it executes arbitrary code. This is very good news if we do not have permissions to read this file, or its size is so small that it is not possible to overwrite it with something more or less useful.The essence of the trick is very simple and elegant. Virtually any executable file in Android has a dependency on the system library libcutils.so. And when you run the executable file, this library will be loaded into the process memory. The feature of shared libraries in ELF format is that they have special sections .init
and .init_array
. Functions whose addresses will be placed in these sections will be executed when the library is loaded into the process. Accordingly, if we compile the library with the function in the section .init_array
and overwrite the file /system/lib/libcutils.so
with it, then when it is loaded into the process, this function will be safely executed with the privileges of the process. __attribute__((constructor)) void say_hello() { payload_main(); }
The function with the constructor attribute is placed in the .init_array
library section when compiled .But how universal is this solution? I would not want processes in different versions of Android to have difficulty with the fact that they will not be able to find certain functions in this vital library. Indeed, rewriting libcutils itself is quite dangerous. But he, in turn, also has dependencies on other libraries. True, other libraries are quite important, and I would not like to touch them. Here comes to the aid of another, a little less simple, but no less elegant hack.DT_SONAME → DT_NEEDED
Each library in the ELF format has a header that contains all the library information that the linker needs. We are interested in information about its dependencies and its name. You can learn all this, for example, with the help of the command objdump -p
: : [.....] STRTAB 0x00001660 STRSZ 0x000014ec GNU_HASH 0x00002b4c NEEDED liblog.so NEEDED libc++.so NEEDED libdl.so NEEDED libc.so NEEDED libm.so SONAME libcutils.so FINI_ARRAY 0x0000fbf0 [.....]
This is the fragment of the title libcutils.so. The NEEDED fields contain dependencies that will be loaded by the linker in the same process as libcutils itself. Notice the SONAME field containing the library name. It is noteworthy that it does not need the linker to work properly, it searches for libraries by the names of their files. So why not parse the ELF header and insert another dependency instead of this unnecessary field? From some less important system library? Here you need to be careful: the length of the name of the new dependency should not exceed the length of the name of the library itself (so I chose libcutils, and not, for example, libc). Fortunately, there is an excellent candidate in mind: libmtp.so. It is large (several tens of kilobytes), with a short name, and is needed only when connected to a computer via USB to transfer files using the MTP protocol.That is, 2 minutes you can live without it. The rest is a matter of technique. Parse the ELF file, find the field with the library name, change the field type from DT_SONAME (0xE) to DT_NEEDED (0x1), and change the name itself to libmtp.so. Done!
Libcutils.so has a new dependency: : [.....] STRTAB 0x00001660 STRSZ 0x000014ec GNU_HASH 0x00002b4c NEEDED liblog.so NEEDED libc++.so NEEDED libdl.so NEEDED libc.so NEEDED libm.so NEEDED libmtp.so FINI_ARRAY 0x0000fbf0 [.....]
Now it’s easy to do: rewrite libmtp.so at our discretion and restart the process on whose behalf we want to run the code. This can be done, again, by dropping the zygote, since not only the android applications, but also many system services, fall behind it.It is worth saying thanks to the authors of the Android.Loki.28.origin virus , from whom I got this great idea from introducing dependencies into the library.
Instead of conclusion
Gone are the days when Android exploits exploited flagrant OS security vulnerabilities, which allowed us to create anything in the system with impunity. Today, Android is well protected, and SELinux, introduced in version 4.3, and enabled by default in 5.0, made a significant contribution to protection. Nevertheless, there are still loopholes in defense, and they still elevate privileges so that they can be used in practice, both for good purposes and for evil. I hope you were interested in finding out how one of the most seemingly useless exploits in Android was able to find useful applications. And I hope that the new version of the touch logger will help me collect the cars of the most valuable user data and will advance the study of the behavioral signs of gestures on the touch screen forward.