
Unscrambling Lua
by Daniel Santos
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 a 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 in order 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.
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Ā in order to enable SSH access.
Figure 2: default-config.xml
Now that I knew what had to be changed, I needed a way to alter the routerās configuration. There are many routes that would lead to enable me to successfully 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. In order to do that I obviously 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 know file format/structure and not very informationĀ .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 targeted 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, specially 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.
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 as 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.
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 in order to support a different kind of lua_Number (more on that latter). 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.
Figure 5: luadec 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 with 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 a āA No-Frills Introduction to Lua 5.1 VM Instructionsā, exactly what I was looking for. I also borrowed some insights and code from this blog postĀ https://openpunk.com/post/7Ā and eventually built a fully working Lua 5.1 bytecode parser.
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:
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.
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.
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. Basically, 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 (http://foo.bar) that builds a substitution map and is able to reverse the opcode swapping process given a set of sample (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
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 was using to encrypt its backup files.
Figure 12: Hardcoded encryption key
At last, I was able to forge my own configuration backup file and includeĀ RemoteSSHĀ parameter in dropbearās configuration and get SSH access to the router.
Figure 13: SSH shell
Article originally published at: https://medium.com/@vovohelo/unscrambling-lua-7bccb3d5660
Author
