目录

对LuaJIT制作的游戏的简单修改转载

目录

对LuaJIT制作的游戏的简单修改(转载)

首先感谢原作者在国内互联网分享这篇教程,因为在国内这方面资料基本没有。

这篇教程的很强大,先不说网游。 说一些单机游戏 类似于 EM引擎 DM引擎 开发的游戏

都可以通过这种方法来调试。

源代码可以去原贴查看

原贴

前言

本次修改的游戏是一个使用了LuaJIT制作的梦幻西游类单机游戏,名字叫做《梦战:碧海旭梦》。

这个游戏在我的硬盘里放了有很长一段时间了,当时水平有限,就没有再修改,直到这几天才重新开始研究。

网上关于LuaJIT的分析资料并不多,而有关单机游戏修改的是没有了,所以我基本上是从零开始研究。

经过几天不断的研究,终于对LuaJIT有了一点点了解,也找到了一些修改的方法。

不过说实话研究这个东西没什么用处,纯粹是我的兴趣,各位姑且看看就好。

因为我也是新手,所以文章中难免有很多错误,如果发现错误请一定要告诉我。

什么是Lua?和LuaJIT有什么关系?

Lua是一个脚本语言,通常是用来嵌入应用程序中的,在游戏中的应用范围也很广,尤其是手游。

而LuaJIT则是Lua的一个高效版本。

用好Lua+Unity,让性能飞起来—LuaJIT性能坑详解 这篇文章对LuaJIT的工作原理有了一定的阐述。

若想了解更多,可以去官方网站看看。

Lua:The Programming Language Lua

LuaJIT:The LuaJIT Project

LuaJIT的字节码和虚拟机

LuaJIT可以把.lua脚本编译成字节码,然后放到虚拟机中运行。

这里的字节码和我们平常看到的机器码不同,LuaJIT的字节码有自己对应的一套指令,

然后在程序中有一块地方负责解释这些字节码(在windows上就是个dll),

这块地方就是LuaJIT虚拟机。

关于LuaJIT的指令网上已经有人总结,Lua LuaJit 指令表(整理)。

也可以直接翻阅LuaJIT的源码(官网可以下载),在lj_bc.h中有定义。

官网的wiki中也有文档,LuaJIT 2.0 Bytecode Instructions。

如果有人有改过Lua的游戏就知道非常难改,因为到处都是“共用代码”。

而这里的“共用代码”实际上就是虚拟机,我们直接修改机器码就等于破坏了虚拟机,所以我们要从字节码下手。

开始修改

首先我们先研究一下跟游戏目录下的文件,在游戏目录中有个lua51.dll,我们随便拿一个PE工具看一下。

这里我用ExeinfoPe查看

dll文件被加壳了,但是问题不大,只是个压缩壳,不脱也没关系。

点击PE查看导出表信息,发现LuaJIT字样,基本上就确定游戏跟LuaJIT有关系了。

第一次进入游戏要过一段剧情,等待游戏正式开始就好。

注意,修改前请先备份存档,游戏有反修改机制,被检测到后直接杀掉存档。(存档文件为save.bhs)

我们尝试修改一下潜力点

LuaJIT中的数据都是double类型的,所以我们用double类型来搜索,并找出是什么修改了这个地址。

其实如果只是为了自己玩游戏的话这里就直接改数值就好了,只要不改太高游戏的反修改检测都不会杀掉你的存档,但是今天主要是研究一下LuaJIT。

这里出现了两条代码,看第一条就行了。记下代码的地址和潜力点的地址,转到x32dbg或者OD中分析,这里我用x32dbg来分析。

Ctrl+G输入6B52F6AF来到该代码处,下F2断点,然后发现游戏马上就被断下来了,这很正常,因为这里的代码就是用来解释字节码的虚拟机,在lua51.dll中。

我们切换到断点页面,选中刚才下的断点,按Ctrl+E来编辑设置断点条件

