Index ¦ Archives ¦ Atom

Kobo Aura H2O electronic reader hacking

I recently bought a Kobo Aura H2O, and while it’s really good to read, I’m a little bit disappointed that I need an account to use the reader. Multiple blog posts like a3nm’s “fnacbook kobo hacking” or mobileread forum threads explain how to avoid registration and tracking, but as they often concern specific hardware, software versions, and data is scattered around many different threads, I think this kind of “review” article may be useful.

Connecting the device to WiFi

After language selection, the home page of the Kobo is asking us if we want to configure the device using WiFi or without WiFi.

I chose the first option; the reasoning behind this step is to allow the Kobo to update itself to the last firmware verssion.

After this step, the Kobo will ask for credentials or prompt an account creation, which is what we want to avoid. To do this, we go back to the home screen and select the second option, to configure the Kobo without WiFi.

Mounting the device

Using the second option enables the device USB port, and allows us to mount one of the device partition:

usb 1-2: new high-speed USB device number 24 using xhci_hcd
usb 1-2: New USB device found, idVendor=2237, idProduct=4227, bcdDevice= 4.01
usb 1-2: New USB device strings: Mfr=3, Product=4, SerialNumber=5
usb 1-2: Product: eReader-4.9.11311
usb 1-2: Manufacturer: Kobo
usb 1-2: SerialNumber: [REDACTED]
usb-storage 1-2:1.0: USB Mass Storage device detected
scsi host3: usb-storage 1-2:1.0
scsi 3:0:0:0: Direct-Access     Linux    File-Stor Gadget 0401 PQ: 0 ANSI: 2
sd 3:0:0:0: Power-on or device reset occurred
sd 3:0:0:0: [sdb] 14139389 512-byte logical blocks: (7.24 GB/6.74 GiB)
sd 3:0:0:0: [sdb] Write Protect is off
sd 3:0:0:0: [sdb] Mode Sense: 0f 00 00 00
sd 3:0:0:0: [sdb] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA

We can mount the device directly (/dev/sdb in my case), as there are no partitions on the device.

Directory Structure

We can look at the directory structure of the device:

./
├── .adobe-digital-editions/
│   └── device.xml
├── Digital Editions/
│   ├── Annotations
│   └── Manifest
├── .kobo/
│   ├── affiliate.conf
│   ├── BookReader.sqlite
│   ├── certificates/
│   ├── device.salt.conf
│   ├── dict/
│   ├── kepub/
│   ├── Kobo/
│   │   ├── Analytics.conf
│   │   └── Kobo eReader.conf
│   ├── KoboReader.sqlite
│   └── version
└── .kobo-images/

We find well-known files, such as the KoboReader.sqlite database, but also dubious ones: the affiliate.conf, Analytics.conf, and the device.xml file that has something to do with Adobe Digital Editions, which is a DRM system I obviouslyy won’t be using. The BookReader.sqlite file is also dubious: it’s an encrypted database.

Affiliate.conf [Clean]

The affiliate.conf only contain the two following lines.

[General]
affiliate=Kobo

As no unique identifier is present, i’ll let it be.

Analytics.conf [Detrimental]

The Analytics.conf contains an unique ID (redacted) and many lines related to google analytics using this ID:

[General]
ClientID=REDACTED-REDA-CTED-REDA-CTEDREDACTED
GAQueue="@Variant(\0\0\0\x11\0\0\0\xf7https://ssl.google-analytics.com/collect?v=1&tid=UA-6177406-38&cid=redacted-reda-cted-reda-ctedredacted&uid=redacted-reda-cted-reda-ctedredacted&av=4.9.11311&an=nickel&sr=1080x1440&ul=fr-fr&t=event&ec=Light&ea=BrightnessAdjusted&el=MenuTapped&ev=0)", @ByteArray(), "@Variant(\0\0\0\x11\0\0\0\xf4https://ssl.google-analytics.com/collect?v=1&tid=UA-6177406-38&cid=redacted-reda-cted-reda-ctedredacted&uid=redacted-reda-cted-reda-ctedredacted&av=4.9.11311&an=nickel&sr=1080x1440&ul=fr-fr&t=event&ec=ReadingExperience&ea=DictionaryLookup&el=fr)", […]

Many events seems tracked through the get parameters of the URLs:

