Unscrambling Lua

Daniel Santos
8 min readJun 3, 2020

During times of pandemic one needs to find interesting things to keep its mind sharp. Because of that, I decided to conduct a security assessment on my home wifi router. After working for a week I could find some stored XSS (CVEs pending) in the router’s web UI but that was not the interesting part. During the tests, I noticed the router’s SSH service was enabled and that is was running an old version of the Dropbear SSH daemon. However, trying to open any kind of SSH channel other than “forwarded-tcpip” and “direct-tcpip” failed, which meant that no type of remote command execution was possible. Remote and local port forwarding were working and because of that, I was able to verify the admin credentials I had for the web interface were also valid for SSH. After looking for official ways of enabling SSH remote access I found a post in the router’s vendor support forum stating that SSH could only be used by their mobile application and that it was a security feature of the product having it disabled. After reading this I thought the only thing any security professional would after reading such a statement, “challenge accepted”.

At this point I had a clear mission, getting shell access to the device through SSH. The first thing I did to accomplish that was downloading the official firmware and start looking for clues that could get me started using binwalk. For those unfamiliar with binwalk, I suggest reading this post by Sergio Prado.

Binwalk’s output
Figure 1: Binwalk output

Reviewing binwalk’s output I was able to confirm that the device runs on MIPS, that it uses the OpenWrt operating system, and that it is running version 3.3.8 of the Linux kernel. Besides, after extracting the gzip compressed data at the end of the firmware file, which was a compressed POSIX tar archive, I was able to get an xml file called default-config.xml. Reviewing the file contents, I was able to identify a section dedicated to dropbear’s configuration. After some googling, I found out that I had to somehow include the RemoteSSH parameter in dropbear’s configuration and set it to on to enable SSH access.

default-config.xml
Figure 2: default-config.xml

Now that I knew what had to be changed, I needed a way to alter the router’s configuration. Many routes would lead to enabling me to accomplish this, but I would like to talk specifically about the one that taught me a lot about the LUA language.

My idea was simple and far from innovative, I was going to try to subvert the restore (from the backup/restore functionality) function of the router’s web UI into loading a tampered configuration containing the parameter I needed to include in dropbear’s section. To do that I had to be able to forge or alter a valid backup file. So I did the most obvious thing one would do and asked the router to backup its current configuration through the web UI, which provided me a file with no readable content, no known file format/structure, and a not very useful .bin extension. Checking the file content’s entropy, I was 99% sure the file was encrypted so I had no choice but to revert the encryption process if I wanted to achieve my goal.

After some investigation, I noticed most Ajax calls the router’s web UI did target URL’s with the following format: http://[router’s ip]/cgi-bin/luci/;stok=[long random hex number]/[some path]. Please keep in mind I had no previous knowledge about OpenWrt besides knowing what it was, so I had to do a little research to learn that luci is the standard web configuration tool for OpenWrt. I then decided to extract the squashfs file system contained in the firmware I had previously downloaded to look for the files related to the backup/restore function and its probable encryption process. The file system had no type of protection, so I was able to easily mount it and browse its files. Some of the files under the usr/lib/lua/luci/ caught my attention, especially the model/crypto.lua. I mean, if I were to revert any encryption process that seamed like the file I should investigate, right? First things first, the file was not readable (unlucky me), but I was able to confirm it was compiled using Lua’s 5.1 bytecode format.

Confirming crypto.lua format
Figure 3: Confirming crypto.lua format

So, I did what any security professional does when he wants to reverse a compiled chunk of code and has no idea how to do that. I googled “How to reverse lua’s bytecode” and thankfully there was already a project to do what I needed https://github.com/viruscamp/luadec. The problem was that after compiling luadec according to instructions and running it against the crypto.lua file the only thing I got back was a “bad header in precompiled chunk” error.

Bad header error
Figure 4: Bad header error

Believe me when I say I do not remember the last time trying to overcome an error message taught me so much. I tried all sorts of things and when I was already pulling the last threads of hair I still have I found this post from a fellow Chinese https://blog.ihipop.info/2018/05/5110.html. What happens is that OpenWrt applies a bunch of patches to the Lua codebase to support a different kind of lua_Number (more on that later). By the time I found this post I had no idea what a lua_Number was, so I blindly followed the tutorial, using a docker container inside a disposable Ubuntu 18.04 VM. The following command can be used to run the container.

docker run — rm -it bestwu/deepin:15.5 bash

After following the steps, I had luadec compiled with all patches required, or so I thought, but running it just gifted me with another error, bad code in precompiled chunk. Trying to use the patched version of luac resulted in the very same error.

