Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

xx消消乐的逆向分析与利用 #32

Open
ohroy opened this issue Aug 2, 2019 · 6 comments
Open

xx消消乐的逆向分析与利用 #32

ohroy opened this issue Aug 2, 2019 · 6 comments

Comments

@ohroy
Copy link
Owner

ohroy commented Aug 2, 2019

前言

今天我们来研究一下如何收获妹子崇拜的眼神,从而获得妹子的芳心...啊不,是如何在妹子面前装一个圆润的漂亮的逼。即帮他减轻喜欢的游戏的压力,让她知道游戏是如此的...无聊。
这款游戏的名字叫《xx消消乐》,怎么样,一看到这个游戏就知道妹子显然是一个清纯的人。
这里,我们的目的是使用多种方式来给这款游戏作弊。
本次探索中可能重要的技术或工具为

  • ida
  • frida
  • lua
  • c/c++

但本文只为技术交流,无任何赢利目的。但恶意修改游戏是违法行为,各位读者需自负责任,不要走上违法犯罪的道路。本文作者不负相关责任。
且本文所分析样本为2019年1月份的某个版本,现行版本不一定适用,因此所有代码仅供参考。同时,我也希望能对游戏行业的开发人员有点警示作用,使之明白安全防护的重要性,以及现在裸奔是多么的危险。

分析

引擎分析

正如之前提到的,分析一个游戏的第一步,显然是先分析到游戏所用的引擎。常见的框架有cocos2dUnity3dunrealengine等。本次探索的目标是使用的lua引擎,这类游戏和U3d一样,都非常的简单,只要反编译到游戏的源代码,则基本如履平地,驰骋疆场。
你问我如何知道它是lua?最简单的方法是拖到ida里面一顿梭,只要含有lua相关关键字的,一般八九不离十就是了,另外,对于安卓来说,包解压后直接看lib目录里是否有libcocos2dlualiblua,libhellolua等即可快速判断出。由此,我们可以得出结论。除了这款游戏之外,《梦幻西游》《奇迹暖暖》等也是这个引擎,也可按本文下述套路一顿梭。

lua引擎的弱点

  1. 基于lua是一种脚本语言的说法,且为了开发和更新方便,一般安全意识较弱的公司,对lua脚本的存放都在资源文件夹里,有的甚至文件根本没有加密。
  2. 稍微安全意识较强的,可能会把lua给打包存放,甚至加密存放。但这些属于徒劳,顶多算是自欺欺人,因为最后在lua虚拟机装载的时候,总要进行解密,我们可以在这个时候勾住装载函数,以获取所有的脚本。本文示例的游戏即是此类型。
  3. 安全意识更强的,则考虑把lua编译后再放入客户端,此时攻击者无法直接获取到lua的源码,取而代之的是获取到编译后的结果。但显然这也是自欺欺人的表现,因为lua解释器开源的,制作一个lua的反编译工具是非常简单的,且现在已经有很多的实现。反编译后依然能得到源码。
  4. 安全意识极强的,可能由上述方案更进一步,既然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)

此时,我们即可成功dump《xx消消乐》的所有代码,如下
kaixin

在图中,我们甚至可以看到注释。。。显然,这家公司心太大了。

无限步数

既然有源码,甚至有注释,这一步的工作可以说是非常简单了。我们一番搜索之后发现上图中所示位置

	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,md5adlerhash等校验,我们一旦对源码进行修改,则意味者被认定为作弊玩家。因此,对于此种lua游戏,可以选择再次加载自身的lua脚本,因为该脚本也和游戏脚本在同一个虚拟机的命名空间之内,由此可以实现变量的覆盖,函数的调用等。

但即便如此,也不可能全然绕过游戏的检测和防御。而关于如何绕过?这个是最有技术含量的,一般来说,功能的实现都是比较简单的,但往后检测的对抗才是智力体力的终极对抗。

@Lat0ur
Copy link

Lat0ur commented Aug 8, 2019

什么时候出E#收费插件dump教程呀。想看看O9k收费躲避和物品管理的一些逻辑

@HongYuuu
Copy link

getStringLen(args[1]) 这函数是不是取size的, js里边没自带这种函数吧

@ohroy
Copy link
Owner Author

ohroy commented Jul 15, 2020

@HongYuuu
对取size的

function getStringLen(p) {
    var length = 0;
    while (1) {
        if (p.readS8() == 0) {
            break;
        }
        length += 1;
        p = p.add(1);
    }
    return length;
}

@supperlitt
Copy link

666,我无话可说。

@supperlitt
Copy link

args[2] = ptr(length);
在c里面应该怎么操作。。。

@supperlitt
Copy link

args[2] = ptr(length);
在c里面应该怎么操作。。。

int* addr = &size;
(*addr) = new_size;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants