Hooking Lua Part 2

18-06-2017 - 13 minutes, 32 seconds -
lua reverse engineering

Introduction

After learning about how to integrate Lua into a C program and some Lua internals, I wanted to put this knowledge to hacking around in a game uses Lua. I selected Syrian Warfare for this purpose. Since the code is still in a state of flux, only small snippets will be shown to make points. A link to the full source will be made available in a later post.

Exploration

As in any of my reverse-engineering endeavors, I searched the binaries for interesting strings:

$ strings SyrianWarfare.exe  | grep -i lua
autoexec.lua
test_autoexec.lua
scripts/global_map.lua
scripts/triggers.lua
$ strings lua.dll  | grep -i lua  | grep Copyright
$Lua: Lua 5.0.2 Copyright (C) 1994-2004 Tecgraf, PUC-Rio $

The game actually provides an interactive console in which Lua code can be executed. sw_lua_version__console.jpg

I started the game in a debugger and put a breakpoint on lua_open. This function is called from two different places while the game starts up: galileo.dll and world2.dll.

galileo.dll:

00772E4C | call dword ptr ds:[<&lua_open>]                                       |
00772E52 | push eax                                                              |
00772E53 | mov dword ptr ds:[esi+4],eax                                          |
00772E56 | call dword ptr ds:[<&luaopen_base>]                                   |
00772E5C | mov ecx,dword ptr ds:[esi+4]                                          |
00772E5F | push ecx                                                              |
00772E60 | call dword ptr ds:[<&luaopen_string>]                                 |
00772E66 | mov edx,dword ptr ds:[esi+4]                                          |
00772E69 | push edx                                                              |
00772E6A | call dword ptr ds:[<&luaopen_table>]                                  |
00772E70 | mov eax,dword ptr ds:[esi+4]                                          |
00772E73 | push eax                                                              |
00772E74 | call dword ptr ds:[<&luaopen_math>]                                   |
00772E7A | mov edi,dword ptr ds:[esi+4]                                          |
00772E7D | call galileo.772CD0                                                   |
00772E82 | mov edi,dword ptr ds:[esi+4]                                          |
00772E85 | push galileo.77F6D8                                                   | 77F6D8:"galileo.console_impl"
00772E8A | push edi                                                              |
00772E8B | call dword ptr ds:[<&lua_pushstring>]                                 |
00772E91 | push esi                                                              |
00772E92 | push edi                                                              |
00772E93 | call dword ptr ds:[<&lua_pushlightuserdata>]                          |
00772E99 | push FFFFD8F0                                                         |
00772E9E | push edi                                                              |
00772E9F | call dword ptr ds:[<&lua_settable>]                                   |
00772EA5 | add esp,28                                                            |
00772EA8 | push esi                                                              |
00772EA9 | call galileo.772FD0                                                   |
00772EAE | mov ecx,dword ptr ss:[esp+10]                                         |
00772EB2 | pop edi                                                               |
00772EB3 | mov eax,esi                                                           |
00772EB5 | pop esi                                                               |
00772EB6 | mov dword ptr fs:[0],ecx                                              |
00772EBD | add esp,14                                                            |
00772EC0 | ret 8                                                                 |

world2.dll:

08170965 | call dword ptr ds:[<&lua_open>]                                       |
0817096B | push eax                                                              |
0817096C | mov dword ptr ds:[edi+10],eax                                         |
0817096F | call dword ptr ds:[<&luaopen_base>]                                   |
08170975 | mov eax,dword ptr ds:[edi+10]                                         |
08170978 | add esp,4                                                             |
0817097B | push eax                                                              |
0817097C | call dword ptr ds:[<&luaopen_math>]                                   |
08170982 | mov esi,dword ptr ds:[edi+10]                                         |
08170985 | add esp,4                                                             |
08170988 | push world2.82580B0                                                   | 82580B0:"Compound = {}
Compound.mt = {}
function Compound.Add (a,b)
  local res = CompoundDomain{a,b,'+'}
  return res
