I discovered factorio a few weeks ago and have been playing only in multiplayer since then. The amount of freedom we have in this game is pretty damn high !
As a server owner, I decided to unlock LUA in the factorio executable in order to have full access to LUA from the mods. As you may know, some modules like "io" and "os" are locked to prevent mods doing awful things to your computer. But I believe more in freedom than in lockups, and also I'm getting my hands dirty in learning reverse engineering, so I decided to work on this. I will be using the Factorio .30 experimental version, 64bits, on Windows.
This thread is an educational one, you won't find any modified binary here. If you want to patch your binaries yourself, you can follow my instructions and try to do it yourself, you will have great fun doing it !
I hope to make this into a serie, as I have more plans for factorio, that I will talk to you about later
Disclamer: I am not a modder myself, and I known almost nothing about LUA. I may write errors here, please let me know if I say something wrong.
This first episode is about unlocking LUA. What would any mod need to use those "io" and "os" modules ?
Check the lua documentation for these modules :
io : http://www.lua.org/manual/5.1/manual.html#5.7 basically read/write files. Could be useful for statistic gathering, mod syncing on multiplayer, etc..
os : http://www.lua.org/manual/5.1/manual.html#5.8 launch processes from your mods, get the system date & time, get environment variables, etc...
1) Finding an entry point for our research :
When we try to use the "io" module from within a mod, the game launches with this error : Require doesn't work from console and from remote.call.
That means there is some check in factorio which checks if the mods use the "require" lua keyword, and prompts this error when it happens. Then the game quits.
We are going to disable this check, so that "require" works again.
2) Disassembling factorio.exe :
Factorio is an executable which contains lua inside it. If you watch your other games folders, you might see exe files as well as dll files. For other games using lua, you might see lua.dll near the game dll. This is not the case here. That means that the source code of lua has been compiled with the other factorio game files.
With IDA (https://www.hex-rays.com/products/ida/s ... ware.shtml) we can disassemble the executable and see what's inside (if you know ASM and C languages)
First , launch IDA then open the factorio executable located in your steam installation. For me it's in D:\Program Files (x86)\Steam\steamapps\common\Factorio\bin\x64\factorio.exe
IDA will ask you to load a file with some parameters, just keep the default ones. When it asks you to use the debug informations, say yes. That helps us in our research.
The diassembling process will take some time depending on your CPU performance. It's a long process, because each instructions in the executable have to be decoded to ASM ones, and IDA also makes links and resolves references between every function call that happen in the process. To see if the analysis is finished, look in the bottom left of the IDA window. If you see "AC:XXXX" changing, i means it hasn't finished When you see 'AU: idle", it's done !
IDA then points you to the entry point of the executable. The first line of code (a funcion) that is executed if you launch it. In our case, it's wmainCRTStartup()
Now, let's go hunt if the error message "Require doesn't work from console" is actually contained in our executable.
For this, open the 'strings' subview in IDA (Menu View -> Open subviews -> Strings).
The "String window" appears. Type 'Alt + T' to search inside, and search for "from console". It's here !
Now double click on this line, it will show you how it's declared in the executable :
Code: Select all
string : .rdata:0000000140948428 00000038 C Require doesn't work from console and from remote.call.
Right click on it's name and select "jump to xref to operand...".
It seems it's used only in one function (we are lucky !).
Code: Select all
signed __int64 __fastcall LuaHelper::packageRequire(lua_State *L)
Here is where IDA jumps to :
Code: Select all
.text:00000001403939D5 loc_1403939D5: ; CODE XREF: LuaHelper::packageRequire(lua_State *)+446j
.text:00000001403939D5 lea r8, [rsp+168h+name]
.text:00000001403939DA cmp [rsp+168h+name.baseclass_0.baseclass_0._Myres], 10h
.text:00000001403939E0 cmovnb r8, qword ptr [rsp+168h+name.baseclass_0.baseclass_0._Bx]
.text:00000001403939E6 lea rdx, aModuleSNotFoun ; "module %s not found; "
.text:00000001403939ED lea rcx, [rsp+168h+message] ; result
.text:00000001403939F2 call ?ssprintf@@YA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@PEBDZZ ; ssprintf(char const *,...)
.text:00000001403939F7 nop
.text:00000001403939F8 xor r14d, r14d
.text:00000001403939FB mov [rsp+168h+path.package], r14
.text:0000000140393A03 mov [rsp+168h+path.path.m_pathname.baseclass_0.baseclass_0._Myres], 7
.text:0000000140393A0F mov [rsp+168h+path.path.m_pathname.baseclass_0.baseclass_0._Mysize], r14
.text:0000000140393A17 mov word ptr [rsp+168h+path.path.m_pathname.baseclass_0.baseclass_0._Bx], r14w
.text:0000000140393A20 lea rdx, [rsp+168h+path] ; path
.text:0000000140393A28 mov rcx, r13 ; L
.text:0000000140393A2B call ?loadPackagePath@LuaHelper@@YAXPEAUlua_State@@AEAVPackagePath@@@Z ; LuaHelper::loadPackagePath(lua_State *,PackagePath &)
.text:0000000140393A30 cmp [rsp+168h+path.package], r14
.text:0000000140393A38 jnz loc_140393ACE
.text:0000000140393A3E mov [rsp+168h+result.baseclass_0.baseclass_0._Myres], 0Fh
.text:0000000140393A4A mov [rsp+168h+result.baseclass_0.baseclass_0._Mysize], r14
.text:0000000140393A52 mov byte ptr [rsp+168h+result.baseclass_0.baseclass_0._Bx], r14b
.text:0000000140393A5A lea r8d, [r14+37h] ; _Count
.text:0000000140393A5E lea rdx, aRequireDoesnTW ; "Require doesn't work from console and f"...
.text:0000000140393A65 lea rcx, [rsp+168h+result] ; this
.text:0000000140393A6D call ?assign@?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QEAAAEAV12@PEBD_K@Z ; std::basic_string<char,std::char_traits<char>,std::allocator<char>>::assign(char const *,unsigned __int64)
.text:0000000140393A72 nop
.text:0000000140393A73 lea rax, [rsp+168h+result]
.text:0000000140393A7B cmp [rsp+168h+result.baseclass_0.baseclass_0._Myres], 10h
.text:0000000140393A84 cmovnb rax, qword ptr [rsp+168h+result.baseclass_0.baseclass_0._Bx]
.text:0000000140393A8D mov [rsp+168h+_What], rax
.text:0000000140393A95 lea rdx, [rsp+168h+_What] ; _What
.text:0000000140393A9D lea rcx, [rsp+168h+pExceptionObject] ; this
.text:0000000140393AA5 call ??0exception@std@@QEAA@AEBQEBD@Z ; std::exception::exception(char const * const &)
.text:0000000140393AAA lea rax, ??_7ScriptException@@6B@ ; const ScriptException::`vftable'
.text:0000000140393AB1 mov [rsp+168h+pExceptionObject.vfptr], rax
.text:0000000140393AB9 lea rdx, _TI4?AVScriptException@@ ; pThrowInfo
.text:0000000140393AC0 lea rcx, [rsp+168h+pExceptionObject] ; pExceptionObject
.text:0000000140393AC8 call _CxxThrowException
.text:0000000140393AC8 ; ---------------------------------------------------------------------------
.text:0000000140393ACD align 2
.text:0000000140393ACE
When it encounters a "require", an exception is thrown :
Code: Select all
.text:000000013FC53AC8 call _CxxThrowException
If you save your file now, add a "require io" in the top of a mod, you will not see the error again.
But let's go further : why not including "io" and "os" automagically so that you don't have to require them in every mod ? After all, it's part of the lua stdlib.
3) enabling "io" and "os" :
LUA being an embedded library in factorio, we can check the lua functions being using from withing factorio.
For example there is this lua_setglobal function used in some locations in the executable (you can check with IDA). To communicate between factorio and the LUA virtual machine, LUA exposes an API (C functions) which uses the stack. To disable a library in lua, like the io one, programmers can use lua_setglobal.
http://stackoverflow.com/questions/1010 ... -lua-heade
For example they will create a variable and set it to zero, then say : the "io" global variable will be zero. This is done in factorio using this code :
I believe now I don't have to put screenshots of how to find that, because you probably know how to do it, right ?
Code: Select all
Function : LuaHelper::initLuaState(lua_State *L, int a2, __int64 a3, int a4)
Code :
v4->top;
v4->top->tt_ = 0;
++v4->top;
lua_setglobal(v4, "io")
Using the previous NOP technique, we juste have to disable those lua_setgloabl calls when they are used with "io" and "os".
4) Testing :
Download any mod from the forum, open a .lua file from it, an in a function, add :
file = io.open("c:\\temp\\toto.txt", "w")
io.close(file)
Launch factorio, and play a new game. You will see an empty "toto" file created in your c:\temp folder.
Next step ? Surprise ! hint : snakes bites !