这里的ecx == 0x0DD35300 意思就是在ecx等于这个数值的时候才断下,而这里的0DD35300就是我们刚才搜到的潜力点的地址。

点击保存后开启断点然后重新运行游戏,发现游戏会变得很卡,这也很正常,因为这里是虚拟机,执行的次数很多,调试器在筛选的时候难免会卡。

随便把某个属性加一点使潜能点改变,游戏就会被断下来,切回到调试器中查看。

这时候的esi值就是指向当前解释的字节码的地址,我们右键点击esi寄存器选择“在内存窗口中转到”,并观察左下角内存窗口。

04B36748就是当前esi的值

这里就是LuaJIT的字节码,每条指令的长度都是4个字节,第1个字节为OP。

注意这里的04B36748的指令是下一条要执行的指令,

实际上改变潜力点的指令在前面04B36744处,对应的字节码就是3A 07 07 06,

顺便再注意下04B36740处的1F 07 05 07

我们查一下指令表(前面有),看看这些是什么东西。

看到之后是不是感觉蒙了,一开始我也是蒙的,后来经过不断的研究发现这里的07 05 07 或者 07 07 06其实是索引,

对应的是LuaJIT栈上的一些东西,这里我就不详细讨论了,主要是我也不太懂。

这两行大概意思就是两个变量相减,然后结果赋值给另一个变量(也就是潜力点)。

这些东西需要自己去写一些LuaJIT的简单程序,然后去调试看内存,再看看LuaJIT对应的源码才能搞清楚一点。

我也读不透LuaJIT的源码,这里我就简单讲一下怎么改就好了。

查看指令表发现这样一条指令,可以直接对变量赋值。

那我们只要修改1F 07 05 07就好了,比如我们要把潜力点变成10000,就改成27 07 10 27 (注意小端序,10 27就是10000的16进制)

看一下效果,

修改成功,想要做成修改器也方便,只要搜一下字节码再改就好了,和aobscan是差不多的。

后记

这游戏还有一些反修改还没有处理,可能会出二,也可能不会。

有关LuaJIT的知识还有很多,我这里讲的只占了很少,当作是抛砖引玉了,有兴趣的可以深入研究,有问题也可以在群里找我,当然我不一定能解答就是了。

前言

本次修改的游戏还是在(一)中所讲的游戏,这一次的目的是要把游戏的反修改处理掉。但是跟(一)中不同的是这一次用到的技术会更高级一点,直接对LuaJIT进行钩我们可以直接使用的Lua自带的调试库来获得大量的信息,并且能够进行修改。

参考了国外大神的文章:

挂钩LuaJIT(原文)

看我如何通过hook攻击LuaJIT(译文)

注入Lua代码以

注入我们自己的Lua代码,我们需要获得游戏调用luaL_newstate返回的lua_State对象,其实就是Lua代码运行的一个环境。如国外大神的文章所说,直接挂钩luaL_newstate这个函数是不太妥当的,因为这时候库还没有加载,debug功能无法使用,所以可以选择挂钩luaL_openlibs这个函数,当然如果挂钩了luaL_newstate也可以的,只需要自己手动一下一下luaL_openlibs就行。为了加载我们的代码,我们还需要得到luaL_loadfilex和lua_pcall两个函数的地址。

确定了所需要的挂钩的函数之后,接下来就是确定挂钩的方式。一般而言是用dll来完成我们的挂钩,但是注入dll的时机却是个问题。luaL_openlibs一般是在luaL_newstate之后马上调用,而luaL_newstate一般是在程序一开始时就会调用。一开始我使用SetWindowsHookEx和第一个参数使用WH_SHELL来注入dll,这样就可以被注入进去,但是经过实验之后效果并不理想,因为这个游戏在创建窗口之前已经被调用了luaL_openlibs,所以我们的hook代码并没有被执行。于是我采用了第二种方法,创建进程的时候直接注入dll。 ,写了一个注入工具(只支持32位)。

接下来就是dll的代码

typedef void* lua_State;