end
function Compound.Sub (a,b)
  local res = CompoundDomain{a,b,'-'}
  return res
end
Compound.mt.__add = Compound.Add
Compound.mt.__sub = Compound.Sub
"
0817098D | push esi                                                              |
0817098E | call dword ptr ds:[<&lua_dostring>]                                   |
08170994 | call world2.817B4C0                                                   |
08170999 | call world2.8178A00                                                   |
0817099E | push FFFFD8EF                                                         |
081709A3 | push esi                                                              |
081709A4 | call dword ptr ds:[<&lua_pushvalue>]                                  |
081709AA | push 0                                                                |
081709AC | push world2.827D5A0                                                   | 827D5A0:&"Material"
081709B1 | push 0                                                                |
081709B3 | push esi                                                              |
081709B4 | call dword ptr ds:[<&luaL_openlib>]                                   |
081709BA | push FFFFFFFE                                                         |
081709BC | push esi                                                              |
081709BD | call dword ptr ds:[<&lua_settop>]                                     |
081709C3 | add esp,28                                                            |
081709C6 | mov eax,edi                                                           |
081709C8 | pop esi                                                               |
081709C9 | pop ecx                                                               |
081709CA | ret 4                                                                 |

At this point I decided to employ the following strategy. Use an injected DLL to hook and instrument Lua library functions and figure out how the game uses these different Lua contexts. The injected DLL uses GetModuleHandle to get a handle to the Lua DLL and GetProcAddress to get addresses of the following functions:

lua_open
lua_close
luaopen_io
lua_dostring
lua_dofile
lua_gettop
lua_tostring
lua_settop
lua_next
lua_tonumber
lua_topointer

lua_pushstring
lua_gettable
lua_pushnil

lua_isstring
lua_isnumber
lua_type

Using Minhook, I detoured lua_open, lua_close, and lua_gettop. The latter function is frequently called and is useful in discovering Lua contexts that exist before the DLL is injected. When a new Lua context is created or discovered, it is saved to an unordered_set.

int hooked_lua_gettop(lua_State *L) {
    if (!lua_States.count(L)) { // if state is already recorded, don't debug print
        wsprintf(debug_string, _T("In lua_gettop, lua_State 0x%p"), L);
        OutputDebugString(debug_string);
        lua_States.insert(L);
    }   
    return orig_gettop(L);
}

Instrumenting Lua function confirmed that there are at least two lua states being used. One is created by world2.dll and is created on save game load and closed when a new save game is loaded.

[1708] In lua_close, lua_State 0x05D269D8
[1708] In lua_open, lua_State 0x05D26970
[1708] In lua_gettop, lua_State 0x05D26D18
[1708] In lua_gettop, lua_State 0x05D26BE0

Another state is created when the game starts up in galileo.dll and has the "game" namespace which contains interesting functions that I will cover in another post. To make learning about game internals easier, I added various debugging functionality to the injected DLL that could be activated with key presses. Pressing F8 key prints out all the known Lua contexts. Since there are multiple contexts, I found it useful to introduce the notion of "currently selected context." The variable game_L holds the address of the currently selected context and by pressing Shift+F7 the next known context is made the currently selected context. Also, I added a helper function that prints out Lua tables using DebugString() when Shift+F8 key is pressed. Calling this function on the "_G" table gives me a list of global functions, tables and strings. From main menu to right after a save game was loaded, the following Lua contexts were found:

Current game_L is lua_State 0x05D26970
lua_State 0x05D26970
lua_State 0x05D26D18
lua_State 0x05D26BE0
lua_State 0x6BA96D30

Here is a listing of the globals with each context highlighted:

lua_State 0x05D26970
0 FUN xpcall = 0x05EC8658
0 FUN ZeroDomain = 0x184480F8
0 FUN TorusDomain = 0x180D50C8
0 FUN tostring = 0x05EC8478
0 FUN gcinfo = 0x05EC8628
0 TAB Compound = 0x05D35C58
0 FUN AxisAttractor = 0x180D5128
0 FUN PreserveRelPos = 0x180D50B0
0 FUN SphereDomain = 0x18448128
0 FUN unpack = 0x05EC84C0
0 FUN dofile = 0x05EC8598
0 FUN Named = 0x180D50F8
0 FUN require = 0x05EC86A0
0 FUN getfenv = 0x05EC8520
0 STR _VERSION = Lua 5.0.2
0 FUN VectorOffsetReaction = 0x180D5068
0 FUN setmetatable = 0x05EC8508
0 FUN next = 0x05EC8550
0 FUN Turbulence = 0x180D5110
0 FUN assert = 0x05EC84D8
0 FUN Pts = 0x180D5038
0 FUN tonumber = 0x05EC8490
0 FUN newproxy = 0x05D35A28
0 FUN CollidePlane = 0x184480B0
0 FUN rawequal = 0x05EC85F8
0 FUN ImpulseSpawner = 0x180D5098
0 FUN collectgarbage = 0x05EC8640
0 FUN System = 0x18448170
0 FUN getmetatable = 0x05D40E20
0 FUN AirDrag = 0x18448038
0 FUN KillOld = 0x18448098
0 FUN VectorFactor = 0x18448050
0 FUN AnimatedColor = 0x18448080
0 FUN AnimatedScalar = 0x18448068
0 TAB _LOADED = 0x05D35C08
0 TAB _G = 0x05D35870
0 FUN Gravity = 0x184480C8
0 FUN HemisphereDomain = 0x184480E0
0 FUN ContextPts = 0x180D5020
0 TAB coroutine = 0x05D35A00
0 FUN pairs = 0x05EC8580
0 FUN ContinuousSpawner = 0x18448020
0 TAB math = 0x05D35A50
0 FUN Material = 0x184481A0
0 FUN pcall = 0x05EC85B0
0 FUN Particles = 0x18448188
0 TAB Pfx = 0x17FF8CA0
0 FUN type = 0x05EC84F0
0 FUN __pow = 0x184482C0
0 FUN rawset = 0x05EC85C8
0 FUN BoxDomain = 0x18448140
0 FUN print = 0x05EC84A8
0 FUN SpawnReaction = 0x180D5080
0 FUN rawget = 0x05EC85E0
0 FUN loadstring = 0x05EC86B8
0 FUN CompoundDomain = 0x18448110
0 FUN TrajectorySpawner = 0x180D5050
0 FUN setfenv = 0x05EC8538
0 FUN DomainBuilder = 0x18448158
0 FUN ipairs = 0x05EC8568
0 FUN error = 0x05D40E38
0 FUN loadfile = 0x05EC8610
lua_State 0x05D26D18
0 TAB render = 0x05CEA700
0 STR OBJECT_DONE_SUFFIX = _shadow
0 FUN spawn_usa = 0x05D2EF40
0 FUN tostring = 0x005A1A28
0 FUN gcinfo = 0x005A1B18
0 FUN showNextMissionObjects = 0x05D2CB70
0 FUN mapOnStartMissionSpecific = 0x05D2CBA0
0 FUN timer_01 = 0x05D2EEB0
0 FUN lose = 0x05D2CB10
0 FUN getfenv = 0x005A1980
0 FUN isValid = 0x05D2CA50
0 FUN pairs = 0x005A19E0
0 FUN assert = 0x005A1A58
0 FUN zone_centr = 0x05D2EDC0
0 FUN tonumber = 0x005A1A10
0 TAB DEMO_MISSIONS = 0x05D2B870
0 TAB console = 0x0056B668
0 FUN win = 0x05D2CAE0
0 STR SQUAD = squad
0 TAB _G = 0x0056B258
0 FUN addSquad = 0x05D2CAB0
0 FUN zone_right = 0x05D2EE20
0 TAB coroutine = 0x0056B550
0 FUN moveUnit = 0x05CE53E8
0 STR TANK = tank
0 FUN addTank = 0x05D2C780
0 FUN loadstring = 0x005A1B60
0 FUN mapOnStart = 0x05D2CB40
0 FUN getNextMission = 0x0056D028
0 FUN mapOnMouseLeftUp = 0x05D2CBD0
0 FUN zone_left = 0x05D2EDF0
0 TAB utils = 0x0056B730
0 FUN xpcall = 0x005A1AE8
0 TAB Missions = 0x05D2B898
0 FUN spawn_enemy = 0x05D2EF10
0 STR _VERSION = Lua 5.0.2
0 STR PLAYER = usa
0 FUN isFunction = 0x0056CF68
0 STR maxZoom = 400
0 FUN __pow = 0x0056DF98
0 TAB Reserves = 0x05D2BFC8
0 STR minZoom = 200
0 FUN addCar = 0x05D2C9C0
0 STR global_map = sa
0 FUN print = 0x005A19F8
0 FUN timer_02 = 0x05D2EEE0
0 FUN setmetatable = 0x00592C00
0 FUN next = 0x005A19B0
0 TAB trigger = 0x05D2A8F8
0 FUN bonus_01 = 0x05D2EE80
0 FUN onGameStarted = 0x05D2EFA0
0 STR SCRIPTS_PATH = scripts/lua/
0 TAB HiddenMissions = 0x05D2B910
0 FUN onDemoMissionStarted = 0x05D2EF70
0 FUN rawequal = 0x005A1A88
0 TAB spawns = 0x05D35820
0 FUN collectgarbage = 0x005A1B00
0 TAB game = 0x05D3B5A8
0 FUN newproxy = 0x0056B500
0 FUN unpack = 0x005A1A70
0 FUN spawnHeli = 0x05CE53B8
0 FUN spawnSquad = 0x05CE5388
0 FUN spawnCar = 0x05CE5358
0 FUN spawnUnit = 0x05CE52F8
0 FUN rawset = 0x005A1AB8
0 FUN spawnTank = 0x05CE5328
0 FUN mapOnLeaveTacticalMap = 0x05CE4C68
0 FUN mapOnOkButton = 0x05CE4C38
0 FUN addMissionReserve = 0x05D2CC00
0 FUN help = 0x0056CF98
0 TAB string = 0x0056B5A0
0 TAB math = 0x0056B618
0 FUN dofile = 0x05BDE468
0 FUN pcall = 0x005A1AD0
0 FUN bonus_02 = 0x05D2EE50
0 FUN require = 0x005A1B78
0 FUN type = 0x005A1A40
0 TAB table = 0x0056B5C8
0 FUN ipairs = 0x005A19C8
0 FUN getmetatable = 0x0056B968
0 TAB _LOADED = 0x0056B578
0 STR HELI = heli
0 FUN rawget = 0x005A1AA0
0 STR CAR = car
0 TAB cheats = 0x0056B780
0 STR spawn_name = Add_01_01
0 FUN setfenv = 0x005A1998
0 FUN addHeli = 0x05D2CA80
0 FUN addUnit = 0x05D2C4B0
0 FUN error = 0x0056F3C8
0 FUN loadfile = 0x05BDE480
lua_State 0x05D26BE0
0 TAB render = 0x05CEA700
0 STR OBJECT_DONE_SUFFIX = _shadow
0 FUN spawn_usa = 0x05D2EF40
0 FUN tostring = 0x005A1A28
0 FUN gcinfo = 0x005A1B18
0 FUN showNextMissionObjects = 0x05D2CB70
0 FUN mapOnStartMissionSpecific = 0x05D2CBA0
0 FUN timer_01 = 0x05D2EEB0
0 FUN lose = 0x05D2CB10
0 FUN getfenv = 0x005A1980
0 FUN isValid = 0x05D2CA50
0 FUN pairs = 0x005A19E0
0 FUN assert = 0x005A1A58
0 FUN zone_centr = 0x05D2EDC0
0 FUN tonumber = 0x005A1A10
0 TAB DEMO_MISSIONS = 0x05D2B870
0 TAB console = 0x0056B668
0 FUN win = 0x05D2CAE0
0 STR SQUAD = squad
0 TAB _G = 0x0056B258
0 FUN addSquad = 0x05D2CAB0
0 FUN zone_right = 0x05D2EE20
0 TAB coroutine = 0x0056B550
0 FUN moveUnit = 0x05CE53E8
0 STR TANK = tank
0 FUN addTank = 0x05D2C780
0 FUN loadstring = 0x005A1B60
0 FUN mapOnStart = 0x05D2CB40
0 FUN getNextMission = 0x0056D028
0 FUN mapOnMouseLeftUp = 0x05D2CBD0
0 FUN zone_left = 0x05D2EDF0
0 TAB utils = 0x0056B730
0 FUN xpcall = 0x005A1AE8
0 TAB Missions = 0x05D2B898
0 FUN spawn_enemy = 0x05D2EF10
0 STR _VERSION = Lua 5.0.2
0 STR PLAYER = usa
0 FUN isFunction = 0x0056CF68
0 STR maxZoom = 400
0 FUN __pow = 0x0056DF98
0 TAB Reserves = 0x05D2BFC8
0 STR minZoom = 200
0 FUN addCar = 0x05D2C9C0
0 STR global_map = sa
0 FUN print = 0x005A19F8
0 FUN timer_02 = 0x05D2EEE0
0 FUN setmetatable = 0x00592C00
0 FUN next = 0x005A19B0
0 TAB trigger = 0x05D2A8F8
0 FUN bonus_01 = 0x05D2EE80
0 FUN onGameStarted = 0x05D2EFA0
0 STR SCRIPTS_PATH = scripts/lua/
0 TAB HiddenMissions = 0x05D2B910
0 FUN onDemoMissionStarted = 0x05D2EF70
0 FUN rawequal = 0x005A1A88
0 TAB spawns = 0x05D35820
0 FUN collectgarbage = 0x005A1B00
0 TAB game = 0x05D3B5A8
0 FUN newproxy = 0x0056B500
0 FUN unpack = 0x005A1A70
0 FUN spawnHeli = 0x05CE53B8
0 FUN spawnSquad = 0x05CE5388
0 FUN spawnCar = 0x05CE5358
0 FUN spawnUnit = 0x05CE52F8
0 FUN rawset = 0x005A1AB8
0 FUN spawnTank = 0x05CE5328
0 FUN mapOnLeaveTacticalMap = 0x05CE4C68
0 FUN mapOnOkButton = 0x05CE4C38
0 FUN addMissionReserve = 0x05D2CC00
0 FUN help = 0x0056CF98
0 TAB string = 0x0056B5A0
0 TAB math = 0x0056B618
0 FUN dofile = 0x05BDE468
0 FUN pcall = 0x005A1AD0
0 FUN bonus_02 = 0x05D2EE50
0 FUN require = 0x005A1B78
0 FUN type = 0x005A1A40
0 TAB table = 0x0056B5C8
0 FUN ipairs = 0x005A19C8
0 FUN getmetatable = 0x0056B968
0 TAB _LOADED = 0x0056B578
0 STR HELI = heli
0 FUN rawget = 0x005A1AA0
0 STR CAR = car
0 TAB cheats = 0x0056B780
0 STR spawn_name = Add_01_01
0 FUN setfenv = 0x005A1998
0 FUN addHeli = 0x05D2CA80
0 FUN addUnit = 0x05D2C4B0
0 FUN error = 0x0056F3C8
0 FUN loadfile = 0x05BDE480
lua_State 0x6BA96D30
0 TAB render = 0x05CEA700
0 STR OBJECT_DONE_SUFFIX = _shadow
0 FUN spawn_usa = 0x05D2EF40
0 FUN tostring = 0x005A1A28
0 FUN gcinfo = 0x005A1B18
0 FUN showNextMissionObjects = 0x05D2CB70
0 FUN mapOnStartMissionSpecific = 0x05D2CBA0
0 FUN timer_01 = 0x05D2EEB0
0 FUN lose = 0x05D2CB10
0 FUN getfenv = 0x005A1980
0 FUN isValid = 0x05D2CA50
0 FUN pairs = 0x005A19E0
0 FUN assert = 0x005A1A58
0 FUN zone_centr = 0x05D2EDC0
0 FUN tonumber = 0x005A1A10
0 TAB DEMO_MISSIONS = 0x05D2B870
0 TAB console = 0x0056B668
0 FUN win = 0x05D2CAE0
0 STR SQUAD = squad
0 TAB _G = 0x0056B258
0 FUN addSquad = 0x05D2CAB0
0 FUN zone_right = 0x05D2EE20
0 TAB coroutine = 0x0056B550
0 FUN moveUnit = 0x05CE53E8
0 STR TANK = tank
0 FUN addTank = 0x05D2C780
0 FUN loadstring = 0x005A1B60
0 FUN mapOnStart = 0x05D2CB40
0 FUN getNextMission = 0x0056D028
0 FUN mapOnMouseLeftUp = 0x05D2CBD0
0 FUN zone_left = 0x05D2EDF0
0 TAB utils = 0x0056B730
0 FUN xpcall = 0x005A1AE8
0 TAB Missions = 0x05D2B898
0 FUN spawn_enemy = 0x05D2EF10
0 STR _VERSION = Lua 5.0.2
0 STR PLAYER = usa
0 FUN isFunction = 0x0056CF68
0 STR maxZoom = 400
0 FUN __pow = 0x0056DF98
0 TAB Reserves = 0x05D2BFC8
0 STR minZoom = 200
0 FUN addCar = 0x05D2C9C0
0 STR global_map = sa
0 FUN print = 0x005A19F8
0 FUN timer_02 = 0x05D2EEE0
0 FUN setmetatable = 0x00592C00
0 FUN next = 0x005A19B0
0 TAB trigger = 0x05D2A8F8
0 FUN bonus_01 = 0x05D2EE80
0 FUN onGameStarted = 0x05D2EFA0
0 STR SCRIPTS_PATH = scripts/lua/
0 TAB HiddenMissions = 0x05D2B910
0 FUN onDemoMissionStarted = 0x05D2EF70
0 FUN rawequal = 0x005A1A88
0 TAB spawns = 0x05D35820
0 FUN collectgarbage = 0x005A1B00
0 TAB game = 0x05D3B5A8
0 FUN newproxy = 0x0056B500
0 FUN unpack = 0x005A1A70
0 FUN spawnHeli = 0x05CE53B8
0 FUN spawnSquad = 0x05CE5388
0 FUN spawnCar = 0x05CE5358
0 FUN spawnUnit = 0x05CE52F8
0 FUN rawset = 0x005A1AB8
0 FUN spawnTank = 0x05CE5328
0 FUN mapOnLeaveTacticalMap = 0x05CE4C68
0 FUN mapOnOkButton = 0x05CE4C38
0 FUN addMissionReserve = 0x05D2CC00
0 FUN help = 0x0056CF98
0 TAB string = 0x0056B5A0
0 TAB math = 0x0056B618
0 FUN dofile = 0x05BDE468
0 FUN pcall = 0x005A1AD0
0 FUN bonus_02 = 0x05D2EE50
0 FUN require = 0x005A1B78
0 FUN type = 0x005A1A40
0 TAB table = 0x0056B5C8
0 FUN ipairs = 0x005A19C8
0 FUN getmetatable = 0x0056B968
0 TAB _LOADED = 0x0056B578
0 STR HELI = heli
0 FUN rawget = 0x005A1AA0
0 STR CAR = car
0 TAB cheats = 0x0056B780
0 STR spawn_name = Add_01_01
0 FUN setfenv = 0x005A1998
0 FUN addHeli = 0x05D2CA80
0 FUN addUnit = 0x05D2C4B0
0 FUN error = 0x0056F3C8
0 FUN loadfile = 0x05BDE480

Conclusion

What's interesting is that every time I executed a command on the in-game console, lua_gettop hook would be called with different Lua contexts. However, if I set a global variable, such as "maxZoom" to a new value, that variable's value would change in all the other contexts. In the next post in this series, I will show various ways I poked the game with the knowledge I gained.