AdobeErrorEncountered
AutoColorToggled
BrightnessAdjusted
CreateBookmark
DictionaryLookup
FontSettings
HomeWidgetClicked
LeaveContent
MainNavOption
NaturalLightAdjusted
NewWifiNetwork
OpenContent
OpenReadingSettingsMenu
PluggedIn
ReadingSettings
SearchExecuted
StatusBarOption
TabSelected
TimeToUpdate
WifiToggle

The GAQueue stores Google-Analytics callbacks until the next time I connect the Kobo to the Internet. When empty, the file looks like this:

[General]
ClientID=REDACTED-REDA-CTED-REDA-CTEDREDACTED
GAQueue=@Invalid()

Version [Detrimental]

The version file is a csv (comma separated values) file containing the device serial number, as well as multiple version numbers:

SERIAL_NUMBER, 4.1.15, 4.9.11311, 4.1.15, 4.1.15, 00000000-0000-0000-0000-000000000378

Device.salt.conf [Detrimental]

The device.salt.conf file contains a salt, but this salt seems unique enough to be a tracking id:

[General]
salt=@ByteArray(\xYY\xYY\bL\xYY\xYY\xYY\xYY\xYY\xYY\xYY\xYY\xYY\xYYZ\xYY)

KoboReader.sqlite [Detrimental]

The KoboReader.sqlite file is a sqlite3 database. It has the following tables:

AbTest                 OverDriveCards         WordList             
Achievement            OverDriveCheckoutBook  content              
Activity               OverDriveLibrary       content_keys         
AnalyticsEvents        Reviews                content_settings     
Authors                Rules                  ratings              
BookAuthors            Shelf                  shortcover_page      
Bookmark               ShelfContent           user                 
DbVersion              SyncQueue              volume_shortcovers   
Dictionary             Tab                    volume_tabs          
Event                  Wishlist

Each of the table has many columns. For example, the user table has the following columns:

0|UserID|TEXT|1||1
1|UserKey|TEXT|1||0
2|UserDisplayName|TEXT|0||0
3|UserEmail|TEXT|0||0
4|___DeviceID|TEXT|0||0
5|FacebookAuthToken|TEXT|0||0
6|HasMadePurchase|BIT|0|FALSE|0
7|IsOneStoreAccount|BIT|0|FALSE|0
8|IsChildAccount|BIT|0|FALSE|0
9|RefreshToken|TEXT|0||0
10|AuthToken|TEXT|0||0
11|AuthType|TEXT|0||0
12|Loyalty|BLOB|0||0
13|IsLibraryMigrated|BIT|1|true|0
14|SyncContinuationToken|TEXT|0||0
15|Subscription|INT|1|0|0
16|LibrarySyncType|TEXT|0||0
17|LibrarySyncTime|TEXT|0||0
18|SyncTokenAppVersion|TEXT|0||0

The full schema of the database is available there.

Looking at differences between an activated and not-yet activated device, we can discover the following:

  • The events sent to google analytics are also stored in the AnalyticsEvent table;
  • The Achievement table is full of shitty achievements, related to the “Reading Life” gamification thing;
  • The AbTest table contains unique IDs and non-evocating names like EPDHome2018, EPDStorefront2018 or KoboPlusDiscoveryEPD. Maybe Kobo is doing A/B testing on the users?
  • The User table contains the following default data: 2a362501-e2cf-41fd-88b1-47ade6d09da4|17314f8f-9d48-4ec2-8cbf-eda1e70b1127|demofinal@magtest.kobo.com|demofinal@magtest.kobo.com|a65c3a5eaebcf65fb184764bb99d2270cb71d8c8cd8bece3a7dcb271204f645a||false|false|false|||||false||0|||;
  • The User table is used to detect if the Kobo has been connected // activated online.

Bypassing registration

To bypass registration with an account, we can put fake data in the KoboReader.sqlite database. Yet filling everything mindlessly will not do; it will allow to bypass the registration, but you won’t be able to find your books after. As of today, the following works:

# echo "INSERT INTO user VALUES('ID','Masterkey','Rémy','nomail','',NULL,'false','false','false','RefreshToken','AuthToken','Bearer','','false','SyncContinuationToken',1,NULL,NULL,'4.9.11311');" | sqlite3 /mnt/.kobo/KoboReader.sqlite

A real SyncContinuationToken field is composed of base64 encoded, dot separated and encapsulated Json data:

# Content of a SyncContinuationToken field in a real user
eyJ0eXAiOjEsInZlciI6bnVsbCwicHR5cCI6IlN5bmNUb2tlbiJ9.eyJJbnRlcm5hbFN5bmNUb2tlbiI6ImV5SjBlWEFpT2pFc0luWmxjaUk2Ym5Wc2JDd2ljSFI1Y0NJNklrNWxlSFJUZVc1alZHOXJaVzRpZlEuZXlJa2RIbHdaU0k2SWs1bGVIUlRlVzVqVkc5clpXNGlMQ0pUZFdKelkzSnBjSFJwYjI1RmJuUnBkR3hsYldWdWRITWlPbTUxYkd3c0lrVnVkR2wwYkdWdFpXNTBjeUk2ZXlKVWFXMWxjM1JoYlhBaU9pSXlNREU0TFRBM0xUSTRWREUxT2pFd09qQXpXaUlzSWt4aGMzUlNaWFIxY201bFpFbGtJanB1ZFd4c0xDSk1ZWE4wVW1WMGRYSnVaV1JKWkhOSVlYTm9Jam90TmpVNU16QTJNVGM0ZlN3aVJHVnNaWFJsWkVWdWRHbDBiR1Z0Wlc1MGN5STZiblZzYkN3aVVtVmhaR2x1WjFOMFlYUmxjeUk2ZXlKVWFXMWxjM1JoYlhBaU9pSXlNREU0TFRBM0xUSTRWREUxT2pFd09qQXlXaUlzSWt4aGMzUlNaWFIxY201bFpFbGtJanB1ZFd4c0xDSk1ZWE4wVW1WMGRYSnVaV1JKWkhOSVlYTm9Jam93ZlN3aVZHRm5jeUk2ZXlKVWFXMWxjM1JoYlhBaU9pSXlNREU0TFRBM0xUSTRWREUxT2pFd09qQXpXaUlzSWt4aGMzUlNaWFIxY201bFpFbGtJanB1ZFd4c0xDSk1ZWE4wVW1WMGRYSnVaV1JKWkhOSVlYTm9Jam93ZlN3aVJHVnNaWFJsWkZSaFozTWlPbTUxYkd3c0lsQnliMlIxWTNSTlpYUmhaR0YwWVNJNmV5SlVhVzFsYzNSaGJYQWlPaUl5TURFNExUQTNMVEk0VkRBeU9qQXhPakkxTGpnMU15SXNJa3hoYzNSU1pYUjFjbTVsWkVsa0lqcHVkV3hzTENKTVlYTjBVbVYwZFhKdVpXUkpaSE5JWVhOb0lqb3dmU3dpU1hOR2RXeHNVM2x1WXlJNlptRnNjMlVzSWxCeVpYWnBiM1Z6VTNsdVkxUnBiV1VpT2lJeU1ERTRMVEE0TFRBeVZEQTRPalE0T2pJMUxqRXhPVGs0T1RaYUluMCIsIklzQ29udGludWF0aW9uVG9rZW4iOmZhbHNlfQ
# base64 decode
{"typ":1,"ver":null,"ptyp":"SyncToken"}{"InternalSyncToken":"eyJ0eXAiOjEsInZlciI6bnVsbCwicHR5cCI6Ik5leHRTeW5jVG9rZW4ifQ.eyIkdHlwZSI6Ik5leHRTeW5jVG9rZW4iLCJTdWJzY3JpcHRpb25FbnRpdGxlbWVudHMiOm51bGwsIkVudGl0bGVtZW50cyI6eyJUaW1lc3RhbXAiOiIyMDE4LTA3LTI4VDE1OjEwOjAzWiIsIkxhc3RSZXR1cm5lZElkIjpudWxsLCJMYXN0UmV0dXJuZWRJZHNIYXNoIjotNjU5MzA2MTc4fSwiRGVsZXRlZEVudGl0bGVtZW50cyI6bnVsbCwiUmVhZGluZ1N0YXRlcyI6eyJUaW1lc3RhbXAiOiIyMDE4LTA3LTI4VDE1OjEwOjAyWiIsIkxhc3RSZXR1cm5lZElkIjpudWxsLCJMYXN0UmV0dXJuZWRJZHNIYXNoIjowfSwiVGFncyI6eyJUaW1lc3RhbXAiOiIyMDE4LTA3LTI4VDE1OjEwOjAzWiIsIkxhc3RSZXR1cm5lZElkIjpudWxsLCJMYXN0UmV0dXJuZWRJZHNIYXNoIjowfSwiRGVsZXRlZFRhZ3MiOm51bGwsIlByb2R1Y3RNZXRhZGF0YSI6eyJUaW1lc3RhbXAiOiIyMDE4LTA3LTI4VDAyOjAxOjI1Ljg1MyIsIkxhc3RSZXR1cm5lZElkIjpudWxsLCJMYXN0UmV0dXJuZWRJZHNIYXNoIjowfSwiSXNGdWxsU3luYyI6ZmFsc2UsIlByZXZpb3VzU3luY1RpbWUiOiIyMDE4LTA4LTAyVDA4OjQ4OjI1LjExOTk4OTZaIn0","IsContinuationToken":false}
# base64 decode of the InternalSyncToken
{"typ":1,"ver":null,"ptyp":"NextSyncToken"}.{"$type":"NextSyncToken","SubscriptionEntitlements":null,"Entitlements":{"Timestamp":"2018-07-28T15:10:03Z","LastReturnedId":null,"LastReturnedIdsHash":-659306178},"DeletedEntitlements":null,"ReadingStates":{"Timestamp":"2018-07-28T15:10:02Z","LastReturnedId":null,"LastReturnedIdsHash":0},"Tags":{"Timestamp":"2018-07-28T15:10:03Z","LastReturnedId":null,"LastReturnedIdsHash":0},"DeletedTags":null,"ProductMetadata":{"Timestamp":"2018-07-28T02:01:25.853","LastReturnedId":null,"LastReturnedIdsHash":0},"IsFullSync":false,"PreviousSyncTime":"2018-08-02T08:48:25.1199896Z"}