typedef int(*_luaL_loadfilex)(lua_State *L, const char *filename, const char *mode);

typedef int(* _luaL_openlibs)(lua_State *L);

typedef int(* _lua_pcall)(lua_State *L, int nargs, int nresults, int errfunc);

_luaL_openlibs luaL_openlibs_original;

_luaL_loadfilex luaL_loadfilex;

_lua_pcall lua_pcall;

BYTE orig_code[5];

BYTE jmp_code[5] = { 0xe9 };

void MyHook();

void ChooseProc();

BOOL APIENTRY DllMain( HMODULE hModule,

DWORD ul_reason_for_call,

LPVOID lpReserved

)

{

switch (ul_reason_for_call)

{

case DLL_PROCESS_ATTACH:

ChooseProc();

break;

case DLL_THREAD_ATTACH:

case DLL_THREAD_DETACH:

case DLL_PROCESS_DETACH:

break;

}

return TRUE;

}

int luaL_openlibs_hook(lua_State *L)

{

WriteProcessMemory(GetCurrentProcess(), luaL_openlibs_original, &orig_code, 5, NULL); //恢复原函数

int ret = luaL_openlibs_original(L);

WriteProcessMemory(GetCurrentProcess(), luaL_openlibs_original, &jmp_code, 5, NULL); //修改原函数

luaL_loadfilex(L, “debug.lua”, NULL) || lua_pcall(L, 0, -1, 0); //需要注入的lua脚本,放在游戏目录下

MessageBox(NULL, L"Hook success", L"Success", MB_OK);

return ret;

}

void MyHook()

{

HMODULE hModule = GetModuleHandle(L"lua51.dll");

if (!hModule)

{

MessageBox(NULL, L"lua51.dll not found!", L"Fail", MB_OK);

return;

}

luaL_openlibs_original = (_luaL_openlibs)GetProcAddress(hModule, “luaL_openlibs”);

luaL_loadfilex = (_luaL_loadfilex)GetProcAddress(hModule, “luaL_loadfilex”);

lua_pcall = (_lua_pcall)GetProcAddress(hModule, “lua_pcall”);

if (!luaL_openlibs_original || !luaL_loadfilex || !lua_pcall)

{

MessageBox(NULL, L"function not found", L"Fail", MB_OK);

return;

}

//保存原函数字节

if (!ReadProcessMemory(GetCurrentProcess(), luaL_openlibs_original, &orig_code, 5, NULL))

{

MessageBox(NULL, L"ReadProcessMemory fail", L"Fail", MB_OK);

}

    //修改原函数
    *(DWORD*)(jmp_code + 1) = (DWORD)luaL_openlibs_hook - (DWORD)luaL_openlibs_original - 5;
    if (!WriteProcessMemory(GetCurrentProcess(), luaL_openlibs_original, &jmp_code, 5, NULL))
    {
            MessageBox(NULL, L"WriteProcessMemory fail", L"Fail", MB_OK);
    }

}

void ChooseProc()

{

WCHAR szPath[MAX_PATH];

WCHAR *p = NULL;

    GetModuleFileName(NULL, szPath, MAX_PATH);
    p = wcsrchr(szPath, L'\\');
     
    if (!wcscmp(p + 1, L"梦战.exe"))        //要hook的进程名称
    {
            MyHook();
            MessageBox(NULL, L"DLL inject", L"Success", MB_OK);
    }

}

通过GetProcAddress就可以直接获得我们需要的三个函数的地址,其中luaL_openlibs是我们需要hook的函数,luaL_loadfilex和lua_pcall则是来加载我们写的 代码比较丑,各种英语语法问题请见谅,看得懂就行。其中ChooseProc是我用SetWindowsHookEx时使用筛选注入进程的,但是因为后来用了注入工具,所以这里会重叠比较多余

。debug.lua

这个就是我们获取信息的lua脚本。参考了国外大神的代码并结合实际游戏之后,我的代码如下。

