首先感谢原作者在国内互联网分享这篇教程,因为在国内这方面资料基本没有。
这篇教程的很强大,先不说网游。 说一些单机游戏 类似于 EM引擎 DM引擎 开发的游戏
都可以通过这种方法来调试。
源代码可以去原贴查看
原贴 http://www.cehigh.com/forum.php?mod=viewthread&tid=724
前言
本次修改的游戏是一个使用了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中并不是很熟悉,我所以没有也。再多研究了