🔐 技术型隐藏页面 · 验证密钥
不管任何方式,总之你现在已经绕过了前面那个只是做个样子的校验,所以恭喜你可以开始阅读这篇 blog!下面将会有不少二进制逆向工程相关内容,你可能需要会一些基础的 arm asm 和一些逆向的经验才能理解。
这篇文章是关于逆向某游戏的,所以先来点…
本文所写内容仅供学习交流,作者对于读者所做的任何行为概不负责,如果跟着本文所写内容操作,您需要对自己的行为导致的任何后果负责!
Ok. 总所周知,某3d立体节奏游戏作为一个在线游戏(虽然只有登陆上传下载还有 LINK 的时候需要用到网络),那么必然存在一个服务器。而且在游戏本体中必然直接硬编码了一个游戏地址,否则游戏的数据就无法对着服务器发送。
在早期版本并没有对这个服务器地址进行什么操作,直接使用明文写在二进制中。但是在3.6.0左右进行了一次比较大的改动,对地址进行了加密。在4.5.0左右版本又对加密算法进行了一些小更新。因此本文主要关于逆向这个加密算法。
之所以逆向这个服务器地址,当然是因为我们想要搭建一个自己的私服。所以在讲述这个过程之前先让我首先对 Lost-MSth大佬表示最深刻的敬意,是他手把手教我如何操作,并且给我很多支持(在这个过程中我犯的傻不计其数)。我的私服程序来源于他发布在 Github 上的 Arcaea-Server。
本文参考了
什么是私服,怎么搭建#
这个其实完全不应该由我来说,在 Arcaea-Server 中已经有良好的文档,请一个字一个字阅读。这个仓库的 Wiki 可以说微言大义,错过了任何内容都是损失。
但是我们想讲讲关于私服本身。下面是一张简单的结构图


那有没有什么做法干净又卫生呢?#
那我们回到最开始的路径。

关于修改地址#
首先我们要先来确定加密的方式和加密的位置。由于更高版本的客户端二进制中加入了更多的混淆,因此我们选一个比较好逆向的客户端。我选取的是4.5.x版本的一个客户端。
当然你选什么都一样,只是后面的版本inline function 太狠了,一个函数长达两万行你的电脑可能需要分析得很久,反正算法没改选一个早期没混淆那么多的分析起来快点。
下面当然是基于4.5.x版本的一个客户端的案例,我讲讲如果你选取别的客户端要怎么分析。 如果你 decompile 的时候发现因为太长无法反编译,那么你可以在ida 的 edit-plugins-hex ray decompiler options中找到 analyse option 3,把第一个改到 1024 第二个改到 256,然后重新反编译就可以了。
6.x 版本api 有了一个 break change!
auth 部分独立开了。 以往的 api 完全在
https://arcapi-v3.lowiro.com/<endpoint>内,目前 auth 部分单独走https://auth.lowiro.com/。但是加密手段完全一致,你依然可以根据下文的手段进行分析,只是 48bytes 变成了 36bytes 罢了。
继续对低版本的分析,首先我们通过 apktool 来将其拆开。我们需要更改的是其下的 lib/armeabi-v7a/libcocos2dcpp.so 文件。我们选取32位,它的逆向更简单,而且动态调试也更加简单。不要忘记删除 lib/arm64-v8a/ 文件夹。否则手机会自动使用64位版本的二进制文件。
我的所有思路参考看雪论坛:[原创] 某 iOS 遊戲抓包、修改,当然这篇文章中的地址已经不适用了,因为这篇文章是给4.2.x 使用的,上文提到4.5.x修改了加密密钥。
使用 ida pro 打开这个文件。IDA PRO 是一款优秀的反汇编工具,这里就不赘述了。
auth/login。之所以搜索这个,是因为登陆必须对服务器进行请求,一定会涉及到服务器地址,这毋庸置疑。


根据分析我们大概可以确定(其实是根据前人的经验)代码结构如下
idx 可以查看引用看看赋值位置


