Corona SDK / Solar2D¶
Corona SDK (rebranded as Solar2D in 2020) builds Android apps using Lua as the scripting language, with a C/C++ runtime handling rendering, physics, and platform APIs. Application Lua code is compiled to Lua bytecode and packed into a resource.car archive inside the APK's assets/ directory. The runtime executes this bytecode through an embedded Lua VM (liblua.so) managed by the Corona native engine (libcorona.so). Solar2D is open-source and primarily used for 2D games, though some utility apps and malware samples have used the framework for rapid cross-platform deployment.
Architecture¶
| Component | Role |
|---|---|
| Lua VM | Executes Lua bytecode via embedded liblua.so (Lua 5.1 based) |
| Corona Runtime | C++ engine managing app lifecycle, rendering, and native API bridging |
| resource.car | Corona Archive containing compiled Lua bytecode files |
| Corona Plugins | Native libraries providing extended functionality (ads, analytics, IAP) |
Execution flow: Android activity launches via com.ansca.corona.CoronaActivity, libcorona.so initializes the runtime and Lua VM, the runtime loads Lua bytecode from resource.car, and main.lua (compiled) executes as the entry point. All application logic runs through Lua calling Corona APIs for rendering, networking, and platform access.
| Library | Purpose |
|---|---|
libcorona.so |
Corona runtime engine |
liblua.so |
Lua 5.1 virtual machine |
libalmixer.so |
Audio mixing library |
libmpg123.so |
MP3 decoding |
libopenal.so |
OpenAL audio backend |
Identification¶
| Indicator | Location |
|---|---|
libcorona.so |
lib/<arch>/ directory |
liblua.so |
lib/<arch>/ directory |
assets/resource.car |
Corona archive with compiled Lua files |
com.ansca.corona.* |
Package prefix in DEX classes |
com.ansca.corona.CoronaActivity |
Main activity class |
Quick check:
Code Location & Extraction¶
resource.car Format¶
The resource.car file is a Corona-specific archive containing compiled Lua bytecode files. It uses a custom header and file table, not a standard archive format.
Unpacking resource.car¶
Use the Car Unpacker tool:
If no unpacker is available, examine the binary structure:
The archive contains files with .lu extensions -- compiled Lua bytecode, not Lua source.
Identifying Lua Bytecode¶
Lua 5.1 bytecode files start with a distinctive header:
| Offset | Value | Meaning |
|---|---|---|
| 0x00 | \x1bLua |
Lua bytecode signature |
| 0x04 | 0x51 |
Lua version 5.1 |
| 0x05 | 0x00 |
Format version (official) |
Lua Decompilation¶
unluac is the primary decompiler for Lua 5.1 bytecode:
Batch decompilation:
luadec is an alternative that handles some constructs unluac struggles with:
| Tool | Strengths | Weaknesses |
|---|---|---|
| unluac | Best overall Lua 5.1 support | Struggles with heavily obfuscated bytecode |
| luadec | Handles some edge cases better | Less maintained, harder to build |
Analysis of Decompiled Code¶
grep -rn "network\.request\|socket\|http" decompiled/
grep -rn "crypto\|encrypt\|decrypt\|key\|password" decompiled/
Key Corona API patterns:
| Pattern | Significance |
|---|---|
network.request() |
HTTP requests -- extract endpoints and parameters |
network.download() |
File downloads from remote servers |
system.getInfo() |
Device fingerprinting |
native.showAlert() |
UI dialogs -- phishing lure text |
store.purchase() |
In-app purchase manipulation |
crypto.digest() |
Cryptographic operations |
io.open() |
Local file access |
Encryption¶
Some Corona/Solar2D apps encrypt their Lua bytecode before packing into resource.car. Indicators:
- Extracted
.lufiles do not start with\x1bLuaheader - Files appear as random bytes with high entropy
- Decompilers fail with format errors
The decryption routine resides in libcorona.so since the runtime must decrypt bytecode before passing it to the Lua VM. The most reliable extraction method is intercepting luaL_loadbuffer to capture bytecode after decryption:
var luaModule = Process.findModuleByName("liblua.so");
if (luaModule) {
var luaL_loadbuffer = luaModule.findExportByName("luaL_loadbuffer");
if (luaL_loadbuffer) {
Interceptor.attach(luaL_loadbuffer, {
onEnter: function(args) {
var buf = args[1];
var size = args[2].toInt32();
var name = args[3].readCString();
console.log("[Lua] Loading: " + name + " (" + size + " bytes)");
var outPath = "/data/local/tmp/lua_dump/" + name.replace(/\//g, "_");
var f = new File(outPath, "wb");
f.write(buf.readByteArray(size));
f.close();
}
});
}
}
This captures every Lua chunk after decryption, producing clean bytecode files that can be decompiled with unluac.
To locate decryption symbols statically:
var coronaModule = Process.findModuleByName("libcorona.so");
if (coronaModule) {
coronaModule.enumerateExports().forEach(function(exp) {
if (exp.name.indexOf("decrypt") !== -1 || exp.name.indexOf("Decrypt") !== -1) {
console.log("[Corona] " + exp.name + " @ " + exp.address);
}
});
}
Hooking Strategy¶
Lua VM Interception¶
The primary hooking point is luaL_loadbuffer in liblua.so, which receives all Lua code before execution:
var luaModule = Process.findModuleByName("liblua.so");
var luaL_loadbuffer = luaModule.findExportByName("luaL_loadbuffer");
Interceptor.attach(luaL_loadbuffer, {
onEnter: function(args) {
this.name = args[3].readCString();
this.size = args[2].toInt32();
console.log("[Lua] Load: " + this.name + " size=" + this.size);
}
});
Lua Function Call Monitoring¶
Hook lua_pcall to trace function execution:
var lua_pcall = luaModule.findExportByName("lua_pcall");
Interceptor.attach(lua_pcall, {
onEnter: function(args) {
var nargs = args[1].toInt32();
var nresults = args[2].toInt32();
console.log("[Lua] pcall nargs=" + nargs + " nresults=" + nresults);
},
onLeave: function(retval) {
if (retval.toInt32() !== 0) {
console.log("[Lua] pcall error code: " + retval.toInt32());
}
}
});
Java-Layer Corona Hooks¶
Java.perform(function() {
var CoronaActivity = Java.use("com.ansca.corona.CoronaActivity");
CoronaActivity.onCreate.implementation = function(bundle) {
console.log("[Corona] Activity created");
this.onCreate(bundle);
};
var CoronaRuntimeTaskDispatcher = Java.use("com.ansca.corona.CoronaRuntimeTaskDispatcher");
CoronaRuntimeTaskDispatcher.send.implementation = function(task) {
console.log("[Corona] Runtime task: " + task.getClass().getName());
this.send(task);
};
});
SSL Pinning Bypass¶
Corona's networking uses Java's HTTP stack under the hood. Standard Android SSL bypass techniques apply:
Java.perform(function() {
var SSLContext = Java.use("javax.net.ssl.SSLContext");
var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
var TrustAll = Java.registerClass({
name: "com.bypass.TrustAll",
implements: [X509TrustManager],
methods: {
checkClientTrusted: function(chain, authType) {},
checkServerTrusted: function(chain, authType) {},
getAcceptedIssuers: function() { return []; }
}
});
var managers = Java.array("javax.net.ssl.TrustManager", [TrustAll.$new()]);
var ctx = SSLContext.getInstance("TLS");
ctx.init(null, managers, null);
SSLContext.setDefault(ctx);
});
If pinning is implemented in Lua code via network.request parameters, patch the decompiled Lua source, recompile with luac, repack into resource.car, and rebuild the APK.
RE Difficulty Assessment¶
| Aspect | Standard Build | Encrypted Lua |
|---|---|---|
| Code format | Lua 5.1 bytecode in resource.car | Encrypted bytecode in resource.car |
| Readability | High after decompilation with unluac | Requires runtime dump first |
| String extraction | Trivial from bytecode | Requires decryption |
| Decompiler quality | Good -- unluac handles most constructs | Same after decryption |
| Patching | Decompile, edit, recompile with luac | Must also handle re-encryption or bypass |
| Overall difficulty | Easy | Moderate |
Corona/Solar2D apps with standard (unencrypted) Lua bytecode are straightforward targets. The resource.car unpacking adds one extra step compared to frameworks that store scripts as loose files, but mature tooling handles this well. Encrypted builds require runtime interception via luaL_loadbuffer hooking to dump decrypted bytecode before decompilation can proceed.