luadec bad code in precompiled chunk
Figure 5: luadec bad code in precompiled chunk
luac bad code in precompiled chunk
Figure 6: luac bad code in precompiled chunk

I was really frustrated as I already invested valuable hours of my free time on the project at this point, but if there is an aspect of my personality I can safely self-diagnose is that I am kind of obsessive about things that challenge me. I decided then to deep dive in Lua’s bytecode I write a parser of my own which could potentially give me any information about why luadec and luac were not working even with the proper patches applied. That was when I found this resource http://luaforge.net/docman/83/98/ANoFrillsIntroToLua51VMInstructions.pdf which, as the name stated, was an “A No-Frills Introduction to Lua 5.1 VM Instructions”, exactly what I was looking for. I also borrowed some insights and code from the blog post at https://openpunk.com/post/7 and eventually built a fully working Lua 5.1 bytecode parser.

lua bytecode parser output
Figure 7: lua bytecode parser output

A had mixed feelings when the parser was done, at the same time I was glad I could make it work, now I had no idea why I was getting the error I was trying to fix in the first place. If my parser could properly parse the files with no errors and if it were properly following the language specifications, why couldn’t luac or luadec do the same? Answering this question took another clueless few days, but eventually, something caught my attention. Reviewing the “A No-Frills Introduction to Lua 5.1 VM Instructions” document I learned the following:

Quotation about return instructions
Figure 8: Quotation about return instructions

If that was the case, every function prototype (instruction block) must end with a return instruction, right? If you want to confirm that yourself, try compiling an empty file with the luac compiler and see what happens. More than that, try compiling any Lua script and you will see that every function block is terminated with a RETURN instruction, even if that instruction is not reachable.

Empty file compiled with luac
Figure 9: Empty file compiled with luac

Why is that important? Well, I noticed the function blocks for the router’s Lua bytecode files were ending with a CLOSE instruction instead of a RETURN! Then it hit me, they were using the oldest play in the book when it comes to obfuscation, instruction swapping. What instruction swapping does is basically act as a substitution cipher for the language opcodes. So the router’s Lua runtime library would perform the actions of a RETURN instruction when the CLOSE opcode was parsed, for example.

CLOSE instead of RETURN
Figure 10: CLOSE instead of RETURN

Now that I knew what was going on, I only needed to choose an approach to reverse the swapping process. There are many ways to beat a simple substitution cipher and this is outside the scope of this article, but, what I eventually decided to use was a known-plaintext attack. Here is how the attack works, in a simple substitution cipher, A always maps to B, so for example, CLOSE always maps to RETURN. What I needed then was a collection of untampered Lua bytecode files and their respective swapped versions. With that, I could create an opcode map and reverse the swapping process. So, if I had file A and B where B is the swapped version of A and for every function block Fi, the number of instructions in Fi is the same in A and B, we can safely assume that every instruction opcode in A is mapped to its respective instruction in B. A good set of candidate files to perform this attack are the luci files, available at https://github.com/openwrt/luci.

I then wrote a python script (https://github.com/bananabr/ulua) that builds a substitution map and is able to reverse the opcode swapping process given a set of samples (swapped) and a reference (original) files. Running the following command, for example, would create a patched version of the tampered crypto.lua file parseable by OpenWrt’s standard Lua 5.1 tools.

python3 ulua.py -r ref/ -s sample/ -f crypto.lua -o crypto.patched.lua
ulua.py output
Figure 11: ulua.py output

Before running the script, I used the following commands to copy every .lua file in the router firmware extracted root filesystem to a directory named sample and to compile every lua script in the libs folder of luci’s source code to a folder named ref.

mkdir ./sample/; find ./squashfs-root/usr/lib/lua/luci/ -iname “*.lua” -exec cp {} ./sample/ \;mkdir ./ref/; find openwrt-luci/libs/ -iname “*.lua” -exec bash -c ‘luac -o ./ref/`basename {}` {}’ \;

I was finally able to use the patched versions of luac and luadec with the files generated by my script and eventually, I looked at the firmware.lua file, which I could confirm was responsible for the backup/restore process. By reviewing luadec’s output I was then able to extract the hardcoded key the router used to encrypt its backup files.

Hardcoded encryption key
Figure 12: Hardcoded encryption key

At last, I was able to forge my configuration backup file and include the RemoteSSH parameter in dropbear’s configuration and get SSH access to the router.

Figure 13: SSH shell
Figure 13: SSH shell

--

--