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 :)