0x00 前言
虽然现在的应用开发越来越趋向于web应用,大型软件也大量使用了现有的框架,随着现有框架和引擎的完善,绝大多数安全问题已经被解决。但是遇到一些定制需求时,开发人员还是不得不从底层一点点进行设计。这时,没有安全经验的开发人员很容易犯下错误,导致严重的安全隐患。本文以一款自主引擎的大型网络游戏为例,展示开发中容易被忽略的隐患。
Lua作为一种功能强大又轻量级的脚本语言,可以非常容易地嵌入其他语言的程序中,越来越多的游戏引擎使用了Lua来实现具体的逻辑过程。这使得我们避开复杂的逆向工程直接分析游戏功能的逻辑成为可能。Lua脚本的一些特性甚至让我们可以直接调试游戏中的脚本,比如使用著名的Decoda Lua调试器。如果配合简单逆向资源包等手段,我们可以轻松获取游戏中的脚本代码(有可能需要反编译),这使得我们的分析过程进一步简化,由黑盒变成了半白盒。
首先我们对游戏进行了简单的逆向来简化脚本分析的难度。通过逆向工程得知,游戏引擎对Lua脚本的依赖度非常高,C++只是用于基本的类和方法以及游戏渲染,几乎所有逻辑都是由脚本完成。同时为了更加方便我们的分析工作,对游戏的资源文件包进行了简单的逆向,成功解包出了游戏客户端的Lua脚本,至此,准备工作全部完成。
下列例子按照被发现的时间排序:
- 隐身
- 任意传送
- 大并发刷金币
- 使用不存在的道具
- 远程代码执行
0x01 隐身(意料之外的“正常功能”)
这个严格来说不算是漏洞,只是一个非常有意思的BUG。
在开发功能的时候,开发人员通常只会考虑实现功能和功能内部基本的安全性。这样并没有错,但是却有一种情况是这种开发模式无法防御的:正常功能特性被滥用。一个典型的案例就是下面这个隐身的BUG。红框中玩家模型处于不可见状态,无法通过点击模型选中,在选中他之前也无法发现他的存在。
这种隐身从逻辑角度来看肯定是不正常的,但不是从代码角度来看,它却又是正常的,所以在测试中也很难被发现。我们来看看实现隐身的代码:
这段代码其实只做了一件事情,那就是频繁的修改人物外观的显示状态,那么为什么人物会消失呢?这就得从基础说起了:游戏的渲染机制是每次模型外观出现改变,将模型删除,使用新的参数重新创建模型。这样,在删除模型和重新创建之间有一个时间差,这段时间内对应的玩家模型是不存在的!
在正常情况下这并不是什么大问题,但是如上面那一段代码,如果极其频繁地修改模型,就会导致模型根本没有机会显示出来,而这种问题如果在设计阶段没有引起注意,到了开发阶段就很难被发现。
0x02 任意传送(危险的未完成功能)
这是本文中最有意思的漏洞了。某一次大更新后,例行解包客户端看看更新了什么内容,无意中看到一个服务器脚本(不要惊讶,我也不知道为什么,但是客户端里确实有一部分服务器脚本~)中增加了一个名为OnCanJoinNormalMap的远程调用函数,看名字似乎是跟进入地图有关。
难道这是传说中的任意传送!小伙伴们惊呆了!实际测试一下,真的可以传送到任意指定坐标!
在一阵惊喜过后,脑洞大开的小伙伴们还写了一个完整的利用工具来实现真正意义上的任意传送(虽然不到一周时间漏洞就修了~):
修复以后的脚本变成了这样:
从注释内容来看,这个接口似乎在上线时根本没有实际使用,这也是开发人员经常犯的错误之一——上线一些根本没有使用但是却已经实现的接口。由于这些接口出于开发阶段,可能并没有完备的防护机制,比如上图这个接口实现,没有任何过滤。这些接口一旦被意外找到,事情就开始变得不可控了。
0x03 使用道具技能(不同操作共用底层实现带来的风险)
我们知道,游戏里通常都有一些可使用的道具,但是很少有人关心过物品使用这一过程是如何实现的,其中可能存在什么问题。
下面我们来看看物品使用的过程到底发生了什么。
首先点击物品的函数是这样的:
函数结尾的OnUseItem才是使用物品的关键,那么这个函数又是如何实现的呢?
可以看到一些物品的处理是由UseItem函数完成,这个函数不再是lua写的函数,而是C++函数了,部分代码如下:
#!c++
KItem::UseItem(DWORD dwBox, DWORD dwX, KTarget& rTarget)
{
......
pItem = GetItem(dwBox, dwX);
......
if (pItem->m_dwSkillID != 0 && pItem->m_dwScriptID == 0)
{
eRetCode = UseItemSkill(pItem, rTarget);
KG_PROCESS_ERROR_RET_CODE(eRetCode == uircSuccess, eRetCode);
}
.....
}
这里有一个不太寻常的调用:UseItemSkill,而从OnUseItem函数中我们也可以看到skill的影子。
从代码中可以看出item对象存在item.dwSkillID, item.dwSkillLevel两个属性,难道说这是使用物品实际上是一种技能释放?也就是说我们可以通过直接调用技能释放接口来使用一些我们根本没有的物品?
测试证明,确实是这样的。
一个20级身上空空如也的小号,在执行了OnAddOnUseSkill(4894,1)后获得了必须装备某道具才能获得的效果(4894为该物品对应的技能ID)。
在使用物品的方法中存在校验物品数量的代码,但是释放技能却不需要校验物品存在,通过直接释放物品对应的技能,我们成功绕过了物品的校验,使用了玩家身上并不存在的物品。
在一些设计中,不同的功能可能会存在同一套底层的实现,如果在底层实现中没有对调用来源进行充分验证,就有可能绕过前端,直接调用底层实现方法来绕过原本的保护机制。
0x04 大并发刷金币(大并发带来的风险以及缺乏保护的未公开接口)
大并发的问题其实很常见,也在很早的时候就有过分析。但是在一些正常情况下不可能存在并发的位置,程序员很有可能忽略了对大并发的防护。
游戏中有一个帮会福利,每周帮会可以使用帮会资金的一部分作为“工资”发给帮会成员,而帮会资金的来源,可以是任务奖励,也可以是玩家捐赠。
这里出现了一进(捐赠)一出(发工资),如果捐赠的接口存在并发问题,那么我们就有可能获取到额外的金币。
通过游戏界面的按钮,找到了捐赠的接口,于是开始了邪恶的计划:
这是原本的帮会资金和个人财富
执行一下我们的邪恶代码
身上的钱变成了负的90万!而帮会资金却增加了90万!我们成功进行了一次透支操作,接下来只需要用其他号把帮会里的资金取出就可以消费了~活脱脱的信用卡套现!
这是一个非常容易被忽略的地方。由于正常玩家操作都是通过界面点击,输入金额进行操作,根本不可能出现并发请求,作为一个不公开的内部接口,程序员对此处毫不设防。但是接口被挖掘出来,用脚本来实现,大并发的问题就凸显出来。
0x05 远程代码执行(永远不要相信用户输入的信息)
这恐怕是所有问题中最严重的了,也是很难被发现的一处。在游戏引擎中,经常需要通过脚本进行一些操作,比如创建界面元素后可能需要脚本完成初始化,这次的问题就出现在了初始化脚本上。
通常大型网游为了信息传递更加方便,聊天栏是允许发送物品链接的,这样一来就会引入一段代码专门用于链接的生成,通常会使用脚本对生成的链接进行初始化,比如下面这个函数:
看似很正常,但是我们发现其中出现了“script=”,这里应该就是附带的初始化脚本了。原本的脚本代码只是设置控件的属性,但是熟悉sql注入的朋友可能会发现一个问题:这里的脚本代码是直接用参数拼接而成,似乎没有过滤。我们可以尝试构造一个特殊的内容截断原本的脚本,执行我们自己的代码。
游戏中玩家聊天信息发送是一个自定义表结构,阅读接收信息的脚本得知,MakeEventLink函数的4个参数中szText, szName, szLinkInfo均是直接来源于接收到的聊天数据。接下来我们开始构造攻击语句
使用引号闭合语句,写入我们自己的代码,这里要注意闭合最后多余的一个引号,否则会导致语法错误无法执行。
效果好象不错的样子
由于问题出在客户端接收聊天信息的代码里,所以这个漏洞可以被远程利用,也就是说在权限足够的情况下甚至可以调用os库远程格盘,不愧是居家旅行,杀人灭口必备漏洞!
0x06 总结
开发过程中有太多容易被忽视的安全问题,其中绝大多数都是过分“信任”造成的。信任同事的代码,信任自己的代码,信任调用来源的合法性,信任用户的操作,正是这些原本不应该的信任给攻击者留下了攻击的空间。开发原本就是一个创造性的工作,我们更应该去怀疑而不是信任。怀疑一切也许并不能提高开发效率,但在关键时刻却可以挽救整个系统。