https://arcapi-v3.lowiro.com/<api_endpoint>
是分成三次加密。而不到48字节则在最后补\0x00。因此我们不能拿到算法本身,我们就只能通过动态调试,通过修改内存值,再拿到每一次的XOR KEY来获取结果。
总结一下,大概算法是这样的:
def get_next_xor_key(last_addr: bytes, last_xor_key):
# do something weird
# calculate next xor key
return next_xor_key
for i in range(3):
addr = "aosiaosciaqwu12hoichoaichoaihcoccqwocihqoqwdwqwd"[16 * i : 16 * (i + 1)]
for j in range(16):
next_addr = next_xor_key[j] ^ addr[j]
next_xor_key = get_next_xor_key(next_addr, next_xor_key)
现在我们有 a^b = c, c^b = a^b^b = a ,所以我们可以通过直接通过拿到内存中的 xor_key 和我们想要的地址直接计算出加密之后的地址。而加密的地址和密钥再 XOR 之后即可获取到解密的结果。算法具体请参照看雪论坛:[原创] 某 iOS 遊戲抓包、修改。
动态调试#
在现在 root 手机变得相对更加困难,更取决于手机厂商是否让root,而非是自己的技术。但是这游戏并没有关于反 debug 的校验,所以我们只需要通过修改安装包,然后直接通过 run-as 命令来控制 debug 就可以实现无 root 权限调试。
关于这一块的内容请阅读Android 无 ROOT 权限动态调试 (By Lost) 这里补充说明一下,在 ida 9.0 mac版本上的 android_server 无后缀是 64位的,有32后缀的是32位的,和教程中的不一样,需要注意。
我们将更改好的软件传输到手机上安装云云…. (推荐直接使用 adb push xxx.apk /storage/emulated/0/ 应该至少有30-40mb/s 速度) 然后将端口转发设置好,并启动ida的服务器。
现在我们应该在哪里下断点呢?显然是
显然这里第一次计算的xor key是固定的 25 42 38 06 4B 6E 4B 3B 63 85 43 CD E6 DF BB 6F (至少到5.10.6还是)。因此我们写一个小程序用于计算加密结果。
# 你的地址 http(s)://ip:port(domain:port)/<api_endpoint>
api_prefix = "https://test.yinmo19.top/natsugakuru/30/"
if len(api_prefix) < 48:
api_prefix += "\x00" * (48 - len(api_prefix))
b = bytes.fromhex("25 42 38 06 4B 6E 4B 3B 63 85 43 CD E6 DF BB 6F")
prefix = api_prefix[:16]
prefix = bytes.fromhex(prefix.encode().hex())
print(bytes(i ^ j for i, j in zip(prefix, b[:16])).hex(" "))
b = bytes.fromhex("25 42 38 06 4B 6E 4B 3B 63 85 43 CD E6 DF BB 6F") # 获取到第二个 xor_key 之后填入
prefix = api_prefix[16:32]
prefix = bytes.fromhex(prefix.encode().hex())
print(bytes(i ^ j for i, j in zip(prefix, b[:16])).hex(" "))
b = bytes.fromhex("25 42 38 06 4B 6E 4B 3B 63 85 43 CD E6 DF BB 6F") # 获取到第三个 xor_key 之后填入
prefix = api_prefix[32:]
prefix = bytes.fromhex(prefix.encode().hex())
print(bytes(i ^ j for i, j in zip(prefix, b[:16])).hex(" "))
现在进 debugger 设置一下
[32] 包名的选项

encrypt_addr_this跳转到
4d 36 4c 76...(M6Lv...) 就说明这个确实是地址(这个是地址的开头,对应 http 的加密结果)。右键选择关联到(synchronize with) hex,


