Introduction
I turned 25 on the 10th of January and one of my friends, Veydh, created a reverse-engineering challenge for me as a gift.
I looked at the board and saw that it was an ESP8266 module.
The ESP8266 WiFi Module is a self contained SOC with integrated TCP/IP protocol stack that can give any microcontroller access to your WiFi network. The ESP8266 is capable of either hosting an application or offloading all Wi-Fi networking functions from another application processor.
Disclaimer: I know nothing about electronics. Some of the things I had to research might be obvious to many, but they definitely weren’t obvious to me.
Observation
I had no idea how to communicate with this thing. I plugged it in and I saw a blue light turn on for a second and then it disappeared. After googling for a bit, I figured that the usb cable that I had plugged in was likely a serial to usb adapter.
I don’t think I’ve had to communicate with anything over a serial port in a very long time. I found Serial and tried to use that. It auto-detected the device and allowed me to start communicating with it. Unfortunately, it defaulted to using an incorrect baud rate for the device’s configuration and I couldn’t make sense of the gibberish that I was seeing.
Veydh prompted me to check the list of WiFi networks that my laptop was detecting and I noticed that the board seemed to create its own called ESP12E Challenge
. It was password-protected and I didn’t have the password at the time.
Finding my first foothold
I’ve read writeups before where people reverse-engineer firmware on embedded devices. The first thing I remember them doing is dumping the ROM so I decided that I would try doing that first.
After some more googling, I found esptool. I tried using this to dump the ROM using the command
esptool.py -p /dev/cu.SLAB_USBtoUART dump_mem 0x40000000 65536 iram0.bin
That seemed to work. From the initial research, I knew that the board used a tensilica xtensa
processor. I had never disassembled anything like that before so I tried using IDA Pro. I looked for the xtensa in the list of supported processors but it wasn’t there so I searched for a plugin and I did fine one. However, once I had it loaded in IDA, I had no idea where to go from there. No code sections had been identified (which I guess I should expect when just handing IDA what is essentially memory dump). I was searching through documentation trying to figure out where the entry point to the code was. While doing this, I was explaining to Veydh what I had tried and he mentioned to me that maybe I was working with an incorrect baud rate and that I should experiment with others and see what happens.
I opened Serial again and tried experimenting with the baud rate. I got some more gibberish. However, once I tried a baud rate of 115200, I got some gibberish followed by some text that I was actually able to read!
So, I got some information about the firmware on the device (NodeMCU) and what turned out to be an interactive Lua shell. Progress!
What’s the wifi password?!
Looking at the information spit out over the serial connection, I saw the list of modules loaded. I got more information about them by reading the NodeMCU docs. My first thought after doing that was that I should be able to list the files on the filesystem and maybe I’d find some Lua source code.
I copied and pasted some example code from the documentation for listing files at the Lua prompt.
Success! I see four files, only one of which seems to be an actual lua file. There’s a HTML file. The other two seem to be compiled lua files which should contain lua bytecode (this should be interesting!).
I didn’t want to have to keep pasting and editing snippets of code throughout the entire challenge so I decided to write a small script to help me read files off the system. I’d never used Pwntools to interact with a serial connection before but I know I’d seen it mentioned in the documentation and I was already somewhat familar with its interface, so it was my first choice.
import sys
import time
from pwn import *
r = serialtube('/dev/cu.SLAB_USBtoUART') #Default baud rate was correct
def read_file(f, n):
code = '''\n\n
if file.open("%s","rb") then #b was necessary for reading the files containing lua bytecode
#I ran into some problems with getting incomplete output so this helped verify that my files were correct.
print(crypto.toHex(crypto.fhash("md5","%s")))
#Base64 for it so we get printable output
print(encoder.toBase64(file.read(%d)))
file.close()
end
''' % (f, f, n)
r.sendline(code)
def restart():
r.sendline('node.restart()')
restart() #Used so I don't have to press the reset button on the board every time
time.sleep(2) #I don't know why this was necessary. I guess it needs some time to re-initialize.
filename = sys.argv[1]
size = int(sys.argv[2])
print "Attempting to read file: %s of size: %d\n" % (filename, size)
read_file(filename, size)
r.interactive() #Should just dump everything that's in stdout and let me interact further if necessary.
I attempted to read the init.lua
file.
$ python read_file_clean.py init.lua 282
Attempting to read file: init.lua of size: 282
[*] Switching to interactive mode
node.restart()
>
NodeMCU custom build by frightanic.com
branch: master
commit: 5073c199c01d4d7bbbcd0ae1f761ecc4687f7217
SSL: false
modules: bit,cron,crypto,encoder,file,gpio,http,mdns,mqtt,net,node,rfswitch,sjson,tmr,uart,websocket,wifi
build built on: 2018-01-07 08:03
powered by Lua 5.1.4 on SDK 2.1.0(116b762)
Server Mode - lack of config file
>
>
>$
>
> if file.open("init.lua"hen
>> print(crypto.toHex(crypto.fhash("md5","init.lua")))
print(encoder.toBase64(file.read(282>> )))
file.close()
end
ab146109bc940e8e115adfb881b240b2
aWYgZmlsZS5leGlzdHMoImNvbmZpZyIpIHRoZW4KICAgIHdpZmkuc2V0bW9kZSggd2lmaS5TVEFUSU9OICkKICAgIHdpZmkuc2V0cGh5bW9kZSggd2lmaS5QSFlNT0RFX04gKQogICAgcHJpbnQoIk5vZGUgTW9kZSIpCiAgICBjb2xsZWN0Z2FyYmFnZSgpCiAgICBkb2ZpbGUoJ21haW4ubGMnKQplbHNlCiAgICB3aWZpLnNldG1vZGUod2lmaS5TT0ZUQVApCiAgICBwcmludCgiU2VydmVyIE1vZGUgLSBsYWNrIG9mIGNvbmZpZyBmaWxlIikKICAgIAogICAgZG9maWxlKCdzZXJ2ZXIubGMnKQplbmQK
> $
I then base64 decoded that output to get the actual contents of the file. I sometimes had to run this a few times before I’d get my hashes to match after I decoded the base64 string.
$ echo 'aWYgZmlsZS5leGlzdHMoImNvbmZpZyIpIHRoZW4KICAgIHdpZmkuc2V0bW9kZSggd2lmaS5TVEFUSU9OICkKICAgIHdpZmkuc2V0cGh5bW9kZSggd2lmaS5QSFlNT0RFX04gKQogICAgcHJpbnQoIk5vZGUgTW9kZSIpCiAgICBjb2xsZWN0Z2FyYmFnZSgpCiAgICBkb2ZpbGUoJ21haW4ubGMnKQplbHNlCiAgICB3aWZpLnNldG1vZGUod2lmaS5TT0ZUQVApCiAgICBwcmludCgiU2VydmVyIE1vZGUgLSBsYWNrIG9mIGNvbmZpZyBmaWxlIikKICAgIAogICAgZG9maWxlKCdzZXJ2ZXIubGMnKQplbmQK' | base64 -D
if file.exists("config") then
wifi.setmode( wifi.STATION )
wifi.setphymode( wifi.PHYMODE_N )
print("Node Mode")
collectgarbage()
dofile('main.lc')
else
wifi.setmode(wifi.SOFTAP)
print("Server Mode - lack of config file")
dofile('server.lc')
end
I definitely saw the Server Mode - lack of config file
message earlier over the serial connection so it seemed that the else
branch was being taken.
Next step: Figure out what server.lc
does!
As usual, when presented with a binary file, it’s always(err.. maybe) a good idea to run strings
on it.
LuaQ
ssid
ESP12E Challenge
filler
abcdefghijklmnopqrstuvwxyz1234567890-_
WlFl_Passw0rd <------- THIS LOOKS PROMISING!!!
wifi
config
startup_cfg
netname_cfg
netpass_cfg
unescape
createServer
listen
string
gsub
...
...
Right away, the string W1F1_Passw0rd
is visible. This is probably (you guessed it) the WiFi password for the network created by the ESP8266.
First flag
After connecting to the network and navigating to the device via my browser, I saw this.
First flag!
I didn’t even need to disassemble the Lua bytecode. However, I knew that strings
probably wouldn’t be enough for the other, more difficult, levels. I was also curious as to how the flag was constructed so that it wouldn’t show up in the output from strings.
I thought that it would be best for me to try to disassemble this one and make sense of it since I already knew what the program was supposed to do. This should be simple right? I’d just need a Lua disassembler or decompiler along with an instruction set reference and I should be on my way right? Maybe…
Detour
I’d never had to disassemble or decompile lua bytecode up to this point in my life so I had to google around to bit to figure out what the best tools were for the job. I ended up taking a look at luadec
and chunkspy
.
I tried luadec
at first.
$./luadec server.lc
./luadec: server.lc: bad header in precompiled chunk
$ ./ChunkSpy.1/ChunkSpy.lua server.lc
Pos Hex Data Description or Code
------------------------------------------------------------------------
0000 ** source chunk: server.lc
** global header start **
0000 1B4C7561 header signature: "\27Lua"
0004 51 version (major:minor hex digits)
0005 00 format (0=official)
0006 01 endianness (1=little endian)
0007 04 size of int (bytes)
0008 04 size of size_t (bytes)
0009 04 size of Instruction (bytes)
000A 08 size of number (bytes)
000B 00 integral (1=integral)
* number type: double
* x86 standard (32-bit, little endian, doubles)
** global header end **
ChunkSpy: A Lua 5.1 binary chunk disassembler
Version 0.9.8 (20060307) Copyright (c) 2004-2006 Kein-Hong Man
The COPYRIGHT file describes the conditions under which this
software may be distributed (basically a Lua 5-style license.)
* Run with option -h or --help for usage information
./ChunkSpy-0.9.8/5.1/ChunkSpy.lua:1351: bad constant type 6 at 169
That also doesn’t work (but at least we got a better error message).
These were the two most recommended tools for dealing with Lua bytecode and neither of them worked. Something was up. I had the line number so I took a look at the source code.
1328 -------------------------------------------------------------
1329 -- load constants information (data)
1330 -------------------------------------------------------------
1331 local function LoadConstantKs()
1332 local n = LoadInt()
1333 func.pos_ks = previdx
1334 func.k = {}
1335 func.sizek = n
1336 func.posk = {}
1337 for i = 1, n do
1338 local t = LoadByte()
1339 func.posk[i] = previdx
1340 if t == config.LUA_TNUMBER then
1341 func.k[i] = LoadNumber()
1342 elseif t == config.LUA_TBOOLEAN then
1343 local b = LoadByte()
1344 if b == 0 then b = false else b = true end
1345 func.k[i] = b
1346 elseif t == config.LUA_TSTRING then
1347 func.k[i] = LoadString()
1348 elseif t == config.LUA_TNIL then
1349 func.k[i] = nil
1350 else
1351 error("bad constant type "..t.." at "..previdx)
1352 end
1353 end--for
1354 end
So it seemed that I was running into an unrecognized constant type. I was curious as to whether this disassembler was working properly on my file at all. I thought that a good way to tell would be to print out the constant types and values as they were loaded. I added a couple print statements and re-ran the disassembler.
$ ./ChunkSpy-0.9.8/5.1/ChunkSpy.lua server.lc
Pos Hex Data Description or Code
------------------------------------------------------------------------
0000 ** source chunk: server.lc
** global header start **
0000 1B4C7561 header signature: "\27Lua"
0004 51 version (major:minor hex digits)
0005 00 format (0=official)
0006 01 endianness (1=little endian)
0007 04 size of int (bytes)
0008 04 size of size_t (bytes)
0009 04 size of Instruction (bytes)
000A 08 size of number (bytes)
000B 00 integral (1=integral)
* number type: double
* x86 standard (32-bit, little endian, doubles)
** global header end **
!!!!!!!!!!!!!!!!!!!!
Loading Code
Loading Constants
Type: 6
!!!!!!!!!!!!!!!!!!!!
ChunkSpy: A Lua 5.1 binary chunk disassembler
Version 0.9.8 (20060307) Copyright (c) 2004-2006 Kein-Hong Man
The COPYRIGHT file describes the conditions under which this
software may be distributed (basically a Lua 5-style license.)
* Run with option -h or --help for usage information
./ChunkSpy-0.9.8/5.1/ChunkSpy.lua:1356: bad constant type 6 at 169
That also wasn’t very helpful. Execution seemed to stop very early. I wanted to take a look at the actual contents of the server.lc
file but I wouldn’t have known much about what i was looking at. I decided to find some documentation on the structure of compiled lua files first.
I found this: http://luaforge.net/docman/83/98/ANoFrillsIntroToLua51VMInstructions.pdf
This was probably the most useful document I found during the entire process and I don’t think I would have been able to complete the challenge without it.
From a combination of looking at the document and looking at the source code of the Chunkspy disassembler, I developed a decent understanding of the structure of compiled Lua files. Each Lua program ends up being treated as a function, where each function has its own constants, code, local functions, etc. Each of those local functions would have their own constants,code, etc as well. This was also made obvious by the recursive nature of the disassembler.
In the load constants function, there were a number of different functions; one for loading each type of constant. Taking a closer look, all of the functions just read a fixed number of bytes from the file except for the LoadString function, which read a 4 byte integer first, containing the length of the string, before reading the actual string.
I felt prepared to take a look at the actual contents of the file at this point, so I did.
$ hexdump -C server.lc | head -n12
00000000 1b 4c 75 61 51 00 01 04 04 04 08 00 00 00 00 00 |.LuaQ...........|
00000010 00 00 00 00 00 00 00 00 00 00 02 04 21 00 00 00 |............!...|
00000020 0a 00 00 00 07 00 00 00 05 00 00 00 09 80 c0 80 |................|
00000030 01 00 01 00 07 c0 00 00 05 00 00 00 09 80 c1 82 |................|
00000040 05 c0 01 00 06 00 42 00 06 40 42 00 45 00 00 00 |......B..@B.E...|
00000050 1c 40 00 01 0a 00 00 00 07 80 02 00 01 00 03 00 |.@..............|
00000060 07 c0 02 00 01 00 03 00 07 40 03 00 24 00 00 00 |.........@..$...|
00000070 07 80 03 00 05 00 04 00 06 40 44 00 45 00 04 00 |.........@D.E...|
00000080 46 80 c4 00 1c 80 00 01 07 c0 03 00 05 c0 03 00 |F...............|
00000090 0b c0 44 00 81 00 05 00 e4 40 00 00 1c 40 00 02 |..D......@...@..|
000000a0 1e 00 80 00 15 00 00 00 06 04 00 00 00 63 66 67 |.............cfg|
000000b0 00 06 05 00 00 00 73 73 69 64 00 06 11 00 00 00 |......ssid......|
Looking at offset 0xa9 (169), as mentioned in the error message earlier, I saw the invalid constant, 6. Looking after it, I see 4 bytes followed by the string “cfg”. The 4 bytes before the actual string, in little endian, had an integer value of 4 which was the length of the string when including the null terminator. This was definitely a string constant.
This was a bit puzzling because the check for
elseif t == config.LUA_TSTRING then
was failing.
I checked the values for the different types of constants in the Chunkspy source code.
-----------------------------------------------------------------------
-- chunk constants
-- * changed in 5.1: VERSION, FPF, SIZE_* are now fixed; LUA_TBOOLEAN
-- added for constant table; TEST_NUMBER removed; FORMAT added
-----------------------------------------------------------------------
config.SIGNATURE = "\27Lua"
-- TEST_NUMBER no longer needed, using size_lua_Number + integral
config.LUA_TNIL = 0
config.LUA_TBOOLEAN = 1
config.LUA_TNUMBER = 3
config.LUA_TSTRING = 4
config.VERSION = 81 -- 0x51
config.FORMAT = 0 -- LUAC_FORMAT (new in 5.1)
config.FPF = 50 -- LFIELDS_PER_FLUSH
config.SIZE_OP = 6 -- instruction field bits
config.SIZE_A = 8
config.SIZE_B = 9
config.SIZE_C = 9
-- MAX_STACK no longer needed for instruction decoding, removed
-- LUA_FIRSTINDEX currently not supported; used in SETLIST
config.LUA_FIRSTINDEX = 1
So strings here are associated with the number 4, but the lua bytecode uses 6. At this point, I thought that maybe the compiler used in NodeMCU was different from the regular compiler and that would cause the disassembler (which expects normally structured Lua bytecode) to fail.
I read the Lua Developer FAQ in the NodeMCU docs. It turns out that a specific implementation of Lua is used called eLua.
NodeMCU Lua is based on eLua, a fully featured implementation of Lua 5.1 that has been optimized for embedded system development and execution to provide a scripting framework that can be used to deliver useful applications within the limited RAM and Flash memory resources of embedded processors such as the ESP8266.
Next goal: Modify the Chunkspy disassembler to work with eLua bytecode.
Compilers…
So my goal now, was to figure out the differences between the compiler that was used to generate the lua bytecode and the regular lua compiler that chunkspy was designed for (at least the differences relevant to the file I was working with).
Thankfully, the source code behind eLua wasn’t difficult to find. I dug around a bit found a list of constants here.
/*
** basic types
*/
#define LUA_TNONE (-1)
#define LUA_TNIL 0
#define LUA_TBOOLEAN 1
#define LUA_TROTABLE 2
#define LUA_TLIGHTFUNCTION 3
#define LUA_TLIGHTUSERDATA 4
#define LUA_TNUMBER 5
#define LUA_TSTRING 6
#define LUA_TTABLE 7
#define LUA_TFUNCTION 8
#define LUA_TUSERDATA 9
#define LUA_TTHREAD 10
This definitely matches up to what I saw in the bytecode earlier. Strings here use a value of 6.
I overwrote the list of constants in Chunkspy with these and tried to disassemble server.lc
again.
Loading Code
Loading Constants
Type: 6
Constant Type: string
Value: cfg
Type: 6
Constant Type: string
Value: ssid
Type: 6
Constant Type: string
Value: ESP12E Challenge
Type: 6
Constant Type: string
Value: filler
Type: 6
Constant Type: string
Value: abcdefghijklmnopqrstuvwxyz1234567890-_
Type: 6
Constant Type: string
Value: pwd
Type: 6
Constant Type: string
Value: WlFl_Passw0rd
...
...
Number of constants: 1024 <----------------- SEEMS LIKE A LOT
Type: 0
Constant Type: nil
Type: 6
Constant Type: string <---------------- THESE LOOK OK
Value: string
Type: 6
Constant Type: string
Value: char
Type: 6
Constant Type: string
Value: tonumber
Type: 5
Constant Type: number
Value: 16
...
...
Type: 0
Constant Type: nil <-------------- LONG LIST OF NILS
Type: 0
Constant Type: nil
Type: 0
Constant Type: nil
Type: 0
Constant Type: nil
Type: 23 <------------------------- WE DEFINITELY DIDN'T DEFINE THIS VALUE ANYWHERE
ChunkSpy: A Lua 5.1 binary chunk disassembler
Version 0.9.8 (20060307) Copyright (c) 2004-2006 Kein-Hong Man
The COPYRIGHT file describes the conditions under which this
software may be distributed (basically a Lua 5-style license.)
* Run with option -h or --help for usage information
./ChunkSpy-0.9.8/5.1/ChunkSpy.lua:1372: bad constant type 23 at 761 <---- OH NO
Progress! Well, sort of…
So I saw that a bunch of constants were definitely being loaded correctly. However, somewhere along the way, Chunkspy tries to load a function with its own list of 1024?! constants. What’s strange as well, is that the first 4 values seem legit but then they’re followed by a very long list of nulls until we hit the value 23.
Now, if you’ve worked in hexadecimal a bit, then by just reading that, you probably have a suspicion as to what’s going on.
We have 4 legit constants and 1024 in hex is 0x400.
Knowing that these integers are represented in little endian format, in memory it would look like this:
00 04 00 00 00
It seems that Chunkspy started reading from the first byte shown (0x00) when it should have started reading the integer from byte 0x04. This seemed like an alignment issue.
I knew that node.compile
mentioned in the NodeMCU documentation was supposed to be able to turn a Lua source code file into Lua bytecode. I found the source code for the module here. I followed it until I got to the section where the binary chunks were being dumped to the file.
I searched for the word align and I found this!
static void Align4(DumpState *D)
{
while(D->wrote&3)
DumpChar(0,D);
}
I saw that it was used in the DumpCode
and DumpDebug
functions so I modified the Chunkspy source to include an Align4
function and called it in the appropriate places.
Once I had done that, I tried to run the disassembler on the file again.
...
...
...
000C ** function [0] definition (level 1)
** start of function **
000C 00000000 string size (0)
source name: (none)
0010 00000000 line defined (0)
0014 00000000 last line defined (0)
0018 00 nups (0)
0019 00 numparams (0)
001A 02 is_vararg (2)
001B 04 maxstacksize (4)
* code:
001C 21000000 sizecode (33)
0020 0A000000 [01] newtable 0 0 0 ; array=0, hash=0
0024 07000000 [02] setglobal 0 0 ; cfg
0028 05000000 [03] getglobal 0 0 ; cfg
002C 0980C080 [04] settable 0 257 258 ; "ssid" "ESP12E Challenge"
0030 01000100 [05] loadk 0 4 ; "abcdefghijklmnopqrstuvwxyz1234567890-_"
0034 07C00000 [06] setglobal 0 3 ; filler
0038 05000000 [07] getglobal 0 0 ; cfg
003C 0980C182 [08] settable 0 261 262 ; "pwd" "WlFl_Passw0rd"
0040 05C00100 [09] getglobal 0 7 ; wifi
0044 06004200 [10] gettable 0 0 264 ; "ap"
0048 06404200 [11] gettable 0 0 265 ; "config"
004C 45000000 [12] getglobal 1 0 ; cfg
0050 1C400001 [13] call 0 2 1
0054 0A000000 [14] newtable 0 0 0 ; array=0, hash=0
0058 07800200 [15] setglobal 0 10 ; startup_cfg
...
...
...
Reversing Part 1
I didn’t intend to go to deep while looking at the disassembly for part 1. My goal here was just to become a bit more familiar with the Lua instruction set and understand how the flag was constructed.
Once again, http://luaforge.net/docman/83/98/ANoFrillsIntroToLua51VMInstructions.pdf was extremely helpful.
I’ll only include and annotate the relevant parts below.
[067] loadk 13 19 ; "abcdefghijklmnopqrstuvwxyz1234567890-_"
[068] setglobal 13 18 ; filler - filler is set to the alphabet seen above
...
...
[090] getglobal 14 18 ; filler
[091] self 14 14 285 ; "sub" - put filler.sub in reg 14 - gets substring
[092] loadk 16 30 ; 6 - start
[093] loadk 17 30 ; 6 - end
[094] call 14 4 2 - get the 6th character in the alphabet
[095] getglobal 15 18 ; filler - repeat of what happened above
[096] self 15 15 285 ; "sub"
[097] loadk 17 31 ; 27
[098] loadk 18 31 ; 27
[099] call 15 4 2
[100] getglobal 16 18 ; filler
[101] self 16 16 285 ; "sub"
[102] loadk 18 32 ; 1
[103] loadk 19 32 ; 1
[104] call 16 4 2
[105] getglobal 17 18 ; filler
[106] self 17 17 285 ; "sub"
[107] loadk 19 33 ; 7
[108] loadk 20 33 ; 7
[109] call 17 4 2
...
...
...
I just opened my python interpreter and used the alphabet and the offsets to construct the flag:
>>> alphabet = 'abcdefghijklmnopqrstuvwxyz1234567890-_'
>>> dec = lambda offsets : ''.join(map(lambda x : alphabet[x-1], offsets))
>>> dec([6,27,1,7,38,25,36,36,38,7,15,20,38,20,8,29,38,23,27,6,27,26])
'f1ag_y00_got_th3_w1f1z'
Reversing Part 2
The device told me to hit the RST button after I got the first flag. I did that and connected to it using Serial again to see what the output would be.
From the init script I had looked at earlier,
if file.exists("config") then
wifi.setmode( wifi.STATION )
wifi.setphymode( wifi.PHYMODE_N )
print("Node Mode")
collectgarbage()
dofile('main.lc')
else
wifi.setmode(wifi.SOFTAP)
print("Server Mode - lack of config file")
dofile('server.lc')
end
I grabbed the file off the device then proceede to run strings
on it.
$ strings main.lc
LuaQ
reconnection_count
setdnsserver
8.8.8.8
8.8.4.4
raw_conf
conf
file
open
config
read
sjson
decode
close
station_cfg
ssid
SSID
SSID_pass
wifi
connect
filler
abcdefghijklmnopqrstuvwxyz1234567890-_
flag3
mqtt
Client
voidboy
x448qtur
sauce/data/
status
OFFLINE
offline
message
172.105.204.74
8090
alarm
ALARM_AUTO
filler
wifi
getmac
publish
sauce/data/
status
ONLINE
subscribe
sauce/returnmsg/
print
subscribed
sauce/node/
cjson
decode
config_editor
ssid
wifiPassword
count_editor
start_IN_hardMeter
start_OUT_hardMeter
start_BV_meter
money_collected
node
restart
wifi
status
reconnection_count
print
If device cannot connect to WiFi, delete config and re-enter Hotspot credentials.
This config file will auto delete after 5 reconnection attempts (~90 seconds)
file
remove
config
deleted config file
node
restart
gpio
write
HIGH
connect
172.105.204.74
8090
filler
publish
sauce/data/flag4
I saw lots of interesting strings in there. Mqtt
for one, which is a well known messaging protocol. There are also a couple of strings like sauce/data/flag4
which look a lot like topic names.
I didn’t see any obvious flags in the strings
output so I proceeded to disassemble the file.
While skimming through the disassembly, I saw another sequence that looked just like the one I’d seen previously when constructing the first flag. I decoded it to see what I what get.
Another flag! Two more to go!
Reversing Part 3
For this part, I decided to take a look at the section of the disassembly that was responsible for communicating over mqtt. I knew that the code was using the mqtt module bundled with NodeMCU so I consulted the documentation over here,
I first looked for the section in the code where it was calling the mqtt.Client
function to get everything set up.
[065] getglobal 3 33 ; mqtt
[066] gettable 3 3 290 ; "Client"
[067] loadk 4 35 ; "001"
[068] loadk 5 36 ; 6
[069] loadk 6 37 ; "voidboy"
[070] loadk 7 38 ; "x448qtur"
[071] call 3 5 2 ; CALL THE mqtt.Client function
mqtt.Client(client_id="001", keepalive=6, username="voidboy", password="x448qtur")
I then looked for where it actually connected:
[099] getglobal 3 32 ; mq
[100] self 3 3 284 ; "connect"
[101] loadk 5 46 ; "x.x.x.x" - REDACTED
[102] loadk 6 47 ; "8090"
[103] loadk 7 3 ; 0
[104] loadk 8 3 ; 0
[105] call 3 6 1
mq.connect(host="x.x.x.x", port=8090, 0, 0)
I then looked at what it was sending:
0 [03] getglobal 1 0 ; mq
0 [04] self 1 1 257 ; "publish"
0 [05] loadk 3 2 ; "sauce/data/"
0 [06] loadk 4 3 ; "status"
0 [07] concat 3 3 4
0 [08] loadk 4 4 ; "ONLINE"
0 [09] loadk 5 5 ; 0
0 [10] loadk 6 5 ; 0
0 [11] call 1 6 1
mq.publish(topic="sauce/data/status", payload="ONLINE", 0, 0)
It seems that it was also sending a second payload:
[063] self 0 0 284 ; "sub"
[064] loadk 2 29 ; 6
[065] loadk 3 29 ; 6
[066] call 0 4 2
[067] getglobal 1 27 ; filler
[068] self 1 1 284 ; "sub"
[069] loadk 3 30 ; 2
[070] loadk 4 30 ; 2
[071] call 1 4 2
[072] getglobal 2 27 ; filler
[073] self 2 2 284 ; "sub"
[074] loadk 4 31 ; 32
[075] loadk 5 31 ; 32
[076] call 2 4 2
[077] getglobal 3 27 ; filler
[078] self 3 3 284 ; "sub"
[079] loadk 5 32 ; 29
[080] loadk 6 32 ; 29
[081] call 3 4 2
[082] getglobal 4 27 ; filler
[083] self 4 4 284 ; "sub"
[084] loadk 6 9 ; 1
[085] loadk 7 9 ; 1
[086] call 4 4 2
[087] getglobal 5 27 ; filler
[088] self 5 5 284 ; "sub"
[089] loadk 7 33 ; 35
[090] loadk 8 33 ; 35
[091] call 5 4 2
[092] concat 0 0 5
[093] getglobal 1 23 ; mq
[094] self 1 1 290 ; "publish"
[095] loadk 3 35 ; "sauce/data/flag4"
[096] move 4 0
[097] loadk 5 21 ; 0
[098] loadk 6 21 ; 0
[099] call 1 6 1
>>> dec([6,2,32,29,1,35])
'fb63a9'
which results in:
mq.publish(topic="sauce/data/flag4", payload="fb63a9", 0, 0)
I put all these pieces together and listening on all topics in the python script below:
import time
import paho.mqtt.client as mqtt
username = 'voidboy'
password = 'x448qtur'
host = 'x.x.x.x' #REDACTED
port = 8090
client = mqtt.Client(client_id="001")
client.username_pw_set(username, password)
def on_message(client, userdata, message):
print("message received ", str(message.payload.decode("utf-8")))
print("message received ", message.payload)
print("message topic=", message.topic)
print("message qos=", message.qos)
print("message retain flag=", message.retain)
def on_connect(client, userdata, flags, rc):
print "Connected with code: %d!" % rc
ret, mid = client.subscribe('#')
print "Subscription: " + ("success"
if ret == mqtt.MQTT_ERR_SUCCESS else "Fail")
client.publish('sauce/data/status', 'ONLINE')
client.publish('sauce/data/flag4', 'fb63a9')
def on_disconnect(client, userdata, rc):
print "Disconnected with code: %d!" % rc
def on_subscribe(client, userdata, mid, granted_qos):
print "Client subscribed : %d " % mid
#set up callbacks
client.on_message = on_message
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_subscribe = on_subscribe
client.connect(host, port=port)
client.loop_start()
time.sleep(100)
After running this, I received a message containing the string: 081f31a1d21d47809bbdbf20e7b34267
I checked its length and it was 32 characters long which made me think that it could probably be a MD5 hash. I performed a reverse lookup online and got the string flg4=1
. There was my 3rd flag!
Reversing Part 4
It was time for me to find my final and fourth flag.
The closure
instruction was probably the trickiest one to understand out of everything I’d seen in the lua bytecode. It turns out that all it does is create an instance of a function and possibly fill in some upvalues.
The function below is the function that gave me the second flag: f1ag_0h_5h1t_u_r3ad1n_m3mz
** function [0] definition (level 2)
** start of function **
string size (0)
source name: (none)
line defined (25)
last line defined (33)
nups (0)
numparams (0)
is_vararg (0)
maxstacksize (12)
* code:
sizecode (143)
[001] getglobal 0 0 ; filler
[002] self 0 0 257 ; "sub"
[003] loadk 2 2 ; 6
[004] loadk 3 2 ; 6
[005] call 0 4 2
[006] getglobal 1 0 ; filler
[007] self 1 1 257 ; "sub"
[008] loadk 3 3 ; 27
[009] loadk 4 3 ; 27
[010] call 1 4 2
[011] getglobal 2 0 ; filler
[012] self 2 2 257 ; "sub"
[013] loadk 4 4 ; 1
[014] loadk 5 4 ; 1
[015] call 2 4 2
[016] getglobal 3 0 ; filler
[017] self 3 3 257 ; "sub"
[018] loadk 5 5 ; 7
[019] loadk 6 5 ; 7
[020] call 3 4 2
[021] getglobal 4 0 ; filler
...
...
...
** function [1] definition (level 2)
** start of function **
string size (0)
source name: (none)
line defined (35)
last line defined (38)
nups (1)
numparams (0)
is_vararg (0)
maxstacksize (3)
* code:
sizecode (10)
[01] getupval 0 0 -- get instance of function [0] above
[02] call 0 1 2 -- call it, f1ag_0h_5h1t_u_r3ad1n_m3mz is placed into
-- register 0
[03] loadk 1 1 ; "," -- , is placed into register 1
[04] getglobal 2 2 ; wifi
[05] gettable 2 2 259 ; "sta"
[06] gettable 2 2 260 ; "getmac"
[07] call 2 1 2 -- the mac address of the wnic is placed into register 2
[08] concat 0 0 2 -- all the values are concat'd
[09] setglobal 0 0 ; msg - 'f1ag_0h_5h1t_u_r3ad1n_m3mz,5c:cf:7f:c1:22:7d'
[10] return 0 1
I got the string 'f1ag_0h_5h1t_u_r3ad1n_m3mz,5c:cf:7f:c1:22:7d'
. I then wrote another small python script to send this string to the topic for flag3.
import time
import paho.mqtt.client as mqtt
username = 'voidboy'
password = 'x448qtur'
#password = 'y448qtur'
host = 'x.x.x.x'
port = 8090
client = mqtt.Client(client_id="001")
client.username_pw_set(username, password)
def on_message(client, userdata, message):
print("message received ", str(message.payload.decode("utf-8")))
print("message received ", message.payload)
print("message topic=", message.topic)
print("message qos=", message.qos)
print("message retain flag=", message.retain)
def on_connect(client, userdata, flags, rc):
print "Connected with code: %d!" % rc
ret, mid = client.subscribe('#')
print "Subscription: " + ("success"
if ret == mqtt.MQTT_ERR_SUCCESS else "Fail")
client.publish('sauce/data/status', 'ONLINE')
msg = 'f1ag_0h_5h1t_u_r3ad1n_m3mz,5c:cf:7f:c1:22:7d'
client.publish('sauce/data/flag3', msg)
def on_disconnect(client, userdata, rc):
print "Disconnected with code: %d!" % rc
def on_subscribe(client, userdata, mid, granted_qos):
print "Client subscribed : %d " % mid
#set up callbacks
client.on_message = on_message
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_subscribe = on_subscribe
client.connect(host, port=port)
client.loop_start()
time.sleep(100)
After I ran my script, I got a message containing the final flag: y0u_c4n_wr1t3_mqtts_n0w
!!!
Conclusion
I had so much fun doing this challenge. Definitely the best thing I received this year for my birthday! I’d never dealt with electronics before and I learned a few useful things about baud rates and how to interact with these sorts of devices. Thanks for reading!
Note: I’ll probably create a pull request for Chunkspy to add an eLua mode.Hopefully the next person won’t have to dig as deep as I had to to get some disassembly :)