-
Notifications
You must be signed in to change notification settings - Fork 15
Description
前言
今天我们来研究一下如何收获妹子崇拜的眼神,从而获得妹子的芳心...啊不,是如何在妹子面前装一个圆润的漂亮的逼。即帮他减轻喜欢的游戏的压力,让她知道游戏是如此的...无聊。
这款游戏的名字叫《xx消消乐》,怎么样,一看到这个游戏就知道妹子显然是一个清纯的人。
这里,我们的目的是使用多种方式来给这款游戏作弊。
本次探索中可能重要的技术或工具为
- ida
- frida
- lua
- c/c++
但本文只为技术交流,无任何赢利目的。但恶意修改游戏是违法行为,各位读者需自负责任,不要走上违法犯罪的道路。本文作者不负相关责任。
且本文所分析样本为2019年1月份的某个版本,现行版本不一定适用,因此所有代码仅供参考。同时,我也希望能对游戏行业的开发人员有点警示作用,使之明白安全防护的重要性,以及现在裸奔是多么的危险。
分析
引擎分析
正如之前提到的,分析一个游戏的第一步,显然是先分析到游戏所用的引擎。常见的框架有cocos2d,Unity3d,unrealengine等。本次探索的目标是使用的lua引擎,这类游戏和U3d一样,都非常的简单,只要反编译到游戏的源代码,则基本如履平地,驰骋疆场。
你问我如何知道它是lua?最简单的方法是拖到ida里面一顿梭,只要含有lua相关关键字的,一般八九不离十就是了,另外,对于安卓来说,包解压后直接看lib目录里是否有libcocos2dlua,liblua,libhellolua等即可快速判断出。由此,我们可以得出结论。除了这款游戏之外,《梦幻西游》《奇迹暖暖》等也是这个引擎,也可按本文下述套路一顿梭。
lua引擎的弱点
- 基于
lua是一种脚本语言的说法,且为了开发和更新方便,一般安全意识较弱的公司,对lua脚本的存放都在资源文件夹里,有的甚至文件根本没有加密。 - 稍微安全意识较强的,可能会把lua给打包存放,甚至加密存放。但这些属于徒劳,顶多算是自欺欺人,因为最后在
lua虚拟机装载的时候,总要进行解密,我们可以在这个时候勾住装载函数,以获取所有的脚本。本文示例的游戏即是此类型。 - 安全意识更强的,则考虑把lua编译后再放入客户端,此时攻击者无法直接获取到
lua的源码,取而代之的是获取到编译后的结果。但显然这也是自欺欺人的表现,因为lua解释器是开源的,制作一个lua的反编译工具是非常简单的,且现在已经有很多的实现。反编译后依然能得到源码。 - 安全意识极强的,可能由上述方案更进一步,既然
lua解释器是开源的,那就对lua解释器进行魔改,使之成为非标准的,则市面上成型的工具不是全都失效了吗?显然是。这种方案是安全程度相对较高的。但也不是绝对安全。因为解释器还是放在客户端,我们只需要分析出解释器修改了那些地方,再同样的修改反编译工具即可。之前发布的触动精灵加密脚本还原即是对应的逆向实现。
实战
获取脚本
根据上述的理论知识,我们直接依次尝试,结果发现此游戏属于运行时解密的类型,(即上述第2种),我们直接使用frida来做勾取
nptr = Module.findExportByName(null, "luaL_loadbuffer");
var keep = null;
var keep2 = [];
var keep1 = null;
if (!nptr) {
console.log("luaL_loadbuffer can be found!");
} else {
console.log("find %d", nptr);
Interceptor.attach(nptr, {
onEnter: function(args) {
var len = args[2].toInt32();
var code = args[1].readCString(len);
send({ path: args[3].readCString(), dump: code });
}
}
}上述脚本,hook了
int luaL_loadbuffer (lua_State *L, const char *buff, size_t sz, const char *name);
这个函数,第一个参数为lua虚拟机指针,第二个为代码的字符串buff,第三个为代码的长度,最后一个则为这个脚本的名字,由此,天时地利人和,参数齐备,我们获取之后发回到frida即可保存在主机上,当然frida这边需要保存。
def savefile(path, data):
create_dir(os.path.dirname(path))
with codecs.open(path, 'w', 'utf-8') as f:
f.write(data)
def on_message(message, data):
if 'payload' in message and message['type'] == 'send':
payload = message['payload']
if 'dump' in payload:
origin_path = payload['path']
data = payload['dump']
savefile(origin_path,data.encode("utf-8"))
return
if message['type'] == 'send':
print("[*] {0}".format(message['payload'].encode('utf-8')))
else:
print(message)在图中,我们甚至可以看到注释。。。显然,这家公司心太大了。
无限步数
既然有源码,甚至有注释,这一步的工作可以说是非常简单了。我们一番搜索之后发现上图中所示位置
mainLogic.theCurMoves = mainLogic.theCurMoves - 1;
if mainLogic.PlayUIDelegate then --------调用UI界面函数显示移动步数mainLogic.theCurMoves即代表剩余步数,我们可以选择每走一步+1,或者一直不减....等等逻辑。
无限精力
经过上面这么一整,显然是舒服了,再也不担心会输了,一口气过了好多关。但接下来问题出现了,精力不够了,怎么办?
只能盘他了。
我们一顿直接搜索jingli然而什么都没找到,看来这家公司技术不怎么行但英语还是不错的。我给我初中同学打了个电话,问了问她精力的英文怎么拼,她骂了我一顿说我神经病然后让我百度翻译....然后一顿翻译以后得知他的英文果然不错。正则搜索energy[\s]+=果然命中。
function UserRef:setEnergy(v)
local key = "UserRef.energy"..tostring(self)
self.energy = v --onlu used for encode
encrypt_integer_f(key, v)
end更牛逼的是这里面的注释竟然写着他有编码,嗯,显然是为了防止类似ce,八门神器等的业余玩家,我百度了一下,发现这个公司的技术负责人竟然在教别人游戏怎么防攻击。。。好吧,我实在不知道该说什么好了。
无限金币/风车币
这个就简单了,就在上面精力的下面
function UserRef:getCoin()
local key = "UserRef.coin"..tostring(self)
return decrypt_integer(key)
end
function UserRef:setCoin(v)
local key = "UserRef.coin"..tostring(self)
self.coin = v --onlu used for encode
encrypt_integer(key, v)
end
function UserRef:getCash()
local key = "UserRef.cash"..tostring(self)
return decrypt_integer(key)
end
function UserRef:setCash(v)
local key = "UserRef.cash"..tostring(self)
self.cash = v --onlu used for encode
encrypt_integer(key, v)
end
function UserRef:getRealTopLevelId()--最高通过关卡而不是最高停留关卡
local topLevel = self:getTopLevelId()
local levelScore = UserManager.getInstance():getUserScore(topLevel)
if levelScore and levelScore.star > 0 then
return topLevel
else
return topLevel - 1
end
end
function UserRef:getTopLevelId()
local key = "UserRef.topLevelId"..tostring(self)
local level = decrypt_integer_f(key)
if level > kMaxLevels then level = kMaxLevels end
return level
end
function UserRef:setTopLevelId(v)
local key = "UserRef.topLevelId"..tostring(self)
self.topLevelId = v --onlu used for encode
encrypt_integer_f(key, v)
end
function UserRef:getStar()
local key = "UserRef.star"..tostring(self)
return decrypt_integer(key)
end
function UserRef:setStar(v)
local key = "UserRef.star"..tostring(self)
self.star = v --onlu used for encode
encrypt_integer(key, v)
end
function UserRef:getHideStar()
local key = "UserRef.hideStar"..tostring(self)
return decrypt_integer(key)
end
function UserRef:setHideStar(v)
local key = "UserRef.hideStar"..tostring(self)
self.hideStar = v --onlu used for encode
encrypt_integer(key, v)
end
function UserRef:getEnergy()
local key = "UserRef.energy"..tostring(self)
return decrypt_integer_f(key)
end
function UserRef:setEnergy(v)
local key = "UserRef.energy"..tostring(self)
self.energy = v --onlu used for encode
encrypt_integer_f(key, v)
end
function UserRef:getUpdateTime()
local key = "UserRef.updateTime"..tostring(self)
return decrypt_number(key)
end
function UserRef:setUpdateTime(v)
local key = "UserRef.updateTime"..tostring(self)
v = tonumber(v) or Localhost:time() --onlu used for encode
self.updateTime = v
encrypt_number(key, v)
end
function UserRef:encode()
local dst = {}
self.updateTime = self:getUpdateTime()
self.energy = self:getEnergy()
self.hideStar = self:getHideStar()
self.star = self:getStar()
self.topLevelId = self:getTopLevelId()
self.cash = self:getCash()
self.coin = self:getCoin()
for k,v in pairs(self) do
if k ~="class" and v ~= nil and type(v) ~= "function" then dst[k] = v end
end
return dst
end
这里面什么都有了,对着修改即可。。
开发者模式
对代码一番研究,竟然发现有开发者模式。
而且就是前几行
_G.kUseSmallResource = true
_G.kScreenWidthDefault = 720
_G.kScreenHeightDefault = 1280
_G.kDefaultSocialPlatform = "ios_all"
_G.kUserLogin = false
_G.isLocalDevelopMode = StartupConfig:getInstance():isLocalDevelopMode()也是牛逼顶天了。开发者模式开启后,界面会多出来一些了不得的功能。
GM模式
这个就不讲实现了吧。。。。
完整frida脚本
function patchLua(src, dst, args) {
var origLength = args[2].toInt32();
var test = args[1].readCString(origLength);
if (test.indexOf(src) > -1) {
var test2 = test.replace(src, dst); //"mainLogic.theCurMoves<100?mainLogic.theCurMove+ 1:mainLogic.theCurMove - 1;");
//test3.writeByteArray(strToBinary(test));
var tmpP = Memory.allocUtf8String(test2);
keep2.push(tmpP);
args[1] = tmpP;
var length = getStringLen(args[1]);
args[2] = ptr(length);
console.log(src + "patch ok");
}
}
nptr = Module.findExportByName(null, "luaL_loadbuffer");
var keep = null;
var keep2 = [];
var keep1 = null;
if (!nptr) {
console.log("open can be found!");
} else {
console.log("find %d", nptr);
Interceptor.attach(nptr, {
onEnter: function(args) {
var origLength = args[2].toInt32();
var test = args[1].readCString(origLength);
//send({ path: args[3].readCString(), dump: test });
if (test.indexOf("mainLogic.theCurMoves - 1") > -1 &&
args[3].readCString().indexOf("BonusStep") < 0) {
var test2 = test.replace("mainLogic.theCurMoves - 1", "mainLogic.theCurMoves + 1"); //"mainLogic.theCurMoves<100?mainLogic.theCurMove+ 1:mainLogic.theCurMove - 1;");
//test3.writeByteArray(strToBinary(test));
var tmpP = Memory.allocUtf8String(test2);
keep = tmpP;
args[1] = tmpP;
var length = 0;
var p = args[1];
var length = getStringLen(args[1]);
args[2] = ptr(length);
}
//修改精力
patchLua("self.energy = v", "v=30\nself.energy = v", args);
//修改风车币
patchLua("self.cash = v", "v=79878\nself.cash = v", args);
//开发者模式
//patchLua("StartupConfig:getInstance():isLocalDevelopMode()", "true",args);
},
onLeave: function(retval) {}
});
}防检测
逆向这么强的吗?那游戏公司不是混不下去了???显然不是,虽然客户端是攻击者的主场,但游戏公司也有自己的主场,那就是各种层出不穷的检测。。
但一般来讲,我们不应该去修改lua源码本身,因为可能大多数的公司都会有lua源码进行类似crc,md5,adler,hash等校验,我们一旦对源码进行修改,则意味者被认定为作弊玩家。因此,对于此种lua游戏,可以选择再次加载自身的lua脚本,因为该脚本也和游戏脚本在同一个虚拟机的命名空间之内,由此可以实现变量的覆盖,函数的调用等。
但即便如此,也不可能全然绕过游戏的检测和防御。而关于如何绕过?这个是最有技术含量的,一般来说,功能的实现都是比较简单的,但往后检测的对抗才是智力体力的终极对抗。
