热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【游戏安全】看我如何通过hook攻击LuaJIT

译者:興趣使然的小胃预估稿费:200RMB投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿一、前言如果你在游戏行业摸爬滚打已久,你肯定听说过Lua这个名词。作为一门强大的脚本语言,

https://img.php1.cn/3cd4a/1eebe/cd5/8343fdbffb0056b5.webp

译者:興趣使然的小胃

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

一、前言

如果你在游戏行业摸爬滚打已久,你肯定听说过Lua这个名词。作为一门强大的脚本语言,Lua已经嵌入到数千种视频游戏中,提供了各种API接口,以便工程人员在游戏客户端以及服务器上添加各种功能。

我会不断强调一个观点:为了让攻击技术更加便捷、更加可靠以及更加高效,最好的方法就是攻击游戏引擎,而不是攻击游戏本身。Hook一大堆函数、定位一大堆地址本身是个很好的办法,然而这意味着只要游戏更新版本,你就需要更新你所使用的偏移量。相反,如果你hook了游戏使用的那些库,这些问题就会迎刃而解。

Lua的普及性使得它成为hook的理想目标。此外,由于游戏开发者使用Lua来添加内容及功能,因此游戏所包含的Lua环境就成为拥有大量功能的强大主机环境。

出于性能要求,使用LuaJIT来替代vanilla Lua是非常常见的场景。因此,在本文中我会探讨如何攻击LuaJIT。只要稍作修改,这种攻击技术也可以应用于vanilla Lua。


二、注入Lua代码

为了创建Lua环境,我们需要调用luaL_newstate返回一个lua_State对象,然后将其作为参数,调用luaL_openlibs即可。有人可能想通过劫持luaL_newstate的执行来注入代码,然而这种方法并不能奏效。因为此时程序库还没有加载,因此加载脚本不会起到任何作用。然而,我们可以劫持luaopen_jit函数,这个函数正是打开程序库时所调用的最后一个函数(参考此处)。

动态链接LuaJIT时,我们可以查找导出表来定位这个函数:

http://p1.qhimg.com/t01e5fd413bc1fc0df0.png

静态链接LuaJIT时,我们可以使用一些特征字符串来进行定位:

http://p9.qhimg.com/t01269755b2029eba06.png

一旦找到这个函数,hook就不是件难事。然而,在hook之前,我们需要找到两个函数:luaL_loadfilex这个函数用来加载我们的Lua脚本,lua_pcall这个函数用来执行Lua脚本。动态链接时,我们可以在导出表中找到这两个函数;静态链接时,我们可以使用“=stdin”字符串来定位第一个函数(参考此处):

http://p4.qhimg.com/t019ae20ae15c7cbe7e.png

定位第二个函数需要多费点功夫,因为该函数没有关联某个特征字符串。然而幸运的是,该函数在内部调用时(参考此处),位于“=(debug command)”之后:

http://p8.qhimg.com/t01b96bfffab9e4a0ec.png

注意,上图中我们还能观察到luaL_loadbuffer函数的地址,牢记这一点,回头要用到。

识别出这些地址后,我们就可以开始写hook代码了:

typedef void* lua_State;
typedef int (*_luaL_loadfilex)(lua_State *L, const char *filename, const char *mode);
_luaL_loadfilex luaL_loadfilex;
typedef int (*_luaopen_jit)(lua_State *L);
_luaopen_jit luaopen_jit_original;
typedef int (*_lua_pcall)(lua_State *L, int nargs, int nresults, int errfunc);
_lua_pcall lua_pcall;
int luaopen_jit_hook(lua_State *L)
{
    int ret_val = luaopen_jit_original(L);
    luaL_loadfilex(L, "C:\test.lua", NULL) || lua_pcall(L, 0, -1, 0);
    return ret_val;
}
BOOL APIENTRY DllMain(HMODULE mod, DWORD reason, LPVOID res)
{
    switch (reason) {
    case DLL_PROCESS_ATTACH: {
            luaL_loadfilex = (_luaL_loadfilex)LOADFILEEX_ADDR;
            lua_pcall = (_lua_pcall)PCALL_ADDR;
            HookCode(OPENJIT_ADDR, luaopen_jit_hook, (void**)&luaopen_jit_original);
            break;
        }
    }
    return TRUE;
}

我的hook代码如上所示,使用的是自己开发的hook引擎。你可以使用Detours或者自己的引擎。需要牢记的是,hook点应该位于DLL中,以便注入到进程中。

现在,创建Lua环境时,“C:test.lua”就会被加载到这个环境中。通常情况下,我首先会注入代码,使用debug.sethook来劫持对Lua函数的所有调用以及相应的参数,以便后续分析:

lua jit.off()
FILEPATH = "C:LuaJitHookLogs" STARTINGTIME = os.clock() GDUMPED = false
function dumpGlobals() local fname = FILEPATH .. "globals" .. STARTING_TIME .. ".txt" local globalsFile = io.open(fname, "w") globalsFile:write(table.show(G, "G")) 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
if string.len(info.name) <= 1 then return end
if (not GDUMPED) then
    dumpGlobals()
    GDUMPED = true
end
local fname = FILE_PATH .. "trace_" .. STARTING_TIME .. ".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")

这段代码可以提取到一堆有价值的全局信息以及跟踪信息,存放在“C:LuaJitHookLogs”目录下。


三、详细分析

如果你非常熟悉Lua,你可以跳过这个部分,不然的话,你可以跟着我分析这个脚本的具体内容。

首先,我调用了jit.off函数,因为debug库无法劫持由jit引擎实时编译的那些调用代码。

在dumpGlobals函数内部,我将名为_G的表打印出来。这是个全局对象表,Lua使用这个表来跟踪全局域内的所有内容,并将跟踪结果以“key, value”键值对形式保存在该表中。你肯定能够想到,这个表的价值非常高。根据你的具体情况,你可能需要晚一点再调用dumpGlobals函数,因为有些游戏在调用第一个函数时并没有把所有的全局变量分配完毕。

我使用了debug.sethook(trace, "c")语句,使Lua在每个函数调用完成之前调用trace这个函数。在trace函数内部,我调用了debug.getinfo(2)以获取被劫持的函数名称。由于trace函数为当前正在使用的函数,也就是说该函数在栈上的级别为1,因此被劫持的函数的级别为2。然后我循环调用了debug.getlocal(2, a),其中a的值从1开始不断累加,直至该语句返回空值(nil)为止。通过这种方式,我们可以循环遍历级别为2的栈,找到所有的本地变量,并将查找结果以键值对的形式保存起来。对某个游戏这样处理后,我找到了如下信息,你可以根据这些信息猜到这是哪个游戏:

type()
(*temporary): table: 074FA7D0
{
    IsDestroyed = false,
    NumOfSpawnDisables = 0,
    SpawnOrderMinionNames = 
    {
        "Super",
        "Melee",
        "Cannon",
        "Caster",
    },
    WillSpawnSuperMinion = 0,
}

我们可以调用type来确定对象的类型,但对象本身就可以告诉我们关于该游戏的一些有趣信息。

如果我们愿意的话,我们可以使用debug.setlocal将参数改成某些函数。


四、劫持Lua代码

在许多情况下,我们需要劫持整个Lua脚本。通过这种方式,我们不需要将跟踪结果拼接起来,就可以详细分析游戏所用的脚本,理解脚本具体功能。Lua代码可以以文件或者缓冲区的形式加载到LuaJIT环境中。由于分析磁盘上的文件比较容易,因此我们会重点关注使用缓冲区的这种情况以及luaL_loadbuffer这个函数。

这个函数实际上有两种表现形式:分别为luaL_loadbuffer以及luaL_loadbufferex。这两个函数基本相同,第一个函数会调用第二个函数,只不过会把最后一个参数设为NULL(参考此处)。你可以会认为,只要hook luaL_loadbufferex这个函数,我们就可以搞定这两种情况,然而事实并非如此。由于LuaJIT主要是针对性能优化而设计的,因此通常情况下luaL_loadbufferex会以内联形式使用。然而,这两个函数最终都会调用如下这个函数(参考此处):

int lua_loadx(lua_State *L, lua_Reader reader, void *data, const char *chunkname, const char *mode);

LuaJIT偏向于使用内联代码,导致这个函数也变成内联形式。然而,这里最有用的是lua_Reader reader,这个回调函数知道如何将void *data转换为包含Lua代码的缓冲区。当LuaJIT加载位于缓冲区中的代码时,reader变为reader_string的地址(参考此处),而void *data所指向的char*字符串即为具体的Lua代码。

reader_string不是内联函数,因为它的地址可以作为回调指针来传递,因此我们可以在luaL_loadbuffer内部找到这个地址:

http://p3.qhimg.com/t01f995a7aaeabaf8df.png

利用这个地址,我们可以构造一个hook,来劫持并显示已加载的所有Lua缓冲区:

typedef const char* (*_reader_string)(lua_State *L, void *ud, size_t *size);
_reader_string reader_string_original;
const char* reader_string_hook(lua_State *L, void *ud, size_t *size)
{
    if (((size_t*)ud)[1] > 0)
        MessageBoxA(NULL, ((char**)ud)[0], "LuaJITHook", MB_OK);
    return reader_string_original(L, ud, size);
}
// from DllMain DLL_PROCESS_ATTACH
HookCode(READERSTRING_ADDR, reader_string_hook, (void**)&reader_string_original);

当然使用对话框来显示并不是优雅的解决办法,不要在意这个细节,你理解我的意思就可以了。


五、总结

这种方法非常强大。许多游戏提供了Lua脚本功能,可以实现自动化、拉高游戏视图以及ESP(透视)黑科技等。不同的游戏使用Lua的方法有所不同,但他们的工作原理都与本文的例子相似。

你可以在这段hook代码的基础上进行修改,添加扫描功能,自动定位这些函数,比如,你可以使用XenoScan这个库来完成这个任务。

如果你有什么意见或者建议,可以随时发表评论,也可以关注我的推特了解我最新发布的信息。


推荐阅读
  • 开机自启动的几种方式
    0x01快速自启动目录快速启动目录自启动方式源于Windows中的一个目录,这个目录一般叫启动或者Startup。位于该目录下的PE文件会在开机后进行自启动 ... [详细]
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • [转]doc,ppt,xls文件格式转PDF格式http:blog.csdn.netlee353086articledetails7920355确实好用。需要注意的是#import ... [详细]
  • 在 Ubuntu 中遇到 Samba 服务器故障时,尝试卸载并重新安装 Samba 发现配置文件未重新生成。本文介绍了解决该问题的方法。 ... [详细]
  • 在JavaWeb开发中,文件上传是一个常见的需求。无论是通过表单还是其他方式上传文件,都必须使用POST请求。前端部分通常采用HTML表单来实现文件选择和提交功能。后端则利用Apache Commons FileUpload库来处理上传的文件,该库提供了强大的文件解析和存储能力,能够高效地处理各种文件类型。此外,为了提高系统的安全性和稳定性,还需要对上传文件的大小、格式等进行严格的校验和限制。 ... [详细]
  • 本文介绍了如何使用Python的Paramiko库批量更新多台服务器的登录密码。通过示例代码展示了具体实现方法,确保了操作的高效性和安全性。Paramiko库提供了强大的SSH2协议支持,使得远程服务器管理变得更加便捷。此外,文章还详细说明了代码的各个部分,帮助读者更好地理解和应用这一技术。 ... [详细]
  • MATLAB字典学习工具箱SPAMS:稀疏与字典学习的详细介绍、配置及应用实例
    SPAMS(Sparse Modeling Software)是一个强大的开源优化工具箱,专为解决多种稀疏估计问题而设计。该工具箱基于MATLAB,提供了丰富的算法和函数,适用于字典学习、信号处理和机器学习等领域。本文将详细介绍SPAMS的配置方法、核心功能及其在实际应用中的典型案例,帮助用户更好地理解和使用这一工具箱。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 如何在Java中使用DButils类
    这期内容当中小编将会给大家带来有关如何在Java中使用DButils类,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。D ... [详细]
  • 检查在所有可能的“?”替换中,给定的二进制字符串中是否出现子字符串“10”带 1 或 0 ... [详细]
  • .NET Core 托管服务优化与实践
    在.NET Core应用中,托管服务的形式主要分为进程内托管(InProcess)和进程外托管(OutOfProcess)。这两种托管方式各有优缺点,本文将深入探讨它们的特点,并结合实际案例,介绍如何根据具体需求选择合适的托管模式,以实现性能优化和资源利用的最大化。此外,文章还将分享一些实用的配置技巧和最佳实践,帮助开发者提升应用的稳定性和可维护性。 ... [详细]
  • 基于Net Core 3.0与Web API的前后端分离开发:Vue.js在前端的应用
    本文介绍了如何使用Net Core 3.0和Web API进行前后端分离开发,并重点探讨了Vue.js在前端的应用。后端采用MySQL数据库和EF Core框架进行数据操作,开发环境为Windows 10和Visual Studio 2019,MySQL服务器版本为8.0.16。文章详细描述了API项目的创建过程、启动步骤以及必要的插件安装,为开发者提供了一套完整的开发指南。 ... [详细]
  • ### 优化后的摘要本文对 HDU ACM 1073 题目进行了详细解析,该题属于基础字符串处理范畴。通过分析题目要求,我们可以发现这是一道较为简单的题目。代码实现中使用了 C++ 语言,并定义了一个常量 `N` 用于字符串长度的限制。主要操作包括字符串的输入、处理和输出,具体步骤涉及字符数组的初始化和字符串的逆序操作。通过对该题目的深入探讨,读者可以更好地理解字符串处理的基本方法和技巧。 ... [详细]
  • 本文详细解析了使用C++实现的键盘输入记录程序的源代码,该程序在Windows应用程序开发中具有很高的实用价值。键盘记录功能不仅在远程控制软件中广泛应用,还为开发者提供了强大的调试和监控工具。通过具体实例,本文深入探讨了C++键盘记录程序的设计与实现,适合需要相关技术的开发者参考。 ... [详细]
  • 本指南介绍了如何在ASP.NET Web应用程序中利用C#和JavaScript实现基于指纹识别的登录系统。通过集成指纹识别技术,用户无需输入传统的登录ID即可完成身份验证,从而提升用户体验和安全性。我们将详细探讨如何配置和部署这一功能,确保系统的稳定性和可靠性。 ... [详细]
author-avatar
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有