接下来应该先看xor_key_addr ,双击查看内存地址和值
B8 97 5E AD FC B7 F0 EF F1 A0 1E 5A 36 4D C1 AA 。我们直接将整行复制下来,粘贴到上面的python脚本中的第二个位置,运行可以得到加密地址的中间16字节 d5 f8 6f 94 d2 c3 9f 9f de ce 7f 2e 45 38 a6 cb,和上面操作一致,写入地址。接下来就可以重复第三次操作,拿到最后一次的 xor_key,加密地址。最后将三次获取的内容拼接起来,一共48字节。以后想要开私服只需要替换掉二进制文件中的对应地址就可以了。
这里我提供一个替换小程序,如果有需要可以改改用。
import os
def generate_patched_so(
filename, before: bytes, to_replace: bytes, suffix: str = "arcapi"
):
if not os.path.exists(filename):
print(f"Didn't find {filename}, skipping patch generation")
return
with open(filename, "rb") as f:
data = f.read()
if data.find(to_replace) != -1:
print(f"{filename} looks to be already patched :)")
return
if data.find(before) == -1:
print(f"{filename} doesn't contain the original modulus.")
return
data = data.replace(before, to_replace)
patched_filename = f"{filename}.{suffix}"
with open(patched_filename, "wb") as f:
f.write(data)
print(
f"Generated address patch to {patched_filename}! To apply the patch, replace the original file with the patched file."
)
origin = bytes.fromhex(
"4d364c763854641402f720ac96b696198bb932c28bde8280cad46122473cf0c63c63f8359caab6e6a6d2d4eab1db7d93"
)
replace_addr = bytes.fromhex(
"owoqowoqciwqociowqhoiqicoiqocuwqcoqioucoqcoqwfhiqohfcowqcjwbfuiocqwhfbwusofihqwqwoifhqohwoqhqhfq"
) # 替换成你的内容
generate_patched_so("libcocos2dcpp.so", origin, replace_addr)
如果找到了对应部分则会在本地目录下生成一份补丁文件libcocos2dcpp.so.arcapi,将这个文件直接覆盖原文件即可。
cp libcocos2dcpp.so.arcapi libcocos2dcpp.so
那么就完成了。
Appendix 1#
c 版的逆向几乎就是上面这样了,接下来是一些最新版本相关的。由于目前地址已经分为 auth.lowiro.com 和 arcapi-v3.lowiro.com,因此两个地方需要寻找。选择下断点的地方,使用登录以外还可以考虑 /bundle_download/ api 的前面,因为这个是最初访问的 api,他会寻找适用于该版本的热更新包(国际服)。
两个地方的加密算法一致,这上问题提到了,下面是一些最新版本你可能出现的问题。
高版本的 arm 架构的 while offset in 16{ xor } 行为可能会被编译期自动向量化为一条 neon 指令,每 16 bytes作为一个 int128 直接进行 veorq_s8,也有可能分为两个 u64 分别 xor,这两种优化方式连带上面提到的while offset in 16{ xor }均在各个版本中出现过,例如 ios 版本逆向发现了 veorq_s8,而安卓版本则看到了仅对于 u64 的 neon。遇到这种情况下断点直接下在这条 neon 指令前面就可以了,本质上就是整个循环一条做完了没什么好说的。
但是有一点需要注意了。由于编译器优化,你会发现有时候你自定义的 api 长度过长会产生奇怪行为——例如最后几位解密失效了。这里并不是因为算法错误,而是因为 loriwo 的 api 就那么长,所以编译器为了少算点自动在最后一次循环的计数器直接设置为特定大小。比如按照刚才所说的
for i in range(3):
addr = "aosiaosciaqwu12hoichoaichoaihcoccqwocihqoqwdwqwd"[16 * i : 16 * (i + 1)]
# 这里,实际上在 i = 2 的时候,j 并不会是 0 到 15,而是 0 到 一个数字(取决于原有的 api 长度)
for j in range(16):
next_addr = next_xor_key[j] ^ addr[j]
next_xor_key = get_next_xor_key(next_addr, next_xor_key)
因此解决办法最简单的就是你别设置一个那么长的 api。
那你要是说我不,行也不行——如果你对一个 auth 的 api 想犟一下还真能做到。我们说过这只是最后一轮计数器的事情,所以稍微懂点汇编,找到计数器的特殊位置

为什么我说只有 auth 的地方可以犟一下,因为 auth 这里只用了一次,剩下的 api 用到的位置每个地方都得改,你肯定没这个精力了(也找不全),所以我还是建议你把 api 设短点。
按 ios 的一次 16 位 veorq_s8 还需要这样操作吗?不需要了!因为一次就 16 位,根本没有计数器。
Appendix 2#
还有。如果我没有 ida 我还能做么?没有 ida 做个锤子破解(bushi) 当然可以,我假定你现在有 mac 了,因为接下来分析的是 ios 的 binary。
总所周知,因为 mac 最近用的 m 系列芯片也是 arm 架构的,所以你可以无缝的直接运行 ios 的软件,既不用跨架构也不用转译,最大的优点就是你下载了一个砸过壳的 ipa,然后cd 到目录进去之后直接./binary 就能启动。这也意味着你可以直接使用 lldb 附加到这个 binary 进行动态调试——所以现在你只需要一个能分析二进制的工具(开源的也有不少,例如ghidra),以及lldb 就可以完成调试和分析功能。下断点之类的指令请自行学习,所有逻辑都是一样的,唯一不同的是这会你只能在汇编层面下断点了,没有伪代码可以看了。
改 memory 也简单不少,你可以直接找到 offset 之后
memory write <address> hex hex hex hex ....
似乎还有机会可以做自动化处理。
后记#
这个逆向过程其实并不难,但是确实是我接触逆向以来的一个比较大的步子。记得那天找了一个旧版的apk,真正逆向出来的时候在深夜,当时逆向出来的时候激动的跳起来,现在想起来真有意思……最后感谢前辈们提供的支持,还有 ida pro,这真的是个好软件。
