-
-
[原创] 记一次复活 cocos2d 安卓客户端
-
发表于: 2024-9-7 22:51 3588
-
我长期都只玩单机游戏,就是因为联网的游戏总有停止运营的一天。最近拿到了一个已经停服的安卓游戏 apk,说是开头会检查更新,进不去游戏,但有人就是很想玩,甚至服务器都准备好了。所以想看看这个更新怎么处理。
首先用 apktool -d *.apk
解包,发现资源里面有一堆加密的 lua 文件。如果没猜错,业务逻辑都会写在这里,而且在这里可能可以绕过更新检查。
先用 jadx 打开 apk,搜索一下 .lua,没有结果。果断冲到 lib/*.so 看 native 层。打开一看,库还挺全,甚至 x86 和 mips 的库都打包了。这可能是 cocos 引擎比较善良,各个平台都照顾了吧。
进 armeabi 里,发现 libgame.so 占空间最大,而且看名字就知道很关键。用 ida 打开看导出表,全部是 cocos2d 的命名空间。可以确定这是 cocos2d 游戏了。
字符串搜索 .lua,还真就直接找到了加载 Lua 的函数。用的是 XXTEA 算法。进去 getDefaultKey 一看就拿到密钥了。
Github 上面的 cocos2d-xxtea 加解密还是挺多的,搜搜就有,但我直接用 Copilot 生成一个也没费多大劲。解密出来,居然是源代码,这就很好。据说新版的 cocos2d 会用 LuaJIT 先处理一遍脚本,就算解密出来也很难用。
安装 apk 到手机,手机连接电脑热点,电脑开 Wireshark 抓包。这样抓包感觉怪怪的,但我之前用 r0capture 没抓到东西,也不知道还有什么其他方式没。
很快嗷,就找到最后卡在一个失败的 dns 请求。
根据这个失败的域名一找,在 libgame.so 里面找到了一个 http 请求的地址。查找这个字符串的引用,找到了获取版本检查地址的函数。
获取版本检查地址的函数呢,有两处调用。第一处是这样的,这是将 native 函数转为 Lua 函数的胶水函数。
在 Lua 脚本中搜索这个函数的名称,果然搜索到了一堆引用。把这些调用都处理掉,重新打包,会发现还是无法进入游戏。甚至删了所有 Lua 脚本都不影响游戏,说明还没有运行到 Lua 脚本这一步。
这就要说到 libgame.so 里面第二处调用了,这处引用往上找去,发现居然是在游戏启动时直接就创建了一个更新界面,因此无法在 Lua 层做完一切了。
这种情况下,如果要改 so 实现的话,我需要是精通 armv7, armv8, X86, MIPS 指令的汇编大佬才能做的尽善尽美。但我不是汇编大佬,所以我们尝试修改更新服务器地址,并模拟服务器行为。
上一节中提到的函数,获取了一个 url,丢给谁了呢?可以看到丢给了 AssetsManger 构造函数的第二个参数。
网上一搜,这是个很常用的 cocos2d 热更新的插件,因此构造函数的参数明明白白。这个 url 起到的似乎是给出版本信息的作用。
然而我照着热更新的教程搭了一遍后才发现,这个地址不正确也没关系,只要看起来返回了一个 http 地址就行,所以不再赘述。
电脑本地起一个 http 服务器,对任何请求都返回一个 http 地址。
重打包一下改了地址的 APK。这指令总是到用的时候才去搜挺烦的,现在记录一下。在 apktool.yml
同目录下执行
启动后,就能够成功看到访问日志,而游戏也顺利进入到了登录界面。
在初次获取更新版本 url 之后,跑的就是 Lua 脚本里面的逻辑了,所以后面改 Lua 脚本然后再加密打包应该就行了。脚本语言真是好文明啊!
全篇看下来好像没啥技术,就当提供个分析思路和做一个记录吧!最喜欢这种没有任何代码混淆的程序了
from
http.server
import
BaseHTTPRequestHandler, HTTPServer
import
urllib
class
SimpleServer(BaseHTTPRequestHandler):
def
do_GET(
self
):
# 解析 URL 参数
parsed_path
=
urllib.parse.urlparse(
self
.path)
query_params
=
urllib.parse.parse_qs(parsed_path.query)
response
=
'http://zheshi.jiechi.com:80/dedizhi'
# 设置响应状态和头
self
.send_response(
200
)
self
.send_header(
'Content-type'
,
'application/json'
)
self
.end_headers()
# 返回响应数据
self
.wfile.write(
str
(response).encode(
'utf-8'
))
# 配置服务器
def
run(server_class
=
HTTPServer, handler_class
=
SimpleServer, port
=
80
):
server_address
=
('', port)
httpd
=
server_class(server_address, handler_class)
print
(f
'Starting server on port {port}...'
)
httpd.serve_forever()
if
__name__
=
=
'__main__'
:
run()
from
http.server
import
BaseHTTPRequestHandler, HTTPServer
import
urllib
class
SimpleServer(BaseHTTPRequestHandler):
def
do_GET(
self
):
# 解析 URL 参数
parsed_path
=
urllib.parse.urlparse(
self
.path)
query_params
=
urllib.parse.parse_qs(parsed_path.query)
response
=
'http://zheshi.jiechi.com:80/dedizhi'
# 设置响应状态和头
self
.send_response(
200
)
self
.send_header(
'Content-type'
,
'application/json'
)
self
.end_headers()