[Lua]纯文本查看 复制代码

?

01

–lua无法debug即时编译过后的代码,根据实际情况选择是否关闭jit。

jit.off()

function re_print(t,prefix,file)

for k,v in pairs(t) do

if type(v) == “function” then

file:write(string.format("%s => %s","_G." … k,v) … “\n”)

–[[

else

file:write(prefix … “.” … k … “\n”)

–]]

end

if type(v) == “table” and k ~= “_G” and k ~= “_G._G” and not v.package then

re_print(v, “\t” … prefix … “.” … k, file)

end

end

end

function dumpGlobals()

local fname = “globals_” … “.txt”

local globalsFile = io.open(fname, “w”)

re_print(_G,"_G",globalsFile)

globalsFile:flush()
globalsFile:close()

end

function trace(event, line)

local info = debug.getinfo(2)

if not info then return end
if not info.name then return end

dumpGlobals()

--下面注释的代码是获取函数信息的,结合了实际情况之后我并没有使用,详情参考国外大神文章
--[[
local fname = "trace_" .. ".txt"
local traceFile = io.open(fname, "a")
traceFile:write(info.name .. "()\n")

local a = 1
while true do
local name, value = debug.getlocal(2, a)
if not name then break end
if not value then break end
traceFile:write(tostring(name) .. ": " .. tostring(value) .. "\n")
a = a + 1
end
traceFile:flush()
traceFile:close()
--]]

end

debug.sethook(trace, “c”)

_G 是 Lua 的类别对象表,这个表储存了很多跟游戏相关的关键信息,有很大的价值。其中 re_print 函数是我从网上参考来的一份可以遍历所有表的代码,因为我只想要获得跟函数有关的信息,所以我在其中加了一句 if type(v)==“ function”来筛选出_G 表中存储的函数。

debug.sethook(痕量, “C”)使 Lua 中在每个函数完成之前调用跟踪这个函数,在跟踪中,我们就可以调用 dumpGlobals 了。但是因为不知道游戏什么时候会把全局变量当调用脚本后,可能会比较卡,但开启游戏后就可以马上关掉游戏了,一般来说游戏开启后在我们没有反应过来时变量都已经分配好了。

观察_G 变量变量表内容

我们使用工具,选择 dll 的路径和游戏的路径并单击执行,在开启游戏后就可以立马关闭游戏,我们可以看到游戏目录下多了个个 globals_ .txt 文件,这就是我们打印出来的变量变量表,我在里面发现了很有意思的东西。

[AppleScript]纯文本查看 复制代码

?

1

2

3

4

5

6

7

…省略一大部分…

_G.领取押镖任务 => function: 0x02b31430

_G.防修改 => function: 0x0562d190 <——防修改关键函数

_G.是否拥有效果 => function: 0x02b314f0

_G.取效果剩余时间 => function: 0x02b31508

_G.洗点 => function: 0x02b31610

…省略一大部分…

这个游戏的 Lua 脚本很多都是用了中文,这个防修改就直接写贝壳了,一下就看到了。知道了函数名称后我们就可以干坏事了

。debug.lua(改)

这里我偷个懒,直接修改一下 debug.lua 内容,把防修改过掉。

[Lua]纯文本查看 复制代码

?

1

2

3

4

5

6

7

8

9

function 反防修改()

return

end

function trace(event, line)

防修改 = 反防修改

end

debug.sethook(trace, “c”)

文件记得使用 GBK 编码格式,不然这个游戏不认文件中的汉字。

我们把防修改直接替换成我们写的反防修改,反防修改是一个空函数,游戏调用防修改这个函数的时候,就相当这样在调用我们的反防修改函数,然后就是啥都没有做。将 debug.lua 放在游戏目录下并使用注入工具注入我们的 dll 就可以过掉反修改。

效果图如下,

没有反防修改时:

有反防修改时:

。当然这个方法非常的不优雅,不过因为我对 Lua 中并不是很熟悉,我所以没有也。再多研究了