Preventing communication with Google, etc.

Updates for the Kobo are not signed, or even encrypted. By putting an archive named KoboRoot.tgz in the .kobo directory, on the partition of the Kobo (which is mounted internally as /mnt/onboard), we can update the Kobo, and for example modify the /etc/hosts file:

mkdir ./etc
echo "0.0.0.0 baddomain.com" >> ./etc/hosts
tar czf KoboRoot.tgz ./etc/hosts
cp KoboRoot.tgz /mnt/.kobo/

Indeed, the content of the archive is extracted into the root of the Kobo system. When unplugged, the Kobo will go through an update cycle, and the KoboRoot.tgz will be deleted.

To check with which domains the Kobo communicate, we can use tcpdump, wireshark or mitmproxy (the latter being more useful to access to actual data sent to http or https endpoints). Looking for dns queries when the Kobo is used in a classical way (i.e. no web bworsing), then we get (192.168.12.214 being the ip address of the Kobo):

% tshark -r dump.pcapng -T fields -e ip.src -e dns.qry.name -2 -R "dns.flags.response eq 0" | sort | uniq
192.168.12.214  api.ipinfodb.com
192.168.12.214  api.kobobooks.com
192.168.12.214  auth.kobobooks.com
192.168.12.214  authorize.kobo.com
192.168.12.214  kbdownload1-a.akamaihd.net
192.168.12.214  kbimages1-a.akamaihd.net
192.168.12.214  mobile.kobobooks.com
192.168.12.214  pool.ntp.org
192.168.12.214  script.hotjar.com
192.168.12.214  social.kobobooks.com
192.168.12.214  ssl.google-analytics.com
192.168.12.214  static.hotjar.com
192.168.12.214  stats.g.doubleclick.net
192.168.12.214  storeapi.kobo.com
192.168.12.214  vars.hotjar.com
192.168.12.214  www.google-analytics.com
192.168.12.214  www.google.com
192.168.12.214  www.google.fr
192.168.12.214  www.googletagmanager.com
192.168.12.214  www.msftncsi.com

Using mitmproxy, we can check whether our assumptions about the tracked events were good. And they are: if you toggle your Wifi, google knows it. If you plug your reader, google knows it. If you do a dictionary lookup, if you adjust the brightness, if you tap the menu, google analytics knows it.

You sideload a book on your Kobo? Google knows it, and knows which book it is, because the ISBN-13 is sent:

Example of google receiving book sideload information through ISBN sending, in this case "The Art of Computer Programming" by Knuth

You can disable those reports to google inside the “confidentiality” section, in the settings of the Kobo; this “feature” is at the end of the pages (it pretends it only share “features” you use on the device, but as seen on the previous screenshot, it also share what you read).

Resources

More ?

You have information about the Kobo Aura H2O you want to share? Email me at rémy@grünblatt.org (replace the accentuated characters with ther non-accentuated counterpart)

© Rémy Grünblatt 🍃. Built using Pelican. Theme by Giulio Fidente on github.