In my last blog post I talked about a simple Lua decompiler I made using my updated Lua 5.1 (de)serializer. I recently added support to re-serialize chunks back into Lua dumps (the equivalent of ldump.c). Using this, I wrote a tiny python script to strip debugging info from Lua 5.1 dumps automagically.
It looks like:
#!/usr/bin/env python3
'''
usage: lstrip [-h] -i FILE [-o FILE]
Strips local and debugging information from Lua 5.1 dump files
options:
-h, --help show this help message and exit
-i FILE, --in FILE specify the input Lua dump to be stripped
-o FILE, --out FILE specify the output dump location
Depends on lundump.py (https://github.com/CPunch/LuaDecompy/blob/main/lundump.py)
'''
from argparse import ArgumentParser
from lundump import Chunk, LuaUndump, LuaDump
class LuaStrip:
def __init__(self, root: Chunk) -> None:
self.root = root
# recursively strips debugging information from proto
@staticmethod
def __stripChunk(chunk: Chunk) -> Chunk:
chunk.locals.clear()
chunk.lineNums.clear()
chunk.upvalues.clear()
# recursively strip child protos
for i in range(len(chunk.protos)):
chunk.protos[i] = LuaStrip.__stripChunk(chunk.protos[i])
return chunk
def genBytecode(self) -> bytearray:
obfChunk = LuaStrip.__stripChunk(self.root)
dump = LuaDump(obfChunk)
return dump.dump()
if __name__ == "__main__":
parser = ArgumentParser(
prog='lstrip',
description='Strips local and debugging information from Lua 5.1 dump files'
)
# arguments
parser.add_argument('-i', '--in',
required=True,
dest='inFile',
metavar='FILE',
help='specify the input Lua dump to be stripped',
type=str
)
parser.add_argument('-o', '--out',
dest='outFile',
metavar='FILE',
default='out.luac',
help='specify the output dump location',
type=str
)
args = parser.parse_args()
# load input dump
try:
obf = LuaStrip(LuaUndump().loadFile(args.inFile))
with open(args.outFile, "wb") as file:
bc = obf.genBytecode()
# disassemble new bytecode
LuaUndump().decode_rawbytecode(bc).print()
# dump to file
file.write(bc)
file.close()
except Exception as e:
print("ERR: %s " % e)
Since I have support for dumping the chunks, it came out a whole lot simpler than I thought.
Usage
Simply compile your target script:
$ cat example.lua && luac5.1 -o example.luac example.lua
local printMsg = function(append)
local tbl = {"He", "llo", " ", "Wo"}
local str = ""
for i = 1, #tbl do
str = str .. tbl[i]
end
print(str .. append)
end
printMsg("rld!")
Then pass the luac dump to lstrip
:
$ lstrip -i example.luac -o out.luac
The luavm doesn’t care about the missing debugging data!
$ lua5.1 out.luac
Hello World!
Does it trip up decompilers though?
Yes. As you can see, passing it to my own decompiler breaks it lol
In fact, to get anything reliable from the decompiler, I have to switch to an aggressive localization mode which makes the output pseudo-code extremely verbose.
located in lparser.py, in the
__init__
for LuaDecomp
And even then, it’s still impossible to recover what used to be a local and what was a temporary expression register (without using some better parsing techniques)
Looking at a more mature decompiler, luadec has similar issues.
-- params : ...
-- function num : 0
local l_0_0 = function(l_1_0, ...)
-- function num : 0_0
local l_1_1 = {}
-- DECOMPILER ERROR at PC5: No list found for R1 , SetList fails
-- DECOMPILER ERROR at PC6: Overwrote pending register: R2 in 'AssignReg'
local l_1_2 = "He"
-- DECOMPILER ERROR at PC7: Overwrote pending register: R3 in 'AssignReg'
-- DECOMPILER ERROR at PC8: Overwrote pending register: R4 in 'AssignReg'
-- DECOMPILER ERROR at PC9: Overwrote pending register: R5 in 'AssignReg'
for l_1_6 = "llo", " ", "Wo" do
l_1_2 = l_1_2 .. l_1_1[l_1_6]
end
print(l_1_2 .. l_1_0)
end
l_0_0("rld!")
Who knew that such a simple stripping of debugging information is enough to trip up decompiliers in the lord’s year of ‘22.